Networking Controller Code
Overview
Fusion has its own simulation timing segment called FixedUpdateNetwork()
which fundamentally is very similar to Unity's FixedUpdate()
in that it is a fixed interval timing segment that executes forward once enough actual time has passed - and that it represents Simulation.
FixedUpdateNetwork()
however is called and managed by Fusion. The most notable difference between FixedUpdateNetwork()
and FixedUpdate()
is that in Server/Client topologies (not in Shared Mode) Fusion will regularly reset State (Networked Properties) on Clients when Server updates arrive, and then re-simulate from that remote Server Tick state forward to the current local Tick.
Also, in ALL topologies Fusion captures State immediately after all FixedUpdateNetwork()
callbacks have executed for a Tick. This makes FixedUpdateNetwork()
the correct location for all simulation code - as FixedUpdateNetwork()
and Simulation are essentially synonymous.
FixedUpdate()
in contrast is completely divorced from FixedUpdateNetwork()
, and should generally be avoided.
Controller Code in Server Client Modes
Using any of the Server/Client topologies (Dedicated Server/Host/Single), it is critical that all controller code simulate inside of FixedUpdateNetwork()
. This is important to remember when importing code from the Asset Store or even reusing old code of your own, as typically these will use Update()
and FixedUpdate()
timing segments.
FixedUpdateNetwork()
is very similar to Unity's FixedUpdate()
in that it is a fixed interval timing segment that increments forward when enough game time has passed (accumulated). The primary difference though is that FixedUpdateNetwork()
in Server Mode will regularly run ticks again.
Any simulation code outside of this will not be aware of re-simulations and will not execute for them, resulting in constant Client desyncs.
Exception to FixedUpdateNetwork Requirement
Simulation code inside of Update()
or FixedUpdate()
can give acceptable results if:
- If you are NOT employing any kind of client prediction for the Object (it exists entirely in the remote timeframe on clients), and only the Server is applying changes to the Object state; AND
- Clients do not need smooth interpolation of the changes made to that Object.
Controller Code in Shared Mode
Shared Mode simulates on fixed Ticks, same as Server Client Modes (Server/Host/Single). However developers may to want to apply input in Update()
as is common in Unity, rather than in FixedUpdateNetwork()
for reasons such as;
- Results in the snappiest user input response
- Existing assets may be coded using
Update()
- Other component systems and assets may expect changes to be made in
Update()
This puts the controlled Object ahead of even the Local Render Timeframe (which is a lerp between the last two simulated Ticks), and results in the Object simulating AND rendering in the real local player time.
However, moving a controlled Object in Update()
is not entirely compatible with the fixed simulation of Fusion. Transform values are captured after FixedUpdateNetwork()
and not after Update()
, so any changes made in Update()
will not be captured until the next simulation (which will happen in a future Unity update). This can produce micro hitching and/or lurching due to the aliasing between the captures and the arbitrary deltaTime based movement in Update()
.
Aliasing in Shared Mode
Because Shared Mode doesn't strictly require controller code to simulate in FixedUpdateNetwork()
, it does introduce the potential for aliasing errors if users opt to only simulate in Update()
. The figure below illustrates the problem. Fusion captures state after FixedUpdateNetwork()
, even if your controller code is moving or changing a state in Update()
. These fixed interval networked values are used by interpolation on other clients, without any consideration for the real time when changes to the values occurred, which can lead to hitching and/or lurching in the interpolated results.
Strategies for Dealing with Aliasing
If this aliasing is a concern or is noticeable, there are some options for addressing it.
NOTE: This aliasing may not be large enough to be a concern, depending on the nature of the value being simulated, as well the relative Framerate and Tick Rate values. At a very high Tick Rate and Frame Rate, the aliasing may be unnoticeable to players. It also may be less of a concern if the Frame Rate is considerably higher than the tick rate.
1) Do Nothing
If the aliasing is minor enough for your game's particular use case and is unnoticeable to the user, you can of course just ignore it and just simulate in Update()
.
2) Move in FixedUpdateNetwork() and Interpolate on the State Authority
One option is to treat Shared Mode like Server Mode, and make all controller code simulation-based and have it execute in FixedUpdateNetwork()
only. Then, use interpolation to lerp between the latest two simulation results in Render()
. Cons: This induces a small amount of input lag.
3) Move in both FixedUpdateNetwork() and Update()
Another option is to simulate using a hybrid of FixedUpdateNetwork()
and Update()
. Moving in FixedUpdateNetwork()
will ensure that the Object moves evenly and correctly for the tick based captures and replication, producing smooth results on remote clients. And additionally moving in Update()
will account for time that has passed since the last tick, you can locally move the Object to the correct position for the local time, without the need for interpolating.
C#
using Fusion;
using UnityEngine;
/// <summary>
/// The objective of this basic controller code is to simulate
/// Object movement both in FixedUpdateNetwork() AND in Update().
/// It is important to move the Object in FixedUpdateNetwork()
/// to produce evenly spaced results, as these snapshots are used
/// to produce smooth interpolation on remote clients.
/// However, if it very common with Client Authority topologies
/// to apply inputs in Update(), as to apply player input
/// immediately to the rendered Object, rather than interpolating
/// between previous simulated states. So it becomes necessary
/// to simulate in both FUN() and Update() to satisfy both
/// Simulation and Interpolation requirements.
/// Simulation in Update() to produce smooth local movement,
// and Simulation in FUN() to capture even networked tick results.
/// </summary>
public class HybridSharedController : NetworkBehaviour, IBeforeUpdate
{
[SerializeField, Unit(Units.PerSecond)]
public float Speed = 5f;
float _lastSimulationTime;
Vector3 _currentInput;
public override void Spawned()
{
// Capture the starting time so our
// first simulation has a correct delta value.
_lastSimulationTime = Runner.SimulationTime;
}
public override void FixedUpdateNetwork()
{
// Only move the State Authority (there is no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Calculate the amount of time that has passed since the last
// FixedUpdateNetwork or Update - as this is our real delta
float timeSinceLastSim = Runner.SimulationTime - _lastSimulationTime;
_lastSimulationTime = Runner.SimulationTime;
transform.position += _currentInput * timeSinceLastSim;
}
// The BeforeUpdate() callback is the same as Update(),
// but it fires BEFORE all Fusion work is done.
// Update() would also work here, but input used in FUN()
// would always be from the previous Update().
void IBeforeUpdate.BeforeUpdate()
{
// Only move the State Authority (no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Collect input every Update(), and store it -
// as we will also use this input in FixedUpdateNetwork()
Vector3 input = default;
if (Input.GetKey(KeyCode.W)) { input += Vector3.forward * Speed; }
if (Input.GetKey(KeyCode.S)) { input += Vector3.back * Speed; }
if (Input.GetKey(KeyCode.A)) { input += Vector3.left * Speed; }
if (Input.GetKey(KeyCode.D)) { input += Vector3.right * Speed; }
_currentInput = input;
// Calculate the amount of time that has passed since last simulation
// (FixedUpdateNetwork or Update), as this is our real delta time
float realtime = Runner.LocalRenderTime + Runner.DeltaTime;
float timeSinceLastSim = realtime - _lastSimulationTime;
_lastSimulationTime = realtime;
transform.position += input * timeSinceLastSim;
}
}