The game loop & phases
The game loop is the heartbeat of the engine. vgai uses a fixed-timestep loop with an accumulator, so gameplay and physics advance at a steady rate no matter the display frame rate.
Fixed timestep
Section titled “Fixed timestep”createGameLoop (packages/engine/src/core/game-loop.ts) advances simulation in fixed
increments — fixedTimestep defaults to 1/60 (60 Hz). Each animation frame it adds
the elapsed real time to an accumulator and runs as many fixed steps as fit:
// game-loop.ts (shape)const fixedDt = config.fixedTimestep ?? 1 / 60;const maxSubSteps = config.maxSubSteps ?? 8;let accumulator = 0;
// per frame:const dt = Math.min(rawDt, fixedDt * maxSubSteps) * timeScale;accumulator += dt;let steps = 0;while (accumulator >= fixedDt && steps < maxSubSteps) { config.update(fixedDt); // one fixed tick accumulator -= fixedDt; steps++;}const alpha = accumulator / fixedDt; // interpolation factorconfig.render?.(alpha);Three things to note:
- Spiral-of-death guard.
rawDtis clamped tofixedDt * maxSubStepsand the loop runs at mostmaxSubSteps(default 8) fixed updates per frame, so a long stall can’t snowball into an ever-growing backlog. - Interpolation alpha.
render(alpha)is called once per frame withalpha = accumulator / fixedDt— the fraction of a step left over — so rendering can interpolate between physics states for smoothness. timeScale. SettingtimeScale(clamped to>= 0) scales elapsed time:0pauses,0.5is slow-motion. TheGameSession’spause()/resume()/step()controls build on this.
The eight phases
Section titled “The eight phases”Every fixed tick, systems and component updates run in one fixed, ordered sequence.
The order is defined once in PHASE_ORDER (packages/engine/src/core/types.ts):
| Phase | Purpose |
|---|---|
input | Sample keyboard / mouse / gamepad. |
prePhysics | Apply forces / intents to rigid bodies before the step. |
physics | Step the Rapier world. |
postPhysics | Read results back; sync body transforms to Object3D; dispatch triggers. |
gameLogic | General gameplay. The default phase for GameComponents. |
animation | Advance animation graphs / mixers. |
preRender | Camera / HUD fixups before drawing. |
render | Draw the frame. |
This order is sacred: data flows one way through it, so you never have to reason about
“did this run before or after physics?” A component that reads physics results belongs in
postPhysics or later; one that pushes forces belongs in prePhysics.
Systems
Section titled “Systems”A system is just a function of delta time: type SystemFn = (dt: number) => void. The
SystemRunner (packages/engine/src/core/system-runner.ts) registers systems into phase
buckets and runs each bucket in PHASE_ORDER. A richer SystemDef adds init() and
dispose() lifecycle hooks alongside its phase and update.
Within a single phase, systems run in registration order — there is no implicit dependency resolution. Ordering is explicit by design (the “one obvious way” principle).
See also
Section titled “See also”- Entities & components — how
update()plugs into a phase. - Scenes & prefabs — what runs at load time.
- Engine API reference — the generated phase list + GameComponent contract.
- Design philosophy — why ordering is sacred.