State scopes
Retain state for individual players or multiplayer sessions using Cloud Code scopes.
Read time 8 minutesLast updated 2 days ago
By default, Cloud Code is stateless, meaning that each time you run a script or module, Cloud Code doesn't remember the data from the previous run. However, for games that require persistent states, such as card, puzzle, and idle games, you can use scopes to preserve information during your scope's lifetime in Cloud Code.
Scopes can retain state for individual players, or a group of players in a multiplayer session.
Implementing Stateful Cloud Code ensures your game scales while operating at minimal cost.
Prerequisites
Before you use Stateful Cloud Code, make sure you meet the following requirements:
- Use Unity 6.7 or later.
- Reference the following NuGet package versions in your module. Modules you create in the Unity Editor reference these packages automatically.
- v0.0.27 or later
com.unity.services.cloudcode.apis - v0.0.9 or later
com.unity.services.cloudcode.core
- If your module uses the scope, install the Multiplayer Services SDK (
MultiplayerSession) in your game client to create and join sessions.com.unity.services.multiplayer
Configuration
To implement state persistence, specify the scope type to determine how to retain your module's class variables within the lifecycle of the selected scope. There are two primary scope types available for configuration:
- : State is accessible only to a specific player. Use the
Playerscope for data that belongs to an individual player, such as progression, currency balances, inventory, or preferences.Playerstate persists until its time to live (TTL) expires, which defaults to two years.Player - : State is accessible to all players in a multiplayer session. Use the
MultiplayerSessionscope for data shared across a session, such as a game board, turn order, or a shared score.MultiplayerSessionstate persists until its time to live (TTL) expires, which defaults to seven days.MultiplayerSession
To configure scoped module functions, annotate your class with and a desired scope type, as demonstrated in the following code examples:
StateScopeusing Unity.Services.CloudCode.Core;// Example module setup for a HelloPlayer module with a Player scope[StateScope(Scope.Player)]public class HelloPlayer{ // Public members are serialized and persisted by default. public int score; // Private members aren't persisted unless you opt them in. [CloudCodeSerializeProperty] private int _counter; // Opt a public member out of persistence, for example, a transient or cached value. [CloudCodeIgnoreProperty] public int sessionCacheHits; [CloudCodeFunction("IncrementPlayer")] public int IncrementPlayer() { return ++_counter; }}
Then, from your game, sign in to get a player ID, and reference that ID for future -scoped server module function calls:
Playerusing UnityEngine;using Unity.Services.Authentication;using Unity.Services.Core;// Example of logging into an anonymous account and referencing the// playerId to be invoked with for future Player-scoped calls.public class CallPlayerCloudCode : MonoBehaviour{ private string m_PlayerID; private async void Start() { await UnityServices.InitializeAsync(); await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Access the currently logged-in player's ID m_PlayerID = AuthenticationService.Instance.PlayerId; }}
Likewise, for scopes, create a session with , and save a session ID for future scoped server module function calls:
MultiplayerSessionMultiplayerServiceusing UnityEngine;using Unity.Services.Authentication;using Unity.Services.Core;using Unity.Services.Multiplayer;// Example of creating a client-hosted session and referencing the// sessionId to be invoked with for future MultiplayerSession-scoped calls.public class CallMultiplayerSessionCloudCode : MonoBehaviour{ private string m_SessionID; private async void Start() { await UnityServices.InitializeAsync(); await AuthenticationService.Instance.SignInAnonymouslyAsync(); var options = new SessionOptions { MaxPlayers = 2 }; var session = await MultiplayerService.Instance.CreateSessionAsync(options); Debug.Log($"Session {session.Id} created! Join code: {session.Code}"); // Access the session's ID m_SessionID = session.Id; }}
Scoped calls using generated modules
The following diagram demonstrates how a function call reaches -scoped or -scoped state, including how every player in the same session shares a single scope instance.
PlayerMultiplayerSessionMultiplayerSessionTo target scoped module functions from your game client, initialize the module with the unique identifier corresponding to the desired player or multiplayer session. This initialization associates the module instance with that ID, ensuring that all subsequent function calls interact with the correct persistent state.
The following code is an example of a Cloud Code function call incrementing a -scoped counter:
Player// Example of an in-game function call to the HelloPlayer modulepublic async Task OnButtonClickedHelloPlayer(){ try { var module = new HelloPlayerBindings(m_PlayerID, CloudCodeService.Instance); var result = await module.IncrementPlayer(); Debug.Log("Player " + "[" + m_PlayerID + $"] Counter: {result}"); } catch (CloudCodeException exception) { Debug.LogException(exception); }}
Scoped calls directly from CloudCodeService
You can call server endpoints directly, without bindings, using on the instance. To make these calls, configure a with the scope type you want and a unique string ID that targets the player or group within that scope.
CallModuleEndpointAsync()CloudCodeServiceCloudCodeScopepublic async Task OnButtonClickedHelloPlayerDirect(){ try { var scope = new CloudCodeScope(ScopeType.Player, m_PlayerID); var result = await CloudCodeService.Instance.CallModuleEndpointAsync<int>( "HelloPlayer", "IncrementPlayer", scope: scope); Debug.Log("Player " + "[" + m_PlayerID + $"] Counter: {result}"); } catch (CloudCodeException exception) { Debug.LogException(exception); }}
Avoid calls into your own scope
Calls into the same scope type and scope ID run one at a time, whichever module they target. Two modules scoped to the same session, or to the same player, count as the same scope.
If a function awaits a call into another module in the same scope, the call waits for the running function to finish, and the running function waits for the call. The wait continues until the gateway times out the request, and the caller fails with a error. The queued call then runs, even though the caller already received the error. Timer callbacks behave the same way.
Gateway TimeoutCaller scope | Target scope | Await the call? |
|---|---|---|
| | Yes. The scopes are different. |
| | Yes. The scopes are different. |
| | No. Same scope. |
| | No. Same scope. |
| Any scope | A stateless module (no | Yes. |
The following function fails with , because both modules use the session scope and the call targets the caller's own session:
Gateway Timeout[StateScope(Scope.MultiplayerSession)]public class Lobby{ readonly IGameApiClient m_Api; public Lobby(IGameApiClient api) { m_Api = api; } [CloudCodeFunction("StartGame")] public async Task StartGame([CloudCodeService] IExecutionContext context) { await m_Api.CloudCode.RunModuleAsync( context, context.ServiceToken, context.ProjectId, "Turns", "Start", new RunModuleArguments( new Dictionary<string, object>(), new ModuleScope(ModuleScopeType.MultiplayerSession, context.ScopeId))); }}
To avoid the wait, use one of the following approaches:
- Keep the logic that both modules need in a plain class, and call its methods directly from each module.
- Put the functions that must run together in the same module, and call them directly as methods instead of through a module call.
- Don't await the cross-module call from inside the running function. Return first, then make the game client call the other module.
A timer callback can't use the last approach. A timer's target function must be in the same module that registers the timer, so a timer callback that awaits a call into the other module waits in the same way.
ScopeType mismatch
If you try to make client calls with a specified other than the one deployed with the module, you will receive a mismatch error.
ScopeTypeScopeTypeThis error can occur when your game client makes the following type of Cloud Code module function call:
- With a scope, but the deployed module has no scope attribute defined.
- Without a scope, but the deployed module has a defined scope attribute.
- With scope, but the deployed module's scope attribute doesn't match.
The following example of a mismatch deploys the module with a session scope, but then calls from the perspective of a scope in the game.
ScopeTypePlayerMultiplayerSessionusing System.Threading.Tasks;using Unity.Services.CloudCode.Core;// Example setup for a HelloPlayer module with a Player scope[StateScope(Scope.Player)]public class HelloPlayer{ public int _counter; [CloudCodeFunction("IncrementPlayer")] public int IncrementPlayer() { return ++_counter; }}// In Game - Note the misconfigured MultiplayerSession Scope - ScopeType mismatchpublic async Task OnButtonClickedHelloPlayerDirect(){ var scope = new CloudCodeScope(ScopeType.MultiplayerSession, m_SessionID); var result = await CloudCodeService.Instance.CallModuleEndpointAsync<int>( "HelloPlayer", "IncrementPlayer", scope: scope); // <--- Will have Error}