Client-Side Prediction and Server Reconciliation

A quick showcase

For this showcase I simulated a server with a update rate of 2 ticks per second to simulate a very slow server

On the left you can see the movement of the character without client-side prediction and server reconciliation: When I try to run in a straight line I'm always pulled back from the server because it has not caught up with my inputs yet.

On the right side  client-side prediction and server reconciliation are ON: I can move smoothly from left to right because I apply the inputs instantly and then reconcile all not already acknowledged inputs

Without Prediction
With Prediction

In the Beginning....

there was a past Project for a Uni course where we had the task to transform a local multiplayer bomber man to a client and server architecture. The basics were already done but not really anything to smooth out all the problems that come with multiplayer once you stop having the client and the server on the same device. The Project itself is rather simple : It's basically just Bomberman which is playable with up to 4 players The game looks like this:

The Messaging Structure:

The Client can send three types of messages:

The Server has also three types of Messages:

Client Prediction and Server Reconciliation

For the client side prediction all I'm doing is taking the input and applying it instantly on the client. These local inputs are then saved in a queue with a sequence number and the input for the Reconciliation part.

//Client.h
struct PredictedState
{
	float x = 0.f;
	float y = 0.f;
};

PredictedState          m_predictedState;
std::deque<PendingInput> m_pendingInputs;    // unacknowledged inputs
int                     m_lastAckedSeq = 0;
int                     input_sequenceNumber = 0;

For the reconciliation part I check if the last acknowledged Number is bigger then the last reconciled Number. If that is the case the client reconciles, reapplies the inputs and then sets his position again based on what the server told him

//Client.cpp
if (m_lastAckedSeq > m_lastReconciledSeq) 
{
		Reconcile(m_lastAckedSeq, 
		serverSnapshot.playerStates[slot].x, 
		serverSnapshot.playerStates[slot].y);

		m_lastReconciledSeq = m_lastAckedSeq;
		players[slot]->Set(m_predictedState.x, m_predictedState.y);
}

In Reconcile the only thing that happens is that the predicted state is updated to the last acknowledged input and the predicted inputs are reapplied.

Conclusion

Overall I'd say that this was a nice small scale project for me to get a better understanding of how client-side prediction and server reconciliation work. At first the concept seemed complicated but once I understood the order in which things happened it became a lot clearer to me. I am happy with the result, and it's nice to see that the movement and bomb placement of the characters now works pretty smooth even when playing to different devices. And a big shoutout to Gabriel Gambettas Fast-Paced Multiplayer Blog that helped me a lot in understanding these concepts.