Documentation

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.
    • com.unity.services.cloudcode.apis
      v0.0.27 or later
    • com.unity.services.cloudcode.core
      v0.0.9 or later
  • If your module uses the
    MultiplayerSession
    scope, install the Multiplayer Services SDK (
    com.unity.services.multiplayer
    ) in your game client to create and join sessions.

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:
  • Player
    : State is accessible only to a specific player. Use the
    Player
    scope for data that belongs to an individual player, such as progression, currency balances, inventory, or preferences.
    Player
    state persists until its time to live (TTL) expires, which defaults to two years.
  • MultiplayerSession
    : State is accessible to all players in a multiplayer session. Use the
    MultiplayerSession
    scope for data shared across a session, such as a game board, turn order, or a shared score.
    MultiplayerSession
    state persists until its time to live (TTL) expires, which defaults to seven days.
To configure scoped module functions, annotate your class with
StateScope
and a desired scope type, as demonstrated in the following code examples:
using 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; }}
Important
By default, the scope's state serializes and persists only the
public
members of your module. To persist a
private
member, annotate it with
CloudCodeSerializeProperty
, and to exclude a
public
member, annotate it with
CloudCodeIgnoreProperty
, as shown in the previous examples. For more information, refer to State serialization.
Then, from your game, sign in to get a player ID, and reference that ID for future
Player
-scoped server module function calls:
using 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
MultiplayerSession
scopes, create a session with
MultiplayerService
, and save a session ID for future scoped server module function calls:
using 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
Player
-scoped or
MultiplayerSession
-scoped state, including how every player in the same session shares a single
MultiplayerSession
scope instance.
To 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.
Note
You need to re-generate your modules after adding
StateScope
annotations before you can implement them and call them from within your game.
The following code is an example of a Cloud Code function call incrementing a
Player
-scoped counter:
// 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
CallModuleEndpointAsync()
on the
CloudCodeService
instance. To make these calls, configure a
CloudCodeScope
with the scope type you want and a unique string ID that targets the player or group within that scope.
public 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
Gateway Timeout
error. The queued call then runs, even though the caller already received the error. Timer callbacks behave the same way.

Caller scope

Target scope

Await the call?

MultiplayerSession
with ID
X
Player
with any player ID
Yes. The scopes are different.
Player
with ID
P
MultiplayerSession
with any session ID
Yes. The scopes are different.
MultiplayerSession
with ID
X
MultiplayerSession
with ID
X
No. Same scope.
Player
with ID
P
Player
with ID
P
No. Same scope.
Any scopeA stateless module (no
StateScope
)
Yes.
The following function fails with
Gateway Timeout
, because both modules use the session scope and the call targets the caller's own session:
[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
ScopeType
other than the one deployed with the module, you will receive a
ScopeType
mismatch error.
This 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
ScopeType
mismatch deploys the module with a
Player
session scope, but then calls from the perspective of a
MultiplayerSession
scope in the game.
using 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}