Skip to content

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

PageWhat it answers
The engine loopFixed step, substep cap, alpha, interpolation
Engine modulesHow a host subsystem plugs in, order, services
Assets and the manifestassets.json, string ids, handles, loaders
The wasm boundaryThe WIT world, the JS shapes, direct vs wasm mode
ECS and game codedefineGame, bitecs, tick order, zero allocation
Gaussian splatsThe four GPU buffers, sorting, slots, the fork
Splat charactersRig, decoders, lift, rig backends, expression spaces
AnimationThe four layers, locomotion blending, head aim
PhysicsJolt 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