This document is about: FUSION 2
SWITCH TO

Player Input

Introduction

Fusion provides a mechanism for collecting Player input every Tick, storing that collected input data in a history buffer, and automatically replicating this data to the Server.

Fusion primarily provides this mechanism to make client-side prediction possible. Tick Inputs are used in Tick Simulation (FixedUpdateNetwork()), on both the Client that is predicting (HasInputAuthority == true) and on the Server in order to produce consistent results between them. The history buffer on the client is used for re-simulation of Ticks.

Input Struct Definition

The input struct has the following constraints:

  • it has to 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); and,
  • for boolean values use NetworkBool instead of bool - C# does not enforce a consistent size for bools across platforms so NetworkBool is used to properly serialize it as a single bit.

Fusion will intelligently map the type of the struct; this permits the use of different structs for different play modes or different parts of the game. When unwrapping the input, Fusion will only return the available input of the correct type.

C#

public struct MyInput : INetworkedInput {
    public Vector3 aimDirection;
}

Buttons

There is a special NetworkButtons type which provides a convenient wrapper for saving button presses in a INetworkInput struct.

To add buttons to an input struct, simply:

  1. Create an enum for the buttons ( Important: it must be explicitly defined and start at 0); and,
  2. Add a NetworkButtons variable to the INetworkedInput.

C#

enum MyButtons {
    Forward = 0,
    Backward = 1,
    Left = 2,
    Right = 3,
}

public struct MyInput : INetworkInput {
    public NetworkButtons buttons;
    public Vector3 aimDirection;
}

The API available for assigning and reading values directly from a NetworkButtons variable is:

  • void Set(int button, bool state): takes the enum value for the button and its state (pressed = true, not pressed = false);
  • bool IsSet(int button): takes takes the enum value for the button and returns its boolean state.

The NetworkButtons type is stateless and therefore does not hold any meta data about the previous state of the buttons. In order to be able to use the next set of methods offered by NetworkButtons, it is necessary to keep track of the previous state of the buttons; this is easily done by creating a [Networked] version of the previous state for each player.

C#

public class PlayerInputConsumerExample : NetworkBehaviour {
    [Networked] public NetworkButtons ButtonsPrevious { get; set; }

    // Full snippet in the GetInput() section further down.
}
For the full snippet, see the __GetInput__ section further down.

With this it becomes possible to compare the current state of the buttons with their previous state to evaluate whether the buttons have just been pressed or released.

  • NetworkButtons GetPressed(NetworkButtons previous): returns a set of values for all buttons which have just been pressed.
  • NetworkButtons GetReleased(NetworkButtons previous): returns a set of values for all buttons which have just been released.
  • (NetworkButtons, NetworkButtons) GetPressedOrReleased(NetworkButtons previous): returns a tuple of values for buttons which have been just pressed and released.

IMPORTANT: Only use Input.GetKey() to assign button values. Do NOT use Input.GetKeyDown() or Input.GetKeyUp() as they are not synchronised with the Fusion ticks and thus might be missed.

Poll Input

Fusion collects input by polling the local client and populating a previously defined input struct. The Fusion Runner only ever keeps track of a single input struct, it is therefore strongly advised to implement the input polling in a single place to avoid any unexpected behaviour.

The Fusion Runner polls input by calling the INetworkRunnerCallbacks.OnInput() method. The implementation of OnInput() can populate any struct inheriting from INetworkInput with the chosen data. The populated struct is passed back to Fusion by calling Set() on the provided NetworkInput.

IMPORTANT:

  1. Having multiple polling sites will result in all but the last input struct version being overwritten.
  2. Input is only polled locally (all Modes).

SimulationBehaviour / NetworkBehaviour

To use OnInput() in a SimulationBehaviour or NetworkBehaviour component, implement the INetworkRunnerCallbacks interface and call NetworkRunner.AddCallbacks() to register the callbacks with the local runner.

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  public void OnEnable(){
    if(Runner != null){
       Runner.AddCallbacks( this );
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {
    var myInput = new MyInput();

    myInput.Buttons.Set(MyButtons.Forward, Input.GetKey(KeyCode.W));
    myInput.Buttons.Set(MyButtons.Backward, Input.GetKey(KeyCode.S));
    myInput.Buttons.Set(MyButtons.Left, Input.GetKey(KeyCode.A));
    myInput.Buttons.Set(MyButtons.Right, Input.GetKey(KeyCode.D));
    myInput.Buttons.Set(MyButtons.Jump, Input.GetKey(KeyCode.Space));

    input.Set(myInput);
  }

  public void OnDisable(){
    if(Runner != null){
        Runner.RemoveCallbacks( this );
    }
  }
}

MonoBehaviour and Pure CSharp

To poll input from a regular CSharp script or a MonoBehaviour, follow these steps:

  1. Implement INetworkRunnerCallbacks and OnInput(); and,
  2. Register the script with the NetworkRunner by calling AddCallbacks() on it.

C#


public class InputProvider : Monobehaviour, INetworkRunnerCallbacks {

  public void OnEnable(){
    var myNetworkRunner = FindObjectOfType<NetworkRunner>();
    myNetworkRunner.AddCallbacks( this );
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {
    // Same as in the snippet for SimulationBehaviour and NetworkBehaviour.
  }

  public void OnDisable(){
    var myNetworkRunner = FindObjectOfType<NetworkRunner>();
    myNetworkRunner.RemoveCallbacks( this );
  }
}

Unity New Input System

To use the new unity input system, the process is the same, but it is necessary to collect the inputs coming from the input action created.

After creating the input action and defining the desired button, generate the C# class and create an instance of it in the code. It is possible to use the events that come in the PlayerInput class as well, as long as the inputs are stored in a local cache to be consumed in OnInput().

The goal is to collect the button state in OnInput() coming from the new input system and not the old one, so apart from the setup part of the system, the rest is basically the same.

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  // creating a instance of the Input Action created
  private PlayerActionMap _playerActionMap = new PlayerActionMap();

  public void OnEnable(){
    if(Runner != null){
        // enabling the input map
        _playerActionMap.Player.Enable();

        Runner.AddCallbacks(this);
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input)
  {
    var myInput = new MyInput();
    var playerActions = _playerActionMap.Player;

    myInput.buttons.Set(MyButtons.Jump, playerActions.Jump.IsPressed());

    input.Set(myInput);
  }

  public void OnDisable(){
    if(Runner != null){
        // disabling the input map
        _playerActionMap.Player.Disable();

        Runner.RemoveCallbacks( this );
    }
  }
}

Poll Input In Low Tick Rates

To collect inputs at low tick rates, it becomes necessary to use Unity's Update function to accumulate any input recorded in a struct which may be consumed later.

In OnInput, this struct will be read and properly transmitted to Fusion through the input.Set() call, then it will be reset to start accumulating inputs for the next tick.

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  // Local variable to store the input polled.
  MyInput myInput = new MyInput();

  public void OnEnable() {
    if(Runner != null) {
        Runner.AddCallbacks( this );
    }
  }

  public void Update()
  {
    if (Input.GetMouseButtonDown(0)) {
      myInput.Buttons.Set(MyButtons.Attack, true);
    }

    if (Input.GetKeyDown(KeyCode.Space)) {
      myInput.Buttons.Set(MyButtons.Jump, true);
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {

    input.Set(myInput);

    // Reset the input struct to start with a clean slate
    // when polling for the next tick
    myInput = default;
  }
}

Poll Input With UI

Polling input with UI follows the same logic as above. Set the NetworkButton from a method called via UI, read and reset it on OnInput.

Read Input

The input can be read by the simulation to modify the existing networked state from its current state to the new one based on the previously polled input. Fusion synchronizes the input struct across the network and makes it available during simulation on the client who has Input Authority and the one who has State Authority (the host).

Contrary to polling input, reading input can be done at as many different places as necessary.

N.B.: The player input is only available for the client with Input Authoriy and State Authority. In the HostMode and ServerMode this means the player client and the host / server, whereas in SharedMode this is one and the same client.

It is not possible to read the input one client on another. Therefore, any changes relying on input need to be saved as a [Networked] state in order for it to be replicated on other clients.

GetInput()

To get the input struct, call GetInput(out T input) in the FixedUpdateNetwork() of on any NetworkBehaviour which has Input Authority over the object in question (e.g. the component controlling the player's movement). The call to GetInput() provides the same input struct that was previously populated in OnInput().

The call to GetInput() will return false if:

  • the client does not have State Authority or Input Authority
  • the requested type of input does not exist in the simulation

GameMode specific information:

  • In HostMode and ServerMode, the input for a given tick is only available to the player and to the Host / Server simulation. Input is NOT shared between players.
  • In SharedMode it is good practice to keep the OnInput() and GetInput() pattern but the lack of a central authority means only the local simulation will ever have access to the local player's input. Input is NOT shared between players.

C#

using Fusion;
using UnityEngine;

public class PlayerInputConsumerExample : NetworkBehaviour {

  [Networked] public NetworkButtons ButtonsPrevious { get; set; }

  public override void FixedUpdateNetwork() {

    if (GetInput<MyInput>(out var input) == false) return;

    // compute pressed/released state
    var pressed = input.Buttons.GetPressed(ButtonsPrevious);
    var released = input.Buttons.GetReleased(ButtonsPrevious);

    // store latest input as 'previous' state we had
    ButtonsPrevious = input.Buttons;

    // movement (check for down)
    var vector = default(Vector3);

    if (input.Buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
    if (input.Buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }

    if (input.Buttons.IsSet(MyButtons.Left)) { vector.x  -= 1; }
    if (input.Buttons.IsSet(MyButtons.Right)) { vector.x += 1; }

    DoMove(vector);

    // jump (check for pressed)
    if (pressed.IsSet(MyButtons.Jump)) {
      DoJump();
    }
  }

  void DoMove(Vector3 vector) {
    // dummy method with no logic in it
  }

  void DoJump() {
    // dummy method with no logic in it
  }
}

Runner.TryGetInputForPlayer()

It is possible to read the input from outside a NetworkBehaviour by calling NetworkRunner.TryGetInputForPlayer<T>(PlayerRef playerRef, out var input). In addition to the INetworkInput type, it requires specifying the player for which the input should be retrieved. N.B.: The same limitations as for GetInput() apply; i.e. on the client with input authority or Server / Host can get the input for the specified player.

C#

var myNetworkRunner = FindObjectOfType<NetworkRunner>();

// Example for local player if script runs only on the client
if(myNetworkRunner.TryGetInputForPlayer<MyInput>(myNetworkRunner.LocalPlayer, out var input)){
    // do logic
}

A Note on Authority

To guarantee full simulation authority, it is key to only collect input values in OnInput() when populating the input struct. The logic based to be executed based on the input should be done completely in the GetInput().

For instance, the following split would be used for firing a bullet:

  • OnInput(): Save the value of the firing button for the player.
  • GetInput(): Check if the firing button was pressed and shot the bullet if it was.

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 example of how to handle this use case 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;

  // Example 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 : MonoBehaviour, INetworkRunnerCallbacks
{
  public void OnInput(NetworkRunner runner, NetworkInput input)
  {
    // For this example each player (4 total) has one Joystick.
    var myInput = new CombinedPlayerInputs();
    myInput[0] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy1_X"), Input.GetAxis("Joy1_Y")) };
    myInput[1] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy2_X"), Input.GetAxis("Joy2_Y")) };
    myInput[2] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy3_X"), Input.GetAxis("Joy3_Y")) };
    myInput[3] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy4_X"), Input.GetAxis("Joy4_Y")) };
    input.Set(myInput);
  }

  // (removed unused INetworkRunnerCallbacks)
}

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);
    }
  }
}
Back to top