Guide

Guide Recipes Pause menu

Pause menu

Pause scene updates while keeping clear visual context for players.

Intermediate ~2 min read

Pause menu

A pause menu can freeze the game, display a menu overlay, and resume cleanly when dismissed. In ExoJS, this is done entirely within a single scene: set scene.paused = true to freeze updates, show a pause overlay on scene.ui, and reverse both on resume.

Approach

One scene, one UI layer. The pause overlay (a Panel + Label) lives on scene.ui and is hidden until paused. Toggling scene.paused stops update() and all scene systems while the scene keeps drawing — so the world stays visible behind the overlay. A BlurFilter on scene.root provides visual separation.

class GameScene extends Scene {
    init(loader) {
        this.player = new Sprite(loader.get(Texture, 'hero'));
        this.addChild(this.player);
        // ... game setup ...

        this.blur = new BlurFilter({ radius: 0, quality: 2 });

        // Pause overlay on the UI layer, hidden until paused.
        this.pausePanel = new Panel({ width: 420, height: 140, cornerRadius: 18, color: new Color(0, 0, 0, 0.6) });
        this.pausePanel.anchorIn(this.ui, 'center');
        this.pausePanel.visible = false;
        this.ui.addChild(this.pausePanel);

        this.pauseLabel = new Label('PAUSED', { fontSize: 56, fontWeight: 'bold' });
        this.pauseLabel.anchorIn(this.ui, 'center');
        this.pauseLabel.visible = false;
        this.ui.addChild(this.pauseLabel);

        this.inputs.onTrigger(Keyboard.Escape, () => this.togglePause());
    }

    update(delta) {
        // Not called while paused — SceneManager skips update() + systems.
        // ... normal game logic ...
    }

    draw(context) {
        context.backend.clear(new Color(20, 24, 34));
        context.render(this.root);
    }

    destroy() {
        this.root.clearFilters();
        super.destroy();
    }

    togglePause() {
        this.paused = !this.paused;
        this.pausePanel.visible = this.paused;
        this.pauseLabel.visible = this.paused;

        if (this.paused) {
            this.blur.radius = 0;
            this.root.filters = [this.blur];
            this.tweens.create(this.blur).to({ radius: 6 }, 0.35).start();
        } else {
            this.root.clearFilters();
        }
    }
}

await app.start(new GameScene());

How scene.paused works

Setting scene.paused = true:

  • Freezes update() — the SceneManager skips update() and all scene-level systems. No game logic runs.
  • Keeps drawing — the scene still renders every frame, so the world remains visible behind the overlay.
  • Tweens on app.tweens still run — tween managers at the application level are unaffected, so blur animations and UI transitions animate smoothly while the game is frozen.

scene.ui widgets are always interactive regardless of scene.paused, so the pause panel’s buttons remain clickable during the pause.

Cleanup

The destroy hook clears the blur filter from scene.root. Without this, the filter array would linger if the scene is ever replaced via app.scene.setScene(...). As a rule, any filter applied in init or togglePause should be cleaned up in destroy.

Switching scenes from the pause menu

If the pause menu offers options like “Return to main menu” or “Quit”, use app.scene.setScene(...) to navigate away — it unloads the current scene and loads the next one, optionally with a fade transition:

// Inside a resume button's onClick handler:
resumeButton.onClick.add(() => {
    this.togglePause(); // unpause first
});

// Inside a "main menu" button's onClick handler:
mainMenuButton.onClick.add(() => {
    this.app.scene.setScene(new MainMenuScene(), { transition: { type: 'fade' } });
});

Examples

Pause Blur Keyboard Open in Playground View source

Preview is paused until you click Play.

import { Application, BlurFilter, Color, Keyboard, Label, Panel, Scene, Sprite, Texture } from '@codexo/exojs';
import { mountControls } from '@examples/runtime';

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

const PAUSE_BLUR_RADIUS = 6;
const PAUSE_FADE_SECONDS = 0.35;

/**
 * Pause without a scene stack: a pause overlay lives on `scene.ui` (always
 * above the world) and is toggled together with `scene.paused`, which skips the
 * scene's `update` + systems while it keeps drawing. The blur tween runs on the
 * app-level TweenManager, so it still animates while the scene is frozen.
 */
class GameScene extends Scene {
    private sprite!: Sprite;
    private time = 0;
    private readonly blur = new BlurFilter({ radius: 0, quality: 2 });
    private pausePanel!: Panel;
    private pauseLabel!: Label;
    private hud!: ReturnType<typeof mountControls>;

    override async load(loader): Promise<void> {
        await loader.load(Texture, { ship: 'image/ship-a.png' });
    }

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

        this.sprite = new Sprite(loader.get(Texture, 'ship')).setAnchor(0.5).setScale(2).setPosition(width / 2, height / 2);
        this.addChild(this.sprite);

        // Pause overlay on the UI layer, hidden until paused.
        this.pausePanel = new Panel({ width: 420, height: 140, cornerRadius: 18, color: new Color(0, 0, 0, 0.6) });
        this.pausePanel.anchorIn(this.ui, 'center');
        this.pausePanel.visible = false;
        this.ui.addChild(this.pausePanel);

        this.pauseLabel = new Label('PAUSED', { fontSize: 56, fontWeight: 'bold' });
        this.pauseLabel.anchorIn(this.ui, 'center');
        this.pauseLabel.visible = false;
        this.ui.addChild(this.pauseLabel);

        this.hud = mountControls({
            title: 'Pause Blur',
            controls: [{ keys: 'Esc', action: 'pause / resume' }],
            hint: 'Press Esc to pause — the scene blurs up behind the menu.',
        });

        this.inputs.onTrigger(Keyboard.Escape, () => this.togglePause());
    }

    override update(delta): void {
        // Not called while paused — the SceneManager skips update() + systems.
        this.time += delta.seconds;
        this.sprite.setRotation(this.time * 80);
    }

    override draw(context): void {
        context.backend.clear(new Color(20, 24, 34));
        context.render(this.root);
    }

    override destroy(): void {
        this.root.clearFilters();
        super.destroy();
    }

    private togglePause(): void {
        this.paused = !this.paused;
        this.pausePanel.visible = this.paused;
        this.pauseLabel.visible = this.paused;

        if (this.paused) {
            this.blur.radius = 0;
            this.root.filters = [this.blur];
            this.tweens.create(this.blur).to({ radius: PAUSE_BLUR_RADIUS }, PAUSE_FADE_SECONDS).start();
        } else {
            this.root.clearFilters();
        }
    }
}

void app.start(new GameScene());

A rotating sprite with a pause overlay — press Esc to freeze the game under a blurred background with a PAUSED label, Esc again to resume.

Where to go next

The next recipe, Split screen, covers rendering the same scene from multiple viewpoints into viewport regions.