Beginning Turn-Based Gaming with iOS 5 Part 2
Note from Ray: This is the seventh iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Enjoy! This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer who runs the Indie Ambitions blog. Check out […] By Jake Gundersen.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Beginning Turn-Based Gaming with iOS 5 Part 2
50 mins
Taking Turns
There are several ways that we can take a turn in our app. When we use automatch for a slot in our game, one of two things will happen. Either
- We’ll get back a new match, with our player as the first participant, or
- We’ll get back an existing match where our player has been placed in a second, third, fourth, etc slot in an existing match.
In both scenarios, didFindMatch will be called. We’ll need to write some code to distinguish between the two cases, because we want different things to happen in each case:
- If we get back a new match, we want to create a clean slate for our story. In our app, we’ll use the string “Once upon a time . . . “ every time we begin a new story.
- If we get back an existing match, then the match.matchData object will be populated with NSData with the story so far. Instead of the starter text, we want to show this in our text view.
We just need to use a reliable test to distinguish between these two cases and deal with them differently.
Each participant has a lastTurnDate property that is null until that participant takes their first turn. We’re going to use the last turn property of the first participant in the participants array to determine if we are the first turn, or if another player has started the match. If we find that lastTurn is null, we’ll assume that we’re dealing with a new match, otherwise we’ll assume that we already have matchData that we’ll be dealing with.
So open up GCTurnBasedMatchHelper.m and replace the didFindMatch method as follows:
-(void)turnBasedMatchmakerViewController:
(GKTurnBasedMatchmakerViewController *)viewController
didFindMatch:(GKTurnBasedMatch *)match {
[presentingViewController
dismissModalViewControllerAnimated:YES];
self.currentMatch = match;
GKTurnBasedParticipant *firstParticipant =
[match.participants objectAtIndex:0];
if (firstParticipant.lastTurnDate) {
NSLog(@"existing Match");
} else {
NSLog(@"new Match");
}
}
This code should be easy to comprehend. We’re getting the participant at the first index in our participants array, then testing to see if the lastTurnDate is populated. If it is, we know we’re entering a match that has already started. If not, we know it’s a new match and we are logging the case either way.
At this point, you’ll need a second device or to switch between sandboxed game center accounts in order to get the existing match scenario. A single account/player can only take the first turn, after that you need a second player involved to test many of these scenarios.
So run the app on both devices (run one of them in the debugger so you can see its log messages). Join a few matches and take your turn on each device, watching what shows up in your log. You should see some new matches, and hopefully some existing matches too, which proves the match-making worked!
Note that as of this writing this tutorial, the time between creating a new automatch and being able to jump in to the second player position can be up to five minutes. This will probably get much faster by the time you are reading it, but don’t be alarmed if you start an automatch on one device and immediately on a second device and instead of putting the second player into the first player’s game, it creates another new match for the second player.
One more word about automatch while we’re on the subject. Automatch doesn’t start matching until the first player is finished taking his/her turn. This is true for an invited slot as well. If you start a game with three players, the third player won’t be invited or automatched until after the first and second player have both taken their turn.
Implementing Our Own Delegate Protocol
Now is a good time to introduce our new delegate protocol. We are going to build a protocol to manage the communication between the GCTurnBasedMatchHelper class and our ViewController. This is better than hard-coding the ViewController into the GCTurnBasedHelper, because it will allow us to more easily reuse this class.
The GCTurnBasedMatchHelper will do some of the work, like distinguishing between a new and existing match, but will then then pass the match through to our ViewController class to handle what do for each case, because that’s particular to the game logic.
Lets go ahead and build the delegate protocol now. Modify GCTurnBasedMatchHelper.h to look like the following (notice the new protocol, and delegate instance variable and property):
#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>
@protocol GCTurnBasedMatchHelperDelegate
- (void)enterNewGame:(GKTurnBasedMatch *)match;
- (void)layoutMatch:(GKTurnBasedMatch *)match;
- (void)takeTurn:(GKTurnBasedMatch *)match;
- (void)recieveEndGame:(GKTurnBasedMatch *)match;
- (void)sendNotice:(NSString *)notice
forMatch:(GKTurnBasedMatch *)match;
@end
@interface GCTurnBasedMatchHelper : NSObject
<GKTurnBasedMatchmakerViewControllerDelegate> {
BOOL gameCenterAvailable;
BOOL userAuthenticated;
UIViewController *presentingViewController;
GKTurnBasedMatch *currentMatch;
id <GCTurnBasedMatchHelperDelegate> delegate;
}
@property (nonatomic, retain)
id <GCTurnBasedMatchHelperDelegate> delegate;
@property (assign, readonly) BOOL gameCenterAvailable;
@property (nonatomic, retain) GKTurnBasedMatch *currentMatch;
+ (GCTurnBasedMatchHelper *)sharedInstance;
- (void)authenticateLocalUser;
- (void)authenticationChanged;
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers viewController:(UIViewController *)viewController;
@end
Also before we forget, switch to GCTurnBasedMatchHelper.m and synthesise the delegate as follows:
@synthesize delegate;
We’ve added the five protocol method declarations and a new instance variable, delegate. The delegate object will be sent the the methods, and it will be up to that delegate (in our case the ViewController class) to implement them.
Let’s quickly go through them now. Then later when we implement them we’ll explain them in more detail.
- enterNewGame. The first method we’ve already discussed the use for. When we are presented with a new game from the didFindMatch method, we want to display the “Once upon a time” starter text to the screen.
- layoutMatch. The layoutMatch method is used when we want to view a match where it’s another player’s turn (just to check the state of the story for example). We want to prevent the player from sending a turn in this case, but we still want to update the UI to reflect the most current state of the match.
- takeTurn. The takeTurn method is for those cases when it is our player’s turn, but it’s an existing match. This scenario exists when our player chooses an existing match from the GKTurnBasedMatchmakerViewController, or when a new turn notification comes in. We’ll talk about notifications a little later in this tutorial.
- recieveEndGame. The receiveEndGame method will be called when a match has ended on our player’s turn, or when we receive a notification that has a match has ended on another player’s turn. For this simple game, we’ll just end the game when we are getting close to the current NSData turn-based game size limit (4096 bytes).
- sendNotice. The sendNotice method happens when we receive an event (update turn, end game) on a match that isn’t one we’re currently looking at. If we receive an end game notice on a match that we’ve got loaded into our currentMatch variable, we’ll update the UI to reflect the current state of that match, but if we receive the same notice on a match other than the one we’re looking at, we don’t want to automatically throw the user into that match, taking them away from the match they are currently looking at. We’ll decide later how to handle those notices.
Let’s go ahead and send the delegate methods in our didFindMatch method for our new match and existing match scenarios. Replace the NSLog methods with calls to the delegate methods, like so:
-(void)turnBasedMatchmakerViewController:
(GKTurnBasedMatchmakerViewController *)viewController
didFindMatch:(GKTurnBasedMatch *)match {
[presentingViewController
dismissModalViewControllerAnimated:YES];
self.currentMatch = match;
GKTurnBasedParticipant *firstParticipant =
[match.participants objectAtIndex:0];
if (firstParticipant.lastTurnDate) {
[delegate takeTurn:match];
} else {
[delegate enterNewGame:match];
}
}
Next, open up ViewController.h and mark it as implementing our new protocol:
@interface ViewController : UIViewController <UITextFieldDelegate,
GCTurnBasedMatchHelperDelegate> {
Then switch to ViewController.m and implement the enterNewGame and takeTurn methods:
#pragma mark - GCTurnBasedMatchHelperDelegate
-(void)enterNewGame:(GKTurnBasedMatch *)match {
NSLog(@"Entering new game...");
mainTextController.text = @"Once upon a time";
}
-(void)takeTurn:(GKTurnBasedMatch *)match {
NSLog(@"Taking turn for existing game...");
if ([match.matchData bytes]) {
NSString *storySoFar =
[NSString stringWithUTF8String:[match.matchData bytes]];
mainTextController.text = storySoFar;
}
}
Pretty simple, eh?
One last thing, we need to set the delegate property of the GCTurnBasedMatchHelper class to our ViewController. Let’s do that in the viewDidLoad method, like so:
[GCTurnBasedMatchHelper sharedInstance].delegate = self;
Build and run on both devices, and you should be able to pass turns around!
Start a new game, either by invitation or automatch, take the first turn, give it a few minutes to make sure the server is updated, then enter that game from the other device. If you have created participants by invitation you should have a game waiting in the your turn section, if done by automatch create another automatch game on the other device with the same number of players. Again, the automatch won’t always put you into the match you want, it does create new matches frequently.
Note that after the other side takes a turn, your screen won’t update because we haven’t added the code in for that. You can get the game to recognize it’s your turn by tapping the game center button, and selecting the game.
Too many matches? At this point I have about five matches that have wrong data and other problems with them. Some of your old games may act wrong because the data in them is incomplete or missing. I’d remove all of them and start fresh. A good way to clean out all this data is to call a method that programmatically deletes all the matches for a user.
Put this code into the completion block for authenticateWithCompletionHandler like so:
if ([GKLocalPlayer localPlayer].authenticated == NO) {
[[GKLocalPlayer localPlayer]
authenticateWithCompletionHandler:^(NSError * error) {
[GKTurnBasedMatch loadMatchesWithCompletionHandler:
^(NSArray *matches, NSError *error){
for (GKTurnBasedMatch *match in matches) {
NSLog(@"%@", match.matchID);
[match removeWithCompletionHandler:^(NSError *error){
NSLog(@"%@", error);}];
}}];
}];
} else {
NSLog(@"Already authenticated!");
}
This code will only clear out the matches for one Game Center account, so you’ll have to run it on both devices. Also, remember when you have this code in it will clear all your matches on startup, so make sure to comment it out when you don’t need it.