Documentation

Cloud Code

Timers in CloudBehaviours

Schedule, cancel, and understand the limits of a server-authoritative CloudBehaviour timer.
Read time 4 minutesLast updated 14 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.
Timers are server-authoritative. The cloud schedules a call back into one of your CloudBehaviour's own endpoints, so a player who closes the game can't dodge a turn clock.

Schedule a timer

Scheduling uses the same generated
I{Behaviour}
interface used for cross-behaviour calls. Inject it, name the endpoint with its real arguments, and run the call with
ScheduleAsync(delay)
:
[StateScope(Scope.MultiplayerSession)]public class TurnClock : CloudBehaviour{ public string CurrentTimerId; public int TurnNumber; [CloudCodeAccess(Access.SessionHost)] public async Task StartTurn([CloudCodeService] ITurnClock self, [CloudCodeService] ITimerService timers) { if (CurrentTimerId != null) { var previous = await timers.GetTimerAsync(CurrentTimerId); previous.Cancel(); // Don't leave an orphaned timer holding a slot } CurrentTimerId = await self.EndTurn(TurnNumber).ScheduleAsync(TimeSpan.FromSeconds(30)); } [CloudCodeAccess(Access.Service)] public void EndTurn(int turnNumber) { if (turnNumber != TurnNumber) return; // A stale timer fires against an old turn CurrentTimerId = null; TurnNumber++; }}
Arguments are ordinary typed parameters on the endpoint.
ScheduleAsync
registers a timer against the deployed endpoint and returns the timer's ID.
StartTurn
is restricted to the session host. Without
[CloudCodeAccess]
, any session member could call it repeatedly and fill the session's timer slots. It also cancels the previous timer before scheduling a new one, so no orphaned timer is left running.
EndTurn
clears
CurrentTimerId
when the timer fires, because a fired timer's ID no longer resolves: calling
GetTimerAsync
with it throws. For other ways to restrict who calls an endpoint, refer to Multiplayer game security in CloudBehaviours.
To inject
ITurnClock
, register it in your module's
ICloudCodeSetup
with
config.AddTurnClockClient()
. Refer to Register the generated interface.

Cancel a timer

ICall
has no cancel of its own. Inject
[CloudCodeService] ITimerService
to fetch and cancel a timer by ID.
You don't need
ITimerService
to schedule a timer, only to cancel or query one.
ScheduleAsync
uses the timer service for you. Don't call
ITimerService.RegisterTimerAsync
directly in a CloudBehaviour: the build fails with
CCSG137
.
GetTimerAsync
throws for a timer that has already fired or been cancelled, so clear a stored timer ID in the endpoint the timer calls, as
EndTurn
does in the preceding example.
The following example cancels the current turn's timer:
[CloudCodeAccess(Access.SessionHost)]public async Task CancelTurn([CloudCodeService] ITimerService timers){ if (CurrentTimerId == null) return; var timer = await timers.GetTimerAsync(CurrentTimerId); timer.Cancel(); CurrentTimerId = null;}

Persist the timer ID

The timer service returns a timer's ID only once, when you schedule it, and it has no way to list your timers later. If you'll need to cancel a timer, store its ID in a persisted member. An ID kept only in memory isn't saved: it can survive between calls, but it's lost whenever Cloud Code reloads your CloudBehaviour, for example after a period of inactivity or a redeploy.
In the preceding examples,
CurrentTimerId
is a public field, so it persists by default, but it also synchronizes to every client in the session. To persist the ID without sending it to clients, make it private and mark it
[CloudCodeSerializeProperty]
:
[CloudCodeSerializeProperty]private string _turnTimerId; // Persisted, never sent to a client[CloudCodeAccess(Access.SessionHost)]public async Task StartTurn([CloudCodeService] ITurnClock self){ _turnTimerId = await self.EndTurn(TurnNumber).ScheduleAsync(TimeSpan.FromSeconds(30));}[CloudCodeAccess(Access.SessionHost)]public async Task CancelTurn([CloudCodeService] ITimerService timers){ if (_turnTimerId == null) return; (await timers.GetTimerAsync(_turnTimerId)).Cancel(); _turnTimerId = null;}

Lock the target down, and don't expect a player

A scheduled endpoint is still an endpoint, so a client can call it directly. Mark it
[CloudCodeAccess(Access.Service)]
so the timer still fires while clients are blocked.
Important
A timer-triggered invocation has
Context.PlayerId
set to
null
, and there's no player access token. Use
Context.ServiceToken
for outbound service calls, and pass what the endpoint needs as arguments instead of reading the calling player.
A timer can target only a public instance endpoint on the CloudBehaviour. A private or static method isn't on
I{Behaviour}
, so scheduling one doesn't compile.

What a timer callback pushes

A timer-triggered invocation pushes synchronized state and declared events to the scope's clients: the player on a
Player
-scoped CloudBehaviour, or every session member on a
MultiplayerSession
-scoped CloudBehaviour. If the CloudBehaviour has per-player state views, each client receives its own view.

Limits

Timers have the following limits:
  • The delay must be greater than zero and no more than 24 hours. The 24 hours are shared: the delays of all timers already pending on the same player or session, plus the new one, can't exceed 24 hours.
  • A player or session can hold at most 10 pending timers at once, shared by every module and CloudBehaviour that uses that scope. A timer frees its slot when it fires or when you cancel it.
  • There's no recurring timer. For a repeating clock, have the scheduled endpoint schedule the next one.
  • Only schedule your own CloudBehaviour's endpoints. Calling
    ScheduleAsync
    on another CloudBehaviour's
    I{Behaviour}
    fails the build with
    CCSG132
    .

Additional resources