Inputs

Correct designing and implementing players inputs in Elympics with examples.

Basics

IInputWriter & IInputReader

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

Serialization

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

    inputReader.Read(out float vMove);
    inputReader.Read(out float hMove);
    inputReader.Read(out bool fire);
    float firePower = 0.0f;
    if (fire)
      inputReader.Read(out firePower);
    

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)

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.

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.

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.

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

	public void ElympicsUpdate()
	{
		var vMove = 0.0f;
		var hMove = 0.0f;
		var fire = false;

		if (ElympicsBehaviour.TryGetInput(ElympicsPlayer.FromIndex(0), out var inputReader))
		{
			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
	}
}

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.

Coding principles

These are the most important 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.

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. Or you could interpret it as zero-input. It really depends on your specific use case.

Advanced

Ownership of object

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. In the previous example we allow all players to create inputs for every PlayerInputController. This was fine because we have been considering an example with only one player present!

But if 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?).

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.

public void ElympicsUpdate()
{
	var vMove = 0.0f;
	var hMove = 0.0f;
	var fire = false;

	// 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.

Examples

FPS-like looking around

To create FPS-like looking you have to read mouse movements properly and process its increments. It is not possible inside OnInputForClient because this method is called during FixedUpdate.

To collect these we have to use Unity Update 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");
		var 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 lest 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();
		}
	}
}