Frame Loop
Overview
Fusion's frame loop is a pair of calls every frame: Client::UpdateFrameBegin(dt) to receive, then Client::UpdateFrameEnd() to send.
Both must run on the same thread, every frame, in that order.
Before the frame loop is running -- while connecting and matchmaking -- the integration must pump RealtimeClient::Service() itself; once UpdateFrameBegin() is driven every frame, it services the transport internally.
The Frame Pair
| Step | Call | Purpose |
|---|---|---|
| 1 | Client::UpdateFrameBegin(dt) |
Service the transport, process incoming state, fire callbacks |
| 2 | (your code) | Read remote state, run game logic, write authority state to Words |
| 3 | Client::UpdateFrameEnd() |
Package and send outgoing state (dirty words, RPCs, StringHeap) |
Client::UpdateFrameBegin(dt)
C++
void Client::UpdateFrameBegin(double dt);
Opens the frame and processes incoming data. The SDK:
- Advances the network clock and internal send clocks by
dt. - Services the realtime client internally (
RealtimeClient::Service(true)) -- no separateService()call is needed while the frame loop runs. - Dispatches the Fusion events queued by the transport (state, RPC and input packets) and updates the Words buffers of remote objects with received data.
- Fires the incoming-data callbacks (see below).
The dt parameter is the elapsed wall-clock time since the last call, in seconds.
This drives network time synchronization.
Call this before your inbound sync -- after UpdateFrameBegin() returns, remote objects' Words buffers contain the latest received state, ready to be read by the integration layer.
Callbacks that fire from within UpdateFrameBegin():
OnObjectReadyfor remote objects that became ready (and have all required objects).OnSubObjectCreatedfor newly created sub-objects.OnObjectOwnerChangedfor ownership transfers.OnObjectOwnerPredictionFailedwhen a Dynamic-mode ownership claim was rejected by the server (see Ownership).OnObjectDestroyed/OnSubObjectDestroyedfor remotely destroyed objects.OnInterestEnter/OnInterestExitfor interest-set transitions.OnRpcfor received RPCs (andOnRpcErrorfor delivery failures).OnMapChangefor map-table updates andOnDestroyedMapActorfor destroyed map actors.OnForcedDisconnectwhen the server force-disconnects the client.
OnInput and OnPredictionReset do not fire here: both fire from inside ObjectRoot::ExecuteInputs(), which the integration calls itself (see the Modes alert below).
If the previous UpdateFrameBegin() was not closed by an UpdateFrameEnd(), the call returns immediately without doing anything.
Client::UpdateFrameEnd()
C++
void Client::UpdateFrameEnd();
Closes the frame and sends outgoing state. When the send-rate clock says it is time to transmit, the SDK:
- Iterates all objects the local client owns (or has authority over).
- Compares each object's Words buffer against its Shadow buffer to detect changes.
- Packs dirty words into a state packet.
- Writes any queued RPCs into the outgoing packet.
- Serializes StringHeap changes for objects with dirty strings.
- Sends the packet via the Notify connection.
- Updates ack tracking for delivery confirmation.
In Client-Server mode (SimulationMode::Authority) the same send clock also flushes pending input from the predicting player to the server before the state send.
Call this after your outbound sync -- authority objects must have their Words buffers populated before UpdateFrameEnd() reads them.
If no UpdateFrameBegin() ran since the last UpdateFrameEnd(), the call is a silent no-op -- nothing is sent and no assertion fires.
The frame pair is identical in both modes.
In Client-Server mode (SimulationMode::Authority) the same loop also drives the input and prediction path: the predicting player gathers input and calls ObjectRoot::QueueInput(), and both sides call ObjectRoot::ExecuteInputs() each frame -- which is where OnInput fires, and where OnPredictionReset fires on the predicting player when authoritative state forces a prediction reconciliation.
See Ownership (Predicted Simulation) and Modes and Topologies.
RealtimeClient::Service() Before the Frame Loop
C++
void RealtimeClient::Service(bool dispatchIncomingCommands = true);
Pumps the Photon transport layer.
Sends outgoing UDP/TCP packets, receives incoming data, handles keepalives and advances the connection state machine.
The dispatchIncomingCommands parameter controls whether received events are dispatched to callbacks immediately.
Call it every frame while not yet driving the frame loop -- during connect, region selection and room join. Without it, the connection state machine stalls: connect attempts never complete, keepalives stop and the server disconnects the client.
C++
realtimeClient.Service(true);
Once UpdateFrameBegin() runs every frame, it services the realtime client internally; an additional external Service() call is unnecessary.
There is also ServiceBasic() which performs a minimal service without dispatching, and SendOutgoingCommands() / DispatchIncomingCommands() for fine-grained control.
Complete Frame Sequence
Here is the recommended integration pattern for a single frame:
C++
void on_frame(double delta) {
// 1. Process incoming state and fire callbacks
// (services the transport internally)
fusionClient->UpdateFrameBegin(delta);
// 2. Read remote state from Fusion buffers (non-authority objects)
for (auto& [id, obj] : fusionClient->AllRootObjects()) {
if (!fusionClient->IsOwner(obj)) {
read_from_words(obj); // Your inbound sync
}
}
// 3. Engine game logic runs (physics, scripts, AI)
// 4. Write local state to Fusion buffers (authority objects only)
for (auto& [id, obj] : fusionClient->AllRootObjects()) {
if (fusionClient->IsOwner(obj)) {
write_to_words(obj); // Your outbound sync
}
}
// 5. Send outgoing state (rate-gated)
fusionClient->UpdateFrameEnd();
}
Call Pairing
UpdateFrameBegin() opens a frame and UpdateFrameEnd() closes it, and the SDK enforces the pairing with an internal flag:
UpdateFrameEnd()silently does nothing unless anUpdateFrameBegin()ran since the last End.- A second
UpdateFrameBegin()without an interveningUpdateFrameEnd()also silently does nothing.
This makes the pair self-correcting -- a mis-ordered loop skips work for a frame instead of corrupting state -- but it also means ordering bugs produce no assertion or log, only silently degraded replication.
The Begin-then-End order gives a single-frame pipeline:
| Phase | Action | Rationale |
|---|---|---|
| 1. UpdateFrameBegin (RECV) | SDK applies incoming state from remotes | Frame starts with the freshest available data |
| 2. Read Words | Integration reads remote state | Game logic sees this frame's incoming state |
| 3. Write Words | Authority writes outbound state | Words buffer populated with this frame's results |
| 4. UpdateFrameEnd (SEND) | SDK packages dirty words and transmits | This frame's writes go out the same frame |
Timing and Send Rate
UpdateFrameEnd() handles rate limiting internally -- call it every frame and let the SDK decide when to actually transmit.
Calling it faster than the send rate is safe; the SDK skips transmission on frames where it is not yet time to send.
The send rate depends on the simulation mode:
- In Shared mode, the interval comes from the
ClientSendRatefield of thefusion_configroom property (default: 30 Hz). There is no client-side setter; the value is applied byClient::Start(). - In Client-Server mode (
SimulationMode::Authority), a separate send clock drives both input and state. Configure it locally withClient::SetAuthoritySendRate()/ read it withGetAuthoritySendRate()(default: 30 Hz).
The dt parameter to UpdateFrameBegin() should be the actual elapsed time.
Do not pass a fixed timestep unless your frame rate is genuinely fixed.
Inaccurate delta values cause network time drift.
UpdateServiceOnly()
C++
void Client::UpdateServiceOnly();
A lightweight alternative that only pumps the socket layer, with event dispatch disabled. Incoming commands are received and queued, not dropped -- they are dispatched once the normal frame loop resumes. Use this during loading screens or scene transitions when you need to keep the connection alive but are not ready to process state updates.
UpdateServiceOnly() replaces the UpdateFrameBegin() / UpdateFrameEnd() pair temporarily.
Resume the normal pair once loading completes.
C++
void on_loading_frame() {
// Keep connection alive during scene load
fusionClient->UpdateServiceOnly();
}
StateUpdatesPause / StateUpdatesResume
C++
void Client::StateUpdatesPause();
void Client::StateUpdatesResume();
Temporarily pauses server-to-client state updates without disconnecting.
StateUpdatesPause() tells the server to stop sending state updates to this client -- use it during map loads and scene transitions so state for the old scene does not pile up while the client cannot process it.
RPCs and connection management continue normally.
Call StateUpdatesResume() after the new scene is loaded and objects are re-bound; the server resumes sending state.
For one second after StateUpdatesResume(), the client ignores incoming server-time samples so that backlogged packets cannot yank the network clock.
C++
// Scene transition
fusionClient->StateUpdatesPause();
unload_old_scene();
load_new_scene();
register_scene_objects();
fusionClient->StateUpdatesResume();
Common Mistakes
| Mistake | Symptom |
|---|---|
Not calling Service() before the frame loop runs |
Connect() task never completes, connection timeout, stuck in connecting state |
| Not calling any update when in a room | Objects never replicate, RPCs never arrive, server disconnects the client |
Passing 0 for dt |
Network time stops advancing, interpolation breaks |
Writing Words after UpdateFrameEnd() |
Changes not sent until next frame |
Calling UpdateFrameEnd() without a preceding UpdateFrameBegin() |
Silent no-op -- nothing is sent that frame, no assertion or log |
Calling UpdateFrameBegin() twice without an UpdateFrameEnd() |
Second call is a silent no-op -- a frame of incoming state processing is skipped |
Related
- Connection -- Connection lifecycle and Service() requirements
- Objects -- Words/Shadow buffers and dirty detection