network simulation loop
title: "Network Simulation Loop" tags: "fusion"
Introduction
Fusion runs a discrete tick-based simulation in consistent time steps, regardless of the network mode being used. The overall process handling this is referred to as "Networked Simulation Loop". All logic is written in sub-classes of NetworkBehaviour
or SimulationBehaviour
, which means regular Unity GameObjects are the center piece of this simulation loop.
In addition to moving snapshots of the world state forward, Fusion can also predict future states based on local player input or any physics based object (turning on client side prediction for physics is optional).
It is possible to write client side prediction logic for any object based on current known data and custom code (e.g. extrapolating ballistic trajectories).
This document explains in more detail the most important concepts relating to how Fusion's simulation loop works.
Ticks
There is a naturally occuring timing discrepancies between networked machines, Fusion handles simulation using discrete abstract units of time called ticks instead of driving it by time directly. Ticks are decoupled from the actual real-world time that passes on any specific client and host. Using ticks instead of a hardware clock allows all clients in a network session to share a common frame of reference for the concept of "time". A tick does not drift and is not subject to precision issues, which is crucial to reason accurately about future and past events across multiple networked machines.
The timestep between each tick is defined by the Simulation > Tick Rate
in the NetworkProjectConfig
. The tick rate is defined in Hertz (Hz); so a timestep of 60 is equal to a delta of 1/60th of a second. This means the system will progress the game state using a timestep of 1/60th of a second, regardless of how much actual time actually passed between the previous and the current tick. In code, the timestep is accessible via the NetworkRunner.DeltaTime
property.
IMPORTANT: The tick rate used for the simulation loop is DIFFERENT from the rendering rate (Frame Per Seconds)!
Each tick has a corresponding state snapshot that explicitly states the "truth" of the world at that point in time. To generate a new snapshot for the next tick, Fusion will invoke FixedUpdateNetwork()
on every SimulationBehaviour
and NetworkBehaviour
to update the part of the overall state of a GameObject for which they are responsible. Fusion refers to this as having State Authority
over the given GameObject
.
There is exactly one machine which has State Authority
for any given object.
- In a Client/Server setup, this is always the server.
- In a Client Authority setup where each client owns their own objects, each client usually maintains authority over the objects they create.
Input Handling
Fusion uses input to to drive client-side prediction. The client predicts the next server state based on its local (outdated) information and the local players' input. When the actual state is received, the client will use this new ground truth as basis to re-simulate a new future state.
To make prediction possible, the input handling has been split into two step.
- Input Polling: Fusion collects the input from the local hardware on the client with Input Authority over a certain object.
- Input Consumption: The input is applied by the local simulation and shared with the server to be included in the authoritative simulation.
For more information on input definition, polling and consumption, please read the Network Input
page in the Manual.
Prediction
Prediction involves the client predicting the future server state based on its current local information. The local information based on the true game state snapshot received from the server is always outdated due to the inherit latency between networked devices. The client uses prediction in an attempt to have the local game state keep up with the server game state. This is necessary to mitigate the impact of network latency on the player experience. The network latency is always equal to the roundtrip from server to client and back. It can be reasoned about in terms of discrete ticks.
Imagine a scenario with the following characteristics:
- Current Server Tick: 100
- Tick rate: 1/60th of second
- Client 1's latency is equal to 4 ticks (2 ticks from server to client + 2 ticks from client to server)
- Client 2's latency is equal to 6 ticks (3 ticks from server to client + 3 ticks from client to server)
Although client are aware of their respective latency, it is the server who informs clients of how many ticks they need to predict in order to keep up. Prediction enables clients to send their input in time for when the server will require it for a given tick. The objects a client has Input Authority over will be predicted using the local input (e.g. player character) thus giving the illusion of immediate response; it is called Input Authority because, while it cannot explicitly control the state of the object, it can control the input driving the state.
For instance, when Client 1 moves their player character, it predicts a future state based on the last validated tick it received from the server. Based on this predictions, Client 1 tells the server what it plans to do 2 ticks into the server's future (i.e. Tick 102). This leaves enough time for the input to travel over the wire and be received by the server in time for it to be consumed during the simulation of the validated state for Tick 102. As long as nothing interfered with Client 1's player movement, the predicted Tick 102 on Client 1 and the validated Tick 102 on the server will be in the same state.
To ensure Client 1 predicts enough ticks ahead in the future to compensate for latency, the server tells it to always be at least 2 ticks ahead of the server state. Client 2 on the other hand needs to predict 3 ticks in the future to keep up due to its higher latency.
As can be gathered from the figures, Client 1 and Client 2 are NOT in the exact same state due to their relative latencies to the server. The clients are aware of the fact that their "truth" is erroneous as the information from the other clients has not reached them yet. This is where replication and reconciliation come into play.
Replication
Fusion manages and simulates state progression in compact memory buffers. When Fusion receives a state snapshot from the server it unpacks the snapshot into the memory buffer. Since the current state kept in the buffer was based on a predicted state, replacing it with the state from the snapshot means the client is effectively snapped back in time to the tick associated with the snapshot received from the server. Fusion then runs the simulation loop once for every tick between the the received state and the client's locally predicted state; this brings the state back where it was before receiving the snapshot, except with a more accurate predicted state.
Reconciliation
When the client receives a new snapshot with a more recent -and therefore more accurate- state of the world, the client reconciliates its local state with the one it. The reconciliation is done by replicating the last server snapshot received by the client and resimulating the local game state from the tick associated with the server snapshot up to the current tick.
The end result is that the server is always in a solid reconciled state and the clients will progressively self-adjust when unexpected events occur. This procedure is referred to as Re-simulation in Fusion, as it re-simulates the effect of old input based on a new validated state to reconciliate the client's state with the server.
This is particularly important if Client 1 and 2 collided on the server. Their positions will be resolved by the server since it exclusively decides on the end state of any object for any given tick; this is the reason why the server is said to have State Authority.
Example
The server sends a steady stream of old data to the clients. When the validated Tick 100 is received by Client 1, it will replicate the validated state of Tick 100 into its memory buffer thus overwritting its previously predicted Tick 100.
- If nothing unexpected happened then the locally predicted Tick 100 will already have been the correct state which means Client 1 and the server are in perfect synchronisation, except Client 1 being 2 Ticks ahead of the server.
- If the validated Tick did not match Client 1's prediction, the validated Tick 100 will be used as a corrected state.
In either scenario, the client will re-apply its input sequentially from 100, 101 and 102 to arrive at a new Tick 103.
Moving forward in time from the perspective of Client 1, it will be in Tick 104 when it receives the corrected Tick 101, applies its input for 102 and 103 and arrives at a corrected state.
Client 2 goes through the exact same procedure during its simulation loop. The only difference is a slight delay in receiving the server validated Tick 1 tick due to its higher latency which it compensates for by predicting further ahead.
FixedUpdateNetwork()
To advance the current tick state to the next tick state, Fusion calls FixedUpdateNetwork()
on all SimulationBehaviour
and NetworkBehaviour
components in the scene. Since FixedUpdateNetwork
is called during both the replication and prediction process, the local state should only be derived from the network state; applying progressive delta updates to the local state from FixedUpdateNetwork
will most likely result in too many changes being applied.
Snapshot Interpolation
Normally each client only have Input Authority over a single, or a select few, GameObjects
; the remaining GameObjects
have their state fed to the client via snapshot updates. Unlike predicted states, snapshots are always behind the server since there is some latency between the time is sent by the server and the time is received and replicated by the client.
Fusion refers to these "remotely" controlled GameObjects
as Proxies.
If snapshots were applied as-is to the current (visual) state of the proxy, it would appear to animate and update at the tick rate of the simulation instead of the render rate of the client. This is to be avoided for several reasons:
- To conserve bandwidth. It is often desirable to run the simulation at a much lower frequency than rendering. For example a 30Hz network simulation frequency and a 120Hz rendering frequency.
- Network packets do not arrive at a consistent rate. Tying the game's rendering frequency to an event that has a large random aspect to it will cause jittery movement.
To decouple the network packet-frequency from the refresh rate of the rendering while providing a smooth state transition, Fusion interpolates the "render state" between two snapshots. The interpolation aims to be smooth in real-time rather than ticks; ideally the interpolation is based on the two most recent snapshot available. However, snapshots may not arrive at a constant rate. Therefore there is a risk of the constant-rate interpolation catching up with the not-constant network snapshots unless a small margin (buffer) prevents this from happening.
One of Fusions many strengths is the interpolation algorithm which adapts the offset and buffering required based on current network conditions. This gives the client as little latency as possible while still providing smooth visuals.
Fusions core components like NetworkTransform
uses interpolation to transform its visual components, but you can access and use Fusions built-in interpolators even for your custom networked properties. To get an interpolator for a custom (networked) property, simply call GetInterpolator<T>(nameof(MyProperty))
. This will return an Interpolator
for MyProperty
of type T
. The interpolated value at the current render time is available as Interpolator.Value
.
In rare cases you may need to use the type-less overload of GetInterpolator
. This alternative method provides direct access to the memory buffers of the property for the relevant ticks and require you to understand how to convert this data to the proper type and interpolate it.
Finally, you may access the core interpolation data by simply calling GetInterpolationData
, however, this will provide the raw pointer to the entire NetworkBehaviour
leaving both indexing and decoding entirely in the hands of the application.