Click here to see your recently viewed pages and most viewed pages.
Hide
Google Play Game Services

Turn-based Multiplayer in C++

This guide shows you how to implement a turn-based multiplayer game using Play Games services in a C++ 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 match

To begin a turn-based match, your game can prompt players to select how they want to be matched to other participants or build the match using some default criteria. Play Games services uses the match configuration data from your game to return a TurnBasedMatch object to your game; every time a participant performs an action, such as taking a turn, the game notifies the other participants via the OnTurnBasedMatchEventCallback or OnMultiplayerInvitationEventCallback.

To start a match, follow these steps:

  1. In your app, gather specifications for the type of turn-based match that the player wants to play and who can participate in the match.
    • Your game can display the built-in player selection UI provided by the SDK or use its own custom UI to gather this data. To learn how to use the player selection UI, see Selecting players with the default user interface.
    • Alternatively, you can implement a Quick Start button to let users bypass the player selection UI, and instead use some default criteria to build the match.
  2. Use a TurnBasedMatchConfig::Builder to create a TurnBasedMatchConfig struct.
  3. Call TurnBasedMatchConfig() and pass in the TurnBasedMatchConfig struct you created. If the game has specified auto-matching, Play Games services will attempt to match players to an existing game; for more details, see Implementing auto-matching.

To create a turn-based match, your game can call these methods:

Method Description
ShowPlayerSelectUI() Optional. Launches the default player selection UI. From this UI, the user can select other Google+ users to invite to the user's match, or request to be auto-matched with random Google+ users.
CreateTurnBasedMatch() Creates a new turn-based match object, or loads an existing match that meets the user's criteria if auto-matching is requested. If auto-matching is requested, the match returned will be either a new match or an existing match where other players have already taken turns. If a new match is returned, the user must take the first turn. Your game is responsible for handling turn-taking regardless of whether a new or existing match is loaded.

Selecting players with the default user interface

Play Games Services provides a default player selection UI that lets players select their friends to invite to a match or select to be auto-matched with random players. To start a match from the default player selection UI, call ShowPlayerSelectUI(). If the call is successful, the game displays a default player selection UI that prompts the user to select match invitees and the number of players for auto-matching. The user's player selection criteria is returned via the provided PlayerSelectUICallback.

  //service_ : std::unique_ptr<gpg::GameServices> service_;
    service_->TurnBasedMultiplayer().ShowPlayerSelectUI(
        MIN_PLAYERS, MAX_PLAYERS, true,
        [this](gpg::TurnBasedMultiplayerManager::PlayerSelectUIResponse const &
               response) {
  ...
        });
      }
    });

When the app receives the PlayerSelectUIResponse via the provided PlayerSelectUICallback, it can use the values provided in the response in order to create a new TurnBasedMatchConfig object, as described in Starting a Match.

  LogI("selected match %d", response.status);
      if (response.status == gpg::UIStatus::VALID) {
    // Create new match with the config
        gpg::TurnBasedMatchConfig config = gpg::TurnBasedMatchConfig::Builder()
            .SetMinimumAutomatchingPlayers(response.minimum_automatching_players)
            .SetMaximumAutomatchingPlayers(response.maximum_automatching_players)
            .AddAllPlayersToInvite(response.player_ids).Create();
        EnableUI(false);
        service_->TurnBasedMultiplayer().CreateTurnBasedMatch(
            config,
            [this](
                gpg::TurnBasedMultiplayerManager::TurnBasedMatchResponse const &
                matchResponse) {
          EnableUI(true);
          if (matchResponse.status == gpg::MultiplayerStatus::VALID) {
            PlayGame(matchResponse.match);
          }

Once a TurnBasedMatchConfig has been created, it can be used to call CreateTurnBasedMatch. The app provides a TurnBasedMatchCallback to CreateTurnBasedMatch. If the call is successful, the callback will return a TurnBasedMatchResponse with the newly created match.

If you want to create a match without using the default player selection UI, your game must build a TurnBasedMatchConfig with the desired player IDs and auto-matching preferences. It does this using the TurnBasedMatchConfig::Builder object.

If you want to create a match without using the default player selection UI, your game must provide the invitee player IDs and auto-match criteria in the TurnBasedMatchConfig object that you pass to CreateTurnBasedMatch().

Optionally, you might want to ensure that only players who are interested in a specific type of game variant are auto-matched together. If there are different versions of your app, your game can also use variants to ensure that only players who are on compatible versions are auto-matched. To specify a variant when creating a turn-based match, use the SetVariant() method. Your game can also use SetExclusiveBitMask() to pair auto-matched players who are interested in playing specific exclusive roles in a game.

Taking the first turn

If the CreateTurnBasedMatch() call is successful, Play Games services returns a TurnBasedMatch via the provided TurnBasedMatchCallback. Before proceeding, remember to initialize the game data as needed by your game. For example, you might need to initialize the starting positions for players in a strategy game or initialize the first hand for players in a card game.

To implement the first turn, follow these steps:

  1. Retrieve the TurnBasedMatch object from the TurnBasedMatchResponse object, then call HasData().
    • If the call returns a false value, this indicates that the game data is uninitialized because no player has taken a turn yet (that is, the current player is the first player in this match). Your game can initialize the game data and store it in a byte array. The size of the game data must be 128 KB or less.
    • If the call returns a true value, this indicates that the game has already started and the game data is already initialized, so make sure your game does not reinitialize the data.
  2. Optionally, call TakeMyTurn() now. If your initial state is based on a random value (for example, a card game where the player starts with a random hand) and your game does not call TakeMyTurn() to persist the data to Google's servers, the player could keep restarting the first turn to try to get a better starting position.
  3. Let the player perform game actions, and if appropriate, update the byte array containing the game data.
  4. Save the game data to Google's servers by calling TakeMyTurn(). Your game can specify the next player's participant ID, or specify AUTOMATCHING_PARTICIPANT to let Play Games services find a player for auto-matching. Your game can let match participants take turns even when not all invited players have joined or when there are auto-match slots still available. To get players into gameplay faster, your game should let players take their turn as soon as they join. When you call TakeMyTurn(), Play Games services sends a notification to the pending participant and updates the turn data on all participants' devices asynchronously.
  5. When the next player takes a turn, make sure to load the initialized data by calling Data() before you let the player perform game actions.

The following snippet shows how you might initialize a match and let the first player take a turn.

gpg::TurnBasedMultiplayerManager& manager = service_->TurnBasedMultiplayer();
gpg::TurnBasedMatchConfig config =
    gpg::TurnBasedMatchConfig::Builder().SetMinimumAutomatchingPlayers(1)
        .SetMaximumAutomatchingPlayers(2).Create();

EnableUI(false);
manager.CreateTurnBasedMatch(
    config,
    [this](gpg::TurnBasedMultiplayerManager::TurnBasedMatchResponse const &
           matchResponse) {
  EnableUI(true);
  if (matchResponse.status == gpg::MultiplayerStatus::VALID) {
    PlayGame(matchResponse.match);
  }
});

Implementing auto-matching

When your game calls CreateTurnBasedMatch(), and requests auto-matching by specifying MinimumAutomatchingPlayers() and/or MaximumAutomatchingPlayers(), Play Games services first attempts to match the player to an existing game. If a match is found that meets the player's auto-match criteria, Play Games services automatically joins the player and any invited players as auto-matched players into the existing match. If no match is found, your game should signal that it is available for auto-matching:

  1. Prompt the initiating player to take the first turn.
  2. Optionally, after the initiating player completes their first turn, your game can also let other invited players who joined the match take their first turns.
  3. To trigger auto-matching, call TakeMyTurn() and pass in AUTOMATCHING_PARTICIPANT in the next_participant parameter.

Handling invitations

Once a player has signed in to your game, the player may receive invitations to join a turn-based match created by another player. To save coding time and provide users with a consistent UI for responding to match invitations across applications, you can use the default match inbox UI provided by the SDK. To launch the default match inbox UI, call ShowMatchInboxUI().

To handle invitations for turn-based matches if your game is not using the default match Inbox UI, you can register a callback via SetOnMultiplayerInvitationEvent. The following methods can be used to manipluate a received invitation:

Method Description
AcceptInvitation Call this method if the user received a match invitation and wants to accept the invitation. Calling this method changes the user's participant status to ParticipantStatus::JOINED. The user can then proceed to take a turn.
DeclineInvitation() Call this method if the user received a match invitation but wants to stop the match from proceeding. Calling this method effectively cancels the match and removes the match from all GameServices objects registered to this user. The user's participant status changes to ParticipantStatus::DECLINED, and the match status changes to MatchStatus::CANCELED. Other participants see this match under the Completed Matches category in the match list UI.
DismissInvitation() Call this method if the user received a match invitation and wants to permanently ignore the invitation. Calling this method removes the invitation from all GameServices objects registered to this user. The user's participant status and the match status stay unchanged. To other participants in the match, the user still appears to be invited.

Taking a turn

After the first player takes a turn, your game can let the same participant or another participant take the next turn. Generally, turn-taking involves fetching the most recent match data from Play Games services, letting the participant interact with your game, and calling TakeMyTurn() to update the match data and pass the turn to another participant.

To implement turn-taking, follow these steps:

  1. Optionally, if you want be notified whenever any participant in the match takes a turn, provide a OnTurnBasedMatchEventCallback to the GameServices::Builder at GameServices creation time. Whenever the match is updated following a player's turn, your listener is notified.
  2. In order for a participant to take a turn, the match must be in the MY_TURN state. Your game can use the TurnBasedMatch object to verify this.
    • Check the match state by calling Status(). A result of MY_TURN indicates that the local participant can take a turn.
  3. Load the most recent match data from Play Games services by calling Data() and render your game accordingly.
  4. Let the user perform game actions and, if appropriate, persist the updated game data to a byte array.
  5. Determine the pending participant who should take the next turn. This is usually dependent on the order of play specified by your game design. The pending participant can be another player in the match or the participant whose turn it is. Your game can also pass the turn to the next player joining by auto-match, by setting the pending participant to AUTOMATCHING_PARTICIPANT.
  6. Call TakeMyTurn() to update the Play Games services with the latest game data and pass the turn to the pending participant. Play Games services returns the status of your match update operation via the provided TurnBasedMatchCallback. If the update is successful, Play Games services sends a notification to the pending participant to inform them that it's their turn.

To take a turn, use this method:

Method Description
TakeMyTurn() After invoking this method, the user sees this match under the Their Turn list in the match list UI.
  • If a participant ID is specified in the method call, that participant becomes the current player and can play a turn. The first time this happens to an invited player, the player receives an invitation notification. Subsequently, the player receives a turn based match notification.
  • If AUTOMATCHING_PARTICIPANT is specified, a participant will attempt to be found via auto-matching. You can only use the special auto-matching participant if there are auto-matching slots available in the current match.
  • Your game can call TakeMyTurn() with the current player as the pending participant. In this case, Play Games services uploads the the game data but no notification is sent.

The following snippet shows how you might implement the current player's turn:

void Engine::TakeTurn(const bool winning, const bool losing) {
  gpg::TurnBasedMultiplayerManager &manager = service_->TurnBasedMultiplayer();

  // When it is MY_TURN, localParticipant is always PendingParticipant().
  gpg::MultiplayerParticipant localParticipant =
      current_match_.PendingParticipant();

  LogI("Taking my turn. local participant id:%s",
       localParticipant.Id().c_str());

  gpg::MultiplayerParticipant nextParticipant =
      current_match_.SuggestedNextParticipant();

  if (!nextParticipant.Valid()) {
    //Error case
    manager.DismissMatch(current_match_);
    return;
  }

  turn_counter_++;
  std::vector<uint8_t> match_data = SetupMatchData();

  //By default, passing through existing participatntResults
  gpg::ParticipantResults results = current_match_.ParticipantResults();

  if (winning) {
    //Create winning participants result
    results = current_match_.ParticipantResults()
        .WithResult(localParticipant.Id(), // local player ID
                    0,                     // placing
                    gpg::MatchResult::WIN  // status
                    );
  } else if (losing) {
    //Create losing participants result
    results = current_match_.ParticipantResults()
        .WithResult(localParticipant.Id(), // local player ID
                    0,                     // placing
                    gpg::MatchResult::LOSS // status
                    );
  }

    //Take normal turn
  manager.TakeMyTurn(
      current_match_, match_data, results, nextParticipant,
      [this](gpg::TurnBasedMultiplayerManager::TurnBasedMatchResponse const &
             response) {
    LogI("Took turn");
  });
}

Randomizing the order of play

In some games, the order of play is randomized every round that players can take turns. To randomize the order of play in your game, you can use the following approach:

  1. In the first round, poll each each participant (that is, each player who has joined by invitation or by auto-match) in the match to determine if they are ready to play.
  2. Once all participants are ready, your game can randomly order their participant IDs, then call takeTurn() consecutively, passing in the participant ID for the next player according to the randomized order.

Saving game state

You can use the turn-based multiplayer API to manage your game state if your game state data fits within 128 KB or less. To save game state with the turn-based multiplayer API:

  1. Call TakeMyTurn() and pass in your game state data as the matchData parameter.
  2. If the call is successful, Play Games services notifies other participants in the match about the update and makes the match data available on all participant devices.
  3. Your game can then call Data() to retrieve the updated game state.

Your game should try to save game data for a partially-finished turn whenever a player's turn is interrupted and the player has to temporarily leave the game (for example, because of an incoming phone call). To do this, override your activity's method to call TakeMyTurn(). Make sure to specify the current player as the pending participant by using the same participant ID as in the last call to TakeMyTurn(). If successful, the call stores the game data in Google's servers but does not generate a new turn notification.

Completing a match

When the match has been played to completion (for example, a user has won the game), your game should call FinishMatchDuringMyTurn() to upload the user's game data and signal to the other participants that the match is over. The match then appears under the Completed Matches category in the user's match list UI.

Once a player calls FinishMatchDuringMyTurn() for the first time in the match, your game cannot call TakeMyTurn() again in this match. Play Games services sends a notification to all other match participants to inform them that the match is over. These participants see this match under Your Turn category in their respective match list UIs. At this point, your game can call ConfirmPendingCompletion() for these participants to acknowledge the match results. Invoking this method also moves the match to the Completed Matches category in the participant's match list UI.

To complete a match, use this method:

Method Description
FinishMatchDuringMyTurn() Calling this method changes the match status changes to MatchStatus::COMPLETED. After FinishMatchDuringMyTurn() is called for the first time in the match, Play Games services sends a notification to the other match participants to inform them that the match is over.
ConfirmPendingCompletion() Calling this method acknowledges the match results of a match with status MatchStatus::PENDING_COMPLETION. After FinishMatchDuringMyTurn().

Leaving a match

Participants can choose to leave at any time during the match, while allowing the match to continue. To signal that a participant is leaving, your game should call either LeaveMatchDuringMyTurn() or LeaveMatchDuringTheirTurn(). When a participant leaves a match, the match can still continue with other participants as long as one of the following conditions is met:

  • There are two or more other remaining participants, or
  • There is one remaining participant and at least one empty auto-match slot available.

Otherwise, the match is canceled.

In general, when a participant leaves a match, another player cannot join and take that participant's place. One exception is when a player who joined by auto-match and who has not taken a turn calls LeaveMatchDuringMyTurn(). In this case, the match attempts to find another player who can take over the place of the participant who left.

To leave a match, your game can call these methods:

Method Description
LeaveMatchDuringTheirTurn() Removes the match from all GameServices objects registered to this user. You should only invoke this method when it is MatchStatus::THEIR_TURN. Calling this method changes the user's participant status to ParticipantStatus::LEFT. If only one more participant remains, the match status changes to MatchStatus::Canceled.
leaveMatchDuringMyTurn()) Removes the match from all GameServices objects registered to the user. You should only invoke this method when match status is MatchStatus::MY_TURN Calling this method changes the user's participant status to ParticipantStatus::LEFT.
  • If only one more participant remains, the match status changes to MatchStatus::CANCELED.
  • If a participant is specified in the method call, that participant becomes the current player and can play a turn. The first time this happens to an invited player, the player receives an invitation notification. Subsequently, the player receives a turn notification.
  • If auto-matching slots are available, the AUTOMATCHING_PARTICIPANT can also be specified for the next participant.

Canceling a match

Your game can end a match for all participants before the match is played to normal completion, by calling CancelMatch(). After calling this method, your game cannot call TakeMyTurn() again in this match; the match now appears under the Completed Matches category in the match list UI.

To cancel a match, use this method:

Method Description
CancelMatch() Removes the match from all GameServices objects registered to the user. You should only invoke this method on the user's turn in an active match. Calling this method changes the user's participant status to ParticipantStatus::LEFT and the match status changes to MatchStatus::CANCELED.

Dismissing a match

Your game can let users dismiss a turn or invitation so that they do not have to see the match again. This hides the match from the dismisser's match list UI and causes gameplay to stall on the dismisser's turn; the match ends when canceled, or on expiration after two weeks. To other participants, the dismisser still appears as a participant in the match. Another player cannot take the dismisser's place.

To dismiss a match, use this method:

Method Description
DismissMatch() Removes the match from all GameServices objects registered to the user. The user's participant status is not changed.

Tracking match expiration

A match can expire if a player does not respond to a match or turn notification for two weeks (for example, after a player whose turn it is selects Dismiss in the default match list UI). You can use the following approach on the game client to identify which player caused a game to expire:

  1. Load a list of the currently signed-in player's list of games by calling FetchMatches().
  2. Filter that list for matches in the MatchStatus::EXPIRED state.
  3. If a match participant has ParticipantStatus::UNRESPONSIVE state, it indicates that this player has let the game expire.

Implementing a rematch

When a match is over, a participant may want to play a rematch with the same set of invited and auto-match opponents. Your game can initiate a rematch by invoking Rematch(). Your game can only invoke this method when the match state is MatchStatus::COMPLETE, and no other participants have requested a rematch. If the call is successful, Play Games services sends invitation notifications to all rematched opponents.

To implement a rematch, use this method:

Method Description
Rematch() Creates a new turn-based match object with the same participants as the previous match. The user must then take a first turn. If another participant already requested a rematch, calling this method returns a MultiplayerStatus::ERROR_MATCH_ALREADY_REMATCHED error.

If your game uses the default match inbox UI, players can also initiate rematches from that UI.