Client-Side Prediction and Server Reconciliation
Example gif:
Here is a nice example where you can see the Client-Side Prediction and Server Reconciliation in action: I die to the bomb and respawn on the corner. The respawn on the corner happens only client side so for the server I am still at my last death position. When I respawn a try to move again the reconciliation snaps me back to the server position and moves me from there.
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 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.