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.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Event Handler Delegate

Our game is coming along well so far, but there's one major problem - we never get updated when the other players take their turn! It's quite annoying having to constantly check by bringing up the Game Center UI.

As you've been playing with the app, you may have noticed we sometimes get badges and/or system notifications of new turns/invitations to your game. This is being done by the GKTurnBasedEventHandler object. This object sends notifications to and badges our app when certain events happen, like when it’s our player’s turn.

There are three delegate callbacks that give our app a way to deal with these notices as they come in. One is the method that gets called if you starts an invite for that game from within the game center app, one when the turn advances (even if it’s not our turn, there’s a callback every time the match changes hands), and one is fired when the game ends.

In order to receive and handle these events, we first need to set ourselves as the delegate of the GKTurnBasedEventHandler. This object is a singleton and the only time we deal with it directly is when we set ourselves as it’s delegate.

Like the view controller delegate protocol, we’re going to be using our GCTurnBasedMatchHelper class to act as an intermediary for all the communication from these notices. So, that’s what we need to set up as the delegate. We have to set the delegate after we have logged in to game center or it may not work. Change the authenticateUser code to the following:

- (void)authenticateLocalUser { 
    
    if (!gameCenterAvailable) return;
    
    void (^setGKEventHandlerDelegate)(NSError *) = ^ (NSError *error)
    {
        GKTurnBasedEventHandler *ev = 
          [GKTurnBasedEventHandler sharedTurnBasedEventHandler];
        ev.delegate = self;
    };
    
    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [[GKLocalPlayer localPlayer] 
         authenticateWithCompletionHandler:
          setGKEventHandlerDelegate];        
    } else {
        NSLog(@"Already authenticated!");
        setGKEventHandlerDelegate(nil);
    }
}

You can see here that I’m setting up a block (we do this so we can pass it into the completionHandler of the authenticate method). The block just gets a pointer to the singleton and uses the pointer to set the delegate to self. Simple.

Then we pass that in as the completionHandler parameter in the case where we need to authenticate. If we don’t need to authenticate then we just call the block.

The block takes an NSError parameter if it’s run by the completionHandler. If we call it directly we can just enter nil for the error (because the error would have been coming from any problem with the authenticate method, we didn’t run it).

Alright, we’re set up to receive callbacks from the GKTurnBasedEventHandlerDelegate, well almost. We also need to set our object as a GKTurnBasedEventHandlerDelegate and implement the methods. Do that now:

In GCTurnBasedMatchHelper.h, change the @interface line to:

@interface GCTurnBasedMatchHelper : NSObject 
  <GKTurnBasedMatchmakerViewControllerDelegate, 
  GKTurnBasedEventHandlerDelegate> {

And add the following code to GCTurnBasedMatchHelper.m:

#pragma mark GKTurnBasedEventHandlerDelegate

-(void)handleInviteFromGameCenter:(NSArray *)playersToInvite {
    NSLog(@"new invite");
}

-(void)handleTurnEventForMatch:(GKTurnBasedMatch *)match {
    NSLog(@"Turn has happened");
}

-(void)handleMatchEnded:(GKTurnBasedMatch *)match {
    NSLog(@"Game has ended");
}

Build and run now. You should be able to test the handleTurn event by letting the other player take a turn, and you should see the log message. w00t!

The handleMatchEnded will require us to write a method that ends the game. The handleInviteFromGameCenter is only fired when you start a game from inside the game center app.

If you send an invite from game center, the callback needs to instantiate a new GKMatchRequest and either programmatically or with the view controller (GKTurnBasedMatchmakerViewController) set up the new match. Invites sent from within the app won’t need to call this method.

Here’s an output to the console for the handleTurn event:

Console output demonstrating handleTurn is called

Handling Invitations

Let’s write handleInviteFromGameCenter next. You may assume erroneously at first, as I did, that this method fires whenever we receive a named invite to a game. This is not what the method is for at all!

There is no method for that, an invite sent from within a game just shows up in your list of available matches. This method handles the incoming data from game center when you create an invite for one of your friends for the game. So, when you switch from game center to the game, there’s information about who you want to invite to a new game included in the callback (playersToInvite). This is called on the inviting player’s game, not the invitee. I include this personal mistake here because I’m not the only one who was confused.

If we get a new invite from game center, we need to instantiate the GKTurnBasedMatchmakerViewController with a GKMatchRequest. This method will give us an array of players that are supposed to be in the match. We’ll use this object to set up the GKMatchRequest. Here’s what that code should look like:

-(void)handleInviteFromGameCenter:(NSArray *)playersToInvite {
    [presentingViewController 
      dismissModalViewControllerAnimated:YES];
    GKMatchRequest *request = 
      [[[GKMatchRequest alloc] init] autorelease]; 
    request.playersToInvite = playersToInvite;
    request.maxPlayers = 12;
    request.minPlayers = 2;
    GKTurnBasedMatchmakerViewController *viewController =
      [[GKTurnBasedMatchmakerViewController alloc] 
        initWithMatchRequest:request];
    viewController.showExistingMatches = NO;
    viewController.turnBasedMatchmakerDelegate = self;
    [presentingViewController 
      presentModalViewController:viewController animated:YES];
}

The first thing we do is get rid of any current modal view controller that is present. After that we set up the GKMatchRequest and then the GKTurnBasedMatchmakerViewController. Note that the showExistingMatches is set to NO. We only want to see the new game view for this match. We set up the delegate and then present our view controller.

If you build and run now you should be able to start a match from the Game Center (it won’t call this method if you do an invite from inside our game) and send the invite. One thing to note, the simulator doesn’t receive any notifications from the GKTurnBasedEventHandlerDelegate, so you’ll have to test this on a device.

Inviting friends in Game Center

Handling the Turn Event

When the handleTurn is called, either the match has moved from one player to another, and it’s still not our turn, or the turn has moved to our player. In addition, the match that’s currently loaded into the game state, may or may not be the match that has received the handleTurn call. We need to distinguish between these scenarios and handle each in turn.

Here’s the code:

-(void)handleTurnEventForMatch:(GKTurnBasedMatch *)match {
    NSLog(@"Turn has happened");
    if ([match.matchID isEqualToString:currentMatch.matchID]) {
        if ([match.currentParticipant.playerID 
          isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
            // it's the current match and it's our turn now
            self.currentMatch = match;
            [delegate takeTurn:match];
        } else {
            // it's the current match, but it's someone else's turn
            self.currentMatch = match;
            [delegate layoutMatch:match];
        }
    } else {
        if ([match.currentParticipant.playerID 
          isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
            // it's not the current match and it's our turn now
            [delegate sendNotice:@"It's your turn for another match" 
              forMatch:match];
        } else {
            // it's the not current match, and it's someone else's 
            // turn
        }
    }
}

We end up with four scenarios. We’ll send delegate methods for three of them, the fourth we’ll ignore, but we’ve set it up here in case you wish to handle it in another game.

The first two are that the current match is the same as the match we’re in. In that case we’ll send takeTurn, if it’s our turn, and layoutMatch if it’s not. We’re still setting the currentMatch to our passed in match, because even though the matchID is the same, the incoming match has updated state data (match.matchData) and the currentParticipant has changed.

If we are sent a notice for a match that we’re not looking at, and it has become our turn, we’ll send the sendNotice delegate method. We’ll use this method to display an alert letting the user know.

If the turns change on matches that we’re not looking at, and it’s not our turn, we’ll do nothing. The users can go looking at those matches by loading them using the GKTurnBasedMatchmakerViewController, we don’t need to interrupt them every time things change on every match. In other kinds of games, we may want to do just that, so that section is available to you.

With the handleTurn method in place, you can now run the game. Each time a new turn is sent, you should see the UI on your game update and the statusLabel should let you know who’s turn it is!

Status label showing the current player's turn

Depending on what the state of your game is, you might get an error saying the sendNotice callback isn't implemented yet. That's our next step anyway, so let’s go ahead and implement the sendNotice method in ViewController.m.

-(void)sendNotice:(NSString *)notice forMatch:
  (GKTurnBasedMatch *)match {
    UIAlertView *av = [[UIAlertView alloc] initWithTitle:
      @"Another game needs your attention!" message:notice 
      delegate:self cancelButtonTitle:@"Sweet!" 
      otherButtonTitles:nil];
    [av show];
    [av release];
}

This is pretty straightforward. We’re just letting them know that another match needs their attention. We’ll let them use the GKTurnBasedMatchmakerViewController to load the match when they are ready. You should be able to get this alert if you are playing multiple games:

Showing an alert view when another game is ready

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.