PlayFab integration
This guide shows you how to persist player information across game sessions using PlayFab. It uses the Unity starter project as a base. PlayFab is a backend platform which allows you to authenticate players, store custom player data and perform other player management features.
In most SpatialOS games, every player is represented as an entity. This allows you to store player information in components during the player’s session. However, when the player leaves the game, the player’s entity is usually deleted along with all of the information stored in the entity’s components.
In order to keep information about the player for the next time they login, you need to save this data somewhere outside of the entity. One method is to use a third party service such as PlayFab.
PlayFab lets you read and write data about a player in a key-value store. In the example in this guide, each player has an outfit. This can be changed by the player and is seen by other players in the game. The outfit selected by a player must persist even when they log out and log back in.
Background about PlayFab
PlayFab provides a Client API and a Server API. The Client API is used for authenticating the player with PlayFab, and reading data about the player. The Server API has full authority over all the players, and it can be used to modify the data for any player.
This means that in SpatialOS, the player can log in to PlayFab from the client worker, and then the Server API can be used by the managed workers to send updates about players.
1. Install the PlayFab SDK
Install the PlayFab Unity Editor Extensions and the PlayFab Unity SDK by following the PlayFab Unity Getting Started guide up to the “Making your first API Call” section.
Enable the Server API: in the PlayFab Editor Extensions, go to the
Settings -> API
tab. Select theEnable Server API
box:
2. Set the PlayFab developer secret key
To use the Server API, PlayFab requires the developer secret key.
So you don’t need to recompile if the secret key changes, you pass the secret key as a command line argument. This also allows you to only give the secret key to the UnityWorkers and not the UnityClients. This is essential to prevent a player hacking a client and retrieving the secret key.
Pass a command-line argument
The command-line arguments used to launch each worker are defined in the worker launch configuration files. To add a command line argument:
Open
workers/unity/spatialos.UnityWorker.worker.json
. The command-line arguments for each target are defined in the arguments object.Modify the arguments for each target to include
+playFabSecret {{SECRET HERE}}
so it looks like this:... "managed": { "linux": { ... "arguments": [ ... "+playFabSecret", "85CG8BAI71IZYWDA9K9KK4..." ] }, "windows": { ... "arguments": [ ... "+playFabSecret", "85CG8BAI71IZYWDA9K9KK4..." ] }, "macos": { ... "arguments": [ ... "+playFabSecret", "85CG8BAI71IZYWDA9K9KK4..." ] } } ...
- In
Bootstrap.cs
, at the beginning of theStart()
method, extract the secret key from the command line arguments and set the value in thePlayFabSettings
class. Add the following import statements.
using PlayFab; using Improbable.Unity.Util; ... public void Start() { PlayFabSettings.DeveloperSecretKey = CommandLineUtil.GetCommandLineValue(Environment.GetCommandLineArgs(), "playFabSecret", ""); ... }
- In
3. Get the player’s PlayFabId
The Server API uses the PlayFabId to identify players. You can keep the PlayFabId in a component on the player entity. The UnityWorkers can then use this when they are sending updates to PlayFab.
Each login request on the Client API returns the PlayFabId. A simple implementation could be for the UnityClient to send over the PlayFabId to the UnityWorker and then it can be added into the player component when the player entity is created. The problem is, the UnityClient could be hacked, and then a different PlayFabId could be sent to the server, effectively allowing someone to login as a different player.
To solve this, you can use the SessionTicket which is also in the result of a PlayFab login request. You send the session ticket to the UnityWorker when the client is requesting a player entity. The UnityWorker can then use the Server API endpoint AuthenticateSessionTicket to authenticate this ticket and get the player’s PlayFabId back in the response. You can then save the PlayFabId in the player’s component when the player entity is created.
The resulting flow is as follows:
Log in to PlayFab on the client side
PlayFab provides multiple ways for a player to log in to PlayFab using the Client API. The documentation for these login requests can be found here.
Create a new script to handle the logging into PlayFab - you can call it
PlayFabLogin.cs
In the
Awake()
method, which is called when the script is enabled, use the PlayFab Client API to login the player. This example uses the PlayFab method LoginWithCustomID:using System.Collections; using System.Collections.Generic; using UnityEngine; using PlayFab.ClientModels; using PlayFab; public class PlayFabLogin : MonoBehaviour { private string sessionTicket = null; public void Awake() { var request = new LoginWithCustomIDRequest{ CustomId = SystemInfo.deviceUniqueIdentifier, CreateAccount = true }; PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnLoginFailure); } private void OnLoginSuccess(LoginResult result) { Debug.Log("Login successful"); sessionTicket = result.SessionTicket; } private void OnLoginFailure(PlayFabError error) { Debug.LogError("Login error:"); Debug.LogError(error.GenerateErrorReport()); } }
- Once the player has logged in, you can download the initial player data. This ensures that the player entity is spawned with the up-to-date data from PlayFab, instead of downloading the data after the player is spawned.
Add another method
GetUserData()
toPlayFabLogin.cs
, and call this method after the user logs in, inOnLoginSuccess
:private void OnLoginSuccess(LoginResult result) { ... GetUserData (); } ... private void GetUserData() { var request = new GetUserDataRequest { }; PlayFabClientAPI.GetUserData(request, OnGetDataSuccess, OnGetDataFailure); } private void OnGetDataSuccess(GetUserDataResult result) { string outfit = "default_outfit"; if (result.Data.ContainsKey("outfit")) { outfit = result.Data["outfit"].Value; } Debug.LogFormat("session ticket: {0}, outfit: {1}", sessionTicket, outfit); } private void OnGetDataFailure(PlayFabError error) { Debug.LogError("Playfab error:"); Debug.LogError(error.GenerateErrorReport()); }
Enable the script by attaching it to the
GameEntry
game object in the UnityClient.Connect a UnityClient by pressing
Play
in your Unity project. You should see:
Connect to SpatialOS after login
Currently, UnityClient is connecting to SpatialOS as soon as the scene loads. This connection
happens in the Start()
method of Bootstrap.cs
.
With this integration, you want to connect to SpatialOS only after the player logs into PlayFab.
Modify Bootstrap.cs
to achieve this:
In
Bootstrap.cs
, add a field to store theCreatePlayerRequest
object which will be sent with theCreatePlayer
command.Add the following
StartClientConnection()
method forPlayFabLogin.cs
to call:private CreatePlayerRequest createPlayerRequest; public void StartClientConnection(string sessionTicket, string outfit) { SpatialOS.Connect (gameObject); }
- Move the line
SpatialOS.Connect(...)
to the location in the snippet below. This is because you only want to connect to SpatialOS immediately on the UnityWorkers:
public void Start() { ... switch (SpatialOS.Configuration.WorkerPlatform) { case WorkerPlatform.UnityWorker: Application.targetFrameRate = SimulationSettings.TargetServerFramerate; SpatialOS.OnDisconnected += reason => Application.Quit(); SpatialOS.Connect (gameObject); break; case WorkerPlatform.UnityClient: Application.targetFrameRate = SimulationSettings.TargetClientFramerate; SpatialOS.OnConnected += CreatePlayer; break; } // This is where you needed to move the line `SpatialOS.Connect(...)` from }
- Move the line
Add the following
Object.FindObjectOfType<Bootstrap>()...
line to theOnGetDataSuccess
method inPlayFabLogin.cs
:using Assets.Gamelogic.Core; ... private void OnGetDataSuccess(GetUserDataResult result) { ... Debug.LogFormat("session ticket: {{SESSION_CODE_HERE}}, outfit: {1}", sessionTicket, outfit); Object.FindObjectOfType<Bootstrap>().StartClientConnection(sessionTicket, outfit); }
This starts the client’s connection to SpatialOS after the client has logged in and got the initial player data.
Send the session ticket and the data to the PlayerCreator
Once you have retrieved the session ticket and the player data, you need to send these to the PlayerCreator to verify the session token, and if successful create the player entity.
In the StarterProject, the player entity is spawned by a command being sent to the PlayerCreator entity from
Bootstrap.cs
. This command is then received inPlayerCreatingBehaviour.cs
on the UnityWorker side, where the player entity is then created. See Client connection lifecycle for more details.In order to include the session ticket and the player data when the player entity is spawned, you need to send these over in the command request object:
- The command that
Bootstrap.cs
sends to create the player entity is defined inschema/improbable/core/PlayerCreation.schema
. Open this file and modify theCreatePlayerRequest
type to include the session ticket and the outfit (or any player data):
type CreatePlayerRequest { string session_ticket = 1; string outfit = 2; }
- The command that
Run
spatial worker codegen
.Modify the
StartClientConnection()
method to include the session ticket and player data in theCreatePlayerRequest
object that will be sent with theCreatePlayer
command:public void StartClientConnection(string sessionTicket, string outfit) { createPlayerRequest = new CreatePlayerRequest (sessionTicket, outfit); SpatialOS.Connect (gameObject); }
- The command is sent to the PlayerCreator entity in the
RequestPlayerCreation()
method. Modify this method to include the command request object that contains the session ticket and the player data:
private void RequestPlayerCreation(EntityId playerCreatorEntityId) { SpatialOS.WorkerCommands.SendCommand(PlayerCreation.Commands.CreatePlayer.Descriptor, createPlayerRequest, playerCreatorEntityId) .OnSuccess(response => OnCreatePlayerCommandSuccess(response, playerCreatorEntityId)) .OnFailure(response => OnCreatePlayerCommandFailure(response, playerCreatorEntityId)); }
- The command is sent to the PlayerCreator entity in the
Verify the session ticket
When the PlayerCreator recieves the CreatePlayer
command, it is now given a PlayFab session ticket
as well as the initial player data.
To make requests to PlayFab from the managed workers, a player’s PlayFabId is needed. The session ticket can be used to verify the player’s login and retrieve their PlayFabId.
The CreatePlayer
command is received by the OnCreatePlayer()
method in PlayerCreatingBehaviour.cs
:
In
PlayerCreatingBehaviour.cs
, add the followingGetPlayerPlayFabId()
method:using System; using PlayFab.ServerModels; using PlayFab; ... private void GetPlayerPlayFabId(string sessionTicket, Action<string> onSuccess, Action<PlayFabError> onFailure) { var request = new AuthenticateSessionTicketRequest() { SessionTicket = sessionTicket }; PlayFabServerAPI.AuthenticateSessionTicket(request, (result) => { var playfabId = result.UserInfo.PlayFabId; onSuccess(playfabId); }, onFailure); }
This method uses the AuthenticateSessionTicket endpoint of the PlayFab Server API to verify the session ticket and get the player’s PlayFabId.
- Modify the
OnCreatePlayer()
method to first get the PlayFabId before creating the player entity:
private void OnCreatePlayer(ResponseHandle<PlayerCreation.Commands.CreatePlayer, CreatePlayerRequest, CreatePlayerResponse> responseHandle) { GetPlayerPlayFabId (responseHandle.Request.sessionTicket, (playfabId) => { var clientWorkerId = responseHandle.CallerInfo.CallerWorkerId; Debug.LogFormat("Playfab Id: {0}, outfit: {1}", playfabId, responseHandle.Request.outfit); var playerEntityTemplate = EntityTemplateFactory.CreatePlayerTemplate(clientWorkerId); SpatialOS.Commands.CreateEntity (PlayerCreationWriter, playerEntityTemplate) .OnSuccess (_ => responseHandle.Respond (new CreatePlayerResponse ((int) StatusCode.Success))) .OnFailure (failure => responseHandle.Respond (new CreatePlayerResponse ((int) failure.StatusCode))); }, (error) => { responseHandle.Respond (new CreatePlayerResponse ((int) StatusCode.ApplicationError)); }); }
- Modify the
Add PlayFabId and player data to player components
In order to identify players when the UnityWorkers make Server API requests, they need to know the player’s PlayFabId. To achieve this, you can keep the PlayFabId in a component.
You can also add a component to store the player data, in this case the outfit. This component can be updated during the game, and the updates can be sent to PlayFab.
Add the following components. Create these schema files in the
/schema
directory:PlayFabData.schema
: holds the player’s PlayFabId, which can be used by the UnityWorkers when they make requests to the PlayFab Server API.package improbable.player; component PlayFabData{ id = 1005; string playfab_id = 1; }
PlayerOutfit.schema
: hold the player’s outfit id, which can be used by the client to render the correct player outfits.
package improbable.player; component PlayerOutfit { id = 1004; string outfit = 1; }
Run
spatial worker codegen
Modify
CreatePlayerTemplate()
inEntityTemplateFactory.cs
to add the components you just defined:public static Entity CreatePlayerTemplate(string clientId, string playFabId, string outfit) { var playerTemplate = EntityBuilder.Begin() ... .AddComponent(new PlayerOutfit.Data(outfit), CommonRequirementSets.SpecificClientOnly(clientId)) .AddComponent(new PlayFabData.Data(playFabId), CommonRequirementSets.PhysicsOnly) .Build(); return playerTemplate; }
- In
PlayerCreatingBehaviour.cs
, modify the linevar playerEntityTemplate = ...
to include the PlayFabId and the player outfit:
var playerEntityTemplate = EntityTemplateFactory.CreatePlayerTemplate(clientWorkerId, playfabId, responseHandle.Request.outfit);
- In
Build the workers, run a local deployment and connect a UnityClient. If you open the inspector and inspect the player’s entity, you should see the PlayFabId:
4. Save player data updates to PlayFab
Now you have a PlayFabId kept in a player component, you can make any updates to a player by including the PlayFabId in the Server API calls from the UnityWorkers.
It’s up to you when to send player updates to PlayFab.
Update PlayFab on every component change
You could send an update to PlayFab every time a component value which you are synchronising with PlayFab changes.
The benefit of this method is that PlayFab is kept as up-to-date with the component value as it can be, as it receives an update as soon as the value changes.
To implement this behaviour:
Create a new script
PlayFabOutfitSaver.cs
. In this script, add readers forPlayerOutfit
andPlayFabData
, and add an annotation to make it only run on the UnityWorkers:using System.Collections.Generic; using UnityEngine; using Improbable.Unity.Visualizer; using Improbable.Unity; using Improbable.Player; using PlayFab.ServerModels; using PlayFab; [WorkerType(WorkerPlatform.UnityWorker)] public class PlayFabOutfitSaver : MonoBehaviour { [Require] private PlayFabData.Reader playFabDataReader; [Require] private PlayerOutfit.Reader outfitReader; }
- Add a method
OutfitUpdated()
as a callback for when the player outfit changes. This method uses the Server API to update the player data with the new outfit. The player is identified by the PlayFabId stored in thePlayFabData
component you made earlier.
private void OutfitUpdated(string newOutfit) { var data = new Dictionary<string, string> { {"outfit", newOutfit} }; var request = new UpdateUserDataRequest() { PlayFabId = playFabDataReader.Data.playfabId, Data = data }; PlayFabServerAPI.UpdateUserData (request, OnUpdateUserDataSuccess, OnPlayFabFailure); } private void OnUpdateUserDataSuccess(UpdateUserDataResult result) { Debug.Log("Outfit updated successfully"); } private void OnPlayFabFailure(PlayFabError error) { Debug.LogError("Playfab error:"); Debug.LogError(error.GenerateErrorReport()); }
- Add a method
Add
OnEnable()
andOnDisable()
methods to register and deregister a callback for when the player outfit changes:public void OnEnable() { outfitReader.OutfitUpdated.Add (OutfitUpdated); } public void OnDisable() { outfitReader.OutfitUpdated.Remove (OutfitUpdated); }
- Now add this script to the player prefab, build all of the entity prefabs and build all of the workers. Any changes to the outfit component will now be saved to PlayFab.
The next time the player starts the game, their player entity will contain the outfit value that they set in the previous game session.
Other times to update PlayFab
The above method of updating PlayFab on every component change would not be suitable for a value that frequently changes, as every change triggers an HTTP request to PlayFab. This could overload the worker, or possibly cause a rate limit to be set by PlayFab.
If you expect the component value could change frequently, you may consider rate limiting the updates to PlayFab yourself. Alternatively, you could just send one update when the client disconnects. You would need to be careful here that the player disconnecting logic is always run. For example, if a deployment is stopped, the player disconnection logic would not be run, and PlayFab may not be updated correctly.