Runtime architecture
Runtime puts the engine in a browser page. It creates the renderer and world for a canvas, prepares assets, and drives the world from a scheduled game loop. The web and Electron guides use the same runtime code.
What the runtime composes
Runtime owns the wiring; each part keeps its behavior in its own package. The sections below follow one part at a time.
| Part | Runtime's role | Owner of the behavior |
|---|---|---|
| Canvas and renderer | Resolve the canvas and create ThreeRenderer | Engine renderer |
| Assets | Resolve the catalog and add custom loaders to the engine defaults | @jolly-pixel/asset and the engine loaders |
| Scenes | Await the initial scene during load() | Engine SceneManager |
| Frames | Feed renderer animation callbacks to world.tick() | @jolly-pixel/loop and World |
| Browser UI | Mount the loading screen, overlays, and readouts | Runtime and @jolly-pixel/ui |
Assets
Runtime.create() fetches and parses a catalog URL once; no catalog means an empty one. Every loader shares the Three.js LoadingManager exposed as runtime.manager. Runtime loads the startup batch itself; the engine loads scene assets.
Creation
create() rejects on an invalid canvas target, a failed catalog request or manifest, or a renderer failure. The catalog resolves first, so a failed request rejects before any renderer exists. Nothing runs until load() or start().
Startup
GPU detection sets the FPS cap and, unless the renderer options fix it, the pixel ratio. Without a screen, the entrance, delay, and completion steps do nothing. Any failure rejects load(), leaves the runtime stopped, and is shown on the screen when one is mounted.
Scene loading
The engine owns this flow. Runtime awaits it for the initial scene and forwards its progress to the loading screen. A ready scene becomes active on the next beginFrame(), so load() can resolve before the scene's awake() runs.
Frame path
FrameScheduler, inside GameLoop, decides the fixed step count and whether to render; the maxFps cap skips rendering while fixed steps still run. World.tick() updates input even when no fixed step is due.
Metrics
One recorder feeds every display. RuntimeMetrics registers the renderer counters, latches them on each draw, and owns the readout panel. The recorder and the counters exist even when nothing is mounted.
Lifecycle
start() focuses the canvas, attaches the focus listeners, focus hint, and view helper, then connects the world and starts the loop. stop() stops the world and loop and releases everything start() attached. Both are idempotent; dispose() also removes the HUD, readout panel, overlay layer, and renderer resources.
Browser surfaces
The overlay layer holds what is drawn above the canvas and never takes pointer events itself. The view helper renders into the canvas after each draw. The loading screen mounts in loadingContainer, outside the overlay.
Details: runtime API, metrics, overlay layer, asset options, scenes and assets, loading screen, and frame scheduling and performance.