Guide

Guide Recipes HUD overlay

HUD overlay

Compose gameplay and UI layers without coupling scene logic.

Intermediate ~2 min read

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 using scene.ui, a screen-fixed layer that every scene owns and that is automatically rendered above the scene’s world content.

Approach

Add HUD elements directly to scene.ui instead of creating a separate overlay scene. The world (game nodes, sprites, graphics) lives under scene.root; the HUD (labels, progress bars, panels) lives on scene.ui and is composited on top automatically — no extra scene or stack needed.

class GameScene extends Scene {
    init() {
        this._angle = 0;

        // HUD elements on scene.ui — screen-space, always on top.
        this._scoreText = new Label('SCORE 1240', { fontSize: 22 });
        this._scoreText.anchorIn(this.ui, 'top-left', 18, 14);
        this.ui.addChild(this._scoreText);

        this._health = new ProgressBar({ width: 220, height: 8, value: 1 });
        this._health.anchorIn(this.ui, 'top-left', 18, 48);
        this.ui.addChild(this._health);
    }

    update(delta) {
        this._angle += delta.seconds * 90;
    }

    draw(context) {
        context.backend.clear(new Color(20, 32, 58));
        // ... draw game world ...
    }
}

Start the scene as normal — scene.ui is rendered automatically:

const game = new GameScene();
await app.start(game);

Why scene.ui, not manual draw-order

The alternative — rendering HUD elements after the world in a single scene’s draw method — works for simple cases. scene.ui gives you:

  • Screen-space anchoring via widget.anchorIn(this.ui, 'top-left' | 'center' | 'bottom-right' | …, offsetX, offsetY) — widgets stay pinned to corners or the center regardless of canvas size.
  • Hit-testing in screen space — UI widgets are clickable and focusable without needing camera-inverse transforms.
  • Independent lifecycle — widgets added in init are automatically cleaned up when the scene is destroyed.

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. The capture is a CallbackRenderPass with a { target }, run once per frame from the scene’s pipeline:

// In GameScene.init: build the capture pass once.
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`
);

// Add the mini-map sprite to scene.ui so it stays in screen space.
const miniSprite = new Sprite(miniRt);
miniSprite.anchorIn(this.ui, 'top-right', -280, 20);
this.ui.addChild(miniSprite);

// Clamp to a circle with a mask
const mask = new Graphics();
mask.fillColor = Color.white;
mask.drawCircle(130, 130, 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

Hud Overlay Scene Open in Playground View source

Preview is paused until you click Play.

import { Application, Color, Graphics, Label, ProgressBar, Scene } from '@codexo/exojs';

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
    },
    clearColor: Color.black,
});

/**
 * A screen-fixed HUD on `scene.ui` sits above the world automatically — no
 * separate overlay scene or stack. The world (a spinning arc) is drawn from
 * `scene.root`; the HUD (a label + a live health bar) lives on `scene.ui` and
 * is auto-rendered on top.
 */
class GameScene extends Scene {
    private angle = 0;
    private time = 0;
    private ring!: Graphics;
    private health!: ProgressBar;

    override init(): void {
        this.ring = new Graphics();

        const title = new Label('HUD Overlay', { fontSize: 22 });
        title.anchorIn(this.ui, 'top-left', 18, 14);
        this.ui.addChild(title);

        this.health = new ProgressBar({ width: 240, height: 12, value: 1 });
        this.health.anchorIn(this.ui, 'top-left', 18, 48);
        this.ui.addChild(this.health);
    }

    override update(delta): void {
        this.angle += delta.seconds * 90;
        this.time += delta.seconds;
        this.health.value = (Math.sin(this.time) + 1) / 2;
    }

    override draw(context): void {
        const { width, height } = this.app.canvas;

        context.backend.clear(new Color(20, 32, 58));
        this.ring.clear();
        this.ring.lineWidth = 20;
        this.ring.lineColor = new Color(90, 180, 255);
        this.ring.drawArc(width / 2, height / 2, 160, 0, (this.angle * Math.PI) / 180);
        context.render(this.ring);
    }
}

void app.start(new GameScene());

A game scene with a rotating ring, a Label and a live ProgressBar on scene.ui — the HUD is always on top with no separate overlay scene.

Where to go next

The next recipe, Camera follow & parallax, covers how to move the camera and create depth with layered parallax.