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 3 of 5 of this article. Click here to view the first page.

What To Do When It’s Not Our Turn

Currently we have the ability to input text and run the game while it’s not our turn. While the API prevents us from updating the game state outside our turn, our app will throw errors and it would be better to indicate to the player when they are unable to enter text.

When it’s not the current player’s turn, we want to update a status label telling the player that the match is currently in another player’s turn. Also, we should disable the text field.

Open up ViewController.xib, drag a label next to to the Game Center icon, and make it as wide as the screen. Bring up the Assistant Editor, make sure ViewController.h is visible, control-drag from the label down inside the @interface and, connect it to an outlet named statusLabel.

Adding a status label into the view controller

We’ll need to make some changes in both the GCTurnBasedMatchHelper class and the ViewController class in order keep track of whose turn it is. Let’s start by editing our didFindMatch method in GCTurnBasedMatchHelper.m:

-(void)turnBasedMatchmakerViewController: 
  (GKTurnBasedMatchmakerViewController *)viewController 
  didFindMatch:(GKTurnBasedMatch *)match {
    [presentingViewController 
      dismissModalViewControllerAnimated:YES];
    self.currentMatch = match;
    GKTurnBasedParticipant *firstParticipant = 
      [match.participants objectAtIndex:0];
    if (firstParticipant.lastTurnDate == NULL) {
        // It's a new game!
        [delegate enterNewGame:match];
    } else {
        if ([match.currentParticipant.playerID 
          isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
            // It's your turn!
            [delegate takeTurn:match];
        } else {
            // It's not your turn, just display the game state.
            [delegate layoutMatch:match];
        }        
    }
}

Note we swapped the if/else statement around a bit here.

We’re checking the current player’s (from the match) playerID against the currently player that’s logged into game center. If they match, it’s our player’s turn. In that case we send the same method. However, if they don’t match then it’s not the current player’s turn (this can happen when it’s someone else’s turn, or when it’s no one’s turn, because the match has ended).

If it’s not our player’s turn, then we’re going to send a different method, the layoutMatch method. This just updates the UI to reflect the current state of the match. In our app, we’ll be doing this a lot, we’ll want the player to be able to watch as the story progresses.

Here’s the implementation for the layoutMatch method. This code should go in ViewController.m:

-(void)layoutMatch:(GKTurnBasedMatch *)match {
    NSLog(@"Viewing match where it's not our turn...");
    NSString *statusString;
    
    if (match.status == GKTurnBasedMatchStatusEnded) {
        statusString = @"Match Ended";
    } else {
        int playerNum = [match.participants 
          indexOfObject:match.currentParticipant] + 1;
        statusString = [NSString stringWithFormat:
          @"Player %d's Turn", playerNum];
    }
    statusLabel.text = statusString;
    textInputField.enabled = NO;
    NSString *storySoFar = [NSString stringWithUTF8String:
      [match.matchData bytes]];
    mainTextController.text = storySoFar;
}

The first thing we’re doing in this method is constructing the string we’ll put in the statusLabel. We need to distinguish between open and closed matches first. We check the GKTurnBasedMatchStatus match.status – if it’s equal to ended, then we have and ended game and we set the statusString to tell the user that.

If we currently waiting on another player, we want to get the position of the player in the array. We’ll add one so we don’t have a Player 0. We construct the string for the player’s turn and set the label to that string.

Next, we are disabling the textInputField so that our player won’t have the ability to enter text. Finally, as we have before, we update the mainTextController to hold the body of the story.

Let’s go back and make a few edits to our other two methods to incorporate the new label and the logic that will turn on the text field. Add this code:

-(void)enterNewGame:(GKTurnBasedMatch *)match {
    NSLog(@"Entering new game...");
    statusLabel.text = @"Player 1's Turn (that's you)";
    textInputField.enabled = YES;
    mainTextController.text = @"Once upon a time";
}

-(void)takeTurn:(GKTurnBasedMatch *)match {
    NSLog(@"Taking turn for existing game...");
    int playerNum = [match.participants 
      indexOfObject:match.currentParticipant] + 1;
    NSString *statusString = [NSString stringWithFormat:
      @"Player %d's Turn (that's you)", playerNum];
    statusLabel.text = statusString;
    textInputField.enabled = YES;
    if ([match.matchData bytes]) {
        NSString *storySoFar = [NSString stringWithUTF8String:
          [match.matchData bytes]];
        mainTextController.text = storySoFar;
    }
}

This code is very similar, except we’ve added some code to enable the text input field and display the status.

A few other things we need to do, Change the properties on the statusLabel so that it’s in word wrap mode and has 2 lines (and make it a bit taller).

Also in the viewDidLoad method, let’s set the statusLabel to something like, “Press the game center button to get started” and disable the textInputField:

textInputField.enabled = NO;
statusLabel.text = @"Welcome.  Press Game Center to get started";

If you build and run now, when you start a new game or enter an existing game to enter text, and have a status label that gives you a better indication of where you are in the match. It doesn’t correctly update the state when you take a turn yet though.

Also note you still have to go back into Game Center and re-select a game to get updates, but we’re one step closer to a functional game!

Taking a turn in our game

Finishing Off the Matchmaker View Controller Delegate Methods

We’re finished with the didFindMatch method. But, we still have to do some minor finishing of the other delegate methods.

The didCancel method only needs to dismiss the view controller, so it’s fine as is. The error method is also satisfactory as is, in a polished implementation we’d want to handle the various errors in a more elegant way, but for our purpose here, logging the error is fine.

But we do have to change the playerDidQuit method, so update it as follows in GCTurnBasedMatchHelper.m:


-(void)turnBasedMatchmakerViewController: 
  (GKTurnBasedMatchmakerViewController *)viewController 
  playerQuitForMatch:(GKTurnBasedMatch *)match {
    NSUInteger currentIndex = 
      [match.participants indexOfObject:match.currentParticipant];
    GKTurnBasedParticipant *part;
    
    for (int i = 0; i < [match.participants count]; i++) {
        part = [match.participants objectAtIndex:
          (currentIndex + 1 + i) % match.participants.count];
        if (part.matchOutcome != GKTurnBasedMatchOutcomeQuit) {
            break;
        } 
    }
    NSLog(@"playerquitforMatch, %@, %@", 
      match, match.currentParticipant);
    [match participantQuitInTurnWithOutcome:
      GKTurnBasedMatchOutcomeQuit nextParticipant:part 
      matchData:match.matchData completionHandler:nil];
}

If the player currently holds the baton and quits a match, that match is stuck, because only the current player can submit a turn, and a next player, and that player has quit. So when the player quits during their turn, we need to hand off the baton for them. That’s what this method does.

This method is called when we quit a game from the view controller and it’s our turn. If it’s not our turn and we quit, then another method, playerQuitOutOfTurn, is called for us and all that is dealt with automatically.

In this case we’re iterating through the list of participants and looking for a participant who doesn’t have a matchOutcome of GKTurnBasedMatchOutcomeQuit. We don’t want to pass the match to a player who has quit. If we try to give the match to a player who has quit we’ll get an error and that turn won’t be recorded.

When we find the next participant in the array that doesn’t have a quit status, we call participantQuitInMatchWithOutcome:nextParticipant:matchData:completionHandler: which assigns an outcome to the quitting player (in this case, quit) passes the match to the next player, and end the turn.

In this game, we don’t need to do anything with the matchData, but pass it on. In other scenarios, the game may require something to be done to the game state before it can be passed on.

While we’re fixing this, we should make some of the same changes to our sendTurn method. In a case like this one, we want to iterate through the participants and make sure the one we’re passing the match to hasn’t quit. In fact, if you build and run now, start a match with three players (in a two player game if one quits the game ends), go around a few times, then have a player quit, then try to pass the quit player the match, you’ll see this:

Current turn isn't as expected error

So change the sendTurn method in ViewController.m to this:


- (IBAction)sendTurn:(id)sender {
    GKTurnBasedMatch *currentMatch = 
      [[GCTurnBasedMatchHelper sharedInstance] currentMatch];
    NSString *newStoryString;
    if ([textInputField.text length] > 250) {
        newStoryString = [textInputField.text substringToIndex:249];
    } else {
        newStoryString = textInputField.text;
    }
    NSString *sendString = [NSString stringWithFormat:@"%@ %@", 
      mainTextController.text, newStoryString];
    NSData *data = [sendString 
      dataUsingEncoding:NSUTF8StringEncoding ];
    mainTextController.text = sendString;
    
    NSUInteger currentIndex = [currentMatch.participants 
      indexOfObject:currentMatch.currentParticipant];
    GKTurnBasedParticipant *nextParticipant;
    
    NSUInteger nextIndex = (currentIndex + 1) % 
      [currentMatch.participants count];
    nextParticipant = 
      [currentMatch.participants objectAtIndex:nextIndex];
    
    for (int i = 0; i < [currentMatch.participants count]; i++) {
        nextParticipant = [currentMatch.participants 
          objectAtIndex:((currentIndex + 1 + i) % 
          [currentMatch.participants count ])];
        if (nextParticipant.matchOutcome != 
            GKTurnBasedMatchOutcomeQuit) {
            break;
        } 
    }
    
    [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;
        }
    }];
    NSLog(@"Send Turn, %@, %@", data, nextParticipant);
    textInputField.text = @"";
    characterCountLabel.text = @"250";
}

First we add the code that runs through all the participants, the first time it finds one where they haven’t quit (usually this will be the first iteration in the loop) it breaks out of the loop. This way we skip over participants who have quit.

The other thing we added here was two bits of unrelated code. First, we update the status if there was an error or if there wasn’t. Second, if the turn was send without problem, we disable the textInputField so that another turn cannot be sent immediately to that game.

If you build and run now, you should see the status update when you send a turn!

Updating the status label when the turn is complete

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.