Ghost-Style Multiplayer: Racing 8 Players With No Collisions

Mars Rally Championship, a free browser rally game, runs its online races with no car-to-car collisions, deliberately. Up to 8 pilots load the same Mars stage at once, see each other as name-tagged translucent rovers, and run their own clock from their own GO. The server is a Colyseus relay: it forwards transforms and ranks reported finish times, and it simulates nothing. That one decision removed client prediction, server reconciliation and rollback from the codebase — and removed rubber-banding from the race.

Diagram: up to 8 pilots' browsers, each simulating its own rover, connected to a central Colyseus relay room that forwards transforms and ranks finish times, with RACE_PROTOCOL_VERSION = 1 checked on both sides at join.

Diagram: each rover is simulated on its own pilot's machine; the relay forwards transforms, ranks finish times and turns away a client whose protocol version differs.

Contact racing is a physics problem wearing a networking costume

I started where everyone starts. I wanted cars that touch. Then I wrote down what touching actually requires.

If two cars can collide, two clients can disagree about the world, and something has to be right. That means an authoritative server running the vehicle simulation — in my case a per-wheel tire model at eight substeps a physics tick, times eight rovers — plus client-side prediction so the local car still answers the key press on the same frame, plus reconciliation to fold the server's correction back into local state without visible jitter. Glenn Fiedler's networked physics series and Valve's Source multiplayer networking article are the canonical descriptions of that machine, and both are honest about the cost: you are not writing netcode, you are writing a second physics engine that has to agree with the first one.

That is a lot of work. What bothered me more is what it buys the player on a bad connection. Every contact racing game I have played eventually produces the same artifact: you are shoved, teleported, or overtaken by someone whose packets arrived late, and a correction lands as a visible position change on your screen. The server was right. You still lost the corner.

So the question stopped being "how do I do this well?" and became "what is contact actually worth here?"

The model I shipped: everyone races the same stage, nobody touches

Ghost-style simultaneous time trial. Everybody in the room loads the same stage, everybody drives at once, and opponents render as translucent rovers with a name plate over the roof — the 25%-opacity ghost mesh the solo system already used, tinted per player. Nothing collides. Your timer starts at your own GO and stops at your own finish line. The server sorts the finish times.

The trade written out plainly, because it is a trade and not a free lunch:

DimensionContact racingGhost-style simultaneous time trial
Server roleAuthoritative physics sim of every car, plus state reconciliation to every clientRelay: forwards transforms, ranks reported finish times, simulates nothing
Latency sensitivityHigh — corrections arrive late and land as visible position changesLow — a late packet delays one opponent's proxy and nothing else
What the player losesNothing, on a good connectionBlocking, drafting, contact, and the pass won with position instead of pace
What the player gainsWheel-to-wheel racing and the drama that comes with itComparable times, no rubber-banding, and a race that survives jitter

The second row decided it. In the ghost model a player on hotel wifi cannot degrade anybody else's race: their proxy gets choppy, or vanishes for half a second, and my car's physics is untouched — their car was never in it.

The server is a relay, and the protocol version is a literal on both sides

Online racing runs against a mars_race Colyseus room. Colyseus handles room lifecycle, schema state sync and matchmaking, and that is all I ask of it. Room state is a small player map — name, tint, transform, checkpoint index, finish time, DNF flag — and the server's only rules are capacity, phase transitions and ranking.

The whole tuning surface is five constants:

// public/js/net.js — mirrored literals from the server's race-config.ts
const RACE_ROOM = 'mars_race';
const RACE_PROTOCOL_VERSION = 1;

const TRANSFORM_SEND_HZ = 15;      // server drops above 25 Hz
const RENDER_DELAY_MS = 150;       // interpolation buffer depth
const STARVE_HIDE_MS = 500;        // hide a remote car after silence
const SNAPSHOT_BUF_CAP = 20;
const PROGRESS_TICK_S = 0.25;

RACE_PROTOCOL_VERSION is the one I would defend hardest. It is a plain literal duplicated in the client and in the server's config, and every create or join call sends it as a join option. If the two disagree, the server refuses the join with race_protocol_mismatch and the client turns that into a readable line: GAME UPDATED — REFRESH THE PAGE TO RACE ONLINE. A browser game deploys under people's feet, and somebody always has a tab open from before the deploy. A skew caught at the door beats one that quietly corrupts a race where half the field reads a field the other half stopped sending. Mirrored constants need a rule to stay mirrored; mine is that both literals move in one change.

The Colyseus client is vendored as a UMD build inside the repo, pinned to the same 0.16.x as the server, and imported lazily the first time someone opens online racing. A live-race feature should not depend on a CDN being reachable, and the rest of the game already runs with no build step.

15 Hz out, 150 milliseconds back

Each client sends its own transform 15 times a second while it is on track — during the countdown too, so the field is visible lined up on the grid before GO. The payload is quantized on the way out, at exactly the precision the ghost recorder already uses: two decimals of position, three of quaternion, one of speed.

S.net.room.send('transform', {
  x: r2(S.carState.pos.x), y: r2(S.carState.pos.y), z: r2(S.carState.pos.z),
  qx: r3(S.carGroup.quaternion.x), qy: r3(S.carGroup.quaternion.y),
  qz: r3(S.carGroup.quaternion.z), qw: r3(S.carGroup.quaternion.w),
  s: Math.round(S.carState.speed * 10) / 10,
});

Fifteen updates a second is nowhere near enough to render smoothly at 60 fps, which is the reason for the second constant. Incoming snapshots go into a small per-player buffer capped at 20 entries — a little over a second of history — and remote cars are drawn not at the newest snapshot but at now - 150 ms, deliberately in the past.

const renderT = performance.now() - RENDER_DELAY_MS;
for (const [, r] of S.remotePlayers) {
  const buf = r.buf;
  const n = buf.length;
  if (n === 0 || renderT > buf[n - 1].t + STARVE_HIDE_MS) {
    r.mesh.visible = false;      // half a second of silence: the car goes away
    continue;
  }
  let i = n - 1;                 // find the pair bracketing renderT
  while (i > 0 && buf[i - 1].t > renderT) i--;
  const f1 = buf[i];
  const f0 = i > 0 ? buf[i - 1] : f1;
  const span = f1.t - f0.t;
  const t = span > 0 ? Math.min(1, Math.max(0, (renderT - f0.t) / span)) : 1;
  r.mesh.position.lerpVectors(_v0, _v1, t);
  r.mesh.quaternion.slerpQuaternions(_q0, _q1, t);
  r.mesh.visible = true;
}

Deliberately rendering late is the part that surprises people, so it is worth stating flatly: interpolation delay is what buys smooth remote motion. At 15 Hz the gap between packets is about 67 ms, so a 150 ms buffer holds roughly two packets in hand. Every frame has a snapshot behind it and a snapshot ahead of it, and the renderer interpolates between two known positions instead of guessing past the newest one. Extrapolation — predicting forward from the last snapshot — is the alternative, and it is what makes remote cars overshoot a corner and snap back. Fiedler's snapshot interpolation write-up is the clearest treatment of this if you want the full derivation.

Diagram: a timeline of one pilot's transforms sent 15 times a second arriving in another pilot's buffer, with the remote rover drawn 150 milliseconds before now, between two snapshots already received.

Diagram: transforms arrive about every 67 ms, and the remote rover is drawn 150 ms behind now, between two snapshots already in hand.

The cost is that opponents are always 150 ms stale. In a contact game that is disqualifying — you cannot lean on a car that is not where it is drawn. In a game where nobody touches anybody, it is invisible. You are chasing a reference, not a hitbox.

When a player's packets stop, their car is hidden after 500 ms rather than frozen in place. A rover parked mid-corner reads as a hazard, and in a game with no collisions a hazard is a lie.

A separate 4 Hz tick drives the standings: rank chip on the HUD, live table on the results screen. Finishers sort by time, everyone else by checkpoints cleared, DNFs last. Checkpoint and finish messages ride events the solo game already emitted, so the race logic needed no call-site edits.

Rooms, invite links, and a countdown nobody wins

The lobby is deliberately thin. Public rooms come from an HTTP list the browser polls every three seconds; private rooms do not list at all, and travel instead as a link ending in ?join=<code> that opens the game with the code prefilled and one button to press. Everyone gets a 20-character call sign stored locally, defaulting to PILOT, and there is no account anywhere — the whole pitch of racing friends in a browser.

The start sequence has one barrier that matters. When the host starts, the room moves to a loading phase and every client loads terrain, reporting back when it is ready; the countdown holds on WAITING FOR PLAYERS until the server flips the phase. Only then does each client reset its own 3-2-1-GO. Every countdown therefore begins within one state patch of every other, and every timer starts at its own GO — so a slow loader delays the race instead of losing it.

Diagram: six steps from creating a race room and sending a join link, through everyone loading and the phase flip, to each client's own 3-2-1-GO and the server sorting finish times.

Diagram: create a room, share the join link, wait while everyone loads, then every client counts down on its own; the server only sorts the finish times.

Two smaller decisions fell out of having no authority. Mid-race pause is disabled, because a local pause would freeze one car while the field kept racing; pressing pause opens a leave-race confirmation instead, and the sim and the timer keep running behind it. Leaving is a DNF. And if the connection drops mid-race, the run does not end — you get a toast saying the race continues offline, and you can still drive it to the finish, because the physics was always local anyway. The finish still counts: an online race's inputs re-simulate on the server exactly like a solo run's, so every finished race goes on the leaderboard under your call sign, dropped connection or not.

What ghost-style racing actually costs

I do not want to undersell this. You lose real things.

You cannot block. You cannot defend a line. You cannot lean on someone through a corner and take a place you did not have the pace for. No contact drama, no first-corner pileup, no rivalry built out of paint trading. It is a race against times, not against cars, and if what you want from multiplayer is the fight, no amount of tuning will get you there from here.

What softens it here is the setting. Mars has 0.020 kg/m³ of air, about 1.6% of Earth's, so aerodynamic drag at 240 km/h works out to roughly 0.02 m/s². Slipstreaming a car you cannot touch would be worth essentially nothing even if I had modelled it. Contact would have mattered; drafting would not. On a game set on Earth, that column of the trade is heavier.

Determinism turns a finish into a checkable artifact

The reason ghosts work in this game is that the simulation is deterministic: a fixed 60 Hz physics step, a world seeded the same for everyone, and inputs recorded every tick. Replay the inputs and you get the run back bit-exact — which is what ghost racing is built on, and the subject of a longer write-up on deterministic replays. Our ghost racing games guide compares how other racing games handle ghosts, in the browser, on downloads and on consoles.

The same property has a second use that I did not plan for, and it is the strongest argument for this whole architecture. If a finished run is an input recording plus a deterministic sim, then a submitted time is not a claim — it is an artifact, and an artifact can be checked rather than trusted.

The leaderboard checks every one. A submission is the run's input recording, and after the cheap checks — stage and time bounds, a physics floor derived from route length and the car's top speed, a rate limit — the server rebuilds the stage from its terrain files and drives the recording headlessly through the same simulation the browser ran. A checksum of the whole simulation state has to match every simulated second, and the finish has to land on the claimed millisecond; a run that does not reproduce is refused, and the time that goes up is the one the server's simulation produced. That is possible at all only because the physics is deterministic, which is the same property that makes a translucent opponent a faithful reference rather than a decoration.

Contact racing forecloses that. Once the server owns the simulation, the client's recording is an opinion about a world it did not author.

Where to try this

  • Online racing — how rooms, invite links and the ranking work from a player's side.
  • Start a race — create a room, copy the link, send it to one person.
  • Ghost racing — the solo half of the same idea, against your own recordings and the leaderboard's.
  • The leaderboard — where validated times land.

The game runs at /play with no download and no account. If you build on this design, I would rather hear the trade was wrong for your game than that this post pretended there wasn't one.

Questions, answered


How does multiplayer racing netcode work?

Most racing games run an authoritative server that simulates every car, while each client predicts its own car locally and reconciles when the server disagrees. Mars Rally Championship takes the lighter path: a relay forwards each player's position 15 times a second, every client simulates only its own car, and the server ranks finish times without simulating anything.

Why do some racing games have no collisions?

Because collisions require an authoritative physics server plus prediction and reconciliation, and even done well that produces late corrections a player experiences as being shoved or teleported. Removing car contact makes latency almost irrelevant, keeps every pilot's time directly comparable, and lets the server be a simple relay. The cost is real: no blocking, no drafting, no contact racing.

What is interpolation delay?

Interpolation delay is rendering other players slightly in the past so their motion can be drawn between two known positions instead of guessed ahead. Mars Rally Championship draws opponents 150 milliseconds behind live, with updates arriving 15 times a second. That buffer holds about two packets in hand, so remote cars move smoothly instead of overshooting corners and snapping back.

Can you do real-time multiplayer in a browser?

Yes. WebSockets carry small binary state patches with low overhead, and a relay room handles up to 8 pilots per race comfortably. Mars Rally Championship uses a Colyseus room for lifecycle and state sync, sends quantized transforms at 15 Hz, and needs no plugin, no download and no account. Ghost-style racing tolerates ordinary home-connection latency well.

Does online racing need an account?

No. Online racing needs a call sign, which is up to 20 characters stored in your browser and defaults to PILOT. There is no sign-up, no email and no download. A private room is shared as a link with a join code in it, and anyone who opens that link lands in the room with the code already filled in.

Keep reading


LOG · October 14, 2026Browser Gaming

Best Racing Games With Leaderboards You Can Trust

Racing games with leaderboards, judged on trust: who checks a run before it posts, whether you can watch it or race its ghost, and if you need an account.

Read the dispatch
LOG · October 14, 2026Browser Gaming

Games Like Smash Karts: Free Multiplayer Driving Games

Games like Smash Karts that run free in a browser: kart combat, car soccer and sandbox picks, plus a weapon-free race for up to 8 friends from one link.

Read the dispatch
LOG · October 14, 2026Browser Gaming

Best Multiplayer Drift Games to Play With Friends

Drift multiplayer games compared: private rooms, public lobbies, tandem and battle modes, collisions, player caps and accounts, browser to PC and console.

Read the dispatch