Multi-Screen Sim Structure
Each Screen owns exactly one model/view pair and is self-contained; the Sim itself does no screen-specific work — it only collects the screens and hands them a title. Code genuinely shared by more than one screen (a constants file, a reusable sub-model, a shared control) lives in a common/ location that every screen imports from, never in one screen importing from another. This keeps a screen removable or reorderable without hunting for cross-screen dependencies.
The core idea
import { Sim, Screen, ScreenView, ScreenIcon, onReadyToLaunch, type ScreenViewOptions } from 'scenerystack/sim';
import { Tandem } from 'scenerystack/tandem';
import { Property } from 'scenerystack/axon';
import { Rectangle, Text } from 'scenerystack/scenery';
// --- common/ : code shared by more than one screen ---
class SharedConstants {
public static readonly PANEL_MARGIN = 10;
}
// --- intro/ : this screen's own model, never imported by other screens ---
class IntroModel {
public reset(): void { /* reset this screen's Properties */ }
}
class IntroScreenView extends ScreenView {
public constructor( model: IntroModel, providedOptions: ScreenViewOptions ) {
super( providedOptions );
const label = new Text( 'Intro', { font: '24px sans-serif' } );
label.center = this.layoutBounds.center;
this.addChild( label );
}
}
function createIntroScreenIcon(): ScreenIcon {
return new ScreenIcon( new Rectangle( 0, 0, 10, 10, { fill: 'blue' } ) );
}
// --- advanced/ : a second, independent screen ---
class AdvancedModel {
public reset(): void { /* reset this screen's Properties */ }
}
class AdvancedScreenView extends ScreenView {
public constructor( model: AdvancedModel, providedOptions: ScreenViewOptions ) {
super( providedOptions );
const label = new Text( 'Advanced', { font: '24px sans-serif' } );
label.center = this.layoutBounds.center;
this.addChild( label );
}
}
function createAdvancedScreenIcon(): ScreenIcon {
return new ScreenIcon( new Rectangle( 0, 0, 10, 10, { fill: 'red' } ) );
}
// --- top level: wire screens into the Sim, nothing screen-specific here ---
const introTandem = Tandem.ROOT.createTandem( 'introScreen' );
const introScreen = new Screen(
() => new IntroModel(),
model => new IntroScreenView( model, { tandem: introTandem.createTandem( 'view' ) } ),
{
name: new Property( 'Intro' ),
homeScreenIcon: createIntroScreenIcon(),
backgroundColorProperty: new Property( 'white' ),
tandem: introTandem
}
);
const advancedTandem = Tandem.ROOT.createTandem( 'advancedScreen' );
const advancedScreen = new Screen(
() => new AdvancedModel(),
model => new AdvancedScreenView( model, { tandem: advancedTandem.createTandem( 'view' ) } ),
{
name: new Property( 'Advanced' ),
homeScreenIcon: createAdvancedScreenIcon(),
backgroundColorProperty: new Property( 'white' ),
tandem: advancedTandem
}
);
onReadyToLaunch( () => {
const sim = new Sim( new Property( 'My Multi-Screen Sim' ), [ introScreen, advancedScreen ] );
sim.start();
} );What goes where
| Location | Contents |
|---|---|
common/ (or a shared module) | Constants, utility functions, and any sub-model/component genuinely used by two or more screens |
<screen-name>/model/ | That screen's model classes — imported only by that screen's own view and the top-level wiring |
<screen-name>/view/ | That screen's ScreenView subclass and any Nodes specific to it |
Top level (where Sim is constructed) | Only the list of Screens and the sim-wide title Property — no per-screen logic |
Each screen also gets its own Tandem subtree (introTandem, advancedTandem above), created from Tandem.ROOT, so PhET-iO's element tree mirrors the screen boundaries — see The PhET-iO Instrumentation Pattern.
name and the icons are optional in the types, but silently degrade if omitted on a multi-screen sim
Screen's name (a TReadOnlyProperty<string>, not a plain string) and its homeScreenIcon/navigationBarIcon may be omitted for a single-screen sim, since there is no home screen or navigation bar to show them in. Nothing throws if you omit them on a multi-screen sim either — Screen falls back to a blank name and a plain colored-rectangle icon (navigationBarIcon further falls back to a scaled copy of homeScreenIcon) — but the home screen and navigation bar become genuinely unusable that way, so supply a real name and homeScreenIcon for every screen once there's more than one.