Concepts
Gameable Engine splits a game in two along one line: the host is a browser page that owns the WebGPURenderer, Jolt physics, input, audio, the asset registry and the splat characters; the guest is all of your game logic, TypeScript compiled to a QuickJS WebAssembly component with bitecs 0.4 inside it. They meet at a single WIT world and a single export per frame — tick(frame-input) -> frame-output — so entity handles are minted by the guest, continuous data crosses as packed list<f32>, structural changes cross as a batched command list, and the only synchronous call back into the host is a physics query. Everything else in the engine is a consequence of that shape: the host loop advances simulation in whole 1/60 s steps and renders whenever the browser asks, blending the gap with alpha; every host subsystem is an EngineModule plugged into that loop in order; content is addressed by string id through a manifest so nothing in game code ever names a URL; worlds are gaussian splats that three.js sorts on the GPU; and a character is the same gaussians rewritten every frame by a compute shader that writes straight into the renderer's own buffers. Because the guest is a value-in, value-out function, npm run dev can run the same TypeScript directly in the host's realm with no build step, and a parity test hashes both modes to keep them honest.
One frame, end to end
browser rAF
│
▼
┌───────────────────┐
│ beginFrame() │ input module latches keys, mouse deltas, gamepads
└─────────┬─────────┘
│
▼
╔═════════╧═════════════════════════════════════════════╗
║ fixedUpdate(1/60), 0..5 times — the simulation step ║
║ ║
║ input state ──┐ ║
║ body buffer ──┼──► frame-input ──► GUEST tick() ║
║ events ──┘ │ bitecs ║
║ │ systems ║
║ ▼ ║
║ frame-output ─┤ ║
║ transforms │ commands ║
║ │ ║
║ apply commands ◄─────────────────────┘ ║
║ spawn / add-body / play-sound / set-expression ║
║ │ ║
║ ▼ ║
║ physics.step(dt) ──► new body transforms ║
╚════════════════════╤══════════════════════════════════╝
│ (leftover time = alpha)
▼
┌───────────────────────────────────────────────────────┐
│ update(dtReal, alpha) — presentation only │
│ interpolate transforms onto three objects │
│ animator: body / additive / face / procedural │
│ character: rig ─► decoders ─► lift ─► GPU buffers │
└────────────────────────┬──────────────────────────────┘
▼
renderer.render(scene, camera)
splats sort back-to-front
│
▼
endFrame()Everything above the alpha line is deterministic and replayable; everything below it is presentation and may be skipped, interpolated or degraded.
The pages
| Page | What it answers |
|---|---|
| The engine loop | Fixed step, substep cap, alpha, interpolation |
| Engine modules | How a host subsystem plugs in, order, services |
| Assets and the manifest | assets.json, string ids, handles, loaders |
| The wasm boundary | The WIT world, the JS shapes, direct vs wasm mode |
| ECS and game code | defineGame, bitecs, tick order, zero allocation |
| Gaussian splats | The four GPU buffers, sorting, slots, the fork |
| Splat characters | Rig, decoders, lift, rig backends, expression spaces |
| Animation | The four layers, locomotion blending, head aim |
| Physics | Jolt bodies, layers and masks, CharacterVirtual, queries |
Where the rules come from
A concept page says how something behaves. AGENTS.md, at the repository root, states the rules that behaviour depends on, and the ADRs say why each one was chosen. When a page and an ADR disagree, the ADR is the decision and the page is the bug.
See also
- Recipes — one task, at most two files
- Glossary — what the jargon means here
- Troubleshooting — symptom first