Introduction

A simple approach to implementing a multiplayer game is to accept:

  • All clients will be behind the server (time-wise), by about ~50-100ms, depending on the client’s ping of course!
  • There will be a slight (again, ~50-100ms) delay between when the client takes an action (e.g. presses a button) and when he/she sees the result.

Client-Side Prediction and Server Reconciliation

If you can accept these two conditions, you no longer have to worry about complexities added by:

  • Client-side prediction
  • Server reconciliation

Client-side prediction is when the client just moves/takes-action immediately based in input, it predicts where the server will move to essentially. Of course by “move” I mean update the game state in some way, this could be moving a character, reducing health, spawning a bullet, etc.

Server reconciliation is when the server receives the client’s input, and then decides what changes to make to the game state (since it is the authority). The server then sends these changes back to the client, which then updates its game state to match the server’s. It should update it “smoothly”.

These two things, though seem simple, are actually quite cumbersome to implement correctly.

Besides, there are a lot of games where having the delay mentioned above (50-100ms) is not a big deal. For example, in strategy games, or more generally, in non-fast-paced games.

Another thing to take into account is that all clients are behind, so it’s fair. Everyone is on an even playing field. Though admittedly, clients closer to the server will have a slight advantage (they will experience less delay).

The Simple Approach

So how do we implement this “simple” multiplayer?

This sequence diagram is a good place to start (explanation is below it):

sequenceDiagram
    participant Client1
    participant Client2
    participant Server

    loop On User Input
        Client1->>Server: Send Input (e.g., key press)
        Client2->>Server: Send Input (e.g., mouse move)
    end

    loop Every 50ms (Example)
        Server->>Server: Process All Queued Inputs, Update Game State,
        Server->>Server: Calculate State-Delta
        Server->>Client1: Send State-Delta
        Server->>Client2: Send State-Delta
    end

    Client1->>Client1: Apply State-Delta, Buffer States, Interpolate
    Client2->>Client2: Apply State-Delta, Buffer States, Interpolate

Let’s start from the top and work our way down.

Client1 and Client2 constantly send their input to the Server. This could be anything, like key presses, mouse clicks, etc.

These inputs are queued up on the Server, and every so often (e.g., every 50ms), the server processes all the queued inputs and updates the game state to reflect it. The server keeps track of what parts of the game state just changed (we’ll talk about how shortly, take a leap of faith for now), and sends these changes (referred to “state-delta” in diagram above) to all clients.

When a client gets a state-delta, it applies it to the last game state it got from the server, and then adds this new state to a buffer (removing an old state if buffer is too big).

The client is then constantly drawing an interpolated version of the game state (interpolated between two buffered states). Remember, the client only has fairly infrequent snapshots of the game state, so if it doesn’t interpolate, the game will look very choppy.

graph TD
    A[Receive Delta from Server] --> B[Apply Delta to Current Game State]
    B --> C[Add New State to Buffer]
    C --> D{Buffer Size > 4?}
    D -- Yes --> E[Remove Oldest State]
    D -- No --> F[Keep Buffer]
    E --> F
    F --> G[Interpolate Between States]
    G --> H[Render Smoothly]