Skip to content

Project

Shard Islands

Real-time multiplayer 3D flight game in the browser — self-architected netcode

Role
Sole engineer — concept, netcode, gameplay, deployment
Status
Live
A light-glider trailing a glowing wake over procedurally generated terrain in Shard Islands, with floating energy cores and a domed beacon on the horizon.
0.003cm
client/server drift over 280m of flight
6× faster
frame time after profiling, 4.3ms → 0.70ms
17 KB/s
wire cost, full 24-player room
20Hz
authoritative server tick

The idea

A frosted glass floor with one glowing handprint. No text, no buttons, nothing to read. Press it and the pane shatters into real Voronoi shard geometry, the camera falls through the hole, and about twelve seconds later you are flying a light-glider over a procedurally generated world with other people in it.

There is no loading screen and no lobby, ever. The room is joined at the moment of the click, in parallel with a fracture animation that takes several seconds regardless. By the time the glass has finished falling you are already connected. If the join fails you simply fly alone and are never told, because a spinner would break the only promise the front page makes.

Every surface is generated in code. There are no models, no textures and no art pipeline in the repository.

One simulation, two machines

The server simulates flight, so it needs to know where the ground is and how a glider moves. Sharing constants was not enough — two implementations of the same model drift, and every drift becomes a correction the player feels as a yank.

So the flight step and the terrain height function exist exactly once, in a shared package imported by both halves, and both sides run identical arithmetic over identical inputs. Clients send the stick, never their position. The room runs the model and publishes the result; the client predicts every frame, keeps its unacknowledged inputs, and on each snapshot adopts the authoritative state and replays whatever the server had not yet seen.

Because both sides run the same step, that replay normally reproduces the prediction exactly — measured at 0.003cm of drift over 280 metres of flight. Whatever residual remains decays as a visual offset rather than a jump. Other players are drawn 100ms in the past, interpolated between the two snapshots bracketing that moment.

Testing timing between two machines

The interesting problems here are all about timing between two machines, and none of them can be checked by hand. Two people with two browsers can tell you a tail-clip happened; they cannot tell you whether it happened for the right reason, or whether the near-misses that should have been misses actually were.

So the tests are headless clients that fly scripted geometry and assert on the outcome: client and server agreeing to 0.003cm over 280m; two craft reaching one energy core in the same tick and it being awarded exactly once; a core crossed at speed between two ticks still being collected; nine simultaneous clients, because player colours are seat modulo eight and two of them must match; eleven assertions over five staged tail-clip geometries; an abandoned browser tab correctly ceasing to be a player; and the deployed bundle refusing the origins it should.

Profiling changed what got optimised

I assumed triangle count would be the constraint. It was not: the terrain draws 180,000 triangles in a single call for free, and half a million triangles of instanced trees and grass cost nothing measurable.

Three quarters of the frame was 260 cloud quads, each carrying its own geometry and material, drawing 404 triangles between them. Draw calls went from 225 to 72, and the frame from 4.3ms to 0.70ms. A full room of 24 players now costs 95 calls and 1.05ms — less than a quarter of what an empty world cost before — and each additional player is worth roughly one draw call. On the wire a full room is 17 KB/s, and filling it slows the server tick by 0.6%.

Deployment

The frontend is Next.js and React Three Fiber on Vercel. The server bundles to a single self-contained CommonJS file with no runtime dependencies, ships in a node:20-alpine image, and runs on one Fly machine.

One region, on purpose. A Colyseus room lives in a single process, so a second region would not be a faster route to this world — it would be a second world, whose players are alone.

Built with

  • Next.js
  • React Three Fiber
  • WebGL
  • Colyseus
  • Fly.io
  • pnpm monorepo

Next

Production e-commerce site — Stripe fulfilment, signed-URL delivery, admin CMS