Stage — the animation & interaction substrate
The stage is the small shared substrate under motion,
tween, andcollision: one tick, a target registry, a fixed phase order, and an event bus. The sibling systems interact through the stage — shared targets and events — never by calling each other, which is exactly what keeps a scene full of animation and collision deterministic. One clock, shared state, decoupled systems.
Status: design. Not built yet. This fixes the spine the animation/interaction family sits on, so the siblings can be built without wiring into one another.
What it is
The moment you have more than one animation system and they need to interact — a sprite's motion stopping when it hits a wall — the tempting thing is to let the systems call each other. That way lies N² coupling and order-dependent bugs, the opposite of the "same data, same result" property motion protects so carefully.
The stage is the fix: a spine the systems all read and write instead of talking directly. It owns three things, and one rule:
- a single tick (the clock everything advances on),
- a target registry (the shared state they meet in),
- an event bus (decoupled interaction),
- and a fixed phase order each tick (the determinism keystone).
motion, tween, and collision become siblings over the stage, not a web of libraries wired into each other.
One tick, one clock
Every subsystem advances on the same frame tick — a fixed timestep, not wall-clock time. motion's speed, tween's durations, collision's tests all count in ticks, so they share one timebase and stay in lockstep. Because the tick is fixed and the state is advanced from it (not from real elapsed time), the same inputs over the same ticks produce the same frames — the base case of the whole family's determinism. (Rendering can run at whatever rate the display allows; the simulation advances in fixed ticks underneath it.)
Targets are the shared state
Every animatable or collidable thing is a target in the stage's registry — carrying position (x / y / heading), other animatable properties (scale, colour, alpha…), and a bound (its collision shape). The systems meet here:
motionwrites a target's position along a path;tweenwrites a target's other properties over time;collisionreads targets' bounds to test overlap.
So they interact through the target, never through each other — register a target once and every system sees it, with no system holding a reference to another. This is what turns three independent libraries into one coherent scene without coupling them.
Fixed phase order — the determinism keystone
Each tick runs a fixed sequence of phases, and this is the single most important rule on the page:
1. Input → 2. Advance (
motion+tweenwrite positions & properties;physicsintegrates dynamic bodies) → 3. Detect (collisiontests the current positions) → 4. Resolve (optional —physicssolves the contacts) → 5. React (events delivered, responses run)
It is deterministic because the order is fixed: no system ever sees another's half-updated state, collision always tests the current post-integration positions, and responses always run after detection and resolution on a settled frame. Same inputs, same phase order, same result — every time, and identically on replay. The Resolve phase is present only when the opt-in physics layer is enabled; without it, Detect flows straight into React.
Interaction is events, not callbacks
When collision finds an overlap it does not call motion.stop — it emits an event ("A overlapped B") onto the bus. Something else — a script, a rule — decides what happens: stop the motion, start a tween, play a sound, adjust a score.
- Mechanism in the core, policy in scripts — the platform's ethos: the stage detects and reports; what a collision means is ordinary AntBASIC / Luau, not baked into the engine.
- Decoupled — emitters don't know who listens; the systems stay independent.
- Deterministic delivery — events raised during Detect are queued and delivered in React, in a defined order, so handling them never perturbs the frame they came from.
How the siblings sit on it
| System | Phase | Reads | Writes | Emits |
|---|---|---|---|---|
motion |
Advance | its op list | target position | (done) |
tween |
Advance | its curves | target properties | (done) |
collision |
Detect | target bounds | — | overlap events |
physics |
Resolve (opt-in) | bodies, contacts | positions, velocities | — |
timeline |
orchestration | its schedule | — (starts/stops siblings) | — |
Each is a sibling library in its own right — same two-page pattern as motion — but all of them share the stage's tick, registry, and bus, and none references another. Adding a new one (a particle system, say) is adding another reader/writer of the same three things, in a known phase — as physics did with the Resolve phase.
The determinism contract, extended
motion's contract — every primitive a deterministic function of progress, no randomness, no wall-clock — holds per system. The stage adds the two rules that make it hold across systems:
- Fixed phase order every tick, so cross-system reads always see settled state.
- Deterministic event delivery, so interaction never depends on emit timing within a frame.
Together: a whole interactive scene — animation and collision and their responses — replays identically from the same inputs. That is the property that lets you record, debug, or run it on the simulator vs the EE and get the same result.
Determinism-safe randomness
Variety and determinism look opposed — math.random or os.time would make every run different and shatter replay. The stage resolves it with one stage-owned PRNG, seeded once per run, that every system draws from instead of the language's global random:
stage.random(…)is the streamparticles, aphysicsscatter, a random spawn, or any "deterministic but varied" effect must use — nevermath.random.- The seed is part of the run's replayable state (alongside inputs and ticks): same seed + same inputs + same ticks → the same run, including everything "random" in it. Randomness stops being the enemy of replay and becomes just another input you record.
- Set it with
stage.seed(s)at the start of a run — or let the stage seed once and report the seed, so a good run can be reproduced.
A small addition with outsized leverage: it is what lets the visible-but-varied siblings exist without any of them breaking the determinism the whole family depends on.
Clients
- AntBASIC / Luau register targets, subscribe to events, and drive the siblings (
DRAWa path,tweena colour, react to a collision in a handler). - The OS runs the tick and the phase loop; the
eeexecutor, later, can run the whole advance on the soft core.
The stage is small on purpose — a tick, a registry, a bus, an order. Everything expressive lives in the siblings; the stage just guarantees they compose without chaos.
Future siblings
The stage is built to grow: a new capability is just a new reader / writer of targets in a known phase, sharing the tick, registry, and event bus, referencing no other sibling, inheriting the determinism contract. So the family can expand for years without the architecture changing. Candidates, roughly by readiness, to add over time:
input— the Input phase: per-tick device sampling, actions, record / replay. (the promised phase — written)particles— emitters of many short-lived targets, Advance phase, on the seeded PRNG. (written)spawn— target lifecycle: pooled create / recycle / despawn, keeping the registry bounded. (written)camera— a per-stage viewport transform: follow, deterministic shake, zoom, letterbox. (written)state— per-target state machines choosing which motion / tween plays; pairs with collision / input. (written)path— waypoint- and spline-following and steering (seek / flee / arrive), emitting motion. (written)- audio cues — sounds triggered on the tick, the audio cousin of timeline. (folded into
audio) tilemap— grid worlds with tile collision, for 2D games. (written)net-sync— deterministic lockstep / rollback netcode: send inputs, not state, the payoff the determinism contract unlocks. (written — the horizon)