This document is about: FUSION 2
SWITCH TO

Shared Mode Player Input

Topology
SHARED AUTHORITY

Introduction

In Shared Mode, the client who has InputAuthority is also the StateAuthority. Because of this, inputs do not need to be sent to a remote authority, and no complex prediction or re-simulation logic is required.

While input handling in Shared Mode is more straightforward than in Client/Server modes, timing caveats still exist. This document details these caveats and provides best practices for handling networked state, transient inputs, and physics.

Note: Code examples use the Unity Input System (Package) in its default 'Dynamic Update' mode. To keep examples concise, Input Action's will be ignored and device inputs will be queried directly. For those using the Legacy Input System, the logic remains the same even if the syntax differs.


Inputs that change Networked State

FixedUpdateNetwork is called at a fixed tick rate. Each call produces a new networked state for that specific tick; Fusion then interpolates between these states to provide a smooth representation of the world. If you handle inputs outside of this (in Update), you will "stamp" over interpolated values, leading to visual jitter.

Guidance: Inputs that change networked state should be handled in FixedUpdateNetwork.

Pattern:

C#

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : NetworkBehaviour
{
    private CharacterController _controller;

    private void Awake()
    {
        _controller = GetComponent<CharacterController>();
    }

    public override void FixedUpdateNetwork()
    {
        // Most inputs can be queried from the Unity Input System 
        // directly in FixedUpdateNetwork
        var keyboard = Keyboard.current;

        var moveDirection = Vector2.zero;

        if (keyboard.wKey.isPressed) { moveDirection += Vector2.up; }
        if (keyboard.sKey.isPressed) { moveDirection += Vector2.down; }
        if (keyboard.aKey.isPressed) { moveDirection += Vector2.left; }
        if (keyboard.dKey.isPressed) { moveDirection += Vector2.right; }
                
        Move(moveDirection.normalized);
    }

    void Move(Vector2 inputDirection)
    {
        // inputDirection is used to move the _controller
        // and adjust the object's rotation
    }
}

Handling Transient Input

Unity’s Input System in 'Dynamic Update' mode refreshes at the beginning of Update. However, because FixedUpdateNetwork runs at a fixed rate, it doesn't always align with Update. One Update frame might contain zero, one, or several FixedUpdateNetwork calls.

This adds some complexity to how transient inputs need to be handled.

1. Button "Taps"

If a player performs a very fast "tap," there is a chance that FixedUpdateNetwork might miss the button state entirely if it doesn't run during those specific frames.

Guidance: To ensure a button press is never missed, you should capture it in Update and clear it after it has been consumed by Fusion in FixedUpdateNetwork.

Pattern:

C#

public class PlayerMovement : NetworkBehaviour
{
    bool _jumpPressed;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
          return;

        // "WasPressed" states are transient and as 
        // such must be captured in Update so that they aren't
        // missed before the next FixedUpdateNetwork invocation
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
          _jumpPressed = true;
        }
    }

    public override void FixedUpdateNetwork()
    {
        if(_jumpPressed)
        {
            DoJump();
        }
        _jumpPressed = false;
    }

    void DoJump()
    {
      // Network effecting jump logic goes here
    }
}

2. Mouse Deltas

Similarly, Unity resets mouse deltas every Update. Checking them only in FixedUpdateNetwork results in lost precision and "stuttery" rotation because you only see the delta from the single frame immediately preceding the FixedUpdateNetwork call.

Guidance: Accumulate movement deltas in Update and process/reset the sum in FixedUpdateNetwork.

Pattern:

C#

public class PlayerMovement : NetworkBehaviour
{
    Vector2 _accumulatedMouseDelta;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
          return;

        // Mouse deltas are transient and must be accumulated
        // in Update so they are not missed before the next
        // FixedUpdateNetwork invocation
        _accumulatedMouseDelta += Mouse.current.delta.ReadValue();
    }

    public override void FixedUpdateNetwork()
    {
        AdjustAim(_accumulatedMouseDelta);

        _accumulatedMouseDelta = Vector2.zero;
    }

    void AdjustAim(Vector2 delta)
    {
      // Apply aiming adjustments to transform here
    }
}

Inputs that change Unity interpolated Rigidbodies

While the above rules are sufficient for most games, there are exceptional cases where we don't rely on Fusion to interpolate the Networked state. One case is Forecasted Rigidbodies. These use Unity's built-in physics interpolation, which occurs between FixedUpdate calls. For more information on Physics integration in Fusion please read Physics

For these types of objects, we want to read Unity input in FixedUpdate. Again, note that transient inputs must be captured in Update and applied in FixedUpdate, as FixedUpdate is not called at the same rate as Update.

C#

[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : NetworkBehaviour
{
    Rigidbody _rb;
    bool _jumpPressed;

    void Awake()
    {
        _rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;
        
        // WasPressed events are transient and must be captured
        // so that they aren't missed before the next FixedUpdate invocation
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
          _jumpPressed = true;
        }
    }

    void FixedUpdate()
    {
        var keyboard = Keyboard.current;
        var moveDirection = Vector2.zero;

        if (keyboard.wKey.isPressed) { moveDirection += Vector2.up; }
        if (keyboard.sKey.isPressed) { moveDirection += Vector2.down; }
        if (keyboard.aKey.isPressed) { moveDirection += Vector2.left; }
        if (keyboard.dKey.isPressed) { moveDirection += Vector2.right; }

        Move(moveDirection.normalized, _jumpPressed);  

        // Set captured transient inputs back to defaults
        _jumpPressed = false;
    }

    void Move(Vector2 inputDirection, bool didJump) 
    {
        // Converts inputDirection and jump flag to forces
        // and applies them to the _rb
    }
}

Tip: For the best outcome with Forecast Physics, we recommend networking the physics-affecting inputs and applying them on the Non-StateAuthorities as well. See an example integration of physics with Fusion


Inputs that have no impact on Networked State

For inputs that don't affect any networked state, they can be handled via standard Unity best practices. Usually, this involves consuming inputs in Update, LateUpdate, to provide the lowest possible latency.

Examples of inputs that might not affect networked state:

  1. Rotating the Camera: This usually demands low input latency and smooth movement. Since this often doesn't have a direct networked impact, inputs can be consumed in LateUpdate
  2. UI Navigation: Follow Unity best practices for handling these events.

Advanced Inputs

Sometimes you need to combine the above patterns. For example, in a first-person shooter, mouse input affects both the character's rotation (which is networked) and the camera's orientation (which isn't).

We also want to make sure that the camera moves "instantly" without waiting for the next FixedUpdateNetwork.

To achieve this, we accumulate input in Update, apply it to the networked transform in FixedUpdateNetwork, and then in LateUpdate use the interpolated transform plus any pending input to rotate the camera.

Pattern:

C#

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : NetworkBehaviour
{
    // Yaw affects the networked transform and the camera
    float _yaw;

    // Pitch is purely visual, it only affects the camera
    float _pitch;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;

        // Follow the guidance, accumulate transient inputs
        // in Update
        Vector2 mouseDelta = Mouse.current.delta.ReadValue();

        const float _sensitivity = 0.1f;
        _yaw += mouseDelta.x * _sensitivity;

        // We also accumulate the pitch for the camera here,
        // so it can be applied in LateUpdate
        _pitch += mouseDelta.y * -_sensitivity;
    }

    public override void FixedUpdateNetwork()
    {
        // The yaw value is consumed to transform this players
        // rotation, which is networked by the NetworkTransform
        // behaviour
        transform.rotation *= Quaternion.Euler(0f, _yaw, 0f);

        // Follow the guidance, reset accumulated input to zero
        _yaw = 0;
    }

    void LateUpdate()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;

        Camera.main.transform.position = transform.position;

        // Use Fusion's interpolated transform yaw + any pending yaw
        // for smooth, low-latency camera rotation
        Camera.main.transform.rotation = Quaternion.Euler(_pitch, transform.eulerAngles.y + _yaw, 0f);
    }
}

Tip: The same approach works for physics-based movement with a Forecasted Rigidbody. Since Unity handles interpolation for these rather than Fusion, you would apply the yaw to the Rigidbody in FixedUpdate instead of FixedUpdateNetwork.

Back to top