Network Input
Introduction
Input definition, polling and consumption lies at the core of Fusion.
Input Struct Definition
The input struct can store as simple or as complex data as needed. Fusion will only transmit data that actually changes. Therefore, although it is advantageous to keep data compact (e.g. use flags for buttons), it is fine to include rarely used fields. One such example is collecting input from several local players even if there is mostly just one - if these other players are not actually providing input they will not affect bandwidth usage of the input struct, only local memory.
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.
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
.
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.
public class PlayerInputExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
...
}
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.
SimulationBehaviour
Fusion automatically calls OnInput()
on all SimulationBehaviour
and NetworkBehaviour
components implementing the INetworkRunnerCallbacks
interface and over which the client has Input Authority
- hence the name.
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
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);
}
}
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.
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 );
}
}
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
using Fusion;
using UnityEngine;
public class PlayerInputExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
public override void FixedUpdateNetwork() {
NetworkButtons buttons = default;
if (GetInput<MyInput>(out var input) == false) return;
buttons = input.Buttons;
// compute pressed/released state
var pressed = buttons.GetPressed(ButtonsPrevious);
var released = buttons.GetReleased(ButtonsPrevious);
// store latest input as 'previous' state we had
ButtonsPrevious = buttons;
// movement (check for down)
var vector = default(Vector3);
if (buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
if (buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }
if (buttons.IsSet(MyButtons.Left)) { vector.x -= 1; }
if (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 limitations as for GetInput()
apply; i.e. on the client with input authority or Server / Host can get the input for the specified player.
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.