Autumn-Winter 2020

One Game Library, Imported by Both the Client and the Server

A mobile multiplayer browser game where client and server linked the same TypeScript module. One source of truth, one fewer class of bug.

The decla.red browser game interface showing a space scene.

My thesis was a renderer; proving it in a real multiplayer loop was the point. A real game loop is a worse audience than a tech demo. That’s the point. So through autumn 2020 I built decla.red on top of SDF-2D: a conquest-style space shooter, two teams, small planets, ray-traced 2D rendering, browser and mobile. The architecture decision worth remembering came out of needing the server and the client to stop lying to each other: one TypeScript module containing the game rules, linked by both sides of the wire.

The split that usually goes wrong

Real-time multiplayer has an awkward two-machine problem. The server has to be authoritative or the game is cheatable; the client has to feel immediate or the game is unplayable. If you write the rules twice, once on each side, they will drift. Eventually a player’s screen will say one thing and the server will think another.

I wanted the server’s “compute the next state” function and the client’s “predict the next state locally” function to be literally the same function. So I put the rules in a shared TypeScript library, published nothing, and had both package.json files link to it.

The win wasn’t elegance, it was the bugs that didn’t happen. Client prediction stopped being an approximation of the server; it was the server, run optimistically and reconciled when the authoritative update came back.

Other choices worth a sentence

  • k-d trees for spatial queries. Once the world held more than a few dozen objects, naive collision and proximity checks dominated the server tick. A k-d tree dropped them out of the profile.
  • Message-passing object model. Lifted from Smalltalk’s doesNotUnderstand: idea. Entities respond to messages they care about and ignore the rest. Easier to extend than the inheritance tree I tried first, and less brittle.
  • Firebase only for server discovery. Not for game state, just for “which servers are currently in the pool.” Tiny consistent store, didn’t need to write one.

What I’d change

  • Observability for desync. Multiplayer systems live or die by visibility into divergence. I had logs; I needed dashboards showing the rate, the shape, and the triggering interaction for every prediction miss. Without those, debugging was guessing.
  • Don’t tangle rendering and networking in the same tree. Both were interesting, both put different kinds of pressure on the architecture, and the directories grew into each other. Separate top-level folders from day one next time.
  • Skip multi-server until the math demands it. I wired up multi-server early because it sounded right. With 16–32 clients per server I was nowhere near needing it; the complexity wasn’t free.