Simulation and time¶
Pressing play runs the game: behaviors, physics, animation, particles, audio and UI advance in fixed 1/60 s ticks
from a snapshot of the scene, and stopping restores that snapshot. The simulation is deterministic, so the same scene
and the same input replay identically, which is what lets agents step, trace and test a game the way a player would
play it. This page covers play control, determinism, the game's own pause and time scale, the process component,
and how real-time frames on fast displays stay smooth without touching the simulation.
The berrybrook windmill: a Wander behavior ("The sails turn slowly in the breeze") turns the sails every tick.
Concepts¶
Play states¶
| State | What happens | How you get there |
|---|---|---|
| Editing | Nothing ticks. Edits go to the scene and the history. Particles and some effects preview live. | Start, or stop |
| Playing | The engine snapshots the scene, then runs fixed ticks of 1/60 s. | play (Cmd+P in the editor) |
| Paused (editor) | Nothing ticks, but step advances exact ticks. |
pause (Cmd+Shift+P) |
| Stopped | The pre-play snapshot is restored: positions, vars, spawned and destroyed entities, everything. | stop (Cmd+.) |
sim_control drives all of them, and step runs N ticks synchronously and returns the logs and errors produced:
sim_control {"action": "play"}
sim_control {"action": "pause"}
sim_control {"action": "step", "ticks": 60}
sim_control {"action": "status"}
sim_control {"action": "stop"}
step takes 1 to 36000 ticks (60 = one second) and works while the editor pause is on, which is how agents test
behaviors frame by frame.
Determinism¶
The simulation replays exactly. Several rules make that true:
| Rule | Consequence |
|---|---|
Fixed ticks of 1/60 s in Engine::step |
Results never depend on frame rate or machine speed. |
| Entities are processed in scene order | The order is part of the scene file and survives undo, redo, save and load. |
| Events are delivered on the next tick, in emission order | No handler sees an event in the same tick it was emitted. |
Randomness is a PCG32 generator seeded by the scene's seed |
random, chance, pick, shuffle and random_int replay. Wander has no access to the wall clock. |
| Every Wander handler run has a budget of 1,000,000 steps | Loops and calls are charged; a runaway loop aborts with an error at its line instead of hanging the engine. |
| Spawns are bounded and deferred | At most 256 spawns per tick and 20,000 entities; spawned behaviors start next tick; destroy takes effect at the end of the tick. |
| Pause and time-scale requests apply at tick boundaries | A pause during which nothing runs is invisible to the simulation. |
| Stop restores the pre-play snapshot | Every run starts from the same state. |
Edits made while playing (by you, by an agent, by a tool) are atomic too: if one fails it is rolled back from a snapshot. They are still undone when play stops.
What happens in one tick¶
Every tick, the runtime first applies pause and time-scale requests made since the last tick and sends
on pause / on resume. It then visits active entities in scene order. For each script it starts new instances
(var initializers, on start, the initial state's on enter), resumes handlers whose wait elapsed, delivers last
tick's events, then physics contacts, key presses, input actions and clicks, and finally runs on tick
(behavior-level first, then the current state's). Physics, navigation, animation, particles, sprites, dialogue, UI,
audio and native modules advance in the same tick.
Two kinds of pause¶
| Editor pause | Game pause | |
|---|---|---|
| Started by | sim_control {"action": "pause"}, the toolbar, Cmd+Shift+P |
pause_game() in Wander, sim_control {"action": "pause_game"} |
| What stops | Everything: no ticks at all | Entities whose process.mode is pausable (the default) |
| What keeps running | Nothing (but step works) |
UI canvases, entities with mode always or when_paused |
| Events | None | on pause and on resume reach every behavior, even stopped ones |
| Purpose | Inspect and debug | A pause menu inside the game |
The process component¶
process decides how an entity and its children run while the game is paused or slowed down, in which order their
behaviors run, and whether their motion is smoothed on screen. Every field defaults to inherit: the value comes from
the nearest ancestor that sets it.
| Field | Values | Meaning |
|---|---|---|
mode |
inherit, pausable, when_paused, always, disabled |
When the entity runs. Root default: pausable. |
priority |
integer | Behaviors run in (priority, scene order): lower first. Not inherited. |
clock |
inherit, game, real |
game follows the time scale; real ignores it (menus, HUD). |
interpolation |
inherit, on, off |
Whether the entity's motion is smoothed between ticks on screen. |
mode |
Runs while the game plays | Runs while the game is paused |
|---|---|---|
pausable |
yes | no |
when_paused |
no | yes (pause menu logic) |
always |
yes | yes (UI, music controllers) |
disabled |
no | no (frozen, still drawn) |
A UI canvas without its own setting runs always on the real clock, so menus work during a pause and in slow
motion. Give a HUD mode: pausable to freeze it with the game.
What each system does¶
| System | While the game is paused | Under a time scale |
|---|---|---|
| Wander | Stopped instances keep their coroutines, timers and state; they still get on pause / on resume; events they handle wait and arrive when they run again; input and clicks are dropped |
dt, timers and waits per instance |
| Physics and navigation | The world holds, unless the physics_world entity runs always |
Steps dt × scale, split into substeps of at most one tick above 1 |
| Animation, sequences | Animators and cutscenes hold their pose | Per entity |
| Particles (CPU) | Emitters hold | Per emitter; GPU effects follow the game clock |
| Sprites, 2D cameras, dialogue | Hold | Per entity |
| UI | A canvas that does not run takes no input (default canvases run) | UI animations use real time |
| Audio | Every bus except ui pauses, so menu clicks still play |
Not pitched |
| Native modules | Not ticked | dt × scale |
Time in Wander¶
| Name | Meaning |
|---|---|
dt |
Seconds per tick for this entity: 1/60 scaled by the time scale, or real on the real clock |
time |
Game time: stops while paused, slows with the time scale |
frame |
The tick counter |
unscaled_dt(), unscaled_time() |
Real time, whatever the pause or scale |
time_scale(x) |
Sets the game clock speed (0 to 10) from the next tick and returns it |
pause_game(), resume_game(), is_paused() |
The game pause |
How to build a pause menu¶
Put the menu logic on the pause menu's ui_canvas entity (canvases run always). Its child panel PausePanel
starts hidden.
behavior PauseMenu
intent "Escape toggles the pause menu; the Resume button closes it."
on action "pause"
if is_paused() then resume_game() else pause_game() end
end
on pause
find("PausePanel").ui.visible = true
end
on resume
find("PausePanel").ui.visible = false
end
on ui "Resume"
resume_game()
end
end
The pause action is bound to Escape and the gamepad Start button in the default input map. Keep the canvas entity
itself enabled: a disabled entity runs nothing, not even on pause.
process_info reports the game clock (paused, time scale, game and real time), every entity with a process
component or a UI canvas, and, for one entity, its effective settings and which ancestor decided them. It warns when
a paused game has nothing that could resume it.
Recipe: bullet time¶
time_scale(0.25) slows everything on the game clock; the HUD and menus on the real clock keep full speed. Budget the
effect in real time with unscaled_dt():
behavior BulletTime
intent "Holding the focus action slows the game to a quarter speed for at most three real seconds."
var budget = 3
on tick
if action("focus") and budget > 0 then
time_scale(0.25)
budget -= unscaled_dt()
else
time_scale(1)
budget = min(3, budget + unscaled_dt() * 0.5)
end
end
end
Give the controller entity process.clock: real so its own dt and timers are not slowed by the effect it controls.
Agents can test the same thing without input:
sim_control {"action": "time_scale", "scale": 0.25}
sim_trace {"entities": ["Hero"], "properties": ["transform.position"], "ticks": 120, "every": 20}
sim_control {"action": "time_scale", "scale": 1}
Render interpolation¶
The simulation ticks at 60 Hz, but displays refresh at 60, 120 (ProMotion) or 144 Hz. Showing each tick for an uneven number of frames reads as stutter, so real-time frames show the world between the last two ticks:
alpha = accumulator / fixed dtsays how far real time is into the next tick.- The engine keeps every entity's transform from before the last tick and, for the time it takes to build one frame, writes the blend of previous and current (lerp for positions, slerp for rotations) into the scene, then restores the tick state. Meshes, cameras, lights, sprites, text, world UI, particle emitters, hair and bone attachments are smoothed this way; skinned meshes blend joint matrices; CPU particles move along their velocity; water, sky and GPU particles use the displayed time.
- The picture lags the simulation by up to one tick, as in every fixed-step design.
The simulation, tools and captures never see these in-between values. viewport_capture shows the exact tick state
unless you pass alpha. The editor viewport and the standalone player render with the current alpha.
Opting out¶
- Per subtree:
process.interpolation: offshows entities exactly at tick positions (pixel art, snapping puzzles). - Per jump:
teleport(e, position)in Wander, or thesim_teleporttool, moves an entity without smearing it across the screen for a frame. Moves longer than 25 m in one tick are never smeared either. - Globally:
sim_control {"action": "play", "interpolation": false}.
behavior Respawn
intent "Falling below the world sends the player back to the checkpoint without a smear."
on tick
if self.position.y < -20 then
teleport(self, find("Checkpoint"))
end
end
end
on frame: cosmetic, display-rate code¶
on frame handlers run once per displayed frame, after interpolation, for touches that look best at display rate:
camera shake, bobbing, flicker, UI tweens. dt is the real time since the last frame (scaled by the time scale) and
time the displayed game time.
behavior Bob
intent "Bob the camera gently at display rate."
on frame
self.position = self.position + (0, sin(time * 9) * 0.03, 0)
end
end
They are cosmetic: they may set position, rotation, scale, color and fields of transform, mesh,
light, camera, sprite, text, ui and light2d, read anything, and call pure functions and read-only queries.
Every write is undone after the frame. Changing vars, wait, timers, go to, move/rotate/look, emit, spawn,
destroy and random are compile errors (frame_not_cosmetic); use noise instead of random. So the simulation is
identical however many frames are shown.
Measuring smoothness¶
sim_trace with display_hz simulates a real display: each sample records the interpolation alpha and what that
frame shows next to the tick values, and the result reports smoothness.stepJitter (0 is perfectly even motion,
about 2 means every other frame repeats a tick) and frame pacing.
sim_trace {"entities": ["Hero"], "properties": ["transform.position"], "ticks": 120, "display_hz": 120, "every": 1}
process_info {}
viewport_capture {"alpha": 0.5, "frame_handlers": true}
perf_stats reports the live viewport's frameFlow, and the standalone player renders through its real pipeline at
a chosen display rate:
Recipe: test a behavior numerically¶
sim_input {"hold": ["w"]}
sim_trace {"entities": ["Hero"], "properties": ["transform.position", "vars.score"], "ticks": 180, "every": 30}
logs {"limit": 20}
sim_trace runs the game for N ticks, samples properties (<component>.<field>, vars.<name>, name, enabled)
and restores the edit-mode scene afterwards. sim_input injects keys, actions, axes, gamepad, mouse, clicks and events
for the next ticks; follow it with sim_control step or a trace.
Pitfalls¶
- Requests apply next tick. After
pause_game()ortime_scale(x), the current tick finishes at the old setting;is_paused()already returnstrue. - A paused game needs a way out. If nothing runs
alwaysorwhen_paused, nothing can callresume_game().process_infowarns about this. - Disabled entities run nothing, including
on pause. Hide a menu withui.visible, not by disabling its canvas. - Interpolation lags by up to one tick. Logic must read tick state (it always does); never try to read the displayed position from a script.
- Edits during play are temporary. Stop restores the pre-play snapshot.
- Five errors disable a script. A runtime error aborts that handler run and is reported in
logs; after five errors the script is disabled. Readlogsafter everystep.
For agents
Test gameplay the way a player would, and prove it with numbers:
sim_control {"action": "play"} # snapshot and start
sim_input {"press": ["space"]} # act like a player
sim_control {"action": "step", "ticks": 60} # advance exactly one second; returns logs and errors
viewport_capture {"view": "scene", "samples": 1} # look at the tick state
sim_trace {"entities": ["Hero"], "properties": ["transform.position"], "ticks": 120}
process_info {} # pause, time scale, smoothing
sim_control {"action": "stop"} # restore the scene
For behaviors with rules, prefer test blocks run by wander_test (see Specs and tests).
Reference¶
- Tools:
sim_control,sim_input,sim_trace,process_info,sim_teleport,logs,viewport_capture - Component:
process - Wander:
pause_game,resume_game,is_paused,time_scale,unscaled_dt,unscaled_time,teleport - Design: docs/ARCHITECTURE.md (Game pause, process modes and time scale; Render interpolation), docs/WANDER.md