C.W.K.
Stream
Lesson 06 of 08 · published

Multiplayer Game State

~13 min · app, games, prediction

Level 0Poller
0 XP0/60 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

Inputs at one rate, state at another

Multiplayer games separate the rates: clients send inputs at high frequency (30-60Hz to match the renderer); the server sends authoritative state at a lower frequency (10-20Hz) plus event messages on game-changing actions (kills, scores, level changes). The client interpolates between server snapshots so movement looks smooth at 60fps even though state arrives at 15.

Client-side prediction

Latency makes "send input, wait for server state" feel awful (100ms+ delay on every keypress). The fix is client-side prediction: apply the input locally immediately, then reconcile when the server's authoritative state arrives. If the server disagrees, snap or smoothly correct. Quake III invented this pattern; every modern multiplayer game uses some form of it.

Code

Client: input loop + prediction + reconcile·javascript
const INPUT_RATE = 1000 / 30; // 30 Hz
let seq = 0;
const pendingInputs = [];

setInterval(() => {
  if (ws.readyState !== WebSocket.OPEN) return;
  const input = {
    seq: seq++,
    keys: getPressedKeys(),
    mouse: getMousePosition(),
    ts: performance.now(),
  };
  // 1. Send to server.
  ws.send(JSON.stringify({ type: 'game.input', data: input }));
  // 2. Apply locally for instant feedback.
  applyInputLocal(input);
  // 3. Remember it for reconciliation.
  pendingInputs.push(input);
}, INPUT_RATE);

ws.on('game.state', (state) => {
  // Server says authoritative position is X for input seq N.
  // Drop everything we already reconciled.
  while (pendingInputs.length && pendingInputs[0].seq <= state.lastInputSeq) {
    pendingInputs.shift();
  }
  // Re-apply any inputs the server hasn't seen yet.
  let pos = state.position;
  for (const i of pendingInputs) pos = predict(pos, i);
  setPosition(pos);
});
Server: snapshot at lower rate·python
async def state_broadcaster(manager, world):
    while True:
        snapshot = world.snapshot()  # current authoritative state
        for room in list(manager.rooms):
            await manager.broadcast(room, {
                'type': 'game.state',
                'data': snapshot,
            })
        await asyncio.sleep(1 / 15)  # 15 Hz

External links

Exercise

Build a 2D 'walk around with arrow keys' demo with two players. Inputs at 30Hz, state broadcast at 15Hz, client-side prediction with reconciliation. Add 100ms artificial latency in the browser; movement should still feel instant locally. Now turn off prediction; observe the input lag.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.