Game Services | Turn-Based Multiplayer: Scripting
This section provides a guide to work with the Turn-Based Multiplayer scripting API of the Game Services module.
You can access the Turn-Based Multiplayer API via the
TurnBased
property of theGameServices
class under theEasyMobile
namespace. The API is intended to be used after the initialization has completed (the user has logged in to Game Center or Google Play Games). Failure to do so is a programming error.
Implementation Checklist
To add turn-based support to your game, write code to handle the following activities:
- Allow the local player to start or join a match. See Match-making.
- Allow the local player to accept or decline an invitation. See Handling Invitations.
- Allow the local player to see the list of existing matches. See Working with Existing Matches.
- Handle incoming matches. See The Match Callback.
- Allow the local player to view the state of a match in progress. See Working with TurnBasedMatch.
- Allow the local player to take a turn in the match. See Taking Turn.
- Allow the local player to leave a match if necessary. See Leaving a Match.
- End a match. See Finishing a Match and Acknowledging a Finished Match.
- Allow the players to replay a finished match. See Rematch.
Match-making
Match-making is the process of creating a new match and fill it with participants. The Turn-Based Multiplayer API allows you to either create a quick match with random (auto-matched) opponents, or create a match using the built-in matchmaker UI in which you can invite friends, nearby players, or random players. Both methods of match making start with creating a match request, which is a set of parameters that describe the match to be created.
Match Request
Before creating a new match, create a MatchRequest
object with appropriate parameters to describe the match you want.
MatchRequest request = new MatchRequest();
// The minimum number of players that may join the match, including
// the player who is making the match request.
// Must be at least 2 (default).
request.MinPlayers = 2;
// The maximum number of players that may join the match, including
// the player who is making the match request.
request.MaxPlayers = 2;
// The match variant. The meaning of this parameter is defined by the game.
// It usually indicates a particular game type or mode (for example "capture the flag",
// "first to 10 points", etc). It allows the player to match only with players whose
// match request shares the same variant number. This value must
// be between 0 and 511 (inclusive). Default value is 0.
request.Variant = 0;
// If your game has multiple player roles (such as farmer, archer, and wizard)
// and you want to restrict auto-matched games to one player of each role,
// add an exclusive bitmask to your match request. When auto-matching with this option,
// players will only be considered for a match when the logical AND of their exclusive
// bitmasks is equal to 0. In other words, this value represents the exclusive role the
// player making this request wants to play in the created match. If this value is 0 (default)
// it will be ignored. If you're creating a match with the standard matchmaker UI, this value
// will also be ignored.
request.ExclusiveBitmask = 0;
Create a Quick Match
To programmatically start a quick match with random opponents, use the CreateQuickMatch
method. When using this method, the underlying platform will attempt to match the local player with other players who also look for random opponents and whose match request is compatible with the local player's match request. No UI will be shown. Once the operation completes, a callback will be invoked to report the results. If successful, a new match will be created with the local player as the current player to take turn.
public void CreateQuickMatch()
{
// First create a match request.
MatchRequest request = new MatchRequest();
request.MinPlayers = 2;
request.MaxPlayers = 2;
// Create a quick match with a random opponent. No UI will be shown.
// OnMatchStarted will be called when the operation completes.
GameServices.TurnBased.CreateQuickMatch(request, OnMatchStarted);
}
// If a match is successfully created, this callback will be called with (true, match).
// Otherwise, it will be called with (false, null).
// This callback is always invoked on the main thread.
void OnMatchStarted(bool success, TurnBasedMatch match)
{
if (success)
{
// A match is created! Go to the game screen and play!
}
else
{
// Show error message.
}
}
Create with the Matchmaker UI
Game Center and Google Play Games both provide a matchmaker UI that allows starting a match by inviting friends, nearby players or picking random opponents. This built-in UI offers the user a standardized and intuitive way to perform these tasks and can save you a significant coding effort of building your own matchmaker UI. Below are screenshots of the matchmaker UI provided by Game Center and Google Play Games, respectively.
To create a match with this UI, simply call the CreateWithMatchmakerUI
method. When the UI is canceled by the user or encounters an error, the corresponding callback will be invoked. When a match is created, the matchmaker UI will close automatically and the registered MatchDelegate
will be called.
public void CreateWithMatchmakerUI()
{
// First create a match request.
MatchRequest request = new MatchRequest();
request.MinPlayers = 2;
request.MaxPlayers = 2;
// Create a match using the built-in matchmaker UI.
// The registered MatchDelegate will be called when a match is created.
GameServices.TurnBased.CreateWithMatchmakerUI(request, OnCancelCallback, OnErrorCallback);
}
// This callback is called when the matchmaker UI is closed.
void OnCancelCallback()
{
Debug.Log("The matchmaker UI has been closed.");
}
// This callback is called when the matchmaker UI encounters an error.
void OnErrorCallback(string error)
{
Debug.Log("The matchmaker UI failed with error " + error);
}
A turn-based match can start with one player
If you're the initiating player (the one who created a match), you'll find the match started with you as the only joined participant. You'll be the one to take the first turn and then pass turn to the next player, who is either invited or auto-matched. The match continues as that player joins and takes the turn.
Handling Invitations
A player being invited to join a turn-based match receives an invitation when the game has assigned that player to take the next turn. Turn-based invitations are handled a bit differently between Game Center and Google Play Games platforms.
- Game Center: invitations are sent as messages in the iMessage app; the user taps a message to accept the associated invitation, which will open the app and invoke the registered
MatchDelegate
. - Google Play Games: there are two cases:
- App is in foreground: if the app is running in foreground and an invitation comes, the
InvitationReceivedDelegate
will be called if it has been registered before. - App is in background or not running: the invitation will come as a notification, the user can tap this notification to bring up the default inbox UI of Google Play Games where he can accept or decline the invitation. If the invitation is accepted, the app will be opened and the registered
MatchDelegate
will be called.
- App is in foreground: if the app is running in foreground and an invitation comes, the
To handle turn-based invitations when the app is in foreground (on Google Play Games platform), you must register an InvitationReceivedDelegate
as soon as the user has logged in - that is, inside the handler of the UserLoginSucceeded
event. This delegate will be called when an invitation arrives. You can accept or decline an invitation programmatically using the AcceptInvitation
or DeclineInvitation
method, respectively. If the invitation is accepted, a new turn-based match object will be created with the local user as the current player to take turn, same as when a new match is created.
void OnEnable()
{
// Subscribe to this event before initializing EasyMobile runtime.
// Normally you should add this script to the first scene of your app.
GameServices.UserLoginSucceeded += GameServices_UserLoginSucceeded;
}
// This handler is called when authentication completes successfully.
void GameServices_UserLoginSucceeded()
{
// Register our invitation delegate.
GameServices.RegisterInvitationDelegate(OnInvitationReceived);
}
// This is called when an invitation is received. 'shouldAutoAccept' will be true
// if the user accepts the invitation through the notification, otherwise it will
// be false (e.g. when the invitation comes whilst the app is in foreground).
// This is always called on the main thread.
void OnInvitationReceived(Invitation invitation, bool shouldAutoAccept)
{
Debug.Log("Received a multiplayer invitation. InvType: " + invitation.InvitationType); // TurnBased or RealTime
if (shouldAutoAccept)
{
// Invitation should be accepted immediately.
// This will create a new match and invoke OnMatchStarted.
GameServices.TurnBased.AcceptInvitation(invitation, OnMatchStarted);
}
else
{
// The invitation comes when the app is in foreground.
// The user has not yet indicated that they want to accept this invitation.
// We should *not* automatically accept it. Rather we should show some popup
// asking if the user would like to accept or decline it.
}
}
// If a match is successfully created, this callback will be called with (true, match).
// Otherwise, it will be called with (false, null).
// This callback is always invoked on the main thread.
void OnMatchStarted(bool success, TurnBasedMatch match)
{
if (success)
{
// A match is created! Go to the game screen and play!
}
else
{
// Show error message.
}
}
Working with Existing Matches
You can use the ShowMatchesUI
method to show a built-in UI that lists all the existing matches that the local user joins. The user can select a match from the list to take turn or view it (if it's not their turn). On Google Play Games platform, this UI also displays all pending invitations for the user to accept or decline. If the user picks an existing match or accepts an invitation, the registered MatchDelegate
will be called, exactly as it was called when a new match was created with the matchmaker UI.
// Show the built-in UI that displays existing matches and pending invitations.
GameServices.TurnBased.ShowMatchesUI();
The Match Callback
Your game must register a MatchDelegate
to handle incoming turn-based matches. This delegate will be called, for example, when a new match is created from the matchmaker UI or an existing one is selected from the matches UI, the local player is assigned to take the next turn, another participant left a match, or a match has ended. It will receive a copy of the newly created or updated turn-based match. The MatchDelegate
must be registered as soon as the user has logged in - that is, inside the handler of the UserLoginSucceeded
event.
void OnEnable()
{
// Subscribe to this event before initializing EasyMobile runtime.
// Normally you should add this script to the first scene of your app.
GameServices.UserLoginSucceeded += GameServices_UserLoginSucceeded;
}
// This handler is called when authentication completes successfully.
void GameServices_UserLoginSucceeded()
{
// Register our match delegate.
GameServices.TurnBased.RegisterMatchDelegate(OnMatchReceived);
}
// This will be called when a new match is created from the matchmaker UI
// or an existing one is selected from the matches UI, the local player
// is assigned to take the next turn, another participant left a match,
// or a match has ended. This is always called on the main thread.
void OnMatchReceived(TurnBasedMatch match, bool shouldAutoLaunch, bool playerWantsToQuit)
{
if (playerWantsToQuit)
{
// This is true only if the local player has removed the match
// in the matches UI while being the turn holder. When this happens you should call the
// LeaveMatchInTurn method to end the local player's turn and pass the match
// to the next appropriate participant according to your game logic.
// Note that this currently happens on Game Center platform only.
}
else if (shouldAutoLaunch)
{
// If shouldAutoLaunch is true, we know the user has indicated (via an external UI)
// that they wish to play this match right now, so we take the user to the
// game screen without further delay.
OnMatchStarted(true, match);
}
else
{
// If shouldAutoLaunch is false, this means it's not clear that the user
// wants to jump into the game right away (for example, we might have received
// this match silently when a turn event comes whilst the app is in foreground).
// So here we should confirm with the user whether they want to start the match or not.
}
}
void OnMatchStarted(bool success, TurnBasedMatch match)
{
if (success)
{
// A match is created! Go to the game screen and play!
}
else
{
// Show error message.
}
}
Working with TurnBasedMatch
Each turn-based match is represented by a TurnBasedMatch
object. Your game normally receives a copy of this object via the MatchDelegate
, or the callback of the CreateQuickMatch
and AcceptInvitation
methods. This object contains all the information about the match being played.
Match Status
The state of the match can be retrieved via the TurnBasedMatch.Status
property (see Match State). You can check this value and take appropriate action for your game depending on the state of the match (e.g. inform the user that the match has ended if its state is Ended
).
Match Participants
You can retrieve the list of the participants who have joined the match via the TurnBasedMatch.Participants
property (see Participants). Each participant is represented by a Participant
object, through which you can obtain information about that participant including display name, image, and status (Invited
, Joined
, Left
, etc.). The local player is represented by the TurnBasedMatch.Self
property, while the participant who holds the current turn is represented by the TurnBasedMatch.CurrentParticipant
property. The total number of players required for the match can be found in the TurnBasedMatch.PlayerCount
property. These pieces of information are likely useful when you want to update your game UI about the participants of the match.
Turn Holder
As mentioned, the participant who holds the current turn is represented by the TurnBasedMatch.CurrentParticipant
property. You can check if the current turn belongs to the local player using the TurnBasedMatch.IsMyTurn
property.
Match Data
The match data can be accessed via the TurnBasedMatch.Data
property (see Match Data). As mentioned, match data is stored as a byte array, and your game is responsible for encoding and interpreting the meaning of this data. When a match is first created the match data is empty (null), and your game should initialize it to something that represents the initial state of the match. As the match progresses, this data should be updated appropriately to reflect the current state of the match.
This property is read-only, as match data is not supposed to be updated directly, but via taking turn.
Taking Turn
When it's time for the local player to take turn, your game typically allows the player to perform certain actions according to the game design. These actions normally advance the state of the game, resulting in an updated match data. Your game can then commit the turn using the TakeTurn
method. This method takes as inputs a TurnBasedMatch
object representing the current match, the new match data and a Participant
object representing the next player to take turn.
// 'match' is a TurnBasedMatch object, normally received in the MatchDelegate.
// 'updatedMatchData' is the data representing the new state of the game after the player takes turn.
// 'nextParticipant' is a Participant object representing the next player to take turn,
// determined by your game logic.
// Once the operation completes, the callback will be called with the result, this callback
// is always called on the main thread.
GameServices.TurnBased.TakeTurn(match, updatedMatchData, nextParticipant, (bool success) => {
if (success)
{
// Turn successfully submitted!
}
else
{
// Error taking turn.
}
});
It's up to your game logic to determine the next participant to take turn. In fact, it's totally legal to specify the player who has just taken turn to take the next turn (see Turn-Taking).
As mentioned, you can query for a list of participants using the
Participants
property of theTurnBasedMatch
object. It's important to take into account how automatch works: if there are open automatch slots in a game, you can callTakeTurn
withnull
as thenextParticipant
parameter to indicate that the next participant to play should be one of the automatch participants.
A typical approach is to pass the turn to an automatch participant while there are automatch slots open. When there are no more automatch slots open (everyone has joined), pass the turn to an actual participant.
// Determines the next participant to take turn.
Participant ChooseNextParticipant(TurnBasedMatch match)
{
if (match.HasVacantSlot)
{
// Hand over to an automatch player.
return null;
}
else
{
// Pick a player among match.Participants,
// according to the game's logic.
}
}
In a two-player game, deciding the next participant can be done simply by finding a participant who is not the local player (current participant).
// Determines the next participant to take turn in a 2-player match.
Participant ChooseBetweenTwoParticipants(TurnBasedMatch match)
{
if (match.HasVacantSlot)
{
// Hand over to the automatch player.
return null;
}
else
{
// Otherwise, pick the other participant (not the current one).
foreach (Participant p in match.Participants)
{
if (!p.Equals(match.Self))
{
return p;
}
}
Debug.LogError("Something is wrong: cannot find the opponent!");
}
}
When writing your game's logic to decide who is the next player to play, keep in mind that players may leave the game before it is finished, so you must take extra care not to pass the turn to a player who has left the game (always check the
Status
property of theParticipant
objects).
Leaving a Match
If the player decides to leave an ongoing match, you can call LeaveMatch
or LeaveMatchInTurn
.
If it's not the player's turn, use LeaveMatch
.
// 'match' is a TurnBasedMatch object representing the match to leave.
GameServices.TurnBased.LeaveMatch(match, (bool success) =>
{
if (success)
{
// Successfully left.
}
else
{
// Error leaving match.
}
});
If it's the player's turn, use LeaveMatchInTurn
. In this case you must specify the next participant to take turn.
GameServices.TurnBased.LeaveMatchInTurn(match, nextParticipant.ParticipantId, (bool success) =>
{
if (success)
{
// Successfully left.
}
else
{
// Error leaving match.
}
});
When a participant leaves a match, the other participants will be informed via the
MatchDelegate
. TheStatus
property of the left participant will then beDone
on Game Center platform, andLeft
on Google Play Games platform.
Finishing a Match
If, during the local player's turn, your game determines that the match has come to an end, call the Finish
method.
Finishing a match requires you to set a
MatchOutcome
for each participant in the match, including the left ones.
// Define the match outcome.
MatchOutcome outcome = new MatchOutcome();
foreach (Participant p in match.Participants)
{
// Set the result or ranking for the participant.
// In this example, we decide that all participants have tied.
// Result can be Won, Lost or Tied; you can also set their ranking (1st, 2nd, 3rd, etc.)
MatchOutcome.ParticipantResult result = MatchOutcome.ParticipantResult.Tied;
outcome.SetParticipantResult(p.ParticipantId, result);
}
// Finish the match.
GameServices.TurnBased.Finish(match, finalMatchData, outcome, (bool success) =>
{
if (success)
{
// Finished match successfully.
}
else
{
// Error finishing match.
}
});
Note that when setting the outcome of the match, you can either give a participant a result (Won
, Lost
or Tied
), or a placement (1st, 2nd, 3rd, etc.).
// Give the participant a Won result.
outcome.SetParticipantResult(p.ParticipantId, MatchOutcome.ParticipantResult.Won);
// Give the participant a 4th placement.
outcome.SetParticipantPlacement(p.ParticipantId, 4);
Acknowledging a Finished Match
When a player finishes a match, the other participants in the match are notified to view the match results via the MatchDelegate
. After they do so, they each must acknowledge that they have seen the final match results. This will cause the match to no longer appear in the list of active matches for each player.
When your game receives with a match with an Ended
state, call the AcknowledgeFinished
method.
void OnMatchStarted(bool success, TurnBasedMatch match)
{
// Other logic...
// If the match is in the finished state, acknowledge it.
if (match != null && match.Status == TurnBasedMatch.MatchStatus.Ended)
{
GameServices.TurnBased.AcknowledgeFinished(match, (bool done) => {
if (done)
{
// Acknowledge finished match successfully!
}
else
{
// Error acknowledging finished match.
}
});
}
}
Rematch
If a match has finished and the local player wants to play it again with the same opponents, you can call the Rematch
method. This will create a new match with the local player as the initiating player, and send invitations to other participants of the match.
public void Rematch(TurnBasedMatch match)
{
// Create a quick match with a random opponent. No UI will be shown.
// OnMatchStarted will be called when the operation completes.
GameServices.TurnBased.Rematch(match, OnMatchStarted);
}
// If a match is successfully created, this callback will be called with (true, match).
// Otherwise, it will be called with (false, null).
// This callback is always invoked on the main thread.
void OnMatchStarted(bool success, TurnBasedMatch match)
{
if (success)
{
// A match is created! Go to the game screen and play!
}
else
{
// Show error message.
}
}
Known Issue
As of version 0.9.57, the Google Play Games Plugin for Unity (GPGS) doesn't really invoke the registered invitation delegate and match delegate if the app is launched from a multiplayer notification. This means the InvitationReceivedDelegate
and MatchDelegate
in our API will also not be called in such case. This doesn't occur when the app is running, either in background or foreground; and it also doesn't occur on Game Center platform.
This is a longstanding issue of GPGS and we encourage you to upvote the issue so it attracts more attention from Google and hopefully will be fixed sooner: GPGS Issue #1139