Skip to main content

Inputs

Basics

IInputWriter & IInputReader

Inputs in Elympics are binary data written in a defined order. This requires developers to ensure compatibility between serialization and deserialization.

inputWriter.Write(vMove); // float
inputWriter.Write(hMove); // float
inputWriter.Write(fire); // bool
if (fire)
inputWrite.Write(firePower); // float
info

In the examples we will consider having one player only. Handling multiple players isn't a lot different, but as a simplification we will leave it for later.

Collecting inputs

To collect inputs Elympics searches for IInputHandler implementations on GameObjects with ElympicsBehaviour component. Then the provided input collecting methods are called during FixedUpdate (see Game Loop)

caution

There can be at most one component with IInputHandler per ElympicsBehaviour

public class PlayerInputController : ElympicsMonoBehaviour, IInputHandler
{
public void OnInputForClient(IInputWriter inputWriter)
{
var vMove = Input.GetAxis("Vertical");
var hMove = Input.GetAxis("Horizontal");
var fire = Input.GetKey(KeyCode.Space);
SerializeInput(inputWriter, vMove, hMove, fire);
}

public void OnInputForBot(IInputWriter inputWriter)
{
var vMove = Random.Range(-1.0f, 1.0f);
var hMove = Random.Range(-1.0f, 1.0f);
var fire = true;
SerializeInput(inputWriter, vMove, hMove, fire);
}

private static void SerializeInput(IInputWriter inputWriter, float vMove, float hMove, bool fire)
{
inputWriter.Write(vMove);
inputWriter.Write(hMove);
inputWriter.Write(fire);
}

{...}
}

From Client - OnInputForClient

Collecting inputs for clients is as simple as getting the current state of mouse, keyboard, or any other controller.

info

Do not use Input.GetKeyDown() or Input.GetKeyUp() as they are not available in FixedUpdate inside which OnInputForClient is called.

From Bot - OnInputForBot

In the example provided above, bot is programmed to perform random moves. However there are endless possibilities to implement bot behaviour. In most situations bots are run along with the server, so they are aware of full state of the game. There is also option to run bots in a similar way to player clients, as independent processes.

Empty OnInput implementation

In case you don't want your bot (or client) to produce any input yet, you can leave OnInput* empty.

public void OnInputForClient(IInputWriter inputWriter) {...}

public void OnInputForBot(IInputWriter inputWriter)
{
// TODO
}

In this case input won't be serialized and sent, and ElympicsBehaviour.TryGetInput will return false like when the input is absent.

Reading inputs

ElympicsMonoBehaviour base class provides a way to access previously generated inputs - they are available in ElympicsBehaviour.TryGetInput. Of course if your class doesn't inherit from ElympicsMonoBehaviour, you can get the necessary component by yourself using GetComponent method.

info

Inputs should only be deserialized (by calling TryGetInput) and applied inside ElympicsUpdate, as they are tick-synchronized and guaranteed to be present only in that context.

public class PlayerInputController : ElympicsMonoBehaviour, IInputHandler, IUpdatable
{
public void OnInputForClient(IInputWriter inputWriter) {...}
public void OnInputForBot(IInputWriter inputWriter) {...}

private float vMove = 0.0f;
private float hMove = 0.0f;
private bool fire = false;
private int inputAbsenceFallbackTicks = 4;

public void ElympicsUpdate()
{
// Parameter 0 means that we try to read input from player 0
if (ElympicsBehaviour.TryGetInput(ElympicsPlayer.FromIndex(0), out var inputReader, inputAbsenceFallbackTicks))
{
inputReader.Read(out vMove);
inputReader.Read(out hMove);
inputReader.Read(out fire);
}

Move(vMove, hMove);
Fire(fire);
}

private void Move(float vMove, float hMove)
{
// Movement implementation
}

private void Fire(bool fire)
{
// Firing implementation
}
}
info

inputReader is reused by Elympics and you have to read whole input before trying to get the next one, otherwise exception will be thrown.

info

inputAbsenceFallbackTicks is an optional parameter (default value is 4) considered by server only if it doesn't receive input from client for a given tick (read more below to find out why this could happen). It instructs the server on how old input should be considered relevant in such cases.

Handling multiple players

Elympics doesn't have object ownership feature built-in (yet!). However it's really easy to implement! It is necessary to prevent players from e.g. controlling other players characters.

Let's get back to our PlayerInputController, where OnInputForClient is implemented. Considering multiple players, we allowed all of them to create inputs for every PlayerInputController. Considering we have 2 or more of those controllers instantiated (we need one for each connected player), OnInputForClient will be called for each of those instances. To prevent this, we have to check which instance is currently allowed to call this method (are we handling the instance responsible for current player?).

tip

Ownership is more or less related to prediction. This means that if player has ownership of an object, they could predict its movement using the same inputs sent to the server applied locally in ElympicsUpdate. (learn more about predition, and its role in the game loop).

To start, we need to set predictability in every character's ElympicsBehaviour to the chosen player.

PredictableFor Player

Then we can use it (through ElympicsMonoBehaviour.PredictableFor property) in the following way:

public void OnInputForClient(IInputWriter inputWriter)
{
// Check for ownership
if (Elympics.Player != PredictableFor)
return;

var vMove = Input.GetAxis("Vertical");
var hMove = Input.GetAxis("Horizontal");
var fire = Input.GetKey(KeyCode.Space);
SerializeInput(inputWriter, vMove, hMove, fire);
}

Secondly, we also need to alter our ElympicsUpdate implementation. In the previous example the input that we used was collected from player 0. Now we have to choose which player (or players) can influence the current PlayerInputController.

private float vMove = 0.0f;
private float hMove = 0.0f;
private bool fire = false;

public void ElympicsUpdate()
{
// PredictableFor is used here to filter only the owner's input
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
{
inputReader.Read(out vMove);
inputReader.Read(out hMove);
inputReader.Read(out fire);
}

Move(vMove, hMove);
Fire(fire);
}
Custom field

You can also use custom serialized int instead of PredictableFor, e.g.

[SerializeField] private int player;
{...}
if (ElympicsBehaviour.TryGetInput(ElympicsPlayer.FromIndex(player), out var inputReader))

but in most cases PredictableFor will be the same as player.

Important coding principles

info

These are the most crucial rules for inputs programming and should always be followed!

Collect-apply split

It is crucial to separate the code of collecting inputs and applying them (which is not the way you usually work on a single-player game). Collecting inputs and serializing them to simple data should be kept inside OnInput* methods. Applying inputs from simple data representation to the game world should be separated and called inside ElympicsUpdate (this is your game logic). This is required by the architecture of server authority paradigm i.e. input collected by clients have to be synchronized and cause exactly the same effect both on the client and on the server instance. If that's not the case, the state on the client will be broken.

info

Inputs are collected on the client only, and synchronized with the server by Elympics. They are later applied on both the client and the server.

Example for moving a character:

  • OnInputForClient(): Save joystick axes positions as float values to the input writer
  • ElympicsUpdate(): Read joystick axes positions from the input reader and move the character appropriately.

Input absence vs empty input

Remember to properly handle the absence of input. Below you can see a simple example explaining how the absence of input can be exploited in your game. Let's modify the previous example a bit:

public void ElympicsUpdate()
{
if (!ElympicsBehaviour.TryGetInput(ElympicsPlayer.FromIndex(0), out var inputReader))
return;

inputReader.Read(out var vMove);
inputReader.Read(out var hMove);
inputReader.Read(out var fire);

Move(vMove, hMove);
Fire(fire);
}

private void Move(float vMove, float hMove)
{
rigidbody.velocity = new Vector2(hMove, vMove);
}

Can you see the danger of not handling the absence of input?

If there is no decelerating force or friction, the rigidbody will continue to move at previously set velocity instead of stopping (like it would with zero-input).

The absence of input could be handled in many other ways. For example you could extrapolate missing input and use the one cached from a previous tick as we did in the first example in this article (recommended way). Or you could interpret it as zero-input. It really depends on your specific use case.

Why would input be absent?

There can be many reasons why input for a particular tick is absent. For example player's lag could have increased, thus not being able to deliver inputs at the appropriate time. You should always be prepared for the case of absent input, even in you generate it for every player at every tick.

Examples

FPS-like camera controller

To create FPS-like camera controller you have to read mouse movements properly and process its accumulated increments. We won't do it inside OnInputForClient because this method is called during FixedUpdate, and with framerate greater than tickrate some increments would be lost.

To collect these, we have to use Unity Update instead and cache mouse movements in a variable. Then, during OnInputForClient we can read current mouse position and serialize it as network input.

public class PlayerInputController : ElympicsMonoBehaviour, IInputHandler, IUpdatable
{
[SerializeField] private float mouseSensivity = 1.5f;

private Vector2 mouseAxis = Vector2.zero;

private void Update()
{
var mouseX = Input.GetAxis("Mouse X");
var mouseY = Input.GetAxis("Mouse Y");
mouseAxis += new Vector2(mouseY, mouseX) * mouseSensivity;
}

public void OnInputForClient(IInputWriter inputWriter)
{
if (Elympics.Player != PredictableFor)
return;

inputWriter.Write(mouseAxis.x);
inputWriter.Write(mouseAxis.y);
}

public void ElympicsUpdate()
{
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
{
inputReader.Read(out float mouseX);
inputReader.Read(out float mouseY);

// Set only if received input
transform.localRotation = Quaternion.Euler(new Vector3(mouseX, mouseY, 0));
}
}

{...}
}

Multiple players controlling the same object

TicTacToe is the perfect example of a game in which both players can change state of a shared game board, so ownership is hard to define.

In such cases, the proper way to configure such shared synchronized object to work seamlessly is to set its predictability to None (ElympicsPlayer.World in code).

Then we can send the last chosen field (desired move) as player's input, and validate inside ElympicsPlayer whether that player is allowed to make such move.

public void GetInputForClient(IInputWriter inputWriter)
{
var field = playerInputProvider.GetLastClickedField();
inputWriter.Write(field);
}

public void ElympicsUpdate()
{
for(var i = 0; i < 2; i++) {
var player = ElympicsPlayer.FromIndex(i);
if (ElympicsBehaviour.TryGetInput(player, out var inputReader))
{
inputReader.Read(out float field);

if (!gameState.ValidateMove(player, field))
continue;

gameState.SetFieldForPlayer(player, field);
gameState.ChangeTurn();
}
}
}