Sim Lifecycle and Startup Sequence
Every SceneryStack simulation goes through the same handful of phases between the page loading and the animation loop running steadily at 60fps. Knowing the real order — not just "it starts" — matters for questions like "why isn't my model constructed yet" or "why did this listener fire before the thing it depends on existed."
Phase 1: asset loading, via onReadyToLaunch
A simulation never constructs its Sim directly at module scope — it wraps construction in onReadyToLaunch (from scenerystack/sim), which waits for SceneryStack's asynchronous asset loader (fonts, images, translated string files) to finish before the callback runs:
import { Sim, Screen, onReadyToLaunch } from 'scenerystack/sim';
onReadyToLaunch( () => {
const sim = new Sim( /* ... */ );
sim.start();
} );Nothing in the scene graph is safe to build before this callback fires — a Text Node created too early could measure its bounds against a font that hasn't finished loading, and a string Property could briefly hold an untranslated fallback. This is why every Screen's model/view factories are deferred functions rather than eagerly-run code: they're only invoked once Sim decides a screen is actually needed, which is always after this phase has completed.
Phase 2: new Sim(...) construction
Constructing Sim itself does relatively little work: it records the list of Screens, builds a HomeScreen and navigation bar if there's more than one, and sets up its own top-level Properties (selectedScreenProperty, activeProperty, dimensionProperty) — see Sim for the full option/member list. Crucially, no individual screen's createModel/createView factory has run yet at this point; Screen only holds those factories, it doesn't call them.
Phase 3: sim.start() — screen initialization
Calling start() is what actually triggers construction. Sim initializes screens (by default, every screen eagerly rather than only the one first shown — see detachInactiveScreenViews for the option that keeps inactive screens' views out of the scene graph once built), calling each Screen's createModel() and then createView( model ) in turn. Sim.isConstructionCompleteProperty becomes true once this has finished for every screen — code that needs to know "has everything actually been built yet" (some PhET-iO tooling, in particular) waits on that Property rather than assuming construction is synchronous with start() returning.
Once construction finishes, start() begins the requestAnimationFrame loop described below. selectedScreenProperty is set (to the HomeScreen for a fresh multi-screen sim, or the single screen for a one-screen sim, unless a query parameter like ?screens=2 requests a specific starting screen) as part of this same phase.
Phase 4: the running animation loop
Once started, a Sim drives one requestAnimationFrame-based loop for the lifetime of the page. Each frame:
- The browser calls the loop's callback with an elapsed time;
Simcomputesdt(elapsed seconds, capped per-screen by thatScreen'smaxDToption to avoid a huge jump after e.g. a backgrounded tab resumes). Simsteps the currently-selected screen's model (if it has astep( dt )method) and then itsScreenView's ownstep( dt )— only the active screen steps; a screen the user isn't looking at does no per-frame work.stepTimer(fromscenerystack/axon) emitsdt, firing anything scheduled viastepTimer.setTimeout/setIntervalwhose accumulated time has elapsed — see Timer, animationFrameTimer, and stepTimer for how this differs fromanimationFrameTimer, which fires every frame independent of which screen (or whether any screen) is active.- The
Displayunderlying the sim repaints, syncing any Node whose visual state changed as a result of the above back to actual pixels — this is the sameupdateDisplay()/updateOnRequestAnimationFrame()mechanism described in Scenery Basics, just already wired up for you bySimrather than something you drive yourself.
Model and view step functions are the only place per-frame model mutation should happen — a Property set directly from an input listener (a drag, a slider) is fine and expected, but time-based motion (an object coasting, an animation easing toward a target) belongs in step(dt), driven by the loop above, not in a setInterval or a listener that assumes a fixed frame rate.
activeProperty pauses the loop's effects, not the loop itself
Setting sim.activeProperty = false doesn't stop requestAnimationFrame from firing — Sim keeps receiving frames, but skips stepping the model/view while inactive, which is what "pausing" a sim actually means at this layer. This is distinct from browserTabVisibleProperty, which reflects whether the browser tab itself is visible and is one of the inputs Sim uses to decide whether to set activeProperty automatically.
Where to go next
- Sim — the full constructor/option/member reference for the class this page walks through
- Screen — the lazy model/view factory pattern
start()invokes - Timer, animationFrameTimer, and stepTimer — the two timer singletons driven by (or independent of) this loop
- Scenery Basics — the
Display/repaint mechanism the loop drives each frame