Core · interface
LoopHooks
Fixed-timestep game loop with render interpolation.
Explained in The loop.
interface LoopHooksimport type { LoopHooks } from '@driftengine/core';In depth
Simulation always advances in fixedDt increments (determinism requirement,
see AGENTS.md); rendering runs once per animation frame and receives the
interpolation factor alpha in [0, 1) between the two most recent sim states.
Properties
| Name | Type | Description |
|---|---|---|
shouldSimulateoptional | () => boolean | Whether the simulation should advance this frame. Absent means always.MorePause is expressed here rather than by stopping the loop, so a paused game keeps rendering — the world stays on screen behind the menu instead of freezing on whatever the last frame happened to be. |
shouldRenderoptional | () => boolean | Whether this frame is worth drawing at all. Absent means always.MoreFor the clip export, and it is not an optimisation. A clip is a fixed 60 frames a second; a display running at 144 offers 2.4 animation frames per clip frame, and the recorder can only take one of them — so the other 1.4 are a full render of a 1080x1920 frame that is thrown away, competing for the GPU with the one that is kept. Worse, on a 100 Hz display no frame lands near enough to every slot and the capture rate falls to 50: the file then claims 60 fps while holding 50, which is precisely the stutter this exists to remove. Skipped time is not lost — it is added to the The 60 above is the example and not a limit, which is worth saying because every sentence
of it names one. This hook is a predicate the caller supplies, so the rate is whatever the
caller returns true at, and |
Methods
simulate
simulate(dt: number, tick: number): voidAdvance the simulation by exactly dt seconds.
| Parameter | Type | Description |
|---|---|---|
dt | number | |
tick | number |
More
tick counts fixed steps from LoopOptions.startTick, and it is the identity every
networked and recorded thing indexes by: an input belongs to a tick, a snapshot is of a tick,
a desync begins at a tick. AGENTS.md already states the contract it makes concrete — "time
is a tick count the caller supplies" — and the loop was the one place that knew the count and
did not hand it over, so a caller wanting one kept a second counter beside this callback and
hoped the two agreed.
A caller with no use for it takes one argument and is unaffected.
render
render(alpha: number, frameDt: number, wallDt: number, frame?: unknown): void| Parameter | Type | Description |
|---|---|---|
alpha | number | |
frameDt | number | |
wallDt | number | |
frame? | unknown | What the frame source delivered, when it delivers anything.
undefined under the window, which is every frame outside a session. An immersive session
hands over the object every pose in that frame is read from, and it is valid only for the
duration of this call. Typed unknown because core does not name an XRFrame; the package
that does narrows it. An implementation written before this parameter existed still satisfies
the interface, which is why it was added at the end rather than anywhere more readable. |