HUD overlay
Compose gameplay and UI layers without coupling scene logic.
HUD overlay
A HUD (heads-up display) renders UI elements on top of the game world — health bars, score text, mini-maps, debug readouts — without those elements moving or scaling with the game camera. The recipe separates world-space content from screen-space content by using two scenes on the stack.
Approach
Push a second scene on top of the game scene. The game scene renders the world. The HUD scene renders only UI elements — positioned in screen-pixel coordinates, independent of any camera view. The HUD scene uses 'overlay' mode so both scenes render and update.
class GameScene extends Scene {
init() {
this._angle = 0;
}
update(delta) {
this._angle += delta.seconds * 90;
}
draw(context) {
context.backend.clear(new Color(20, 32, 58));
// ... draw game world ...
}
}
class HudScene extends Scene {
init() {
this._bar = new Graphics();
this._text = new Text('SCORE 1240', { fill: 'white', fontSize: 22 });
this._text.setPosition(18, 14);
}
draw(context) {
this._bar.clear();
this._bar.fillColor = new Color(0, 0, 0, 0.45);
this._bar.drawRectangle(0, 0, 800, 56);
this._bar.drawRectangle(18, 40, 220, 8); // health bar
context.render(this._bar);
context.render(this._text);
}
}
Push the HUD scene after the game scene starts:
const game = new GameScene();
const hud = new HudScene();
await app.start(game);
await app.scene.pushScene(hud, { mode: 'overlay' });
Why two scenes, not draw-order
The alternative — rendering HUD elements after the world in a single scene’s draw method — works for simple cases. The two-scene approach gives you:
- Independent
init/destroylifecycles — the HUD can be pushed, popped, and replaced without touching game code. - Camera independence — the HUD scene’s root is always in screen space, never affected by the game view.
Mini-map with a mask
A mini-map is a special HUD element: it needs a second View to render the world from a zoomed-out perspective into a RenderTexture, which is then displayed as a sprite in the corner of the HUD scene. The capture is a CallbackRenderPass with a { target }, run once per frame from the game scene’s pipeline:
// In GameScene: build the capture pass once, run it each frame from the pipeline.
const miniRt = new RenderTexture(260, 260);
const capturePass = new CallbackRenderPass(
(context) => {
// Render the world from a wide view into miniRt (immediate-mode draws via context.backend)
this.drawWorld(context.backend);
},
{ target: miniRt }, // redirects the callback's output off-screen; clears the target first if you pass `clear`
);
// In HudScene:
const miniSprite = new Sprite(miniRt).setPosition(530, 20);
// Clamp to a circle with a mask
const mask = new Graphics();
mask.fillColor = Color.white;
mask.drawCircle(660, 150, 120);
miniSprite.mask = mask;
The CallbackRenderPass with { target } redirects the callback’s drawing into the texture; inside a target redirect, issue immediate-mode draws through context.backend (a context.render(node) there would reset the view back to the camera). The mask clips the mini-map to a circle. See Render targets and Masks for details.
When DOM UI is a better fit
In-canvas HUD works well when the UI is tightly coupled to game state — health bars that react to damage, score text that animates, mini-maps that track player position. For static UI with complex layout (settings menus, inventory grids, text-heavy dialogs), standard HTML/CSS rendered as DOM overlays on top of the canvas is often simpler. The two approaches coexist: the canvas handles the game, DOM handles the menus. Focus and event routing between them requires coordination (pointer-events: none on the DOM when the game needs the pointer), but the architectural separation is clean.
Examples
A game scene with a rotating ring, a HUD scene with a top-bar overlay, and transparent input so the HUD is visible but doesn’t block interaction.
Where to go next
The next recipe, Camera follow & parallax, covers how to move the camera and create depth with layered parallax.