Skip to content

Game Loop

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

EarlyUpdate → FixedUpdate → Update → LateUpdate → Render → EndOfFrame
PhasePurpose
EarlyUpdateInput polling, network message ingestion. Runs once per frame before anything else.
FixedUpdatePhysics stepping and deterministic gameplay. May run 0—5 times per frame at a constant timestep.
UpdateGeneral game logic. Component update(dt) calls happen here. Variable dt.
LateUpdateCamera follow, constraint solving, anything that must run after game logic. Variable dt.
RenderDisplay sync — sprite transforms are flushed to the GPU, particles are drawn.
EndOfFrameDeferred entity destruction, component cleanup, event queue flush.

The FixedUpdate phase uses an accumulator to deliver a deterministic timestep regardless of frame rate:

const engine = new Engine({
fixedTimestep: 1000 / 60, // ~16.67 ms 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 += frameDeltaMs
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:

class CameraFollow extends Component {
update(dt: number) {
// dt varies each frame — smooth but not deterministic
const target = this.sibling(Transform).position;
this.camera.lerp(target, 5 * dt);
}
}

For gameplay that must be deterministic (physics, game state), use fixedUpdate(dt) instead where dt is always the constant fixedTimestep converted to 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 followLateUpdateRuns after all positions are finalized
Sprite syncRenderPush final transforms to the GPU
Entity cleanupEndOfFrameSafe to remove entities after all systems have run
OptionDefaultDescription
fixedTimestep1000 / 60 (~16.67 ms)Milliseconds per fixed-update tick
maxFixedStepsPerFrame5Maximum fixed-update iterations per frame

These can be tuned at engine creation time. A lower fixedTimestep (e.g. 1000 / 120) gives more precise physics at the cost of more CPU work. A higher value (e.g. 1000 / 30) is cheaper but less accurate.