ignifx0.x · unpublished
GitHub

Recipes·skills/ignifx/references/recipes/build-a-pause-menu.md

Build a pause menu

<!-- Generated by `pnpm docs:recipes`. Do not edit by hand. -->

Game UI in ignifx is HTML: @ignifx/ui puts one absolutely positioned <div> over the canvas, and Dialog builds a titled panel with buttons inside it. Real DOM means real focus, so Tab and Enter already work; this recipe adds the gamepad by moving document.activeElement from a navigate action.

Pausing is two independent switches. app.pause() stops the fixed loop and the three update callbacks, so the one script that must keep running declares static updateWhenPaused = true. Swapping the action maps is the other: disable "Player", enable "UI". Read an action only while its map is enabled — actions.get searches enabled maps and throws IGX-0801 otherwise, which is why each branch below reads only its own map.

Focus is real, so Tab and Enter work with no code once open() has focused the first button, and the script disposes the dialog it built in onDestroy — the overlay outlives any one entity.

A press on a pointer-events: auto element never reaches gameplay, because @ignifx/input reads pointerdown from the canvas and the overlay is the canvas's sibling. A drag that started on the UI does still move <Pointer>/delta, so a camera script checks app.ui.pointerOverUi.

menu.input.json beside this file is what input/menu.input.json resolves to; its "UI" map ships disabled.

typescript
import { Script, createApp } from "@ignifx/core";import { input } from "@ignifx/input";import { Dialog, ui } from "@ignifx/ui";import type { ScriptCallbacks } from "@ignifx/core";import type { InputActionsAsset } from "@ignifx/input";/** Owns the pause dialog: it opens it, navigates it, closes it, and disposes it. */class PauseMenu extends Script implements ScriptCallbacks {  static typeId = "recipes/PauseMenu";  static updateWhenPaused = true;  #dialog: Dialog | null = null;  #armed = true;  awake(): void {    const buttons = ["Resume", "Quit"].map((label) => ({ id: label.toLowerCase(), label }));    this.#dialog = new Dialog(this.app.ui, { title: "Paused", layer: "menu", buttons });    this.#dialog.onChosen.connect(this.#chose, { owner: this });  }  readonly #chose = (id: string): void => {    if (id === "resume") {      this.close();    }  };  update(): void {    const actions = this.app.input.actions;    if (this.#dialog?.isVisible !== true) {      if (actions.get("pause").wasPressedThisFrame) {        this.open();      }    } else if (actions.get("back").wasPressedThisFrame) {      this.close();    } else {      this.#navigate(actions.get("navigate").vector.y);    }  }  open(): void {    this.#dialog?.show();    this.app.input.actions.map("Player").enabled = false;    this.app.input.actions.map("UI").enabled = true;    this.#dialog?.element?.querySelector("button")?.focus();    this.app.pause();  }  close(): void {    this.#dialog?.hide();    this.app.input.actions.map("UI").enabled = false;    this.app.input.actions.map("Player").enabled = true;    this.app.resume();  }  onDestroy(): void {    this.#dialog?.dispose();  }  // One move per push of the stick — a held stick is not a key repeat — over real DOM focus.  #navigate(y: number): void {    const push = Math.abs(y) >= 0.5;    this.#armed ||= !push;    const buttons = [...(this.#dialog?.element?.querySelectorAll("button") ?? [])];    if (!push || !this.#armed || buttons.length === 0) {      return;    }    this.#armed = false;    const at = buttons.findIndex((button) => button === document.activeElement);    buttons[(Math.max(at, 0) + (y < 0 ? 1 : -1) + buttons.length) % buttons.length]?.focus();  }}const canvas = document.querySelector("canvas");if (!(canvas instanceof HTMLCanvasElement)) {  throw new Error("ignifx renders into a <canvas> element.");}const app = await createApp({ canvas, extensions: [input(), ui({ scaling: "fit" })] });app.registerComponents([PauseMenu]);app.input.loadActions(await app.assets.loadAsync<InputActionsAsset>("input/menu.input.json"));app.world.createEntity("Pause Menu").addComponent(PauseMenu);await app.start();

Source: examples/recipes/build-a-pause-menu/main.ts