Moving avatars
Input in Elympics is processed in a server-authoritative manner. That means it has to be sent to the server before it can be applied. Find out how you can employ this mechanism in your game to allow players to control their characters.
Starting point
No previous experience with Elympics input system is required to complete this tutorial.
The scene that is your starting point should contain the Elympics prefab, two uncontrolled player characters and optionally some basic environment (e.g. a horizontal plane for 3D objects to "walk" on).
This tutorial only covers synchronization of Rigidbody
and Rigidbody2D
as CharacterController
component is currently not supported by Elympics.
Input handler script
For Elympics to acknowledge player input we have to prepare a script implementing IInputHandler
interface. Let's name it InputController
:
public class InputController : MonoBehaviour, IInputHandler
{
public void OnInputForClient(IInputWriter inputSerializer)
{
// TODO
}
public void OnInputForBot(IInputWriter inputSerializer)
{
// TODO
}
}
Gathering the input
Elympics expects us to serialize game input using an instance of IInputWriter
provided as the only argument of OnInputForClient
and OnInputForBot
methods.
OnInputForClient
is responsible for providing the input for characters controlled by human players. For now, we'll only serialize the value of a single axis.
public void OnInputForClient(IInputWriter inputSerializer)
{
inputSerializer.Write(Input.GetAxis("Horizontal"));
}
OnInputForBot
provides the input for bot-controlled characters. It may remain empty if bots are not used or no actions are expected from them:
public void OnInputForBot(IInputWriter inputSerializer)
{ }
Applying the input
After the input is sent to the server, it can be retrieved as IInputReader
using TryGetInput
method of ElympicsBehaviour
property of ElympicsMonoBehaviour
class. ElympicsMonoBehaviour
extends MonoBehaviour
provided by Unity. Aside from allowing the script to be attached as a component, it makes Elympics-specific properties and methods available.
Calls to TryGetInput
are allowed only within ElympicsUpdate
method, so we need to implement IUpdatable
interface as well.
After swapping the parent class and implementing a new interface, our script looks as follows:
public class InputController : ElympicsMonoBehaviour, IInputHandler, IUpdatable
{
// [...]
public void ElympicsUpdate()
{
// TODO
}
}
One last thing to consider is how we identify characters. We could put a serialized field in our InputController
and then fill it with player IDs (starting from 0) after attaching the script to scene objects. But there's a less error-prone method – using PredictableFor
property available on ElympicsMonoBehaviour
. The reason for this is explained in the next step).
The identifier is used when calling TryGetInput
. Input will only be available on the server and on the client which provided it – if TryGetInput
can't access the data, it returns false
.
public void ElympicsUpdate()
{
var horizontalMovement = 0.0f;
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputDeserializer))
{
inputDeserializer.Read(out horizontalMovement);
}
// TODO
}
It's highly recommended that the script uses default values if no input is received from player. Otherwise, the lack of input itself becomes a special case of input and can cause issues that are difficult to debug. The matter is described in detail here.
This behaviour may change in the future.
TryGetInput
is attached to ElympicsBehaviour
because it only receives input associated with the network ID of the object.
Having implemented all Elympics-related stuff, we can now proceed to actually use the input, changing velocity
of associated rigidbody.
The final script is provided below:
- Rigidbody
- Rigidbody2D
using UnityEngine;
using Elympics;
[RequireComponent(typeof(Rigidbody))]
public class InputController : ElympicsMonoBehaviour, IInputHandler, IUpdatable
{
private Rigidbody _rigidbody;
public void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
}
public void OnInputForClient(IInputWriter inputSerializer)
{
inputSerializer.Write(Input.GetAxis("Horizontal"));
}
public void OnInputForBot(IInputWriter inputSerializer)
{ }
public void ElympicsUpdate()
{
var horizontalMovement = 0.0f;
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputDeserializer))
{
inputDeserializer.Read(out horizontalMovement);
}
_rigidbody.velocity = new Vector3(horizontalMovement, _rigidbody.velocity.y, _rigidbody.velocity.z);
}
}
using UnityEngine;
using Elympics;
[RequireComponent(typeof(Rigidbody2D))]
public class InputController : ElympicsMonoBehaviour, IInputHandler, IUpdatable
{
private Rigidbody2D _rigidbody;
public void Awake()
{
_rigidbody = GetComponent<Rigidbody2D>();
}
public void OnInputForClient(IInputWriter inputSerializer)
{
inputSerializer.Write(Input.GetAxis("Horizontal"));
}
public void OnInputForBot(IInputWriter inputSerializer)
{ }
public void ElympicsUpdate()
{
var horizontalMovement = 0.0f;
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputDeserializer))
{
inputDeserializer.Read(out horizontalMovement);
}
_rigidbody.velocity = new Vector3(horizontalMovement, _rigidbody.velocity.y, _rigidbody.velocity.z);
}
}
Attaching the script
All that's left is to add our script component to character game objects. Each object requires its own script as it is a separate synchronized entity.
Attaching any script inheriting from ElympicsMonoBehaviour
results in ElympicsBehaviour
being attached as well, displaying a friendly editor:
ElympicsBehaviour
s identify synchronized game objects (each using unique network ID) and decides who can predict their state. The latter setting is particularly important. Depending on "Predictable for:" value, the state of objects may be simulated by clients before the authoritative version is received from the server.
Characters controlled by players should be predictable to players controlling them – input can be applied immediately giving seamless experience. More about this concept can be found here.
And so, predictability for characters looks like this:
The predictability setting is accessible in ElympicsMonoBehaviour
using PredictableFor
property which we used in the previous step. Because we need to set it either way, providing an additional serialized field for associated player ID would only introduce unnecessary mess.
Result
With the input handler script implemented we're halfway there!
The server runs a perfect simulation and clients predict what they can. Now we only need to consider data sent from the server to clients. This page describes how Elympics network data exchange looks in detail.
Rigidbody synchronization
Synchronization of many Unity-provided components (including rigidbodies) can be enabled in a trouble-free way using ready-made Elympics synchronizers (script components).
Elympics Behaviour (added automatically in the previous section) detects which synchronizers are suitable for its game object and displays a handy button for adding/removing them. Let's add a rigidbody synchronizer for each character:
Result
That's it! After adding the synchronizer everything works as expected:
You may have to lower the tolerance values of synchronized rigidbody properties in order to minimize stuttering of non-predictable opponents. Don't set it to 0 though, as it may result in unneeded reconciliation of current player.
Further steps – extending the input
Synchronizing just a single axis may be not enough. Let's see how we could extend our input handler script to move characters in four directions by reading "Vertical" axis.
First, we need to serialize the new input in OnInputForClient
method:
public void OnInputForClient(IInputWriter inputSerializer)
{
inputSerializer.Write(Input.GetAxis("Horizontal"));
inputSerializer.Write(Input.GetAxis("Vertical"));
}
And then, we have to handle that value in ElympicsUpdate
:
public void ElympicsUpdate()
{
var horizontalMovement = 0.0f;
var verticalMovement = 0.0f;
if (ElympicsBehaviour.TryGetInput(PredictableFor, out var inputDeserializer))
{
inputDeserializer.Read(out horizontalMovement);
inputDeserializer.Read(out verticalMovement);
}
_rigidbody.velocity = new Vector3(horizontalMovement, _rigidbody.velocity.y, verticalMovement);
}
Input must be deserialized in the same order it is serialized.
Synchronizers don't need to be upated in any way, they already synchronize all the required values.
That's all! The final effect is presented below:
Resources
- FPS template, where similar mechanics are implemented