Learn WebSocket - Multiplayer Game Basics
Episode 23 of 34

Learn WebSocket - Multiplayer Game Basics

This episode builds a multiplayer game: a client-server architecture with an authoritative server, state synchronization with delta compression, input handling, latency mitigation with client prediction, and game-specific optimization.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Multiplayer games are the hardest test for WebSocket: every millisecond counts, cheating must be prevented, and players on different networks must see the same world. Failures here are felt instantly — characters teleporting, shots not hitting their target.

Episode 23 covers multiplayer game basics: an architecture with an authoritative server, state synchronization, input collection, latency mitigation, and the optimization that lets 60 players play smoothly in one world.

Game Architecture

The Authoritative Server

The rules run on the server, not the client. The client only sends input and receives the official state.

Client-server game flow
client input -> server memvalidasi -> server update state -> broadcast

An authoritative server prevents cheating: a player cannot just send "I moved here". The client sends "right key pressed", the server computes where the character moves and announces it.

Why Not Just P2P

P2P eliminates server cost, but creates problems: one cheating player can affect everyone, and syncing among N players becomes N-squared connections. For small-scale games, a simple authoritative server is the best balance.

State Synchronization

Tick Rate and Snapshot

Games run in fixed time steps called ticks.

JSGame loop with a fixed tick
const TICK_RATE = 20; // 20 tick per detik
let tick = 0;
 
setInterval(() => {
  tick += 1;
  for (const pemain of pemainList) {
    prosesInput(pemain, tick);
    updateFisika(pemain, tick);
  }
  broadcastState(tick);
}, 1000 / TICK_RATE);

TICK_RATE = 20 means the state is computed 20 times per second and broadcast every tick. All players see the same tick, so the world runs consistently for everyone.

Delta Compression

Sending every player's position every tick balloons. Send only what changed.

JSSending a delta state
function broadcastState(tick) {
  const delta = [];
 
  for (const pemain of pemainList) {
    if (pemain.posisi !== pemain.posisiTerakhir) {
      delta.push({
        id: pemain.id,
        x: Math.round(pemain.posisi.x * 10) / 10,
        y: Math.round(pemain.posisi.y * 10) / 10,
      });
      pemain.posisiTerakhir = pemain.posisi;
    }
  }
 
  io.emit("state", { tick, delta });
}

Math.round(... * 10) / 10 rounds the position to one decimal — enough for visuals, much smaller than full floats. Players who are still do not get sent at all.

Input Handling

The Server Input Buffer

Input from clients arrives irregularly; the server buffers it, then processes per tick.

JSPer-player input buffer
const buffer = new Map();
 
socket.on("input", (input) => {
  input.tick = socket.tickTerakhir;
  const list = buffer.get(socket.id) || [];
  list.push(input);
  buffer.set(socket.id, list);
});
 
function prosesInput(pemain, tick) {
  const inputs = (buffer.get(pemain.id) || [])
    .filter((i) => i.tick <= tick);
  for (const input of inputs) {
    terapkan(input);
  }
  buffer.delete(pemain.id);
}

Input carrying a tick number lets the server order it correctly. Validation stays on the server: maximum speed, plausible positions, and actions allowed by the game state.

Latency Mitigation

Client Prediction

Without prediction, the player's character feels like it slides with one latency of lag. Client prediction runs the movement on the client first, then corrects when the server state arrives.

JSClient-side prediction
function inputKiri() {
  // jalankan lokal untuk respons instan
  pemainLokal.x -= KECEPATAN / 60;
  kirimKeServer("input:kiri");
}

The client moves the character instantly for responsiveness, then the server sends the official state to compare against. If the server state differs from the prediction, the client corrects — usually imperceptibly because the difference is small.

Interpolation and Dead Reckoning

For other players' characters, do not jump to the new position — interpolating between two snapshots makes the movement smooth. Dead reckoning extrapolates the position from the last velocity, so opponents keep appearing to move even when a packet is briefly lost.

Game-Specific Optimization

Interest Management

Players do not need to see the whole map.

JSSending state only to the nearby area
function tetangga(pemain) {
  return pemainList.filter((p) => {
    const dx = p.x - pemain.x;
    const dy = p.y - pemain.y;
    return dx * dx + dy * dy < 40000; // radius 200 unit
  });
}

tetangga(pemain) limits broadcasts by distance — the Area of Interest (AOI) pattern. A player on the left side of the map does not receive updates from a player on the right side, drastically cutting network load.

Update Prioritization

Not all updates are equal: the position of a player you are dueling matters more than a distant enemy. Prioritize per update and send the most urgent ones when bandwidth is limited.

Closing

Episode 23 revealed the secret of multiplayer games that feel smooth: an authoritative server enforces the rules, ticks divide time, delta and AOI save the network, and prediction and interpolation hide latency.

Key takeaways:

  • An authoritative server prevents cheating by executing the rules on the server.
  • A fixed tick rate keeps all players on the same time.
  • Delta compression and rounding shrink the state size.
  • An input buffer enables ordered per-tick processing.
  • Client prediction gives instant response without waiting for the server.
  • Area of Interest trims irrelevant updates.

In the next episode we cover file transfer over WebSocket: chunked transfer, binary data, progress tracking, and chunk size and compression optimization.

Learn WebSocket - Multiplayer Game Basics | Learn WebSocket