Adding Turn-based Multiplayer Support to Your iOS Game

This guide shows you how to implement a turn-based multiplayer game using Google Play games services in an iOS application.

Before you begin

If you haven't already done so, you might find it helpful to review the turn-based multiplayer game concepts.

Before you start to code your turn-based multiplayer game:

Starting a turn-based multiplayer game

Your main screen is the player's primary entry point to start a turn-based multiplayer game. To start the game, your app can present these options to the user:

  • Quick match button. Lets the user bypass the player selection UI and play against randomly selected opponents (via auto-matching).

  • Invite players button. Lets the user invite friends to join a game session or specify some number of random opponents for auto-matching. When the player selects this option, your app should display either the built-in player selection user interface (UI) provided by the SDK or your own custom UI.

To start a match, your game must first send a match creation request to Google Play games services, which then returns a match object to your app. Google Play games services tracks the match and participant status during the lifecycle of the turn-based multiplayer game. When an event occurs that affects the status of the match or its participants, Google Play games services notifies the relevant delegate objects in your app so that you can handle these events.

To create a match, call the createMatchWithConfig method in the GPGTurnBasedMatch class and pass in an appropriate GPGMultiplayerConfig object. The configuration information for creating the match can come from the local player (that is, the user who is logged in to the device where your app is running) through the player selection UI or can be provided programmatically by your app.

Implementing a quick match

The following code shows how your app might create a quick match game with two auto-matched players (including the local player).

- (void)startQuickMatchGame:(id)sender {
  GPGMultiplayerConfig *gameConfigForAutoMatch = [[GPGMultiplayerConfig alloc] init];
  // We will automatically match with one other player
  gameConfigForAutoMatch.minAutoMatchingPlayers = 1;
  gameConfigForAutoMatch.maxAutoMatchingPlayers = 1;

  [GPGTurnBasedMatch createMatchWithConfig:gameConfigForAutoMatch
                         completionHandler:^(GPGTurnBasedMatch *match, NSError *error) {
    if (error) {
      NSLog(@"Received an error trying to create a match %@", [error localizedDescription]);
    } else {
      [self takeTurnInMatch:match];
    }
  }];
}

When creating a match with auto-matching, Google Play games services might return a match that is already in progress. This occurs when the match configuration sent by your game to Google Play games services has the same configuration as an existing match that is underway (that is, the other player has already taken a turn). Because Google Play games services might return a match that is in progress, be sure to check for game data in the returned match object.

Implementing player selection using the default UI

To invite specific players to a match, your game must add their player IDs to the invitedPlayerIds array in the GPGMultiplayerMatchConfig object. This is handled for you if your game uses the default player selection UI provided by the SDK.

The following code shows how you might display the default UI to let a local player invite specific players or random opponents.

// GPGPlayerPickerLauncherDelegate methods
- (int)minPlayersForPlayerPickerLauncher {
    return 1;
}

- (int)maxPlayersForPlayerPickerLauncher {
    return 3;
}

- (IBAction)inviteMyFriends:(id)sender {
    // This can be a 2-4 player game
    [GPGLauncherController sharedInstance].playerPickerLauncherDelegate = self;
    // This assumes your class has been declared a GPGPlayerPickerLauncherDelegate
    [[GPGLauncherController sharedInstance] presentPlayerPicker];
}

Handling common user interactions

When the match is in progress, players might want to perform actions such as taking a turn in the current match, or accepting or declining an invitation to another match. To simplify your coding, your app can use the default match list UI and the GPGTurnBasedMatchViewController object to handle user interactions that are common in turn-based multiplayer games.

To show the current match list, present the match list launcher.

- (IBAction)seeMyMatches:(id)sender {
    self.justCameFromMatchVC = YES;
    [GPGLauncherController sharedInstance].turnBasedMatchListLauncherDelegate = self;
    [[GPGLauncherController sharedInstance] presentTurnBasedMatchList];
}

The following sections describe how your game can handle common user interactions by using the GPGTurnBasedMatchDelegate object. The GPGTurnBasedMatchListLauncherDelegate method turnBasedMatchListLauncherDidSelectMatch receives all messages regarding state changes in matches.

Play button clicks

If players click on the Play button when it is their turn, your app should allow them to take a turn. If players click on a match panel but not directly on the Play button, your app can handle this action as though they clicked on Play. Alternatively, your app could display an action sheet or a UIAlert dialog.

If players click on the match panel when it is not their turn, your app can simply ignore the click. However, a better approach is to display the game to the player without letting the player take a turn.

- (void)turnBasedMatchListLauncherDidSelectMatch:(GPGTurnBasedMatch *)match {
    NSLog(@"Clicking turnBasedMatchListLauncherDidSelectMatch");

    NSString *matchInfo;
    switch (match.userMatchStatus)
    {
        case GPGTurnBasedUserMatchStatusTurn:         //My turn
            [self takeTurnInMatch:match];
            break;
        case GPGTurnBasedUserMatchStatusAwaitingTurn: //Their turn
            [self viewMatchNotMyTurn:match];
            break;
        case GPGTurnBasedUserMatchStatusInvited:

            // This might be a good time to bring up an alert sheet or a dialog
            // box that shows you something about the match. Or you could just
            // take a turn as if the player had clicked the takeTurn button.

            // In this example, the game brings up a UIAlert about the match
            matchInfo = [NSString stringWithFormat:@"Created by %@. Last turn by %@ on %@",
            match.creationParticipant.player.displayName,
            match.lastUpdateParticipant.player.displayName,
                    [NSDate dateWithTimeIntervalSince1970:match.lastUpdateTimestamp / 1000]];

            [[[UIAlertView alloc] initWithTitle:@"Match info"
                    message:matchInfo
                    delegate:nil
                    cancelButtonTitle:@"Okay"
                    otherButtonTitles:nil] show];
            break;
        case GPGTurnBasedUserMatchStatusMatchCompleted: //Completed match
        [self viewMatchNotMyTurn:match];
        break;
    }
}

When the match is completed

If the player clicks on the match panel but the match is already completed, your app can simply ignore the click. However, a better approach is to display to the player that the match is in the finished state.

The following code shows how you can implement the matchEnded method for when an opponent concludes a game by clicking the Finish Match button.

- (void)matchEnded:(GPGTurnBasedMatch *)match
         participant:(GPGTurnBasedParticipant *)participant
         fromPushNotification:(BOOL)fromPushNotification {
    if (fromPushNotification) {
        self.matchFromNotification = match;
        self.lobbyAlertType = LobbyAlertGameOver;
    }
    [self refreshPendingGames];
}

The following code shows how you can implement the matchEnded method for when the match concludes normally.

- (void)matchEnded:(GPGTurnBasedMatch *)match
        withParticipant:(GPGTurnBasedParticipant *)participant {
    NSLog(@"Match has ended!");
    [self refreshPendingGames];
}

Rematching

A player can also initiate a rematch if that match is in the finished state. This creates a new match that is associated with the same set of participants (including auto-matched opponents) as the match that already finished. If the player selects the rematch option from the UI, the controller performs a rematch operation on the player’s behalf. If successful, the system invokes the didRematch method in your app.

The following code shows how you can implement rematching.

- (void)turnBasedMatchListLauncherDidRematch:(GPGTurnBasedMatch *)match {
    // This is similar to creating a new match.
    [self takeTurnInMatch:match];
}

On match invitations

The player can choose to accept or decline an invitation to another match. If the player accepts the invitation from the UI, the controller performs a join operation on the player’s behalf. If successful, the system invokes the didJoinMatch method in your app. Your app should then bring the player to the match that was accepted.

- (void)turnBasedMatchListLauncherDidJoinMatch:(GPGTurnBasedMatch *)match {
    [self takeTurnInMatch:match];
}

If the player declined the match invitation, the controller performs a decline operation on the player’s behalf. If successful, the system invokes the didDeclineMatch method in your app.

- (void)turnBasedMatchListLauncherDidDeclineMatch:(GPGTurnBasedMatch *)match {
    NSLog(@"Did decline match called. No further action required.");
}

Your app should handle the following scenarios:

  • The app is loaded in memory (the app could be running in the foreground, running in the background, or suspended) when the user receives an invitation to join a match. To see how to handle this scenario, refer to Match invitation notification.
  • The app is not loaded in memory when the user receives an invitation to join a match.

If your app is not loaded in memory when an invitation notification arrives, you should allow the player to launch the app from the notification. The following example shows how your app can handle incoming invitations in the application:didFinishLaunchingWithOptions: handler.

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    for (UIViewController *nextVc in
            [(UINavigationController *)self.window.rootViewController
            childViewControllers]) {
        if ([nextVc class] == [LobbyViewController class]) {
            NSLog(@"Found our lobby view controller!");
            [GPGManager sharedInstance].turnBasedMatchDelegate =
                    (LobbyViewController *)nextVc;
            break;
        }
    }

    // Look to see if our application was launched from a notification
    NSDictionary *remoteNotification =
            [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];

    // If one exists, see if it's a game invitation
    if (remoteNotification) {
        if ([[GPGManager sharedInstance] tryHandleRemoteNotification:remoteNotification]) {
            // didReceiveRealTimeInviteForRoom is being called in your app delegate.
        } else {
            // You probably want to do other notification checking here.
        }
    }

    return YES;
}

Handling notifications

When a turn-based multiplayer event occurs, Google Play games services sends a push notification to all iOS devices on which the user is logged in. To receive these notifications in your app, follow these steps:

  1. Make sure that your app is enabled to receive push notifications, as described in Register for push notifications.
  2. Specify a turnBasedMatchDelegate object to receive turn-based match event notifications. You are encouraged to set this object in the application:didFinishLaunchingWithOptions method in your appDelagate.

    [GPGManager sharedInstance].turnBasedMatchDelegate = mainMenuViewController;
    
  3. In the application:didReceiveRemoteNotification method of your appDelegate, call the tryHandleRemoteNotification method of your GPGManager to handle the notification. This call passes the notification to your turnBasedMatchDelegate if it detects that the notification is for an invitation to join a match or for a match event.

    - (void)application:(UIApplication *)application
        didReceiveRemoteNotification:(NSDictionary *)userInfo {
      NSLog(@"Received remote notification! %@", userInfo);
      if ([[GPGManager sharedInstance] tryHandleRemoteNotification:userInfo]) {
        // The payload looks like it's from Google Play Games. A
        // didReceiveTurnBasedInviteForMatch method is being
        // called in our delegate.
      } else {
        // Call other methods that might want to handle this
        // remote notification
      }
    }
    
  4. Handle the incoming notification in the methods of your turnBasedMatchDelegate (see the sections below for examples). Note that the methods are invoked when the player receives a push notification or when the game state is updated as part of a regular data refresh performed by the system. The system sets a boolean flag to indicate whether the method was a called in response to a push notification or data refresh.

The following sections list some suggested implementations in your app for handling notifications for turn-based multiplayer events.

Match invitation notification

When the player receives an invitation to a match while the app is running, the system invokes the didReceiveTurnBasedInviteForMatch method. The following example shows how your game can bring up an alert box to allow users to accept the match.

- (void)didReceiveTurnBasedInviteForMatch:(GPGTurnBasedMatch *)match
                     fromPushNotification:(BOOL)fromPushNotification {
  // Only show an alert if you received this from a push notification
  if (fromPushNotification) {
    GPGMultiplayerParticipant *invitingParticipant =
        [match participantForId:match.lastUpdateParticipantId];
    // This should always be true
    if ([match.pendingParticipantId isEqualToString:match.localParticipantId]) {
      NSString *messageToShow =
              [NSString stringWithFormat:@"%@ just invited you to a game. Would you like to play now?",
              invitingParticipant.player.displayName];
      [[[UIAlertView alloc] initWithTitle:@"You've been invited!"
                                  message:messageToShow
                                 delegate:self
                        cancelButtonTitle:@"Not now"
                        otherButtonTitles:@"Sure!",
          nil] show];
      self.matchToTrackFromNotification = match;
    }
  }
  // Tell users they have matches that might need their attention,
  // no matter how your app reaches this method.
  [self refreshInterfaceBasedOnPendingMatches];
}

Player’s turn notification

When it is the player’s turn, the system invokes the didReceiveTurnEventForMatch method. The following example shows how your game can display an alert box to prompt the player to take a turn.

- (void)didReceiveTurnEventForMatch:(GPGTurnBasedMatch *)match
                        participant:(GPGMultiplayerParticipant *)participant
               fromPushNotification:(BOOL)fromPushNotification {
  // Only show an alert if you received this from a push notification
  if (fromPushNotification) {
    if ([match.pendingParticipantId isEqualToString:match.localParticipantId]) {
      NSString *messageToShow = [NSString stringWithFormat:
              @"%@ just took their turn in a match. "
              @"Would you like to jump to that game now?",
          participant.player.name];
      [[[UIAlertView alloc] initWithTitle:@"It's your turn!"
                                  message:messageToShow
                                 delegate:self
                        cancelButtonTitle:@"No"
                        otherButtonTitles:@"Sure!",
          nil] show];
      self.matchToTrackFromNotification = match;
    }
  }
  [self refreshInterfaceBasedOnPendingMatches];
}

Match ended notification

When a match has ended, the system invokes the matchEnded method. The following example shows how your game can display an alert box to inform the player that the match is over.

- (void)matchEnded:(GPGTurnBasedMatch *)match
             participant:(GPGMultiplayerParticipant *)participant
    fromPushNotification:(BOOL)fromPushNotification {
  // Only show an alert if you received this from a push notification
  if (fromPushNotification) {
    NSString *messageToShow = [NSString
        stringWithFormat:@"%@ just finished a match. "
                         @"Would you like view the results now?",
        participant.player.name];
    [[[UIAlertView alloc] initWithTitle:@"Match has ended!"
                                message:messageToShow
                               delegate:self
                      cancelButtonTitle:@"No"
                      otherButtonTitles:@"Sure!",
        nil] show];
    self.matchToTrackFromNotification = match;
  }
  [self refreshInterfaceBasedOnPendingMatches];
}

Retrieving a list of matches

One common action upon receiving a turnBasedMatchDelegate call might be to update your in-game UI depending on whether or not your player has any matches that are currently waiting from a response from them.

You can retrieve matches using the GPGTurnBasedMatch object. The following code shows how to enumerate all of the available matches.

[GPGTurnBasedMatch allMatchesWithCompletionHandler:^(NSArray *matches, NSError *error){
    NSInteger gamesToRespondTo = 0;
    for (GPGTurnBasedMatch* match in matches) {
        if (match.status == GPGTurnBasedUserMatchStatusInvited ||
                match.status == GPGTurnBasedUserMatchStatusTurn)
            gamesToRespondTo++;
    }
    NSString* buttonText;
    if (gamesToRespondTo > 0) {
        buttonText = [NSString stringWithFormat:@"All my matches (%ld)",
                 (long) gamesToRespondTo];
    } else {
        buttonText = @"All my matches";
    }
    [self.viewMyMatchesButton setTitle:buttonText forState:UIControlStateNormal];
}];

Retrieving participant information

When the local player takes a turn, it is up to your app to tell Google Play games services who the next player is. So it is important to understand the player information you get back in your GPGTurnBasedMatch object.

The participant list is a list of all players who are currently in the game, including the local player. This list will be in the same order for all players in the match. If a player has been invited (but not yet accepted the invitation), they will be in the participant list. If a player has left the game (via the didLeaveTurnOutOfMatch delegate method, for instance), they will be in the participant list with a status of GPGTurnBasedParticipantStatusLeft.

The GPGMultiplayerMatchConfig object which is included with the match object contains a minAutoMatchingPlayers and maxAutoMatchingPlayers property which is updated as the service adds more auto-matched players to the current match.

The participant list from the match object's participants property provides a list of all participants (including the local player). Participant properties include:

  • displayName. This property contains either the name of the logged-in user or something like "player_29384" if the participant was auto-matched.
  • player.avatarUrl. This property contains either the profile picture associated with the user's account or a generic image if the participant was auto-matched.
  • participantId. This property contains a unique string that is used to identify the player in a match and any subsequent rematches. The ID is generated every time a match is created or when the player joins by auto-match. The ID is not the same as the player ID associated with the logged-in user.
  • player.playerID. The participant's player ID if the participant was invited via the player selection UI.

For information about the local player, you should retrieve the localParticipant property from the match object.

Taking a turn

You typically take a turn by calling the GPGTurnBasedMatch object's takeTurnWithnextParticipantId:data:results:completionHandler: method.

In the method call:

  • The nextParticipantId parameter is the participant ID of the player who is supposed to go next. This can be the next player, nil if you want this to go to an (as-yet-unknown) auto-match player, or even the local player again, if you want to continue the current player's turn. For more information, see the section below.
  • The data parameter is the game data. This is an NSData object up to 128k in size. When creating this data object, make sure to use the same encoding (UTF8, UTF16, UTF16LittleEndian, etc.) that you are using on the Android version of your game.

Determining who goes next

You can retrieve a participants list by following the steps described in Retrieving participant information and use this information to determine the participantId of the player who goes next in a game.

The following simplified example uses a round-robin approach to determine the next player to take a turn. In an actual implementation, your app should also skip over any participants who have left the game. To see a more complete code implemention, please refer to the TBMPSkeleton sample app.

- (NSString *)determineWhoGoesNext:(GPGTurnBasedMatch *)match {
  if (!match) {
    // This isn't really a state I should be in.
    // Probably raise an exception here.
    return nil;
  }

  // First, we can look at where I am in the
  // deterministically-ordered participants array.
  NSUInteger myIndex = [match.participants indexOfObject:match.localParticipant];
  if (myIndex == NSNotFound) {
    return nil;
  }

  // Next, let's look at how many people in total are in the
  // round-robin match. This includes participants as well as
  // players for auto-match.
  int totalPlayers = match.participants.count + match.matchConfig.minAutoMatchingPlayers;

  if (totalPlayers == 1) {
    // You're the only one left! You shouldn't really get to
    // this state normally because the
    // match should switch to a completed state.
    // Probably the safest thing to do now is just return
    // the current player again.
    return match.localParticipant;
  }

  NSUInteger playerToGoNext = (myIndex + 1) % totalPlayers;

  // Remember, this number might be larger than the participant
  // array. If it is, that means we're
  // ready to invite our next automatch player
  NSString *nextParticipantId;
  if (playerToGoNext < match.participants.count) {
    nextParticipantId =
        ((GPGMultiplayerParticipant *)match.participants[playerToGoNext]).participantId;
  } else {
    // Setting our participantID to nil is our way of
    // telling the system, "Please add the next auto-match player"
    nextParticipantId = nil;
  }
  return nextParticipantId;
}

Using game data

Your app can store and retrieve game data using the data property of the GPGTurnBasedMatch object. Google Play games services does not understand or interpret this data in any way. It is simply treated as NSData (or ByteArray on Android) up to 128k in size that your game can read in, alter, and send to the next player when the local player's turn is complete.

Using match results

Your app can store and retrieve match results as an NSArray of GPGTurnBasedParticipantResult objects. These objects have a few properties that let Google Play games services (and other participants) know about the outcome of the match, including:

  • participantId. The ID of the match participant that this result is referring to.
  • placing. An (optional) integer that can declare whether the player ended up in 1st, 2nd, 3rd, etc. place
  • result. A GPGTurnBasedParticipantResultStatus enum that specifies the player's final status. This can be:

    • GPGTurnBasedParticipantResultStatusWin
    • GPGTurnBasedParticipantResultStatusLose
    • GPGTurnBasedParticipantResultStatusTie
    • GPGTurnBasedParticipantResultStatusNone. Used for games where nobody wins or loses.
    • GPGTurnBasedParticipantResultStatusDisconnect. Used for games where players can leave mid-match.
    • GPGTurnBasedParticipantResultStatusDisagreed. Used for games where the client does not agree with the results reported by the other clients,

When storing results, you do not need to include results for all players. For instance, if you were playing a 4-player game in which one player got eliminated, you could create a results array in which only includes that single player with a placing of 4 and a status of GPGTurnBasedParticipantResultStatusLose.

Removing a player from a match

There are two scenarios where your app can remove a player from a game:

During normal gameplay

There are some games in which players are eliminated early, while the other players continue to play out the rest of the game. Usually, these eliminated players have lost the game, but not always. For instance, in a 4-player game where the goal is to be the first player to reach the end, you might "eliminate" the first place player, while allowing the remaining players to keep playing.

To eliminate a player in this manner, your game client should make sure to:

  • Add the eliminated client to the results array, and
  • Ensure that all other game clients skip over this player's turn in the future.

If your game removes a player during normal gameplay, the player will see the match in their list of matches (until the match completes), but the match will always be listed under somebody else's turn in the match list UI. The player can also view the match and see the state of the game. The player will be notified when the match is complete.

There are multiple ways to implement this approach. Here is an example:

  1. In the client of the player who has been eliminated, create a results array, then add that player to the array and assign a valid result status (as listed above), depending on your game logic.
  2. Your app should then allow the player to take a normal turn, update the game data, and pass in the updated results array.
  3. When determining who goes next, all game clients should skip over any players who are already in the results array. If your app determines that the local player is the only player not yet in the results array, your app should inform the player that they have won (or lost) then call the finishWithData method.

The player elects to leave the match

This scenario is less common, and is the equivalent of letting a player break all connections to a match. Your app might allow the local player to leave, for example, to quit a game with an abusive player in the match. We recommend using the previous approach if you want to give the player an in-game resign option, instead of calling leave.

If your app allows a player to leave the match outside of normal gameplay, the match will no longer show up in that player's match UI. In addition, the player will not be able to view the state of the game, and will not be notified when the match is complete.

By default, the leaving player does not have an opportunity to update the game state. To update the leaving player's game state, your app will first need to let the player take a turn by assigning the local player as the next player, and then leave the match.

If there is only one player remaining in the match after a player leaves the game, the match is silently cancelled by Google Play games services. If you do not want matches to be cancelled in this manner, make sure your game can detect when this scenario occurs and call finishWithData method instead.

Completing a game

You complete a game by calling the GPGTurnBasedMatch object's finishWithData:results:completionHandler: method. This is very similar to the takeTurn call above, except that there is no participantId, and the results parameter is more likely to be a complete array of results. Once this method is called, the game is reported as over to all participants in the game.

Enviar comentarios sobre…

Play Games Services for iOS
Play Games Services for iOS