Documentation

Cloud Code

Stateful Cloud Code

Build event-driven games using Stateful Cloud Code workflows with scopes.
Read time 3 minutesLast updated 2 days ago

Important
This page describes an experimental feature that might change significantly before release. It's not recommended to use or rely on experimental features in production environments due to potential instability.
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.
Note
Stateful Cloud Code requires you to reference the following NuGet package versions in your modules:
  • com.unity.services.cloudcode.apis
    v0.0.26 or later
  • com.unity.services.cloudcode.core
    v0.0.4 or later

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
    : States are only accessible to a specific player and persist for the lifetime of that player. The default time to live for
    Player
    states is two years.
  • MultiplayerSession
    : States are accessible to all players and persist for the lifetime of a game's session. The default time to live for
    MultiplayerSession
    states is seven days.
To configure scoped module functions, annotate your class with
StateScope
and a desired scope type, as demonstrated in the following code example:
// Example module setup for a HelloPlayer module with a Player scope[StateScope(Scope.Player)]public class HelloPlayer{ [CloudCodeSerializeProperty] private int _counter; [CloudCodeFunction("IncrementPlayer")] public int IncrementPlayer() { return ++_counter; }}
Important
By default, the scope's state only serializes and persists the data for
public
members of your module. To allow serialization of
private
members, annotate them with
CloudCodeSerializeProperty
as shown in the previous example. Similarly, you can use
CloudCodeIgnoreProperty
to prevent serialization of
public
members.
Then, from your game, sign in to get a player ID, and reference that ID for future
Player
-scoped server module function calls:
// 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:
// 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

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 also make calls directly to server endpoints without bindings using
CallModuleEndpointAsync()
with the Cloud Code Service instance. To make these calls, configure a
CloudCodeModuleScope
with the desired scope type and unique string ID targeting the player or group within the specified scope type.
public async Task OnButtonClickedHelloPlayerDirect(){ try { var scope = new CloudCodeModuleScope(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); }}

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.
// Example setup for a HelloPlayer module with a Player scope[StateScope(Scope.Player)]public class HelloPlayer{ public int _counter; [CloudCodeFunction("IncrementPlayer")] public int IncrementPlayer() { // ... }}// In Game - Note the misconfigured MultiplayerSession Scope - ScopeType mismatchpublic async Task OnButtonClickedHelloPlayerDirect(){ var scope = new CloudCodeModuleScope(ScopeType.MultiplayerSession, m_SessionID); var result = await CloudCodeService.Instance.CallModuleEndpointAsync<int>( "HelloPlayer", "IncrementPlayer", scope: scope); // <--- Will have Error}