Skip to content

Game Loop

Every frame, the engine runs systems in six ordered phases:

EarlyUpdate → FixedUpdate → Update → LateUpdate → Render → EndOfFrame
PhasePurpose
EarlyUpdateInput polling. Runs once per frame before anything else.
FixedUpdatePhysics stepping and deterministic gameplay. May run 0—5 times per frame at a constant timestep.
UpdatePhysics interpolation, then general game logic. Component update(dt) calls happen here. Variable dt.
LateUpdateUI layout, particle simulation, constraint solving, anything that must run after game logic. Variable dt.
RenderDisplay sync: transforms are copied to the display objects, then lighting and the debug overlay draw.
EndOfFrameInputClearSystem 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.

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.

PhaseOrder
EarlyUpdatelogger 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)
UpdatePhysicsInterpolationSystem (-100, physics) → ProcessSystem (500, core) → ComponentUpdateSystem (1000, core)
LateUpdateParticleSystem (0, particles) → UILayoutSystem (200, ui) → UIRootLayoutSystem (200, ui-react) → FloatingOverlaySystem (201, ui) → UIFocusSystem (202, ui) → UIFocusRelayoutSystem (203, ui)
RenderDisplaySystem (0, renderer) → LightingSystem (100, lighting) → DebugRenderSystem (9999, debug)
EndOfFrameInputClearSystem (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.

PhaseEditor systems
EarlyUpdateDestroyFlushSystem (0, editor), which runs asset releases after the flush that took their entities out
RenderDormantVisualSystem (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:

BandRunsShipped systems
below 0before the engine’s producers: reads external input or the previous step’s stateinput poll, physics interpolation
0producersphysics step, particles, display
100–201consumers of the producerslighting, UI layout
500timingboth process systems
1000game codeboth component passes
above 1000after game codeinput clear, debug overlay

Four rules follow from the table:

  • Equal priorities run in add order. For plugin systems that is plugin install order: UILayoutSystem runs before UIRootLayoutSystem because ui-react depends on ui.
  • 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 update or fixedUpdate is visited by that same pass, after the entity that created it. An entity re-activated with setActive(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.

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, FixedUpdate typically 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 maxFixedStepsPerFrame prevents the engine from trying to “catch up” with dozens of ticks at once (the spiral of death).

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).

A quick reference for where to put different kinds of logic:

WhatPhaseWhy
Input pollingEarlyUpdateRead fresh input before any logic runs
Physics stepFixedUpdateDeterministic simulation at a constant rate
Game logicUpdateComponent update(dt) calls, AI, timers
Camera followUpdateA component update(dt), reading the positions that get drawn this frame
UI layoutLateUpdateRuns after game logic has settled every position
Sprite syncRenderPush final transforms to the GPU
Entity cleanupEndOfFrameSafe to remove entities after all systems have run
OptionDefaultDescription
fixedTimestep1 / 60 (~0.0167 s)Seconds per fixed-update tick
maxFixedStepsPerFrame5Maximum 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.