screen overview
Table of contents
- utils
- Screen (class)
- ScreenEnterContext (type alias)
- ScreenInternals (type alias)
- ScreenState (type alias)
- ScreenTeardown (type alias)
utils
Screen (class)
One screen of an app - a main menu, a game, a pause overlay, a settings page. A screen is a DOM
layer (layer) over which it has full control, plus a lifecycle driven by a ScreenManager:
enter() once when it first comes on top of the stack, onCovered()/onUncovered() whenever
another screen is put on top of it or taken off again, exit() when it leaves the stack.
A screen instance is used once: create a new one for every push, and pass what it needs to its
constructor. A screen needs no world. One that has a world (usually only the game) creates it in
enter() with the adapters of its choice, renders it into a canvas it adds to layer, and
registers it with addWorld, which is what lets the manager silence and pause it while it is
covered and dispose it on exit.
class GameScreen extends Screen {
constructor(private readonly level: string) {
super()
}
async enter(ctx: ScreenEnterContext) {
const world = this.addWorld(await createWorld(this.layer))
await world.loader.loadLevelFromUrl(`levels/${this.level}.json`, 'level', {
onProgress: ctx.reportProgress,
signal: ctx.signal,
})
this.addTeardown(this.keyboard.bind('Escape').subscribe((down) => down && this.screens.push(new PauseScreen())))
world.start()
}
}
Signature
export declare class Screen
enter (method)
Builds the screen: create DOM in layer, create and load worlds, subscribe to things. Called
once, when the screen first comes on top of the stack. While the returned promise is pending
the layer is kept hidden and the manager shows its loading view, fed by ctx.reportProgress.
Register everything that has to go away with the screen through addWorld/addTeardown as
soon as it exists, not at the end: if enter() throws or is aborted halfway, that is all that
gets cleaned up (exit() is not called for a screen that never finished entering).
Don't await a ScreenManager operation in here (a boot screen awaiting push(menu)):
operations run one after another, and that one only starts once this transition, which is
waiting for enter(), is over - it never resolves. Call it without awaiting; it runs right
after this screen has entered.
Signature
public abstract enter(ctx: ScreenEnterContext): void | Promise<void>;
exit (method)
Called when the screen leaves the stack after having entered. May return a promise (a
fade-out); the transition waits for it - so, as in enter(), never await a ScreenManager
operation in here.
The screen stays visible after this: its layer is kept on the page, inert, with its worlds paused, until the next screen (or the loading view) shows, so the page is never blank in between. Only then is the layer removed and the teardowns run and the worlds disposed - which may be after the next screen started entering, so what this screen holds briefly coexists with the next screen's loading.
Signature
public exit(): void | Promise<void>
onCovered (method)
Called when another screen is put on top of this one. By now the manager has already made the layer inert, switched off the input of this screen's worlds and (unless told otherwise) paused them. Override for anything else the screen runs on its own.
Signature
public onCovered(): void
onUncovered (method)
Called when this screen is on top again, after its worlds got their input and clock back.
Signature
public onUncovered(): void
addWorld (method)
Hands a world to the screen: the manager switches its input off and pauses it while the screen is covered, and it is disposed when the screen exits. Returns the world, for chaining.
Call it right after creating the world, before loading into it. Whether a covered world is
paused is decided by the push that covers it (pauseBelow) and by the world itself: a world
with localPauseAllowed === false (a joined network session) keeps running, and a change of
that flag while the screen is covered pauses or resumes it. A world paused for a hidden tab
(pauseWhenHidden) when it gets covered stays paused until the screen is uncovered. Its input
is switched off either way.
Signature
protected addWorld<W extends GgWorld<any, any>>(world: W): W
addTeardown (method)
Registers something to clean up when the screen is gone: a function to call, an rxjs
Subscription to unsubscribe, or anything with dispose(). They run in reverse order of
registration, before the screen's worlds are disposed.
For a screen that entered, that is once its layer is no longer on the page (layer.isConnected
is false by then): after exit(), when the next screen or the loading view shows - possibly
after the next screen started entering, so a world or canvas disposed here briefly coexists
with the next screen's loading. Until then the screen looks as it did, with its worlds paused.
A screen that never finished entering (aborted, or enter() threw) gets its teardowns at once.
Signature
protected addTeardown(teardown: ScreenTeardown): void
coverWorld (method)
Signature
private coverWorld(owned: OwnedWorld): void
pauseCoveredWorld (method)
Signature
private pauseCoveredWorld(owned: OwnedWorld): void
releasePause (method)
Signature
private releasePause(owned: OwnedWorld): void
uncoverWorld (method)
Signature
private uncoverWorld(owned: OwnedWorld): void
ScreenEnterContext (type alias)
What Screen.enter is given.
Signature
export type ScreenEnterContext = {
/**
* Aborted when the screen is removed while it is still entering (the player went back during
* loading, another screen replaced it). Pass it to every load so `enter()` ends promptly.
*/
signal: AbortSignal
/**
* Feeds the loading view shown while `enter()` is pending. Takes a loader's `LoadProgress` as is
* (`onProgress: ctx.reportProgress`) or a plain 0..1 fraction.
*/
reportProgress: (progress: LoadProgress | number) => void
}
ScreenInternals (type alias)
What ScreenManager drives a screen through. Not for app code.
Signature
export type ScreenInternals = {
attach(manager: ScreenManager, layer: HTMLElement): void
setState(state: ScreenState): void
setCovered(covered: boolean, pauseWorlds: boolean): void
/**
* After `exit()`: the screen has left the stack, but its layer stays on the page until what
* replaces it is shown. Marks it exited, stops its keyboard and suspends its worlds (input off,
* paused) so nothing changes on the layer until `teardown()`.
*/
leave(): void
/** Removes the layer from the page, then runs the teardowns and disposes the worlds. */
teardown(): void
}
ScreenState (type alias)
Where a screen is in its life:
pending: in the stack, not entered yet (pushed just now, or placed under other screens byScreenManager.resetand waiting to be uncovered for the first time);entering: itsenter()is running;active: entered and on top of the stack;covered: entered, with another screen on top of it;exited: removed from the stack. A screen is never used again after that.
Signature
export type ScreenState = 'pending' | 'entering' | 'active' | 'covered' | 'exited'
ScreenTeardown (type alias)
Something a screen cleans up when it exits - see Screen.addTeardown.
Signature
export type ScreenTeardown = (() => void) | { unsubscribe(): void } | { dispose(): void }