Robust Game Center Achievement code - ios

In just about every example of submitting achievements to Game Center, I see this code
[achievement reportAchievementWithCompletionHandler:^(NSError *error)
{
if (error != nil)
{
// Retain the achievement object and try again later (not shown).
}
}];
Problem is, that one little comment is about 99% of the work. I've spent the last few hours trying to figure this out and it seems to be an endless set of edge cases of sending and resending and save and loading data.
Does anyone know of a nice tutorial (or sample code) on this that actually explains the hard part?
It's not as simple as just saving them to a file and loading them later. You get into trouble when you start having to retain multiple achievements and submit them later and then they all come back failed (in blocks!) and you have to save them again... quickly/safely... because the App might quit and you don't want to loose them.
I'm pulling my hair out.

I don't think you need to pull your hair out.
I think the basic model is this:
(a) Independent of Game Center, your game has a saved state (which you always need anyway, to restore the player to where he was when your game is quit/backgrounded). This state should include all the normal stuff you need to restore your game state, plus it should include flags for all of the achievements in your game.
(b) Game Center also stores all of the achievements in your game. When you connect to Game Center, you download the signed-in player's achievements. (Complications arise as to who the signed-in player is, but your question is not about that, so let's assume the "one true player" is always the signed-in Game Center player.)
(c) Whenever the player makes an achievement, first update the appropriate achievement flag in your game's saved state data. Second, try to tell Game Center about that achievement. If it works, update your copy of Game Centers achievements with the new data.
(d) If it doesn't work, you have an achievement marked in your persistent state that is not marked in your copy of the Game Center state. At various convenient times (e.g., whenever another achievement is earned, when you end a level, when your app starts, when you app quits, etc.) check if there are any discrepancies between what is in your own state vs. what is in the Game Center state. A discrepancy is an update that you need to re-send to Game Center. Try to send, but if it fails again, just wait until the next opportunity (e.g., whenever another achievement is earned, when you end a level, when your app starts, when you app quits, etc.) to try again. You will never lose the data because it's in your local, true picture of the user's state. (The only way you'd lose it is if the user uninstalls your app before you've been able to successfully get it to Game Center, but then, what can you do?)

Related

How to get notified when a user leaves home in the SetSDK?

I'm using the SetSDK to show users the available bike share count at their nearby station whenever they leave their home. I'm following the example available in the pod documentation but am not getting the notification. Here is my code,
SetSDK.instance.onDeparture(.home) { notification in
// below is my code using the notification
let bikeStationCount = getAvailableBikes(stationsNear: notification.location)
showNotification(withCount: bikeStationCount.count, atStation: bikeStationCount.station)
}
But I'm not getting my app to show any notification, any idea what is wrong?
It looks like your code is correct. The only thing to keep in mind is that the SetSDK will learn the user's home over time, with most users it usually takes 1-2 days for it to have enough certainty to call a place Home.
Have you had a chance to let the SDK learn long enough that it is giving you notifications now?
Hey, cool use-case!

Is localPlayer still authenticated, even if authentication fails

Is a valid, even in case of an authentication error?
GKLocalPlayer.localPlayer.authenticateHandler =
^(UIViewController *viewController, NSError *error)
{
if (error)
{
bool a = GKLocalPlayer.localPlayer.authenticated;
}
else
{
This happens for instance when I have an authenticated player, moves the app to the background, disables the WiFi, and then move the app to foreground again. My hope is that GameCenter just continues with a cached account?
I find the manual a bit ambiguous.
From https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/GameKit_Guide/Users/Users.html#//apple_ref/doc/uid/TP40008304-CH8-SW11:
"As soon as your game moves to the background, the value of the local player object’s authenticated property becomes and remains invalid until your game moves back to the foreground. You cannot read the value to determine if the player is still authenticated until Game Kit reauthenticates the player and calls your authentication handler. Your game must act as though there is not an authenticated player until your completion handler is called. Once your handler is called, value stored in the authenticated property is valid again."
Is the value valid even though the authentication failed?
I have a long-running Bug with Apple on this. It's been closed and re-opened during the ongoing dialog. The question of whether or not .authenticated is valid seems to depend on your perspective
Apple views this as working-as-intended as you have cached information which Apple believes lets you go ahead and play your game, you can display leaderboards, etc. Apple says that .authenticated is indeed valid in this state. I've seen some developers on this forum agree with that perspective, although I don't have links handy to their posts.
In practice, though, if you attempt to do any subsequent game center operation while in this state, it will fail because you're not really authenticated. You can't save games, load matches, etc. Any leaderboard you display will be stale, cached data.
It appears to me that Apple loathes for players to ever see a problem resulting from their infrastructure. Thus, with this mechanism you attempt to drive forward by faking the state, hoping the problem works itself out later. In my games, that strategy never pans out and eventually reaches an unrecoverable situation after users have invested time/effort into the game. So, like your code above, I rely on what the NSError says. If is says "error" then I treat the player as unathenticated, providing UI prompts to correct the situation.
I've documented more details on my approach here: https://stackoverflow.com/a/37216566/1641444

GameKit Error says player not authenticated but .authenticated property = true

I have an turn-based game using GameKit in iOS. Generally, my authentication with Game Center works. My game works and I can for periods of time send moves back and forth. However, relatively frequently but not constantly, when I try to perform an action (e.g., finding a new match) over Game Center it fails with the error:
Error Domain=GKErrorDomain Code=6 "The requested operation could not be completed because local player has not been authenticated." UserInfo={NSLocalizedDescription=The requested operation could not be completed because local player has not been authenticated.}
Yet, the GKLocalPlayer.localPlayer().authenticated = true, both immediately before getting the error and after. After getting this error, the authentication viewController does not get presented and GameKit methods that rely on authentication cease to function (they don't come back to life ). Then, if I send the app to the background and then bring it back to the foreground, the error does not return when I retry the action (without requiring new login and password entry).
According to this similar observation:
GKLocalPlayer authentication not working, but isAuthenticated returns YES (Game Center sandbox)
"1.Game Center fails to complete authentication if your device has incorrect dates. So, go ahead and check the current date.
You might have done this. I trust you - iOS Simulator >> Reset Content and Settings"
I am getting this problem on the device, not the simulator, ruling out #2. Could someone help me with #1? I may be naive here, but my iPhone and iPad have the right time and date. Is there something or somewhere else I should be checking and setting?
Then, if this is not the issue, what else could be the problem? I am running iOS9.
I've seen this too, and have an open bug with Apple. In my case, I'm testing with multiple physical devices, and only one of those devices ever encounters this. On that particular device, I'm logged into iTunes using a personal account, but logged into game center using a test account.
From what I can see, when the device is using the same login for iTunes, iCloud and game Center, there are no problems. But, when I try to mix-and-match the accounts for whatever testing I need to do, then I randomly get error 6.
There doesn't seem to be anyway to recover from this, except to have the user re-login into game center. Of course, there's no way to present the login view controller again, so the only option is for the user to leave the game, kill it off, and star up again (which re-starts the game center authentication process). I trap the error now, give the user a message saying Game Center has logged them out, and explain what they need to do about it.
But as I said, when I stopped mixing up credentials on the device, the problem went away.
BTW, on a slightly related topic, I've come to view the .authenticated property as completely unreliable. It will be TRUE in conditions where you do not actually have connection to Game Center. I opened a bug on this, too. Apple closed it saying this was working as intended by using "cached" data. Thus, it will report authenticated when it's not authenticated and give you access only to outdated match and leaderboard data which had previously been saved locally.

Is there any way to call saveCurrentTurnWithMatchData without sending a push notification?

I have a Game Center game that allows players to make multiple moves per turn. In iOS 6, Apple implemented a great feature in saveCurrentTurnWithMatchData that allows you to do just that- it saves the data to game center to prevent cheating by repeatedly redoing a move for instance, without advancing to the next player.
The problem is, I have discovered that this actually triggers the same Push Notification taht gets sent when the player does end their turn. So other players in the game will see a badge on the app's icon and mistakenly think it's their turn when it isn't.
Has anyone found a workaround for this? Any way to call saveCurrentTurnWithMatchData without sending a push notification? If not, this seems like a design flaw that should probably be brought to Apple's attention.
I agree, this seems like a design flaw. I am also developing a turn-based game whereby a player can take several actions before passing control over to the next player. Meanwhile, I want other players to witness every action while they are looking at the game. If the other players are not running the app, I want them to receive a push notification only when the control is passed to another player.
Instead of using saveCurrentTurnWithMatchData:, I use endTurnWithNextParticipants: but I specify the current player rather than the next. This seems to do the trick:
NSTimeInterval interval = 86400; // seconds in a day
[currentMatch
endTurnWithNextParticipants:[[NSArray alloc] initWithObjects:currentMatch.currentParticipant,nil]
turnTimeout:interval matchData:[self packMatchData]
completionHandler:^(NSError *error) {
if (error) {
// handle error
}
}
];

Turn based game center displays the state of game incorrect when offline

I am using iOS 6 Game Center API for turn based games.
When the device is disconnected from internet
In the completion handler of the method
[currentMatch endTurnWithNextParticipant:nextParticipant matchData:data completionHandler:^(NSError *error) {
if (error) {
NSLog(#"%#", error);
} else {
//save the new state of the game
}
I get an error. But then, game center standard UI that displays matches list, says "Their turn". when connected again it changes to "Your turn".
The code from famous tutorial at http://www.raywenderlich.com/5509/beginning-turn-based-gaming-with-ios-5-part-2 has the same exact problem.
How I should handle this problem?
If you are using iOS 6 Game Center API then you will have to use
-endTurnWithNextParticipants:turnTimeout:matchData:completionHandler:
because...
–endTurnWithNextParticipant:matchData:completionHandler: Deprecated in iOS 6.0
http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKTurnBasedMatch_Ref/Reference/Reference.html
The thing is, that when you use GC methods that change status of the match (matchData and synchronization info in this case), data is uploaded to the GC server so that other player(s) get the update. If you're disconnected and ignore the error, your local GKTurnBasedMatch and its matchData change, as well as your synchronization info (which is used to determine if it is your turn to act among other things).
However, since you are diconnected, only your local instance of GCTurnBasedMatch is updated (you get error so that you app is aware of that). When you're reconnected, your app authenticates the user and updates match state (if you're following the tutorial code). Updating match data reverts the sync data (so it's still your turn).
At this point, you should either submit the turn again (provided that you cached gameData that was passed to GC while you were disconnected) and/or call updateMatchData so that your local GKTurnBasedMatch and its matchData get in sync with what's on the server. You should also re-layout your game board with previous turn's data if you didn't re-submit turn after reconnection.

Resources