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:

  1. Advances the network clock and internal send clocks by dt.
  2. Services the realtime client internally (RealtimeClient::Service(true)) -- no separate Service() call is needed while the frame loop runs.
  3. Dispatches the Fusion events queued by the transport (state, RPC and input packets) and updates the Words buffers of remote objects with received data.
  4. 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():

  • OnObjectReady for remote objects that became ready (and have all required objects).
  • OnSubObjectCreated for newly created sub-objects.
  • OnObjectOwnerChanged for ownership transfers.
  • OnObjectOwnerPredictionFailed when a Dynamic-mode ownership claim was rejected by the server (see Ownership).
  • OnObjectDestroyed / OnSubObjectDestroyed for remotely destroyed objects.
  • OnInterestEnter / OnInterestExit for interest-set transitions.
  • OnRpc for received RPCs (and OnRpcError for delivery failures).
  • OnMapChange for map-table updates and OnDestroyedMapActor for destroyed map actors.
  • OnForcedDisconnect when 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:

  1. Iterates all objects the local client owns (or has authority over).
  2. Compares each object's Words buffer against its Shadow buffer to detect changes.
  3. Packs dirty words into a state packet.
  4. Writes any queued RPCs into the outgoing packet.
  5. Serializes StringHeap changes for objects with dirty strings.
  6. Sends the packet via the Notify connection.
  7. 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.

Modes

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 an UpdateFrameBegin() ran since the last End.
  • A second UpdateFrameBegin() without an intervening UpdateFrameEnd() 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 ClientSendRate field of the fusion_config room property (default: 30 Hz). There is no client-side setter; the value is applied by Client::Start().
  • In Client-Server mode (SimulationMode::Authority), a separate send clock drives both input and state. Configure it locally with Client::SetAuthoritySendRate() / read it with GetAuthoritySendRate() (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
  • Connection -- Connection lifecycle and Service() requirements
  • Objects -- Words/Shadow buffers and dirty detection
Back to top