This document is about: FUSION 1
SWITCH TO

This page is a work in progress and could be pending updates.

3 - Prediction

Overview

Fusion 103 will explain prediction and how it is used to provide snappy feedback on the client in a server authoritative network game.

At the end of this section, the project will allow the player to spawn a predicted kinematic ball.

Consult the Manual for an in-depth description of this topic

Kinematic Object

To be able to spawn any object, it must first have a prefab.

  1. Create a new empty GameObject in the Unity Editor
  2. Rename it to Ball
  3. Add a new NetworkTransform component to it.
  4. Fusion will show a warning about a missing NetworkObject component, so go ahead and press Add Network Object.
  5. Change Interpolation Data Source to Predicted and set it to World Space.
  6. Add a Sphere child to the Ball
  7. Scale it down to 0.2 in all directions
  8. Drag the child to the InterpolationTarget of the NetworkTransform component on the parent object. This allow the NetworkTransform to separate the smooth interpolated visual (child object) from the main networked object itself (which will snap to network state).
  9. Remove the collider from the child sphere
  10. Create a new sphere collider on the parent object instead and give it radius 0.1 so that it completely covers the visual representation of the child object.
  11. Add a new script to the game object and call it Ball.cs
  12. Finally drag the entire Ball object into the project folder to create a prefab
  13. Save the scene to bake the network object and delete the prefab instance from the scene.
Ball Prefab
Ball Prefab

Predicted Movement

The goal is to have instances of the Ball behave identically on all peers simultaneously.

"Simultaneous" in this context means "on the same simulation tick", not the same actual world time. The way this is achieved is as follows:

  1. The server runs a simulation at specific, evenly spaced, ticks and calls FixedUpdateNetwork() on each tick. The server only and always moves forward from one tick to the next - this is exactly like FixedUpdate() for regular Unity behaviours in the local physics simulation. After each simulation tick the server calculates, compresses and broadcasts the changes in network state relative to the previous tick.
  2. Clients receive these snapshots at regular intervals, but obviously always lagging behind the server. When a snapshot is received, the client sets its internal state back to the tick of that snapshot, but then immediately re-simulates all ticks between the received snapshot and the clients current tick, by running its own simulation.
  3. The clients current tick is always ahead of the server by a wide enough margin, that the input it collects from the user can be sent to the server before the server reaches the given tick, and needs the input to run its simulation.

This has a number of implications:

  1. Clients run FixedUpdateNetwork() many times per frame and simulates the same tick several times as it receives updated snapshots. This works for networked state because Fusion resets it to the proper tick before calling FixedUpdateNetwork(), but this is not true for non-networked state, so be real careful how you use local state in FixedUpdateNetwork().
  2. Each peer can simulate a predicted future state of any object based on known previous position, velocity, acceleration and other deterministic properties. The one thing it cannot predict is the input from other players, so predictions will fail.
  3. While local inputs are applied instantly on the client for immediate feedback, they are not authoritative. It is still the snapshot generated by the server that eventually defines that tick - local application of input is just a prediction.

With that in mind, open the Ball script, change the base class to NetworkBehaviour to include it in Fusions simulation loop, and replace the pre-generated boilerplate code with an override of Fusions FixedUpdateNetwork().

In this simple example, the Ball will move with a constant velocity in its forward direction for 5 seconds before de-spawning itself. Go ahead and add a simple linear motion to the objects transform, like this:

C#

using Fusion;

public class Ball : NetworkBehaviour
{
  public override void FixedUpdateNetwork()
  {
    transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

This is almost exactly the same code used to move a regular non-networked Unity object, except that the time step is not Time.deltaTime but Runner.DeltaTime corresponding to the time between ticks. The secret to why this works across the network on a seemingly local property like the Unity transform is of course the NetworkTransform component added earlier. NetworkTransform is a convenient way to ensure that transform properties are part of the network state.

The code still needs to de-spawn the object after a set time has expired so it does not fly off into infinity, eventually looping around and hitting the player in the neck. Fusion provides a convenient helper type for timers, aptly named TickTimer. Instead of storing the current remaining time, it stores the end-time in ticks. This means the timer does not need to be sync'ed on every tick but just once, when it is created.

To add a TickTimer to the games networked state, add a property to the Ball named life of type TickTimer, provide empty stubs for the getter and setter and mark it with the [Networked] attribute.

C#

[Networked] private TickTimer life { get; set; }

The timer should be set before the object is spawned, and because Spawned() is called only after a local instance has been created, it should not be used to initialize network state.

Instead, create an Init() method that can be called from the Player and use it to set the life property to be 5 seconds into the future. This is best done with the static helper method CreateFromSeconds() on the TickTimer itself.

C#

public void Init()
{
  life = TickTimer.CreateFromSeconds(Runner, 5.0f);
}

Finally, FixedUpdateNetwork() must be updated to check if the timer has expired and if so, de-spawn the ball:

C#

if(life.Expired(Runner))
  Runner.Despawn(Object);

All in all, the Ball class should now look like this:

C#

using Fusion;

public class Ball : NetworkBehaviour
{
  [Networked] private TickTimer life { get; set; }
  
  public void Init()
  {
    life = TickTimer.CreateFromSeconds(Runner, 5.0f);
  }
  
  public override void FixedUpdateNetwork()
  {
    if(life.Expired(Runner))
      Runner.Despawn(Object);
    else
      transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

Spawning Prefabs

Spawning any prefab works the same as spawning the player avatar, but where the player spawn was triggered by a network event (the player joining the game session), the ball will be spawned based on user input.

For this to work, the input data structure needs to be augmented with additional data. This follows the same pattern as movement, and requires three steps:

  1. Add data to the Input Structure
  2. Collect data from Unity's Input
  3. Apply the Input in the players FixedUpdateNetwork() implementation

Open NetworkInputData and add a new byte field called buttons and define a const for the first mouse button:

C#

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
  public const byte MOUSEBUTTON1 = 0x01;

  public byte buttons;
  public Vector3 direction;
}

Open BasicSpawner, go to the OnInput() method and add checks for the primary mouse button and set the first bit of the buttons field if it is down. To ensure that quick taps are not missed, the mouse button is sampled in Update() and reset once it has been recorded in the input structure:

C#

private bool _mouseButton0;
private void Update()
{
  _mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
}

public void OnInput(NetworkRunner runner, NetworkInput input)
{
  var data = new NetworkInputData();

  if (Input.GetKey(KeyCode.W))
    data.direction += Vector3.forward;

  if (Input.GetKey(KeyCode.S))
    data.direction += Vector3.back;

  if (Input.GetKey(KeyCode.A))
    data.direction += Vector3.left;

  if (Input.GetKey(KeyCode.D))
    data.direction += Vector3.right;
  
  if (_mouseButton0)
    data.buttons |= NetworkInputData.MOUSEBUTTON1;
  _mouseButton0 = false;

  input.Set(data);
}

Open the Player class and, inside the check for GetInput(), get the button bits and spawn a prefab if the first bit is set. The prefab can be provided with a regular Unity [SerializeField] member that can be assigned from the Unity inspector. To be able to spawn in different directions also add a member variable to store the last move direction and use this as the forward direction of the ball.

C#

[SerializeField] private Ball _prefabBall;
private Vector3 _forward;
...
if (GetInput(out NetworkInputData data))
{
  ...
  if (data.direction.sqrMagnitude > 0)
    _forward = data.direction;
  if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
  {
      Runner.Spawn(_prefabBall, 
      transform.position+_forward, Quaternion.LookRotation(_forward), 
      Object.InputAuthority);
  }
  ...
}
...

To limit the spawn frequency, wrap the call to spawn in a networked timer that must expire between each spawn. Only reset the timer when a button press is detected:

C#

[Networked] private TickTimer delay { get; set; }
...
if (delay.ExpiredOrNotRunning(Runner))
{
  if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
  {
    delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
    Runner.Spawn(_prefabBall, 
    transform.position+_forward, Quaternion.LookRotation(_forward), 
    Object.InputAuthority);
...

The actual call to Spawn needs to be modified a bit because the ball requires additional initialization before it is synced. Specifically, the Init() method added earlier must be called to ensure that the tick timer is set correctly.

For this purpose, fusion allow a callback to be provided to Spawn() that will be invoked after instantiating the prefab, but before it is synchronized.

In summary, the class should look like this:

C#

using Fusion;
using UnityEngine;

public class Player : NetworkBehaviour
{
  [SerializeField] private Ball _prefabBall;
  
  [Networked] private TickTimer delay { get; set; }
  
  private NetworkCharacterControllerPrototype _cc;
  private Vector3 _forward;
  
  private void Awake()
  {
    _cc = GetComponent<NetworkCharacterControllerPrototype>();
    _forward = transform.forward;
  }
  
  public override void FixedUpdateNetwork()
  {
    if (GetInput(out NetworkInputData data))
    {
      data.direction.Normalize();
      _cc.Move(5*data.direction*Runner.DeltaTime);
  
      if (data.direction.sqrMagnitude > 0)
        _forward = data.direction;
  
      if (delay.ExpiredOrNotRunning(Runner))
      {
        if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
        {
          delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
            Runner.Spawn(_prefabBall, 
            transform.position+_forward, Quaternion.LookRotation(_forward), 
            Object.InputAuthority, (runner, o) =>
            {
              // Initialize the Ball before synchronizing it
              o.GetComponent<Ball>().Init();
            });
        }
      }
    }
  }
}

Final step before testing is to assign the prefab to the _prefabBall field on the Player prefab. Select PlayerPrefab in the project, then drag the Ball prefab into the Prefab Ball field.

Back to top