This document is about: FUSION 2
SWITCH TO

Client-Server Player Input

Topology
DEDICATED SERVER & CLIENT HOST

Introduction

In Client-Server topologies, Fusion uses a Network Input system to share inputs with the server and enable Client-Side Prediction, Rollback, and Re-Simulation.

To achieve this, Fusion collects inputs on the client and stores them in a history buffer. This buffer is synchronized with the host. The client producing the input for a NetworkObject is the InputAuthority; the host/server is the StateAuthority. It's possible for a client to also be the Host, in which case they can be both the InputAuthority and StateAuthority for a given object

Inputs are consumed in FixedUpdateNetwork on the:

  • InputAuthority: To simulate local movement immediately (Prediction).
  • StateAuthority: To process the authoritative move and define the "true" state.
  • InputAuthority (again): To re-simulate frames when receiving new data from the StateAuthority (Rollback & Re-simulation).

Note: Code examples use the Unity Input System (Package) in its default 'Dynamic Update' mode. Examples query Keyboard and Mouse devices directly rather than using Input Actions. For those using the Legacy Input System, the logic remains the same even if the syntax differs.


Basic Input — End to End

This section walks through the minimum steps to get player input working with Fusion: define an input struct, poll input, and consume it.

1. Define an Input Struct

All input to be networked must be defined in a struct implementing INetworkInput.

C#

public struct GameplayInput : INetworkInput
{
    public Vector2 MoveDirection;
}

Note: INetworkInputs should adhere to the following constraints

  • It must inherit from INetworkInput.
  • It can only contain primitive types and structs.
  • The input struct and any structs it holds have be top-level structs (i.e. cannot be nested in a class).
  • Use NetworkBool instead of bool - C# does not enforce a consistent size for bools across platforms so NetworkBool is used to properly serialize it for networking.

2. Poll Input (OnInput)

Fusion gathers inputs by calling INetworkRunnerCallbacks.OnInput() on the InputAuthority. Populate your input struct and pass it to Fusion via input.Set().

This makes sure that the input becomes part of the history buffer and synchronizes it with the StateAuthority.

C#

public class PlayerInput : NetworkBehaviour, INetworkRunnerCallbacks
{
    public override void Spawned() => Runner?.AddCallbacks(this);
    public override void Despawned(NetworkRunner runner, bool hasState) => runner?.RemoveCallbacks(this);

    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        var myInput = new GameplayInput();
        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;

        myInput.MoveDirection = moveDirection.normalized;

        input.Set(myInput);
    }

    ...
}

Tip: Add the PlayerInput script alongside your PlayerMovement script on your player prefab.

3. Consume Input (GetInput)

Call GetInput(out T input) in FixedUpdateNetwork of any NetworkBehaviour. This provides the correct input for the tick being simulated — whether it's a new prediction or a re-simulation of an old tick.

C#

public class PlayerMovement : NetworkBehaviour
{
    public override void FixedUpdateNetwork()
    {
        if (GetInput(out GameplayInput input))
        {
            // Its important that we apply normalization here, to effectively
            // sanitize inputs on the StateAuthority, we can't trust data from
            // clients
            var direction = input.MoveDirection.normalized;

            DoMove(direction);
        }
    }
}

Note: GetInput returns true only for the InputAuthority and StateAuthority. All other clients (proxies) receive false.

Tip: Though rare it's also possible for the StateAuthority to get false returned if an input doesn't make it in time for processing (packet-loss, latency spike). Extra logic can be added to cache the last good input and re-use it in this case.

With these three pieces you have a working networked input loop. The rest of this page covers the rules, gotchas, and patterns that build on this foundation.


Avoiding Missed Inputs

Some input values only exist briefly — a mouse delta that resets every frame, or a button press that lasts just a few frames. Unlike a movement key that stays held down, these can be lost if they aren't captured at the right moment.

This happens because Unity's input system refreshes in Update, but OnInput runs at a fixed timestep alongside FixedUpdateNetwork. One Update frame might contain zero, one, or several OnInput calls — so brief inputs need to be accumulated in Update to avoid being missed.

Mouse Deltas

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

Guidance: Accumulate look rotation in Update by adding each frame's delta to a running total. Pass the total to Fusion in OnInput.

Pattern:

C#

public struct GameplayInput : INetworkInput
{
    public Vector2 MoveDirection;
    public Vector2 LookRotation;
}

public class PlayerInput : NetworkBehaviour, INetworkRunnerCallbacks
{
    public override void Spawned() => Runner?.AddCallbacks(this);
    public override void Despawned(NetworkRunner runner, bool hasState) => runner?.RemoveCallbacks(this);

    private GameplayInput _input;

    void Update()
    {
        if (HasInputAuthority == false)
            return;

        // Mouse deltas reset each Update, so accumulate
        // into a running total to preserve for the next
        // OnInput callback
        _input.LookRotation += Mouse.current.delta.ReadValue();
    }

    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        input.Set(_input);
    }
}

public class PlayerMovement : NetworkBehaviour
{
    public override void FixedUpdateNetwork()
    {
        if (GetInput(out GameplayInput input))
        {
            transform.rotation = Quaternion.Euler(0f, input.LookRotation.x, 0f);
        }
    }
}

Button "Taps"

To add buttons (jump, sprint, fire, etc.) and correctly capture their transient states (became pressed, became released), use the NetworkButtons type. This packs multiple button states into a single bitmask — 1 bit per button — and provides helper functions for detecting transitions.

Define the buttons as an enum and add a NetworkButtons field to your input struct:

C#

public enum EInputButton
{
    Jump,
    Sprint,
}

public struct GameplayInput : INetworkInput
{
    public NetworkButtons Buttons;
}

If a player performs a very fast "tap," there is a chance that OnInput might miss the button state entirely if it doesn't run during those specific frames. To handle this, button inputs must also be accumulated in Update.

Pattern:

C#

public class PlayerInput : NetworkBehaviour, INetworkRunnerCallbacks
{
    public override void Spawned() => Runner?.AddCallbacks(this);
    public override void Despawned(NetworkRunner runner, bool hasState) => runner?.RemoveCallbacks(this);

    private bool _resetAccumulatedInputs;
    private GameplayInput _input;

    void Update()
    {
        if (_resetAccumulatedInputs)
        {
            _input.Buttons.Set(EInputButton.Jump, false);
            _resetAccumulatedInputs = false;
        }

        // Latch jump to true if pressed this frame — once set, it stays
        // set until OnInput has consumed it
        if (Keyboard.current.spaceKey.isPressed)
        {
            _input.Buttons.Set(EInputButton.Jump, true);
        }

        // Sprint is a held state, we only care about its value
        // at the Update proceeding the OnInput call so its
        // safe to overwrite the previous value
        _input.Buttons.Set(EInputButton.Sprint, Keyboard.current.leftShiftKey.isPressed);
    }

    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        input.Set(_input);

        // Defer reset to the next Update — this ensures the latched
        // state persists if multiple FixedUpdateNetwork calls happen
        // within a single frame
        _resetAccumulatedInputs = true;
    }

    ...
}

public class PlayerMovement : NetworkBehaviour
{
    // Store previous button state as a Networked property so that it
    // is accessible to the StateAuthority and correct during re-simulation
    [Networked] NetworkButtons _previousButtons { get; set; }

    public override void FixedUpdateNetwork()
    {
        if (GetInput(out GameplayInput input))
        {
            // Detect the tap by comparing against previous tick's buttons
            if (input.Buttons.WasPressed(_previousButtons, EInputButton.Jump))
            {
                DoJump();
            }

            if (input.Buttons.IsSet(EInputButton.Sprint))
            {
                DoSprint();
            }

            _previousButtons = input.Buttons;
        }
    }

    ...
}

Tip: GetPressed() / GetReleased() are also available for bulk comparison — they return a NetworkButtons value representing all buttons that changed state, which you can then check with IsSet():

C#

var pressed  = input.Buttons.GetPressed(previousButtons);
var released = input.Buttons.GetReleased(previousButtons);

if (pressed.IsSet(EInputButton.Jump)) { DoJump(); }

Responsive Camera

A common complaint with networked first-person cameras is that look rotation feels delayed compared to offline. This happens when directly moving the camera in FixedUpdateNetwork in response to input — remember FixedUpdateNetwork happens at a fixed rate, which might be slower than the Update rate.

Guidance: Read the accumulated look rotation directly in Render() to move the camera at the full render framerate, without waiting for the next fixed tick. The networked look rotation that travels through OnInputFixedUpdateNetwork handles the authoritative player rotation on the server — the camera is purely a local visual.

Pattern:

C#

[RequireComponent(typeof(PlayerInput))]
public class PlayerMovement : NetworkBehaviour
{
    ...

    public override void Render()
    {
        if (Camera.main == null)
            return;

        // LookRotation is a local field updated every Update(),
        // so reading it here gives zero-delay camera response.
        var lookRotation = GetComponent<PlayerInput>().LookRotation;

        Camera.main.transform.rotation = Quaternion.Euler(lookRotation.x, lookRotation.y, 0f);

        // Assuming that this is a NetworkTransform, the position
        // obtained here will use interpolated values so will update
        // smoothly in Render() between FixedUpdateNetwork calls
        Camera.main.transform.position = transform.position;
    }
}

Note: For full render predicted movement, see the Advanced Input Handling section.


Full PlayerInput Example

The snippets above each demonstrate a single technique. The following class combines them into a production-ready PlayerInput that accumulates all input in Update and passes it to Fusion in OnInput. This pattern is taken from the Fusion Starter sample.

C#

public sealed class PlayerInput : NetworkBehaviour, INetworkRunnerCallbacks
{
    public Vector2 LookRotation => _input.LookRotation;

    private GameplayInput _input;

    public override void Spawned() => Runner?.AddCallbacks(this);
    public override void Despawned(NetworkRunner runner, bool hasState) => runner?.RemoveCallbacks(this);

    private void Update()
    {
        if (HasInputAuthority == false) return;

        var lookRotationDelta = new Vector2(
            -Mouse.current.delta.y.ReadValue(),
             Mouse.current.delta.x.ReadValue());

        _input.LookRotation = ClampLookRotation(
            _input.LookRotation + lookRotationDelta);

        var keyboard = Keyboard.current;
        var moveDirection = Vector2.zero;
        if (keyboard.wKey.isPressed) moveDirection.y += 1f;
        if (keyboard.sKey.isPressed) moveDirection.y -= 1f;
        if (keyboard.aKey.isPressed) moveDirection.x -= 1f;
        if (keyboard.dKey.isPressed) moveDirection.x += 1f;

        _input.MoveDirection = moveDirection.normalized;

        _input.Buttons.Set(EInputButton.Jump, keyboard.spaceKey.isPressed);
        _input.Buttons.Set(EInputButton.Sprint, keyboard.leftShiftKey.isPressed);
    }

    public void OnInput(NetworkRunner runner, NetworkInput networkInput)
    {
        networkInput.Set(_input);
    }

    private Vector2 ClampLookRotation(Vector2 lookRotation)
    {
        lookRotation.x = Mathf.Clamp(lookRotation.x, -30f, 70f);
        return lookRotation;
    }
}

Further Examples

Multiple Players Per Peer

Often referred to as "couch", "split-screen", or "local" multiplayer. It is possible for multiple human players to provide inputs to a single peer (such as a game console with multiple controllers), while also participating in an online multiplayer game. Fusion treats all players on one peer as part of a single PlayerRef (PlayerRef identifies the network peer, not the individual human players), and makes no distinction between them. It is left open for you to decide what a "player" is and what input they provide.

One approach is to define your INetworkInput struct with a nested INetworkStruct for each player.

C#

public struct PlayerInputs : INetworkStruct
{
    // All player-specific inputs go here
    public Vector2 dir;
}

public struct CombinedPlayerInputs : INetworkInput
{
    // For this example we assume 4 players max on one peer
    public PlayerInputs PlayerA;
    public PlayerInputs PlayerB;
    public PlayerInputs PlayerC;
    public PlayerInputs PlayerD;

    // Indexer for easier access to nested player structs
    public PlayerInputs this[int i]
    {
        get {
            switch (i) {
                case 0:  return PlayerA;
                case 1:  return PlayerB;
                case 2:  return PlayerC;
                case 3:  return PlayerD;
                default: return default;
            }
        }

        set {
            switch (i) {
                case 0:  PlayerA = value; return;
                case 1:  PlayerB = value; return;
                case 2:  PlayerC = value; return;
                case 3:  PlayerD = value; return;
                default: return;
            }
        }
    }
}

Collecting inputs for multiple players:

C#

public class CouchCoopInput : NetworkBehaviour, INetworkRunnerCallbacks
{
    ...

    // Cache connected gamepads
    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        var myInput = new CombinedPlayerInputs();
        var gamepads = Gamepad.all;

        for (int i = 0; i < Mathf.Min(gamepads.Count, 4); i++)
        {
            var stick = gamepads[i].leftStick.ReadValue();
            myInput[i] = new PlayerInputs() { dir = stick };
        }

        input.Set(myInput);
    }

    ...
}

Getting inputs for simulation:

C#

public class CouchCoopController : NetworkBehaviour
{
    // Player index 0-3, indicating which of the 4 players
    // on the associated peer controls this object.
    private int _playerIndex;

    public override void FixedUpdateNetwork()
    {
        if (GetInput<CombinedPlayerInputs>(out var input))
        {
            var dir = input[_playerIndex].dir;
            // Convert joystick direction into player heading
            float heading = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            transform.rotation = Quaternion.Euler(0f, heading - 90, 0f);
        }
    }
    ...
}

Samples

Input handling is highly context-dependent; a high-precision fighting game requires a different approach than a physics-based racer. To see these patterns applied in a real-world context, we recommend exploring our Game Samples. These samples demonstrate how to tailor Fusion’s input system to specific genres and provide a great starting point for your own implementation.


Advanced Topics

For competitive or high-fidelity titles, the following techniques can further reduce perceived latency and improve fidelity:

  • Render Prediction — Instead of interpolating the character's visual position between the last two fixed ticks, run the full movement simulation in Render() to predict ahead. This can reduce system latency from ~12ms down to ~4ms. See Render Behavior and the Advanced KCC in general for more details.
  • Input Smoothing — Raw mouse deltas are noisy and produce micro-jitter at high frame rates. Averaging deltas over a short time window (10–20ms) produces smoother camera rotation at the cost of a few milliseconds of lag. See Input Smoothing in the Advanced KCC documentation for more details.
  • IBeforeUpdate Timing — Capturing input in IBeforeUpdate.BeforeUpdate() instead of Update() ensures the latest values are available just before Fusion's tick pipeline runs, slightly reducing latency at low frame rates. This can be seen in practice in the Starter Shooter Sample.
  • Full Advanced Implementation — The BR200 sample is a good demonstration of the above techniques in practice, if you are aiming for high fidelity input handling you should definitely check this out
Back to top