DarkRift 2 Tutorial for Unity 3D – Part 11 – Synchronize physics over network (2/2)

Reminder : You can find all the DarkRift2 related articles here 
You can find the entire project on my official GitHub

Prepare the bouncy ball to be synchronized

On the script NetworkBouncyBall, we want to receive the ball position from the server. So we need to listen to the event “MessageReceived” from the client manager. As said in the previous article, messages order is not guaranteed, thus we need to store the last received message from the server wich contains the serverTick (it’s incremented each FixedUpdate()).

We also need the clientTick wich is the counter that is incremented by the client each FixedUpdate in the NetworkBouncyBall Script. This counter will help us to do reconciliation.

The reconciliation is the process wich will correct the information of the client compared to those of the server. As the client send the data each 10 ticks, we’ll need a list of all client position each fixed update.

Here are the new properties we need in the NetworkBouncyBall script :

/// <summary>
/// Last received message from the server
/// </summary>
public BouncyBallSyncMessageModel lastReceivedMessage;

/// <summary>
/// Tick counted by the client
/// </summary>
public int clientTick = -1;

/// <summary>
/// Contains data wich will be generated by the client
/// </summary>
public List<BouncyBallSyncMessageModel> reconciliationInfoList;

Listen for the message receive events

As said, the client need to receive messages from the server, As you already did it in the past articles, i just will give you the code :

public override void Start()
{
	base.Start();

	////////////////////////////////////
	// Get references
	rigidbodyReference = GetComponent<Rigidbody>();

	//If we are on client side
	if (!Equals(ClientManager.instance, null))
	{
		//////////////////
		/// Suscribe to events
		ClientManager.instance.clientReference.MessageReceived += UpdateFromServerState;
		reconciliationInfoList = new List<BouncyBallSyncMessageModel>();
	}
}

private void OnDestroy()
{
	//If we are on client side
	if (!Equals(ClientManager.instance, null))
	{
		ClientManager.instance.clientReference.MessageReceived -= UpdateFromServerState;
	}
}

You can notice here that there is 2 interesting things :

  • Initialization of the reconciliation data : reconciliationInfoList. (Be sure to use the namespace using System.Collections.Generic)
  • Unsuscribe to the event on the OnDestroy function. It’s a good practice to always to that.

Now, we finally can create our function UpdateFromServerState :

/// <summary>
/// update from the server state
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UpdateFromServerState(object sender, DarkRift.Client.MessageReceivedEventArgs e)
{
	if (e.Tag == NetworkTags.InGame.BOUNCY_BALL_SYNC_POS)
	{
		//Get message data
		BouncyBallSyncMessageModel syncMessage = e.GetMessage().Deserialize<BouncyBallSyncMessageModel>();

		//If this is the first time we receive the message
		if (Object.Equals(null, lastReceivedMessage))
		{
			//Update data
			rigidbodyReference.velocity = syncMessage.velocity;
			rigidbodyReference.transform.position = syncMessage.position;
			clientTick = syncMessage.serverTick;
			lastReceivedMessage = syncMessage;
		}

		//If the message regards this object and is older than the previous one
		if (id == syncMessage.networkID && syncMessage.serverTick > lastReceivedMessage.serverTick)
		{
			lastReceivedMessage = syncMessage;
		}
	}
}

This needs some explanations.

First time we receive the message

When it’s the first time we receive this message, we need to initialize some data as :

  • Client tick : this is the counter of the ball in the client side. When we receive the first message from the server, we set the same value than the server. It will be used to make reconciliation.
NOTE : using the fixed update function to increment the counter is not the safest way. I heard that sometimes, Unity can skip some fixed update. That why if you plan to do a multiplayer game, you need a strongest way to handle counters.
  • Synchronize immediately the velocity and the position.
  • Store the message in the last received message

Receive following messages

For the following messages, we only need to store the message received in the property lastMessagereceived.

Reconciliation

At this point, we only synchronize the position and the velocity of the ball from the first received message .

As it’s a game object affected by physics, we need to do correction, if needed, in the FixedUpdate() function.

In this function we’ll perform these following actions :

  • Increment the client tick counter
  • Add data to the reconcialiation info list.
  • Performs reconciliation

Here is the code :

private void FixedUpdate()
{
	//If we are on server side
	if (!Equals(GameServerManager.instance, null))
	{
		if (GameServerManager.instance.currentTick % 1 == 0)
			SendBallPositionToClients();
	}
	else if (!Equals(ClientManager.instance, null) && clientTick != -1)
	{
		clientTick++;
		reconciliationInfoList.Add(new BouncyBallSyncMessageModel
		{
			position = transform.position,
			serverTick = clientTick,
			velocity = rigidbodyReference.velocity
		});

		Reconciliate();
	}
}

How it will works ?

As the physic engine will works both on client and server, we need to check frequently if the divergence between client and server is not to important.

When we receive the message from the server, we need to check the position of the ball at this specific tick on the client. For that, we’ll use the reconciliationInfoList to take the snaphot of the ball at this tick (wich is the server tick received).

If the difference is above a certain threshold, we’ll correct basically the position and the velocity of the client immediately.

/// <summary>
/// Reconciliate the client with the server data
/// </summary>
private void Reconciliate()
{
	if (reconciliationInfoList.Count() > 0)
	{
		//Get the position of the client at this specific frame
		BouncyBallSyncMessageModel clientInfo = reconciliationInfoList.Where(i => i.serverTick == lastReceivedMessage.serverTick).FirstOrDefault();

		//If there is more than 50 tick that the ball has not been updated depending to the server position
		if (reconciliationInfoList.Count() > 50)
		{
			rigidbodyReference.velocity = lastReceivedMessage.velocity;
			rigidbodyReference.transform.position = lastReceivedMessage.position;
			clientTick = lastReceivedMessage.serverTick;
			clientInfo = lastReceivedMessage;
		}                

		if (!Equals(clientInfo, null))
		{
			//Check for position divergence
			if (Vector3.Distance(clientInfo.position, lastReceivedMessage.position) >= 0.05f)
			{
				//Update data
				rigidbodyReference.velocity = lastReceivedMessage.velocity;
				rigidbodyReference.transform.position = lastReceivedMessage.position;
			}

			//Empty the list
			reconciliationInfoList.Clear();
		}            
	}
}

If a reconcialiation is made, we empty the list to free memory.

NOTE : In this tutorial, i clear the list only when a reconcialition occurs. In your game, i suggest you to limit the length of the list to not consumme  
a large amount of memory, especially if there is no often reconciliation

Let’s try the result

At this point of the tutorial, we receive the ball position each 10 tick. (as defined in the previous article). The ball only bounce on the Y axis. Let’s try it. I will
will launch the server scene on Unity then build my client an launch it.

To build the client executable, you need to select both MainClientScene and MainGameScene like that (The order is important). Unselect Server Build if already select :
NOTE : I added two cubes to make the ball bounce randomly each start

Here is the result :

Seems to be good ! we finally synchronized our physic game object.

As the position of the ball is lead by the physic engine, it’s a little complicated to make interpolation. We definitively can do that to make the reconciliation smoother.

Improvement ideas

There is a lot to do to increase the playability, the smoothness, … Here is some tips that you can use :

  • Increase the rate of messages sending by the server
  • Use math to predict the ball position to make the ball move smoother on the client screen when reconciliate
  • Force reconcialition each X tick should be adapted to each client connection

Thanks for reading !

Thanks for reading this last article about how to synchronize a physic object bewteen a server and a client with Dark Rift 2. If you liked it, you can thanks me by :

  • Following me on twitter
  • Give me you feedback

See you soon for another tutorial or a Unity 3D tips
thanks you, David.