On this page
  1. Phases
  2. Lifecycle groups
  3. Engine system order
  4. Fixed and presentation clocks
  5. System placement
  6. Disabling phases
  7. Direct phase execution

Phases

A system names the phase that runs it:

world:addSystem({
    name = "game.StepEnemies",
    phase = tecs.ecs.phases.FixedUpdate,
    run = stepEnemies,
})

world:addSystem({
    name = "game.FadeTints",
    phase = tecs.ecs.phases.Update,
    run = fadeTints,
})

FixedUpdate runs on the simulation clock. Update runs once per frame on the presentation clock. The phase gives game systems an order relative to engine systems.

Application calls world:startup() once, world:update(dt) every iteration, and world:shutdown() at teardown. Events do not occupy a phase; see Observer timing.

Lifecycle groups

The phase tree hangs from tecs.ecs.phases.AllGroups:

  • startup() runs StartupGroup: PreStartup, Startup, and PostStartup.
  • update(dt) runs MainGroup: First, PreUpdate, FixedUpdateGroup, Update, PostUpdate, RenderGroup, and Last.
  • shutdown() runs ShutdownGroup: PreShutdown, Shutdown, and PostShutdown.

FixedUpdateGroup contains FixedFirst, FixedPreUpdate, FixedUpdate, FixedPostUpdate, and FixedLast.

RenderGroup contains RenderFirst, PreRender, Render, PostRender, and RenderLast.

Application startup runs after the entry plugin registers its systems and entities. Startup work therefore finishes before the first frame and does not inflate that frame's dt.

Engine system order

The engine installs its work into the same tree:

Phase Engine work
First Advance frame-clock sequences
FixedFirst Latch fixed input, snapshot transforms, advance fixed-clock sequences
FixedUpdate Run TTL and physics
FixedPostUpdate Copy physics poses
FixedLast Leave fixed-input mode
Update Advance presentation-clock sequences
PostUpdate Compose relative transforms, play sounds, encode animation, lay out text, sync emitters
RenderFirst Extract the world into a frame packet
RenderLast Sample relative-transform dirtiness

Plugins install optional rows such as physics, animation, text, and particles. Every world installs the builtin rows.

Extraction runs in RenderFirst. A system that changes what the current frame draws must run before extraction. PostUpdate provides the last general phase for that work.

A change made after extraction draws one frame late rather than never. Spawning, despawning, and writing a component the renderer draws from all reach the instance buffer on the next frame's extraction, even though the frame's dirty marks are cleared in between. Latency is the whole of the cost, so a system that has to run in Render, PostRender, RenderLast, or Last is free to write; one that needs the current frame to show its change still belongs earlier.

GPU submission does not run as a system. After world:update returns, Application acquires a frame, calls Renderer:render, and submits it. Render, PostRender, and RenderLast remain available to game systems even though the renderer itself does not submit there.

Fixed and presentation clocks

Fixed phases receive the configured timestep as dt. world:update consumes accumulated time in whole steps and caps one frame at ten steps, so a long stall cannot create an unbounded catch-up loop.

Variable phases receive the frame dt. Use them for presentation work that should follow display rate rather than simulation rate.

world:getFixedTiming() returns the timestep, the unconsumed accumulator, and an interpolation alpha clamped to [0, 1]:

local timestep, accumulator, alpha = world:getFixedTiming()

PreviousTransform2D lets the renderer interpolate an entity between its last two fixed poses. tecs.SnapshotTransforms copies the current pose in FixedFirst before simulation changes it.

world:fixedStepCount() counts completed fixed steps. The fixed clock and its count advance even when no fixed system exists or callers disable the fixed group.

System placement

Systems within one phase follow insertion order unless before or after names another system. The engine table above supplies the names and boundaries that game plugins commonly order around; Systems covers those constraints.

Concrete phases expose position for inspection. The world assigns it, and callers must treat it as read-only. Groups expose a read-only children tree and have no position. Select phases by object instead of storing numeric positions.

A custom phase must enter the world's pipeline before a system can use it:

world:registerPhase(MyPhase)
world:addSystem({
    name = "game.CustomStep",
    phase = MyPhase,
    run = customStep,
})

registerPhase assigns a missing position and enables the phase. Registration rejects an invalid phase object, and system registration rejects a phase that the pipeline does not know.

Disabling phases

Disabling a group also disables its descendants:

world:disablePhase(tecs.ecs.phases.FixedUpdateGroup)
world:enablePhase(tecs.ecs.phases.FixedUpdateGroup)

Disabling FixedUpdateGroup stops its systems but not the fixed clock.

Disabling RenderGroup also stops RenderFirst, so extraction stops updating the frame packet. GPU submission still draws the last packet because it runs outside the phase tree. To pause gameplay while presentation continues, use the state stack and logic queries.

Direct phase execution

world:runPhase(phase, dt) dispatches one phase or group immediately:

world:runPhase(tecs.ecs.phases.RenderGroup, dt)

It honors disabled state, including disabled ancestors. Re-enable a phase before calling it directly.

Unlike world:update, runPhase neither unwinds existing deferred scopes nor clears dirty bits afterwards. That contract supports custom loops that run parts of the phase tree on separate ticks.