Fair co-op at 300 ms ping: the netcode behind Dead Rounds

How a browser game keeps four-player co-op fair and responsive - a server that decides everything, client-side movement prediction, lag-compensated shooting and zombie pathfinding over a navmesh.

Dead Rounds uses an authoritative server: the server decides every zombie's movement, every hit, every point and every purchase, while each player's browser predicts only that player's own movement. Shots are lag-compensated by rewinding zombies up to 300 ms to where the shooter saw them. This post walks through how that works over WebSockets in a browser, and what we measured.

Why an authoritative server

In a co-op game it is tempting to let each player's computer report its own kills. It is simpler and feels great on a good connection. It also means anyone can open the browser console and give themselves a million points, and two players can disagree about whether a zombie is dead.

Our server runs the whole match at 30 ticks per second: players, zombies, rounds, doors, purchases. Clients send inputs and get back snapshots. Because the same simulation code also runs inside the tab for solo play, there is exactly one version of the game rules.

Movement: predict, then reconcile

Waiting for the server before moving would make every step feel 100 ms late. Instead:

  1. The browser samples input 60 times per second and sends each input command with a sequence number.
  2. It runs the same movement code the server runs (same speed, same collision, same physics engine, Rapier) and moves your character immediately.
  3. The server replays those commands, then tells the client the last one it processed and where that left you.
  4. The client starts from the server's position and replays every command the server has not processed yet. If the result differs slightly from what you see, the difference is smoothed out over a few frames instead of snapping.

Because both sides run identical code, corrections are rare.

Solid zombies make it harder

Zombies in Dead Rounds are solid: you can slide around one but cannot push through a crowd. But your browser only knows where zombies were a moment ago. So the client guesses where each nearby zombie is right now (its last known position, moved along its velocity by about one round trip) and blocks your predicted movement against those guesses. Each pending command remembers the zombies it was predicted with, so replaying it later gives the same answer.

We tested this with simulated round trips from 70 to 330 ms. Corrections only happen when you first touch a moving zombie, and they are usually under 15 cm and smoothed.

Shooting: rewind time

When you fire, your screen shows the muzzle flash, tracer and recoil right away. The message to the server includes your aim, a seed for the bullet spread and the exact time your screen was showing. The server:

  1. Checks you could fire (fire rate, ammo, not mid-reload or mid-swap).
  2. Rewinds every zombie to where it was at that moment, up to 300 ms back.
  3. Traces the shot, applies damage, points and kills, and sends back hit markers.

So a player with a slow connection hits what was under their crosshair, and the server still has the final word.

Zombie pathfinding on a navmesh

Zombies find their way with a navigation mesh, baked from the map's collision geometry with recast-navigation. Zombies have no physics colliders; they stay out of walls by staying on the navmesh.

Opening a door changes the navmesh. Rebuilding the whole mesh for our biggest map took about 190 ms, which would freeze the server for every player in the room. So the navmesh is now split into 6.4 m tiles, and opening a door rebuilds only the tiles that door touches: about 1 to 5 ms, 15 ms in the worst case.

Spawning uses the same mesh. A zombie climbs in through a window chosen by walking distance to the nearest player, not straight-line distance, so a window just behind a wall but a long walk round still counts as far. A zombie that makes no progress toward anyone for 20 seconds is quietly removed and respawned at a window from which it can reach you, so a round never stalls on one stuck zombie.

Dropped connections

A WebSocket that closes without a goodbye does not end your match. The server keeps your slot, points and guns for 30 seconds, and the client retries in the background with backoff. On reconnect you get the same player id and your movement acknowledgements carry on from where they stopped.

Numbers

If you want to feel it at your own ping, start a co-op room and bring three friends.

All posts