Game Loop
The Six Phases
Section titled “The Six Phases”Every frame, the engine runs systems in six ordered phases:
EarlyUpdate → FixedUpdate → Update → LateUpdate → Render → EndOfFrame| Phase | Purpose |
|---|---|
| EarlyUpdate | Input polling. Runs once per frame before anything else. |
| FixedUpdate | Physics stepping and deterministic gameplay. May run 0—5 times per frame at a constant timestep. |
| Update | Physics interpolation, then general game logic. Component update(dt) calls happen here. Variable dt. |
| LateUpdate | UI layout, particle simulation, constraint solving, anything that must run after game logic. Variable dt. |
| Render | Display sync: transforms are copied to the display objects, then lighting and the debug overlay draw. |
| EndOfFrame | InputClearSystem clears this frame’s input edges (isJustPressed, isJustReleased). Then deferred entity teardown runs: onDestroy, detach from scene, and the entity:destroyed event. An entity’s isActive already read false earlier, at the destroy() call itself. |
What Runs When
Section titled “What Runs When”Within a phase, systems run in ascending priority. This is every system the engine packages ship, in the order one frame runs them. Engine steps that are not systems are in italics.
| Phase | Order |
|---|---|
| EarlyUpdate | logger frame stamp, scene-time frame tick, transition tick → InputPollSystem (-100, input) |
FixedUpdate (0 to maxFixedStepsPerFrame times) | scene-time fixed tick → PhysicsSystem (0, physics) → ProcessFixedUpdateSystem (500, core) → ComponentFixedUpdateSystem (1000, core) |
| Update | PhysicsInterpolationSystem (-100, physics) → ProcessSystem (500, core) → ComponentUpdateSystem (1000, core) |
| LateUpdate | ParticleSystem (0, particles) → UILayoutSystem (200, ui) → UIRootLayoutSystem (200, ui-react) → FloatingOverlaySystem (201, ui) → UIFocusSystem (202, ui) → UIFocusRelayoutSystem (203, ui) |
| Render | DisplaySystem (0, renderer) → LightingSystem (100, lighting) → DebugRenderSystem (9999, debug) |
| EndOfFrame | InputClearSystem (9000, input) → destroy-queue flush |
The level editor’s preview scene adds three systems of its own to that order. A game never sees them.
| Phase | Editor systems |
|---|---|
| EarlyUpdate | DestroyFlushSystem (0, editor), which runs asset releases after the flush that took their entities out |
| Render | DormantVisualSystem (100, editor), which draws the dormant placements after the renderer’s pass, then OverlaySystem (110, editor), which redraws the editor’s grid, gizmo, and handles |
The priorities fall into bands. Pick the band a new system belongs to and choose a number inside it:
| Band | Runs | Shipped systems |
|---|---|---|
| below 0 | before the engine’s producers: reads external input or the previous step’s state | input poll, physics interpolation |
| 0 | producers | physics step, particles, display |
| 100–201 | consumers of the producers | lighting, UI layout |
| 500 | timing | both process systems |
| 1000 | game code | both component passes |
| above 1000 | after game code | input clear, debug overlay |
Four rules follow from the table:
- Equal priorities run in add order. For plugin systems that is plugin
install order:
UILayoutSystemruns beforeUIRootLayoutSystembecauseui-reactdepends onui. - A system added at runtime waits for its phase’s next run. Add a system while its own phase is running and it first runs at that phase’s next run. Add it during an earlier phase of the same frame and it runs its phase this frame; during a later phase, next frame. A system removed while its phase is running does not run again.
- The component passes visit entities in insertion order, live. An
entity spawned or acquired from a pool during
updateorfixedUpdateis visited by that same pass, after the entity that created it. An entity re-activated withsetActive(true)keeps its original position and is visited by that pass only if it sits after the activator. - Anything a component schedules first advances one pass later. The
process systems (priority 500) have already run when component code
(priority 1000) calls
pc.run, starts a slot, or starts a tween, so the process first advances on the next pass of its clock. A system below 500 that schedules a process sees it advance in the same pass.
Fixed Timestep Accumulator
Section titled “Fixed Timestep Accumulator”The FixedUpdate phase uses an accumulator to deliver a deterministic timestep
regardless of frame rate:
import { Engine } from "@yagejs/core";
const engine = new Engine({ fixedTimestep: 1 / 60, // ~0.0167 s per tick (default) maxFixedStepsPerFrame: 5, // cap to prevent spiral of death});Each frame the engine adds the real elapsed time to an internal accumulator.
While the accumulator holds enough time for another tick, the engine runs one
FixedUpdate pass and subtracts fixedTimestep from the accumulator:
accumulator += frameDeltaSeconds
while (accumulator >= fixedTimestep && steps < maxFixedStepsPerFrame) { runFixedUpdate(fixedTimestep) accumulator -= fixedTimestep steps++}This means:
- On a 60 Hz display,
FixedUpdatetypically runs once per frame. - On a 144 Hz display, it may run zero times in some frames and once in others.
- If a frame takes unusually long (e.g. tab was in background), the cap of
maxFixedStepsPerFrameprevents the engine from trying to “catch up” with dozens of ticks at once (the spiral of death).
Variable Delta Time
Section titled “Variable Delta Time”All phases except FixedUpdate receive the real elapsed time since the last
frame as dt (in seconds). This value fluctuates with frame rate:
import { Component, Transform } from "@yagejs/core";import type { CameraComponent } from "@yagejs/renderer";
class CameraChase extends Component { constructor(private readonly camera: CameraComponent) { super(); }
update(dt: number) { // dt varies each frame — smooth but not deterministic const target = this.sibling(Transform).position; this.camera.position = this.camera.position.lerp(target, 5 * dt); }}The example only shows dt at work. For a real camera, spawn a
CameraEntity with a follow target instead. See
Following a Target.
For gameplay that must be deterministic (physics, game state), use
fixedUpdate(dt) instead where dt is always the constant fixedTimestep
(in seconds).
Where Things Happen
Section titled “Where Things Happen”A quick reference for where to put different kinds of logic:
| What | Phase | Why |
|---|---|---|
| Input polling | EarlyUpdate | Read fresh input before any logic runs |
| Physics step | FixedUpdate | Deterministic simulation at a constant rate |
| Game logic | Update | Component update(dt) calls, AI, timers |
| Camera follow | Update | A component update(dt), reading the positions that get drawn this frame |
| UI layout | LateUpdate | Runs after game logic has settled every position |
| Sprite sync | Render | Push final transforms to the GPU |
| Entity cleanup | EndOfFrame | Safe to remove entities after all systems have run |
Configuration Defaults
Section titled “Configuration Defaults”| Option | Default | Description |
|---|---|---|
fixedTimestep | 1 / 60 (~0.0167 s) | Seconds per fixed-update tick |
maxFixedStepsPerFrame | 5 | Maximum fixed-update iterations per frame |
These can be tuned at engine creation time. A lower fixedTimestep (e.g.
1 / 120) gives more precise physics at the cost of more CPU work. A
higher value (e.g. 1 / 30) is cheaper but less accurate.
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/core-concepts.md.