State synchronization

What is network state and how is it synced between server and players?

Basics

ElympicsSnapshot

Snapshot is a binary representation of whole synchronized state. Its structure is described as follows:

  • Tick: number describing time quantum in which the snapshot was captured
  • Data: dictionary of binary data gathered from ElympicsBehaviours
    • key NetworkId: - int index
    • value Data: - binary chunk of data

ElympicsBehaviour

ElympicsBehaviour represents a single object synchronized through the network between server and clients as a part of a snapshot. It is described by unique networkId assigned to it. It enables all Elympics components attached to its parent i.e.:

  • IObservable - empty base interface for everything else, classes implementing this will have ElympicsVars gathered
  • IInputHandler - inputs gathering, better described in the previous section
  • IInitializable - initializing things right after Elympics initialization
  • IUpdatable - contains ElympicsUpdate method to advance game logic in deterministic way
  • IStateSerializationHandler - synchronizing non-ElympicsVar objects with network state
  • IClientHandler - client callbacks for client related actions
  • IServerHandler - server callbacks for game related actions
  • IBotHandler - bot callbacks for bot related actions
  • IReconciliationHandler - client reconciliation tweaks for local state altering e.g. force local camera rotation to stay in the same position even if server says otherwise

and:

  • ElympicsVar - all variables representing synchronized state

ElympicsVar

It’s a base class for all variables shared between clients and server e.g. ElympicsInt, ElympicsVector3 etc. They’re used to:

  • hold underlying synchronized variables (respectively int, Vector3 etc.)
  • serialize and deserialize those variables into binary snapshot
  • check if value has changed over time
  • check if local predicted state matches the data received from server

Serialization / Deserialization

Serialization

Mainly used on Server to collect state and send it to players. Also called on Clients with prediction turned on to save predicted state for further comparison.

The snapshot is collected in the following manner:

  • ElympicsSystem - (Client, Server or Bot) - GetSnapshot
  • ElympicsBehaviourManager - iterating over ElympicsBehaviours in networkId ascending order
  • ElympicsBehaviour - calling Serialize over ElympicsVars and collecting all byte data into a single array.

Deserialization

Used only on Clients to apply incoming data from Server. The snapshot is applied in the following manner:

  • ElympicsSystem - (Client, Server or Bot) - 2 cases whether Client has enabled prediction
    • Without prediction
      • ElympicsBehaviourManager - iterating over ElympicsBehaviours in networkId ascending order
      • ElympicsBehaviour - calling Deserialize over ElympicsVars.
    • With prediction
      • Checking if predicted state is correct, if it’s not correct then
        • ElympicsBehaviourManager - iterating over Predictable ElympicsBehaviours in networkId ascending order
        • ElympicsBehaviour - calling deserialize over ElympicsVars.
      • ElympicsBehaviourManager - iterating over Not Predictable ElympicsBehaviours in networkId ascending order
      • ElympicsBehaviour - calling Deserialize over ElympicsVars.
      • Applying predicted input

Initializing ElympicsVars

ElympicsVars can be initialized in 2 ways, statically or using Initialize.

Static

Simple static assign in class field

private readonly ElympicsInt                 _ticksAlive     = new ElympicsInt(10);
private readonly ElympicsArray<ElympicsBool> _playerAccepted = new ElympicsArray<ElympicsBool>(5, () => new ElympicsBool(false));

Initialize

It requires class to implement IInitializable interface for Initialize method.

public class TemplateBehaviour : ElympicsMonoBehaviour, IInitializable
{
	[SerializeField] private ElympicsBehaviour[] multipleGameObjectReferences;

	private ElympicsArray<ElympicsBool>      _playerAccepted              = null;
	private ElympicsList<ElympicsGameObject> _listWithElympicsGameObjects = null;

	public void Initialize()
	{
		int numberOfPlayers = ElympicsConfig.Load().GetCurrentGameConfig().Players;
		_playerAccepted = new ElympicsArray<ElympicsBool>(numberOfPlayers, () => new ElympicsBool(false));

		_listWithElympicsGameObjects = new ElympicsList<ElympicsGameObject>(() => new ElympicsGameObject(null));
		foreach (var behaviour in multipleGameObjectReferences) 
			_listWithElympicsGameObjects.Add().Value = behaviour;
	}
}

Available types

Simple

  • ElympicsBool
  • ElympicsInt
  • ElympicsFloat
  • ElympicsString
  • ElympicsGameObject

Structs

  • ElympicsVector2
  • ElympicsVector3
  • ElympicsQuaternion

Collections

  • ElympicsArray
  • ElympicsList

ValueChanged

It’s an event called when the value of ElympicsVar changes (taking accuracy tolerance into account).

private readonly ElympicsFloat _timerForDelay  = new ElympicsFloat();

private void Awake()
{
	_timerForDelay.ValueChanged += OnValueChanged;
}

private void OnValueChanged(float lastValue, float newValue)
{
	if (lastValue <= 0 && newValue > 0)
		animator.SetTrigger(true);
}

Prebuilt Synchronizers

Reusable components to synchronize non-trival objects using Elympics.

How it works is explained later on this page, here.

They can be added manually or through buttons in ElympicsBehaviour and respective components are required on the same GameObject.

Synchronizers

ElympicsGameObjectActiveSynchronizer

Simply synchonizing GameObject.active property.

ElympicsTransformSynchronizer

Used to synchronize Transform parameters such as:

  • localPosition
  • localScale
  • localRotation

ElympicsRigidBodySynchronizer

Used to synchronize RigidBody parameters such as:

  • position
  • rotation
  • velocity
  • angularVelocity
  • mass
  • drag
  • angularDrag
  • useGravity

ElympicsRigidBody2DSynchronizer

Used to synchronize RigidBody2D parameters such as:

  • position
  • rotation
  • velocity
  • angularVelocity
  • drag
  • angularDrag
  • inertia
  • mass
  • gravityScale

Coding principles

These are the most important rules for state programming and should always be followed!

Simple variables affecting gameplay have to be synchronized

Lets take a look into snake game with random apple position. The player can’t see it because only server are choosing its position using Random, and he will think that the apple is always at the (0,0) position.

private Vector2 applePosition = Vector2.zero;
private bool isEaten;

public void ElympicsUpdate()
{
	// Server authoritive decision
	if (Elympics.IsServer) {
		if (isEaten) {
			applePosition = new Vector2(Random.Range(-10.0f, 10.0f));
			isEaten = false;
		}
	}
}
private ElympicsVector2 applePosition = new ElympicsVector2(Vector2.zero);
private ElympicsBool isEaten = new ElympicsBool();

public void ElympicsUpdate()
{
	// Server authoritive decision
	if (Elympics.IsServer) {
		if (isEaten) {
			applePosition.Value = new Vector2(Random.Range(-10.0f, 10.0f));
			isEaten.Value = false;
		}
	}
}

It will be a surprise for him if he eats an apple but does not get a point!
Solution for this problem is to synchronize variables.

Other variables affecting gameplay have to be closely linked to ElympicsVars

Lets take a look on example similar to previous one. We are gonna move player (change his transform.position) using input, and we forgot to add ElympicsTransformSynchronizer to player GameObject.

No transform sync

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

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

	transform.localPosition += new Vector2(hMove, vMove);
}

Even if the position would be correct for the first ticks in the matter of time and some indeterministic events the position as viewed by the player will differ more and more from what is on the server.

There is no elegant solution for 2 predictables contact

In this case we are going to analyze what happens when player affects other player behaviour.

Player colliding with other player

Both players have ElympicsRigidBodySynchronizer, and we can predict physics in our game. When we collide with others there will be some inaccuracy in position prediction, like on this diagram:

A->   B
 A->  B
  A-> B
   A->B
   A->B
   A->B
      A->B
       A->B
        A  B
        A  B
        A  B
        A  B

this happens because the position of the colliding enemy cannot be predicted by us and therefore we have to wait for server acknowlegment of position change. Then we can observe some bigger jump in position change - it depends on latency with server our client experiences.

Player hitting other player
public void ElympicsUpdate()
{
	if (!ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
		return;

	inputReader.Read(out int chosenPlayerForLightningAttack);

	players[chosenPlayerForLightningAttack].hp -= 10;
}

In this case, the HP indicator will show us altered value on our game client, but just for one tick and then when server acknowledge us about the change:

100 -> 90 -> 100 -> 100 -> 90 -> 90 -> 90

Be careful mixing predictable and unpredictable code

Common problem, when creating lots of dependent ElympicsBehaviours.
If you create multiple scripts e.g. one for controlling player and input and one for spawn point be careful if they are predictable to the same player. Otherwise actions from predictable script which call methods from unpredictable scripts can make the game stuttering.

// PlayerInputController - *Predictable for players*
public void ElympicsUpdate()
{
	if (!ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
		return;

	inputReader.Read(out bool setSpawn);
	inputReader.Read(out bool spawn);

	if (setSpawn)
		spawnController.SetSpawnPoint(transform.localPosition);

	if (spawn)
		transform.localPosition = spawnController.GetSpawnPoint();
}
```

// SpawnController - *Unpredictable*
private ElympicsVector3 spawnPoint = new ElympicsVector3();
{/**Other players spawn points**/}

public void SetSpawnPoint(Vector3 newSpawnPoint)
{
	spawnPoint.Value = newSpawnPoint;
}

public Vector3 GetSpawnPoint()
{
	return spawnPoint.Value;
}
// PlayerInputController - *Predictable for players*
public void ElympicsUpdate()
{
	if (!ElympicsBehaviour.TryGetInput(PredictableFor, out var inputReader))
		return;

	inputReader.Read(out bool setSpawn);
	inputReader.Read(out bool spawn);

	if (setSpawn && Elympics.IsServer)
		spawnController.SetSpawnPoint(transform.localPosition);

	if (spawn && Elympics.IsServer)
		transform.localPosition = spawnController.GetSpawnPoint();
}
```

// SpawnController - *Unpredictable*
private ElympicsVector3 spawnPoint = new ElympicsVector3();
{/**Other players spawn points**/}

public void SetSpawnPoint(Vector3 newSpawnPoint)
{
	spawnPoint.Value = newSpawnPoint;
}

public Vector3 GetSpawnPoint()
{
	return spawnPoint.Value;
}

Doing Spawn in the next few ticks would teleport player to old spawn point, because SetSpawnPoint will be overriden by recently received server snapshot and set to correct when this information make round trip between client and server. After spawn point change, the client has to correct his position and make additional teleport.

NetworkIds

All NetworkIds of ElympicsBehaviours has to be unique.

Keep in mind that they also describe execution order of all methods called by Elympics like ElympicsUpdate or Initialize. You can read more about this here.

If you want to check order of ElympicsBehaviour there is a button in Elympics GameObject - Refresh Elympics Behaviours.

NetworkId can be set to custom in ElympicsBehaviour component but it has to be unique.

In case of any problems with NetworkIds there is an option Tools -> Elympics -> Reset Networkd Ids.

Advanced

WIP

Synchronized reference

  • ElympicsGameObject and underlying NetworkId
  • Elympics.TryGetBehaviour

PredictableFor

  • player with ownership
  • instant feedback on state with cached inputs

VisibleFor

  • server data intended only for one of the players
  • not seen synchronized reference with bad design

Optimizing snapshots size

  • turning off synchronizer parts like ElympicsRigidBodySynchronizer component -> inertia -> tick
  • ElympicsBehaviour component -> StateUpdateFrequency - will be included in snapshot less often with not changing variables

Accuracy tolerance

  • ElympicsRigidBodySynchronizer component -> Position -> Tolerance
  • adapting tolerance to e.g. position change on big map
  • less reconciliations based on indeterminism on collisions
  • Comparers implementations, how to check if equals in given tolerance

Creating your own synchronizer

  • IStateSerializationHandler
  • case study of rigidbody synchronizer