Execution order key (§39); default PRIORITY_PARTICLES.
Whether ParticleSystem.dispose has run. Disposal is terminal (§83):
a disposed system refuses its mutating entry points with
INVALID_APPLICATION_STATE (§89) rather than silently working.
Tracked emitters, in the order they will be stepped.
Number of tracked emitters.
Stops stepping everything. The system stays registered.
Drops every tracked emitter (§39 teardown).
Deliberately does not reset or clear them: the system is a driver, not
an owner, and disposing an application should not empty pools the
application may still be checksumming, serialising, or drawing one last
frame from. AnimationSystem.dispose draws the same line.
Steps every tracked emitter once, in insertion order.
The delta is time.fixedDeltaTime — the scaled simulation delta. The
scheduler already applied timeScale when filling its accumulator (§10:
accumulator += elapsedRealTime * timeScale), so a fixed step is one
fixedDeltaTime of scaled simulation time and timeScale changes the
number of steps per frame. Multiplying by time.timeScale here would scale
the same time twice and particles would run at timeScale² —
AnimationSystem states the identical rule.
The time is time.simulationTime, which §39's scheduler advances
before invoking a fixed step, so it is the time the step being produced
ends at (step n sees n · fixedDeltaTime). That value is handed to
ParticleForceField.sample(position, velocity, time, out) — §27's time
argument, which an animated field (turbulence, a gusting wind) reads. Two
consequences worth stating:
ParticleEmitter.step's own default for that argument is the emitter's
local elapsed time, measured from its construction and taken at the
start of the step. Driving an emitter from this system therefore feeds
fields the world simulation clock, offset by one fixedDeltaTime
from the emitter-local default. That is the right choice — two emitters in
one scene must see one clock, or a shared wind field blows in two
different directions — and it is why the argument is passed explicitly
rather than left to default.Emission and death bookkeeping belong to the emitter (emitter.ts pins the
simulate-then-emit order and the per-spawn RNG draw count); this method adds
nothing to them.
No per-registration setup is needed (§39).
Starts stepping emitter and returns it, so an emitter can be built and
tracked in one expression. Idempotent — tracking twice keeps the original
position in the iteration order and steps it once.
Stops stepping emitter. Returns whether it was tracked.
Steps every tracked particle emitter once per fixed simulation step (§39 step 5).
What it tracks: emitters, not renderables (decision, WP-9.4)
tracktakes a SteppableEmitter. It does not take aParticleRenderable, and the demo composes accordingly (particles.track(fountain.emitter)). Three reasons, in order of weight:benchmarks/particles-100k.mjs) and the §33 golden (tests/determinism/phase9-particles.test.ts) both step 100 000 and 100 particles respectively with no scene, no canvas and no render list. If the system took renderables, headless particle simulation would require constructing a node purely to throw it away.buildRenderList(see the module header); the emitter's per-step job is driven here. Tracking the renderable would suggest this system owns both, which is exactly the confusion the header exists to prevent.ParticleRenderable.emitteris public for this. It is documented as "public because the application steps it";system.track(r.emitter)is that sentence, spelled.The cost, stated: one extra
.emitterat the call site, and the possibility of adding a renderable to the scene while forgetting to track its emitter — which draws a static puff of particles rather than throwing. That failure is visible in one frame, and the alternative failure (a headless simulation that cannot be built) is not.No auto-untracking (decision, WP-9.4)
AnimationSystemdrops a player that isfinishedor"stopped", because those states are unrecoverable. An emitter has no such state: one with zero live particles and no emission rate is exactly one burst away from being busy again (emitter.emit(n)), andreset()revives any emitter at all. Dropping a quiet emitter would silently break both. Emitters leave this system only when something calls ParticleSystem.untrack or ParticleSystem.clear.Mutation during a step
The tracked set is a
Setiterated live, mirroringAnimationSystemandMotionSystem(a snapshot would allocate every step). Those areSetsemantics: an emitter untracked during the step is not stepped again in that step, and one tracked during the step is stepped in the same step. Track and untrack outside the step if that distinction matters.Allocation (§7b, plan D7)
fixedUpdateallocates oneSetiterator per step and nothing else — the same one-per-stepAnimationSystemandMotionSystemallocate, and the reason the per-particle work insideParticleEmitter.stepis the part that is strictly zero-allocation. At the §112 budget that is one small object against 100 000 particles.