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
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:
- JOIN: (1 byte) this tells the server we have joined
- LEAVE: (1 byte) this informs the server that we have left
- INPUT: (1 byte) sends a input to the server which can be FORWARD,BACKWARD,LEFT,RIGHT OR BOMB_PLACED
The Server has also three types of Messages:
- JOIN_RESULT: (1 byte) Informs the player if they joined successfully
- STATE: the current state of the game with all the relevant information (positions of the players (4x1 byte), server tick (1byte), last precessedInputSequence (1byte), who placed bombs(1 byte) and additional 0-4 bytes if bombs are placed)
- EXPLOSION: (2 bytes + 4x1 integer for player scores ) Message that tells the client that a bomb has exploded and updates player scores
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.