Event Caching
The Event Cache
An event sent with a caching option is stored by the server as part of the room state.
Every player who joins later receives the cached events automatically, as if it had been present when they were sent.
The operation is selected per send through EventOptions.Caching; see Custom Events for the sending basics.
A joining player first receives the room and player properties, then the cached events, and only then the live events sent after the join. The cached events arrive in the order they reached the server, so the replay reproduces the original sequence. Only events sent unreliably can deviate from the send order, because their arrival order at the server is not guaranteed.
The smallest useful example of the pattern is a round timer: cache one round-start event carrying the start timestamp. Every player who joins mid-round receives it during the replay and derives the remaining round time locally, with no extra protocol.
Caching Options
EventOptions.Caching selects the cache operation and defaults to EventCache::DoNotCache.
| Value | Meaning |
|---|---|
DoNotCache |
Deliver to the current players only. The default. |
MergeCache |
Key/value-oriented merge into the sender's cached event. |
ReplaceCache |
Key/value-oriented replace of the sender's cached event. |
RemoveCache |
Remove the sender's matching cached event. |
AddToRoomCache |
Add the event to the room cache, tied to the sending player. |
AddToRoomCacheGlobal |
Add the event to the room cache without an owner, so it outlives the sender. |
RemoveFromRoomCache |
Remove matching events from the room cache. |
RemoveFromRoomCacheForActorsLeft |
Remove cached events of players who already left. |
SliceIncIndex |
Increment the cache slice index. |
SliceSetIndex |
Set the cache slice index to CacheSliceIndex. |
SlicePurgeIndex |
Purge the cached events of one slice. |
SlicePurgeUpToIndex |
Purge all slices up to CacheSliceIndex. |
The values fall into three groups.
MergeCache, ReplaceCache and RemoveCache manage a per-actor cache slot and are oriented at key/value event payloads.
AddToRoomCache, AddToRoomCacheGlobal, RemoveFromRoomCache and RemoveFromRoomCacheForActorsLeft manage the room cache, an ordered list that can hold any number of events.
The Slice values carry a cache instruction instead of a payload to cache; they control the slice mechanism described further down.
An event is not added to any cache when its options target a subset of the room: TargetGroup = ReceiverGroup::MasterClient, a non-empty TargetPlayers or a non-zero InterestGroup all disable caching for that send.
Actor Cache and Room Cache
Every cached event is stored with an owner, and that owner determines the event's delivery and lifetime.
Events cached with MergeCache belong to the actor cache.
Events cached with AddToRoomCache belong to the room cache but its lifetime is tied to the sending player.
Events cached with AddToRoomCacheGlobal belong to the global room cache and are not tied to any player: their owner is the room itself, addressed as player number 0 when removing.
Cached events are replayed to every joining player, and rejoining is no exception: a player that leaves and comes back receives its own cached events again.
For global room cache events the sender id is always 0.
When a player leaves the room for good, the server drops that player's actor and room cache events automatically (see the cleanup notes below).
A global room cache event survives until it is removed explicitly or the room closes.
Choose AddToRoomCache for state that should disappear with its player, and AddToRoomCacheGlobal for world state that must outlive whoever happened to send it.
Cache a level-state event so late joiners receive it, and remove it once it no longer applies:
C++
struct LevelState
{
int32_t OpenedDoors;
};
constexpr uint8_t LevelStateCode = 4;
LevelState state{3};
// Cached without an owner: it survives even if this client leaves.
EventOptions cache;
cache.Caching = EventCache::AddToRoomCacheGlobal;
client.SendEvent(LevelStateCode, state, cache);
// Later: remove the matching cached event once the state is obsolete.
EventOptions remove;
remove.Caching = EventCache::RemoveFromRoomCache;
client.SendEvent(LevelStateCode, state, remove);
Removing Cached Events
A send with RemoveFromRoomCache does not deliver anything.
Its fields form a filter, and every cached event matching the filter is removed from the room cache.
Two fields make up the filter: the event code and the selected senders, the payload plays no part in the match.
An event code of 0 acts as a wildcard and matches every cached code.
TargetPlayers selects which players' cached events are searched: include 0 to address the global room cache entries, or leave the vector empty to search the entries of all senders.
The payload is ignored entirely: a removal with a different payload, or with none at all, removes the same entries, so same-code events cannot be removed selectively by content.
When a player leaves for good, the server removes that player's actor and room cache events automatically, so joins do not replay the past of players who are gone.
A player that is merely inactive, it left with willComeBack in a room with a player TTL, keeps its cached events until it times out or leaves for good.
Use RemoveFromRoomCacheForActorsLeft to clean up events from the global room cache from players that already left.
Cache Slices
Cache slices segment the room cache into numbered epochs: the room starts at slice index 0, every cached event is stored in the current slice, and the slice operations move the index or discard whole slices at once.
This suits round-based games, each round caches into its own slice, and a finished round is purged in one call instead of event by event.
SliceIncIndex advances the index by one and SliceSetIndex sets it to EventOptions.CacheSliceIndex; SlicePurgeIndex deletes the cached events of the slice given by CacheSliceIndex, and SlicePurgeUpToIndex deletes the slices up to it.
A slice-operation send is not delivered as an event; the other players observe only the index change.
Index changes are reported to the room through the OnCacheSliceChanged callback, which receives the new index.
A joining client can pass JoinRoomOptions.CacheSliceIndex to start the replay at a chosen slice instead of receiving the full cache.
Advance the slice when a new round starts, and drop the finished round's events later:
C++
constexpr uint8_t RoundStateCode = 6;
// Every client learns about slice changes through the callback.
RealtimeCore::Common::ScopedSubscription sliceChanges = client.OnCacheSliceChanged.Subscribe(
[](int cacheSliceIndex) {
// A new round started; cached events land in this slice from now on.
});
// Advance to the next slice when a new round starts.
EventOptions nextRound;
nextRound.Caching = EventCache::SliceIncIndex;
client.SendEvent(RoundStateCode, static_cast<uint8_t>(1), nextRound);
// Later: drop round 0's cached events.
EventOptions cleanup;
cleanup.Caching = EventCache::SlicePurgeIndex;
cleanup.CacheSliceIndex = 0;
client.SendEvent(RoundStateCode, static_cast<uint8_t>(0), cleanup);
A client that joins for the running round skips the earlier slices:
C++
JoinRoomOptions lateJoin;
lateJoin.CacheSliceIndex = 1;
auto join = client.JoinRoom(PHOTON_STR("match-42"), lateJoin);
Keeping the Cache Small
The cache is replayed on every join, so its size directly stretches every future join. A joining player is fully in the room only once the replay completes, and live events sent in the meantime are delivered only after the cached ones. Keep the cache to what a late joiner genuinely needs.
The server limits the total number of cached events per room, counting actor cache, room cache and global room cache entries together. A room that exceeds the limit is closed: the players already in the room can continue, but no new player can join and inactive players cannot rejoin.
Remove events with RemoveFromRoomCache the moment they stop being relevant and purge whole game phases through cache slices.
For state with a single current value per room, custom properties are often the better fit than a growing event cache.