GKTurnBasedMatch match-making waiting until another player matched - iphone

I find the default turn based matchmaking process unintuitive for users. The matchmaker returns a match even when the other player is not yet found and then local user has to take turn in a fresh game after which he/she has to wait in game screen. Instead of that I would rather have the users stay in an interactive screen where they can cancel their match request or wait until they are matched (then I decide who starts first). Is there any way to do this, not a workaround but a robust one?
My current code, doing nothing towards my purpose, is below:
-(IBAction)gcMatchPressed:(id)sender
{
GKMatchRequest *request = [[GKMatchRequest alloc] init];
request.minPlayers = 2;
request.maxPlayers = 2;
request.playerGroup = PLAYERGROUP;
[GKTurnBasedMatch findMatchForRequest:request withCompletionHandler:^(GKTurnBasedMatch *match, NSError *error) {
if (match != nil)
{
self.gcMatch = match;
[self performSegueWithIdentifier:#"Multiplayer" sender:self];
}
} ];
}

Related

How to get local player score from Game Center

How to get score of local player from Leaderboard Game Center? I tried this code, but it returns nothing. Anybody know how to solve it, or is there better way how to get score?
- (NSString*) getScore: (NSString*) leaderboardID
{
__block NSString *score;
GKLeaderboard *leaderboardRequest = [[GKLeaderboard alloc] init];
if (leaderboardRequest != nil)
{
leaderboardRequest.identifier = leaderboardID;
[leaderboardRequest loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error) {
if (error != nil)
{
NSLog(#"%#", [error localizedDescription]);
}
if (scores != nil)
{
int64_t scoreInt = leaderboardRequest.localPlayerScore.value;
score = [NSString stringWithFormat:#"%lld", scoreInt];
}
}];
}
return score;
}
I think, that method have to wait for completion of [leaderboardRequest loadScoresWithCompletionHandler: ...
Is it possible?
Your code appears to not have any bugs that I can see. I would recommend displaying the standard leaderboard interface to see if your code that reports the scores is actually working correctly. If so, you should see the scores in the leaderboard. The code below works in my game, and I know the score reporting is working properly because it shows in the default game center UI.
GKLeaderboard *leaderboardRequest = [[GKLeaderboard alloc] init];
leaderboardRequest.identifier = kLeaderboardCoinsEarnedID;
[leaderboardRequest loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) {
if (error) {
NSLog(#"%#", error);
} else if (scores) {
GKScore *localPlayerScore = leaderboardRequest.localPlayerScore;
CCLOG(#"Local player's score: %lld", localPlayerScore.value);
}
}];
If you aren't sure how, the code below should work to show the default leaderboard (iOS7):
GKGameCenterViewController *gameCenterVC = [[GKGameCenterViewController alloc] init];
gameCenterVC.viewState = GKGameCenterViewControllerStateLeaderboards;
gameCenterVC.gameCenterDelegate = self;
[self presentViewController:gameCenterVC animated:YES completion:^{
// Code
}];
You cannot return score outside the block. In this code first "return score" will be executed before the method "loadScoresWithCompletionHandler". Additionally you haven't set initial value for "score", this method will return completely random value.
I suggest you to put your appropriate code inside the block, instead of:
int64_t scoreInt = leaderboardRequest.localPlayerScore.value;
score = [NSString stringWithFormat:#"%lld", scoreInt];
The leaderboard request completes after the return of your method. This means that you are returning a null string.
The method you put the leaderboard request in should be purely for sending the request. The method will be finished executing before the leaderboard request is complete thus your "score = [NSString stringWithFormat:#"%lld", scoreInt];" line is being executed AFTER the return of the "score" which is null until that line is executed.
The solution is to not return the outcome of the completion handler using the method that sends the request. The score is definitely being retrieved correctly, so just do whatever you need to with the score inside of the completion handler. You have no way to know when the completion handler is going to be executed. This, in fact, is the reason why Apple allows you to store the code to be executed in a block! Although, it can be confusing to understand how to work with blocks that will definitely be executed later, or in your situation, sometime after the method is returning.
The best way to handle your situation is to not return anything in this method and just use the "score" variable as you intend to after the block has set score to a non-null value!

endTurnWithNextParticipant deprecation

Thought I fixt my problem with the deprecation, I now found out I haven't. My game is not functioning correctly.
In the new endTurnWithNextParticipant there is also a timeout for the next player (the timeinterval they have to answer on there turn) I have set that on 86400 (seconds in a day)
However the game doesn't send a turn at all anymore, I can set the second to for example 1 then it would send the turn but still later then it used to before the deprecation. even If I set the interval to 0.
I think the problem is that the turn is send to the player that has just send a turn.
this is the code: (the commented line is the what I used but is now deprecated)
[currentMatch endTurnWithNextParticipants:currentMatch.participants turnTimeout:86400 matchData:data completionHandler:^(NSError *error){
//[currentMatch endTurnWithNextParticipant:nextParticipant matchData:data completionHandler:^(NSError *error) {
if (error) {
NSLog(#"%#", error);
statusLabel.text = #"Oops, there was a problem. Try that again.";
} else {
statusLabel.text = #"Your turn is over.";
textInputField.enabled = NO;
}
}];
I hope someone can help me out.
Instead of currentMatch.participants, you may want to try
[[NSArray alloc] initWithObjects:nextParticipant,nil]
That way, the only player that's being sent is nextParticipant, the same way you had it in the deprecated code.

performSelector enters method on all instances, but only one thread finishes

This may be a naive question, but I'll ask it anyway as I cannot find any documentation that clears up this issue in my head.
I'm running iOS5.1 both on device and in the simulator with Xcode45-DP4.
I have a loop that iterates over an array of a number of instances of a class. In that loop I use performSelector on the instances to start a thread that does some relatively slow network operations — pulling down data that I'd rather do in the background.
[arrayOfFriends enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Friend *f = (Friend*)obj;
iOSSLog(#"%d", idx);
[f performSelectorInBackground:#selector(showDescription) withObject:nil];
-(void)fetchTwitterStatus
{
iOSSLog(#"Trying to fetch twitterstatus %# %#", self.hash, self.twitterUserName);
[mLocalTwitterUser fetchTwitterAPIUserStatusWithScreenName:twitterUserName
withCompletionHandler:^(NSArray *arrayOfStatus, NSError *error) {
if(error) {
iOSSLog(#"%#", error);
} else {
iOSSLog(#"Got twitterstatus %# %d", self.twitterUserName, [arrayOfStatus count]);
#synchronized(items) {
[items addObjectsFromArray:arrayOfStatus];
}
}
}];
}
In my test case there are four instances. Each instance gets its selector, you know..selected. The first three definitely start but only the last actually completes as indicated by the log line that says "Got twitterstatus..." Which is weird.
I can also verify that the method the selector calls "fetchTwitterStatus"
What is the little fundamental nugget of multithreading that I'm missing here?
EDIT: here's fetchTwitterAPIUserStatusWithScreenName...quite a bit here, but effectively it's calling the Twitter API Endpoint user_timeline with a JSON response.
- (void)fetchTwitterUserStatusWithScreenName:(NSString *)screenname
excludeReplies:(BOOL)excludeReplies
withCompletionHandler:(OtterTwitterSearchHandler)completionHandler
{
self.twitterAPIStatusHandler = completionHandler;
//self.fetchTwitterUserStatusHandler = completionHandler;
NSString *urlString = [NSString stringWithFormat:#"https://api.twitter.com/1/statuses/user_timeline.json?screen_name=%#&include_rts=true&include_entities=true&exclude_replies=%#&count=50", screenname, excludeReplies?#"true":#"false"];
NSURL *url = [NSURL URLWithString:urlString];
#warning this isn't the way to do it - just checking the cache for refresh of the scroller
[[ASIDownloadCache sharedCache]removeCachedDataForURL:url];
iOSSRequest *request = [[iOSSRequest alloc] initWithURL:url
parameters:nil
requestMethod:iOSSRequestMethodGET];
NSMutableDictionary *oauthParams = [NSMutableDictionary dictionary];
[oauthParams setObject:[[Twitter sharedService] apiKey] forKey:kASIOAuthConsumerKey];
[oauthParams setObject:[[Twitter sharedService] apiSecret] forKey:kASIOAuthConsumerSecret];
[oauthParams setObject:[self oAuthAccessToken] forKey:kASIOAuthTokenKey];
[oauthParams setObject:kASIOAuthSignatureMethodHMAC_SHA1 forKey:kASIOAuthSignatureMethodKey];
[oauthParams setObject:#"1.0" forKey:kASIOAuthVersionKey];
[oauthParams setObject:[self oAuthAccessTokenSecret] forKey:kASIOAuthTokenSecretKey];
request.oauth_params = oauthParams;
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (error) {
if (self.twitterAPIStatusHandler) {
self.twitterAPIStatusHandler(nil, error);
self.twitterAPIStatusHandler = nil;
}
} else {
NSMutableArray *recentStatusForTwitterUser = [[NSMutableArray alloc]init];
NSArray *array = [Twitter JSONFromData:responseData];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
TwitterStatus *twitterStatus = nil;
twitterStatus = [[TwitterStatus alloc]initWithDictionary:obj];
[recentStatusForTwitterUser addObject:twitterStatus];
}];
if (self.twitterAPIStatusHandler) {
self.twitterAPIStatusHandler(recentStatusForTwitterUser, nil);
self.twitterAPIStatusHandler = nil;
}
}
}];
}
I'd suggest using the asynchronous abstractions already provided where possible. It would be a fairly rare/unique situation where you need to deal with threads directly.
I've found treating each network-based background task as a synchronous NSOperation on a queue works really well.
Get a new instance of NSOperationQueue, configure it, add tasks to it, and manage the queue. The benefit of this approach is that each task can be implemented as a simple synchronous task, and the queue takes care of concurrency. Optionally you can set dependencies (this task must complete before that one).
What is the little fundamental nugget of multithreading that I'm
missing here?
That taking non-multithreaded code and spinning off a random number of threads by performing an arbitrary method in the background is doomed to failure.
Concurrency is a design pattern that must be carefully considered from the start (or is a monumental refactoring effort).
First, you don't want to spawn a thread per network connection. Secondly, given that these are just HTTP requests, you would want to use the systems built in classes for asynchronous HTTP communications. Finally, your concurrency model must exactly specify how you are keeping all data in isolation until you hit whatever mechanism you are using to synchronize the data back into the central store.
Hard to say where that code is going off the rails without seeing more information.

loadValuesAsynchronouslyForKeys and multiple values loading

I would like to asynchronously load the duration, time (timestamp the video was created) and locale of an Asset.
All of the sample code shown by Apple for the usage of 'loadValuesAsynchronouslyForKeys:keys' is always shows with only one key. ie:
NSURL *url = aUrl;
AVAsset asset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = [NSArray arrayWithObject:#"duration"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
NSError *error = nil;
AVKeyValueStatus durationStatus = [asset statusOfValueForKey:#"duration" error:&error];
switch (durationSatus) {
case AVKeyValueStatusLoaded:
// Read duration from asset
CMTime assetDurationInCMTime = [asset duration];
break;
case AVKeyValueStatusFailed:
// Report error
break;
case AVKeyValueStatusCancelled:
// Do whatever is appropriate for cancelation
}
}];
Can I assume that if one item's status is 'AVKeyValueStatusLoaded', the other values can be read at the same time in the completion block? ie:
[asset tracks]
[asset commonMetadata];
[asset duration]
No, you can't assume that. One of my methods looks at two keys, playable and duration, and I have found that playable is often available while duration isn't yet. I therefor have moved the loadValuesAsynchronouslyForKeys: code into a separate method shouldSave:. The shouldSave: method I call from a timer in a method called saveWithDuration:. Once saveWithDuration: receives a non-zero duration, it goes ahead and saves stuff. To avoid waiting too long, I use an attempt counter for now -- in the future, I'll refine this (you'll notice that the error instance isn't really used at the moment)
- (void)shouldSave:(NSTimer*)theTimer {
NSString * const TBDuration = #"duration";
NSString * const TBPlayable = #"playable";
__block float theDuration = TBZeroDuration;
__block NSError *error = nil;
NSArray *assetKeys = [NSArray arrayWithObjects:TBDuration, TBPlayable, nil];
[_audioAsset loadValuesAsynchronouslyForKeys:assetKeys completionHandler:^() {
AVKeyValueStatus playableStatus = [_audioAsset statusOfValueForKey:TBPlayable error:&error];
switch (playableStatus) {
case AVKeyValueStatusLoaded:
//go on
break;
default:
return;
}
AVKeyValueStatus durationStatus = [_audioAsset statusOfValueForKey:TBDuration error:&error];
switch (durationStatus) {
case AVKeyValueStatusLoaded:
theDuration = CMTimeGetSeconds(_audioAsset.duration);
break;
default:
return;
}
}];
NSUInteger attempt = [[[theTimer userInfo] objectForKey:TBAttemptKey] integerValue];
attempt++;
[self saveWithDuration:theDuration attempt:attempt error:&error];
}
Technically you can't. The docs for loadValuesAsynchronouslyForKeys:completionHandler: says that
The completion states of the keys you
specify in keys are not necessarily
the same—some may be loaded, and
others may have failed. You must check
the status of each key individually.
In practice, I think this is often a safe assumption -- as you've noted, Apple's StitchedStreamPlayer sample project just looks at the status of the first key.
No you cannot assume so. I usually rely on #"duration" key to create an AVPlayerItem and start playback since loading of #"playable" generally doesn't guarantee that the asset is ready yet. Then I spawn a timer to check periodically whether others keys such as #"tracks" are loaded or not similar to what Elise van Looij has mentioned above.
Also, side note - do remember that the completionHandler in loadValuesAsynchronouslyForKeys is called on an arbitrary background thread. You will have to dispatch it to main thread if you are dealing with UI or AVPlayerLayer.

Game Center inviting friends programmatically

I'm facing difficulty inviting a friend to the match.
GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
request.minPlayers = 2;
request.maxPlayers = 2;
request.playersToInvite = [NSArray arrayWithObjects: #"G:1102359306",nil ];
// GKMatchmakerViewController *mv = [[GKMatchmakerViewController alloc] initWithMatchRequest:request];
// [self presentModalViewController:mv animated:YES];
[[GKMatchmaker sharedMatchmaker] findMatchForRequest:request withCompletionHandler:^(GKMatch *match, NSError *error) {
if (error) {
NSLog([error description]);
}
else if (match != nil) {NSLog(#"good match");
//self.chatMatch = match;
//self.chatMatch.delegate = self;
//[self chatReady];
}
else {
NSLog(#"other error");
}
}];
The problem is I never receive the invitation notification on second device logged to the account - G:1102359306.
When I use GKMatchmakerViewController (uncomment above 2 lines) and comment GKMatchmaker block I have automatically checked the good friend - G:1102359306 and when I invites him the notification with accept/decline is shown, that's how I know it's correct.
Do you see anything wrong with the code above? I want to use my own UI to handle the multiplayer mode. The strange problem is I also don't see in console any logs good match/other error, and the [error description] is only printed when I call the above code twice - it says that the previous req was canceled.
You cannot programmatically invite a specific set of players to a match. The findMatchForRequest:withCompletionHandler: documentation says this:
The match request’s playersToInvite property is ignored; to invite a specific set of players to the match, you must display a matchmaker view controller.
There is no public API that does what you want.