A pluggable, genre-agnostic multiplayer game engine — shared state sync, client-side prediction, and WebSocket transport, with games plugged in as a single object.
TagTag, a small real-time tag game, is the reference application built on top of it — proof that a game can plug in without the engine knowing anything about tagging, colors, or dots.
The engine (packages/state, engine-client, engine-client-pixi, engine-server) owns
everything genre-agnostic: connection lifecycle, snapshot broadcast, client-side prediction and
reconciliation, rendering plumbing, and raw input capture. None of it knows what a "game" is — it
just takes a ruleset: one plain object bundling a game's entity shape, reducer, rendering
hook, input mapping, and tuning. Swapping which game runs is a one-line import change in the
composition-root apps, with zero edits under any packages/engine-* package.
TagTag (packages/rulesets/tagtag) is the first ruleset built against this contract, and
currently the only app running on the engine: everyone controls a dot, one dot is "it" (red, sad
face 😢), and touching them passes it on to you.
- Open the client in two or more browser tabs (or on multiple devices over LAN).
- Move with WASD.
- Once a second player joins, one of you is randomly picked as the tagger (red, sad face). Everyone else gets a happy face and their own color.
- Touch the tagger (or have them touch you) to pass "it" on — there's a 3-second cooldown after each tag, during which the tagger flashes.
- Playing solo pauses the chase — you'll need a friend (or another tab) to actually get tagged.
Requires Bun.
bun install
bun run dev:server # http://localhost:3000
bun run dev:client # http://localhost:5173Open the client URL in two tabs and start moving — that's the whole demo.
Both dev servers bind to all interfaces, so other devices on the same network can join at
http://<host-machine-ip>:5173. The client figures out the WebSocket URL from the page's
hostname automatically — no config needed.
| Package | Role |
|---|---|
packages/state |
Generic engine core (shared types, reducer helper) |
packages/engine-client |
Generic client-side prediction/reconciliation math |
packages/engine-client-pixi |
Generic Pixi rendering client (WebSocket, ticker, input) |
packages/engine-server |
Generic Bun/WebSocket multiplayer server |
packages/rulesets/tagtag |
The example game: entity shape, rules, rendering, sound |
apps/client |
Composition root — mounts the Pixi client with tagtag |
apps/server |
Composition root — boots the server with tagtag |
The four packages/engine-*/state packages never import or reference tagtag (or any
ruleset) by name — a game is always just an object handed to the engine at the composition root.
See docs/ARCHITECTURE.md for a full package-by-package walkthrough.
Bun workspaces · Bun + Hono + native WebSocket (server) · Vite + Svelte + PixiJS (client) · TypeScript throughout.
docs/ARCHITECTURE.md— how every package fits together, the wire protocol, and what TagTag's rules actually dodocs/ENGINE_API.md— the formal engine ↔ ruleset contract (what a new game needs to implement to plug in)docs/ROADMAP.md— what's built, what's next, and why it's sequenced that waydocs/DEPLOYMENT.md— deploying the client and server together as one Railway service
