Sunday, February 26, 2012

Game On, TurnBased Game Center

I was excited to see Apple add the TurnBased system to their GameCenter last year. Since it's beta release I have been testing with it on and off for some time now to get the best experience from it that I believe I can offer. The difference for me is I approached this TurnBased GameCenter as a partial replacement for the PHP async system I was mildly developing before Apple's announcement.

Where I started in PHP;
I started with a SQLLite model that was going to mirror the online storage of the games. This maybe an over ambitious approach to a game largely played while online and connected, but to develop good UX I felt that the game should not just be a brick when no connectivity is around. (Even NewToy's games had offline modes before the take over)
GameCenter for 4.1 was in full swing and I made the decision to leap frog off the GameCenter generated Apple player_ids for organizing user accounts, while avoiding making people create some new account in my system. What this meant was if a user signed into the game with a GameCenter account, I would store there player_id, alias, and push-token. I even devised a pattern for flagging the account for open invites, direct invites, or dnd.
The challenge here was when the app had time to refresh was to synchronize the records of turns that were behind. This could be one turn record back, two if a 3 player game, maybe the phone was changed or deleted, then all records would need to be resync'd. In this design I was storing each turn as a record of 5 or more fields and doing these local vs server index checks to avoid transferring too much repeat json data.
So what did this mirror solve? The ability to look at the game board, plan your next move, even make your next move and cache the result for later upload, if and when no connectivity was available. Additional side effects, less lock down loading screens. If any persistent data about the handful of games a player is playing is saved and synced locally then I could stay away from locking down the interface with a loading overlay while online request get fulfilled.

Use Apple's servers;
Apple announces Api changes to handle games non-realtime, and right out of the gate they solve one trick that I was trying to work around. Push Notifying new players about a game they've yet to ever download. Even though GameCenter could always Push-Notify a friend of a game invite. But that process must have  gone a little like: Mike invites John to play a game, John is busy watching a Movie, Mike is left hang'in. Or John hasn't heard of this game, he visits the AppStore and reads about the game, Mike is tired of sitting at the invite screen waiting to connect, Mike closes the game and the invite is lost.
So now with the TurnBasesMatch objects the user involved are tracked, the whole game state needs to be compressed and fit into 4kb or less, and push notifications are handled by Game Center and I don't even need to request a token. So whats left for me to do?
If you are leaving all the data on Apple's end and only updating from request then there are a few online calls that seem to get chained up rather quickly.
- authenticateWithCompletionHandler: This one gets you in the door but is the first request you have to wait for before you can get updated info.
- loadMatchesWithCompletionHandler: This gets you some rudimentary data, nothing you can show many details about. Telling how many matches the player is involved in, who's turn it is (sorta) and when the last turn was taken (again sorta) I say this because the individual GKTurnBasedParticipant objects in the GKTurnBasedMatch object have the timestamps, if you'd like to show when the matches last turn was taken I figure a sort is the easiest way: 

NSSortDescriptor* sort = [NSSortDescriptor sortDescriptorWithKey:@"lastTurnDate" ascending:YES];
GKTurnBasedParticipant* pat = [[self.gkMatch.participants sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]] lastObject];
self.lastTurnDate = pat.lastTurnDate;

That's not enough, we also need to show the user "who's" turn it is, the GKTurnBaseMatch only gave use the player_ids. So first thing is to get the ids into an array.

NSMutableArray* playerids = [NSMutableArray arrayWithCapacity:[m_gkMatch.participants count]];

[m_gkMatch.participants enumerateObjectsUsingBlock:^(GKTurnBasedParticipant* obj, NSUInteger idx, BOOL *stop) {
        [playerids addObject:obj.playerID];
}];

- loadPlayersForIdentifiers: Once you have this array of GKPlayers back with alias for the player_ids you sent in you can start to match up the ids from the GKTurnBasedParticipants and display a name for the user who's turn it is.
Then finally before you can rebuild you game board from the 4kb of data, you have to request it.
- loadMatchDataWithCompletionHandler:
Having all these separated calls can seem like a bit A->B->C->D but to tell you the truth it's not that different from the calls I was designing in PHP. Data that can plug together but can really be handled in any order or concurrently if you store it right.




Synchronizing GKObjects:
Well the idea isn't much different, When an opportunity to get data now from the local storage beats the need to retrieve data, I have something to show the user. If after a request is fulfilled and data has changed then I have a new opportunity to update things behind the scene while the user is not looking at something, or slide in the additional data.


For example: The Users phone knows of a game thats in progress, he is waiting for a friend to take a turn. Because this games data is being sync'd the players stored locally are Mike and John, As soon as the app is shown that info about John is already known and I don't have to retrieve the alias again before displaying it. Another invite comes in for a 3 player game including Mike, John, and now ID:32789462 well I can display this new cell in a table and use a temp alias of 'New Player' while the request goes out to get the players alias. In the mean time Mike opens the game board and looks at the last few turns. With the correlation of the loadPlayersForIdentifiers feeding directly back to the CoreData storage of the Player ManagedObject then UITableViews, GameBoradViewControllers can be notified of the changes to the Alias value independent of the Match Object.


In addition here are some other methods that I've built to help puzzle together some of the GKTurnBased separations.


Is it this local user's turn?
+ (bool)IsLocalTurn:(GKTurnBasedMatch*)aMatch {

    if ([[GKLocalPlayer localPlayer].playerID isEqualToString:aMatch.currentParticipant.playerID]) return true;
    else return false;
}


How many of these players are still in the game?
+ (NSArray*)ActivePlayers:(GKTurnBasedMatch*)aMatch {
    if (!aMatch) return NULL;
    NSMutableArray* actives = [NSMutableArray arrayWithCapacity:[aMatch.participants count]];
    [aMatch.participants enumerateObjectsUsingBlock:^(GKTurnBasedParticipant* player, NSUInteger idx, BOOL *stop) {
        if (player.status != GKTurnBasedParticipantStatusDeclined && player.status != GKTurnBasedParticipantStatusDone) {
            [actives addObject:player];
        }
    }];
    return [actives copy];
}


The next player should be a active user, or the game will get stuck, or error.
+ (GKTurnBasedParticipant*)NextPlayerAfter:(GKTurnBasedParticipant*)player InMatch:(GKTurnBasedMatch*)aMatch {
    if (!aMatch) return NULL;
    NSArray* actives = [SOTurnManager ActivePlayers:aMatch];
    int acount = [actives count];
    if (acount > 1) {
        int index = [actives indexOfObject:player] + 1;
        return [actives objectAtIndex:index%acount];
    }
    return NULL;
}



No comments:

Post a Comment