Unity3D Tutorials – Multiplayer – Latency Compensating Methods

First see my previous tutorial about multiplayer basics at:
http://www.lucedigitale.com/blog/unity3d-tutorials-multiplayer-introduction/

What is LAG?

In online gaming, LAG is a noticeable delay between the action of players and the reaction of the server. Although LAG may be caused by high latency, it may also occur due to insufficient processing power in the client and/or server.

The tolerance for lag depends heavily on the type of game. For instance, a strategy game or a turn-based game with a low pace may have a high threshold or even be mostly unaffected by high delays, whereas a twitch gameplay game such as a first-person shooter with a considerably higher pace may require significantly lower delay to be able to provide satisfying gameplay.

LAG Compensation

First, to follow this lesson you need to see my tutorial about Multiplayer Introduction here:

http://www.lucedigitale.com/blog/unity3d-tutorials-multiplayer-introduction/

In the previous lesson you will notice a small delay between the input and the actual movement, this is because the position is updated after the new data is received. All we can do is predict what is going to happen based on the old data.

One method to predict the next position is by taking the velocity into account. A more accurate end position can be calculated by adding the velocity multiplied by the delay.

Unity3D – Our own synchronization method

MAIN TOP MENU> Edit> Project Settings > Network> Send Rate: 15
The standard settings in Unity3D is that a package is being tried to send 15 times per second, but our eyes are faster than 15 fps and we will see the LAG!
It is not a good practice use a greater value than 15, because we have to optimized the data will send thought the network.

The previous setup was:
Project> Cube (our player prefab)> Inspector> Network View> Observed: Transform

The new setup is:
Project> Cube (our player prefab)> Inspector> Network View> Observed: DRAG AND DROP here ‘Movement (Script)’ component
We will send thought the network our Movement.js to write our own synchronization method.

Movement.js


#pragma strict
 
function Start () {
 
}// END Start()
 
function Update () {
 
 var horiz : float = Input.GetAxis("Horizontal"); // get AD buttons input
 var vert : float = Input.GetAxis("Vertical");    // get WS buttons input
 
 // Solo il server, cioè quello che parte per primo potrà spostare il cubo
 // Object instanted by me
 if (networkView.isMine){
 		transform.Translate(Vector3(horiz,0,vert));      // move along X -> AD and Z -> WS
	} else {
	enabled = false;
	}
}// END Update()

// We can write our own synchronization method START ############################
// This function is automatically called every time it either sends or receives data.
// By using stream.Serialize() the variable will be serialized and received by other clients.
function OnSerializeNetworkView(stream: BitStream, info: NetworkMessageInfo)
{
    var syncPosition : Vector3 = Vector3.zero; // management of position of rigid body
    if (stream.isWriting)
    {
        syncPosition = rigidbody.position; 
        stream.Serialize(syncPosition);
    }
    else
    {
        stream.Serialize(syncPosition);
        rigidbody.position = syncPosition;
    }
}
// We can write our own synchronization method END ###############################

Unity3D – Interpolation

To smooth the transition from the old to the new data values and fix these latency issues, interpolation can be used.

In computer animation, interpolation is inbetweening, or filling in frames between the key frames. It typically calculates the in between frames through use of (usually) piecewise polynomial interpolation to draw images semi-automatically.

We will interpolate between the current position and the new position received after synchronization.

Movement.js


#pragma strict
 
function Start () {
 
}// END Start()
 
function Update () {
 
 // User Input Control START ---------------------------------------------------
 var horiz : float = Input.GetAxis("Horizontal"); // get AD buttons input
 var vert : float = Input.GetAxis("Vertical");    // get WS buttons input
 
 // Solo il server, cioè quello che parte per primo potrà spostare il cubo
 // Object instanted by me
 if (networkView.isMine){
 		transform.Translate(Vector3(horiz,0,vert));      // move along X -> AD and Z -> WS
	} else {
		SyncedMovement();
	}	
 // User Input Control END -----------------------------------------------------	
	
}// END Update()

// We can write our own synchronization method START ############################

private var lastSynchronizationTime: float = 0f;
private var syncDelay: float = 0f; // Delay between Updates
private var syncTime: float = 0f;
private var syncStartPosition: Vector3 = Vector3.zero; // Current Position
private var syncEndPosition: Vector3 = Vector3.zero; // New Position
 
function SyncedMovement ()
{
    syncTime += Time.deltaTime;
    rigidbody.position = Vector3.Lerp(syncStartPosition, syncEndPosition, syncTime / syncDelay);
}// END SyncedMovement()
   
// This function is automatically called every time it either sends or receives data.
// By using stream.Serialize() the variable will be serialized and received by other clients. 
function OnSerializeNetworkView(stream: BitStream , info: NetworkMessageInfo )
{
    var syncPosition: Vector3 = Vector3.zero;
    // The BitStream class represents seralized variables, packed into a stream.
    // if the BitStream is currently being read= TRUE
    // se il flusso di dati sta per essere scritto la posizione è quella ricevuta
    if (stream.isWriting) 
    {
        syncPosition = rigidbody.position;
        stream.Serialize(syncPosition);
    }
    // se il flusso NON stà per essere scritto, interpola i dati INIZIO ---------
    else
    {
        stream.Serialize(syncPosition);
 
        syncTime = 0f;
        syncDelay = Time.time - lastSynchronizationTime;
        lastSynchronizationTime = Time.time;
 
        syncStartPosition = rigidbody.position;
        syncEndPosition = syncPosition;
    }
    // se il flusso NON stà per essere scritto, interpola i dati FINE -----------
}

// We can write our own synchronization method END ###############################

Play and try, now the transitions look smooth! YEAH!

For italian people: come funziona?

1. Se il player è stato generato da me muovilo con GetAxis, altrimenti non posso muoverlo

...

 if (networkView.isMine){
 		transform.Translate(Vector3(horiz,0,vert));
...

2. altrimenti muovilo utilizzando l’interpolazione

...
function SyncedMovement ()
{
    syncTime += Time.deltaTime;
    rigidbody.position = Vector3.Lerp(syncStartPosition, syncEndPosition, syncTime / syncDelay);
}// END SyncedMovement()
...

3. L’interpolazione viene attivata solamente nel momento in cui il flusso di dati non è in scrittura

...
else
    {
        stream.Serialize(syncPosition);
 
        syncTime = 0f;
        syncDelay = Time.time - lastSynchronizationTime;
        lastSynchronizationTime = Time.time;
 
        syncStartPosition = rigidbody.position;
        syncEndPosition = syncPosition;
    }
...

Unity3D – Prediction

You will notice a small delay between the input and the actual movement. This is because the position is updated after the new data is received.
All we can do is predict what is going to happen based on the old data.

One method to predict the next position is by taking the velocity into account. A more accurate end position can be calculated by adding the velocity multiplied by the delay.

Movement.js


#pragma strict
 
function Start () {
 
}// END Start()
 
function Update () {
 
 // User Input Control START ---------------------------------------------------
 var horiz : float = Input.GetAxis("Horizontal"); // get AD buttons input
 var vert : float = Input.GetAxis("Vertical");    // get WS buttons input
 
 // Solo il server, cioè quello che parte per primo potrà spostare il cubo
 // Object instanted by me
 if (networkView.isMine){
 		transform.Translate(Vector3(horiz,0,vert));      // move along X -> AD and Z -> WS
	} else {
		SyncedMovement();
	}	
 // User Input Control END -----------------------------------------------------	
	
}// END Update()

// We can write our own synchronization method START ############################

private var lastSynchronizationTime: float = 0f;
private var syncDelay: float = 0f; // Delay between Updates
private var syncTime: float = 0f;
private var syncStartPosition: Vector3 = Vector3.zero; // Current Position
private var syncEndPosition: Vector3 = Vector3.zero; // New Position
 
function SyncedMovement ()
{
    syncTime += Time.deltaTime;
    rigidbody.position = Vector3.Lerp(syncStartPosition, syncEndPosition, syncTime / syncDelay);
}// END SyncedMovement()
   
// This function is automatically called every time it either sends or receives data.
// By using stream.Serialize() the variable will be serialized and received by other clients. 
function OnSerializeNetworkView(stream: BitStream , info: NetworkMessageInfo )
{
    var syncPosition: Vector3 = Vector3.zero;
    var syncVelocity: Vector3 = Vector3.zero;
    // The BitStream class represents seralized variables, packed into a stream.
    // if the BitStream is currently being read= TRUE
    // se il flusso di dati sta per essere scritto la posizione è quella ricevuta
    if (stream.isWriting) 
    {
        syncPosition = rigidbody.position;
        stream.Serialize(syncPosition);
        
        syncVelocity = rigidbody.velocity;
        stream.Serialize(syncVelocity);
    }
    // se il flusso NON stà per essere scritto, interpola i dati INIZIO ---------
    else
    {
        stream.Serialize(syncPosition);
        stream.Serialize(syncVelocity);
 
        syncTime = 0f;
        syncDelay = Time.time - lastSynchronizationTime;
        lastSynchronizationTime = Time.time;
 
        syncEndPosition = syncPosition + syncVelocity * syncDelay;
        syncStartPosition = rigidbody.position;
    }
    // se il flusso NON stà per essere scritto, interpola i dati FINE -----------
}

// We can write our own synchronization method END ###############################

You will notice the transitions are still smooth and the latency between your input and the actual movement seem less.

My official website: http://www.lucedigitale.com

References:

– http://docs.unity3d.com/Manual/net-HighLevelOverview.html
– http://www.inetdaemon.com/tutorials/internet/ip/whatis_ip_network.shtml
– http://whatismyipaddress.com/nat
– http://vimeo.com/33996023#
– http://www.paladinstudios.com/2013/07/10/how-to-create-an-online-multiplayer-game-with-unity/

Extra resources:

– http://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
– http://developer.valvesoftware.com/wiki/Lag_Compensation
– http://developer.valvesoftware.com/wiki/Working_With_Prediction
– http://www.gamasutra.com/resource_guide/20020916/lambright_01.htm