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 ofbool
- C# does not enforce a consistent size for bools across platforms soNetworkBool
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:
- Create an enum for the buttons ( Important: it must be explicitly defined and start at 0); and,
- Add a
NetworkButtons
variable to theINetworkedInput
.
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.
}
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:
- Having multiple polling sites will result in all but the last input struct version being overwritten.
- 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:
- Implement
INetworkRunnerCallbacks
andOnInput()
; and, - Register the script with the
NetworkRunner
by callingAddCallbacks()
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
andServerMode
, 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 theOnInput()
andGetInput()
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);
}
}
}