---
name: pixi-vn-minigames
description: Use when adding a minigame (snake, quiz, puzzle, timing/reaction game, ...) to a Pixi'VN visual novel — the `useMinigame` lifecycle hook pattern, using PixiJS directly on its own layer instead of `gameLayer`, hotkeys for controls, linking the minigame to a route, and why saving/restoring minigame state is the developer's own responsibility.
---
# Pixi'VN Minigames
Official docs: [pixi-vn.com/start/minigames](https://pixi-vn.com/start/minigames).
Pixi'VN has no dedicated minigame API — a minigame is just PixiJS code (and, for its HUD/UI, a
regular framework UI) wired into the game through the same primitives used elsewhere: canvas
layers, hotkeys, and navigation. This skill is about how those pieces fit together for a minigame
specifically, not about introducing new APIs.
## When to use this skill
Load this skill whenever a task involves building or wiring in a minigame: a snake/arcade-style
game, a timing/reaction challenge, a puzzle, a quiz mini-mode, or anything else the player breaks
away from the main narration to play. Not for the narrative canvas itself (`pixi-vn-canvas`) or for
general app UI screens unrelated to a minigame (`pixi-vn-ui`).
## Core mental model
- **A minigame renders on its own PixiJS layer, not on `canvas.gameLayer`.** `gameLayer` is the
save-able narrative scene graph (backgrounds, characters — see `pixi-vn-canvas`); a minigame's
moving pieces (snake segments, falling blocks, ...) are transient and don't belong there. Use
`canvas.layers.add(name, new Container())` to get a separate, non-save-able layer for the minigame,
exactly like a PixiJS UI layer (`pixi-vn-ui`) — the official template even reserves a constant for
it, `CANVAS_MINIGAME_LAYER_NAME`.
- **The minigame's HUD/menus (score, game-over overlay, control buttons) should be regular UI**, not
PixiJS — built with React/Vue/whatever the project's UI already uses (`pixi-vn-ui`), positioned
over the canvas the same way any other UI is.
- **A minigame is launched from the narrative, then the player returns to it.** The recommended
approach is to link the minigame to its own route (e.g. `/minigame/snake`) and navigate to it from
a step, then navigate back when it ends — see `pixi-vn-ui`'s "Navigating between UI screens"
section and [interface-navigate](https://pixi-vn.com/start/interface-navigate). Every minigame
example below is written as a route file for this reason, not a bare component.
- **Saving and restoring the minigame's current state (score, board, position, ...) is entirely the
developer's responsibility.** `Game.exportGameState()` does not know about a minigame in progress
— see `pixi-vn-saves`. If a minigame's progress must survive a save/reload, persist it yourself
(e.g. via [game storage](https://pixi-vn.com/start/storage), see `pixi-vn-storage`).
## Routing structure: one route per minigame, plus a shared layout route
With file-based routing (e.g. [TanStack Router](https://tanstack.com/router/latest), used by the
official templates), each minigame gets its own file under `src/routes/minigame/`, so a project with
several minigames ends up with `/minigame/snake`, `/minigame/quiz`, etc.:
```tsx title="src/routes/minigame/snake.tsx"
import { createFileRoute } from "@tanstack/react-router";
import SnakeGame from "@/screens/minigames/SnakeGame";
export const Route = createFileRoute("/minigame/snake")({
component: SnakeGame,
});
```
Anything shared by _every_ minigame (pausing narration/audio while one is open, common hotkeys, a
shared HUD wrapper) doesn't belong copy-pasted into each of those files. Factor it into a single
pathless layout route, `src/routes/minigame.tsx`, that renders an `` for whichever
minigame route is active — the same pattern the official React template already uses for `/game`
(`src/routes/game.tsx` wraps every `/game/*` screen with `usePauseGameWhenMenuIsOpen` +
`useGameHotkeys` + ``):
```tsx title="src/routes/minigame.tsx"
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/minigame")({
component: MinigameLayout,
});
function MinigameLayout() {
// hooks/providers shared by every minigame go here
return ;
}
```
**What actually goes in `src/routes/minigame.tsx` is a judgment call to make while building the
_first_ minigame, not a file to scaffold reflexively** — work through these questions before adding
anything to it:
- **Is there actually anything shared across minigames?** If the game's settings menu, pause menu,
or quick-actions wheel aren't reachable while a minigame is open, and there's no common
hotkey/HUD/provider every minigame needs, `src/routes/minigame.tsx` has nothing to do — leave it
empty (just the ``) or skip creating it at all and let each minigame route stand alone.
- **Are there menus specific to minigames only** (a minigame pause menu, a "quit minigame and return
to story" confirmation, a minigame-specific settings panel)? If so, that's exactly what belongs in
the shared layout, the same way `src/routes/game.tsx` centralizes `usePauseGameWhenMenuIsOpen` and
`useGameHotkeys` for every `/game/*` screen.
- **Should the same menus as `src/routes/game.tsx` (settings, save/load, quick actions) be reachable
from inside a minigame?** If yes, don't assume they behave identically — check case by case whether
each one still makes sense mid-minigame. Saving is the clearest example: `Game.exportGameState()`
knows nothing about in-progress minigame state (see the Gotchas section below), so a save
triggered from inside a minigame either needs to be disabled/hidden there, or the minigame needs to
persist its own state first (`pixi-vn-storage`) so the save is still meaningful. Don't wire in the
game's menus unchanged and assume they "just work" inside a minigame.
## The `useMinigame` lifecycle hook
Every official template ships a `useMinigame` hook (e.g. `src/lib/hooks/minigame-hooks.ts`) that
handles the repetitive part of wiring a minigame into a layer: creating the layer once, running an
optional async `onStart` before the game begins, calling back into your `game(layer)` function, and
cleaning the layer up (plus your `onExit`) when the screen unmounts.
```ts title="lib/hooks/minigame-hooks.ts (React)"
import { CANVAS_MINIGAME_LAYER_NAME } from "@/constants";
import { canvas, type Layer } from "@drincs/pixi-vn";
import { Container } from "pixi.js";
import { useEffect, useRef } from "react";
export function useMinigame(
game: (layer: Layer) => void,
props?: {
onStart?: () => Promise;
onExit?: (layer: Layer) => void;
},
) {
const loading = useRef(false);
useEffect(() => {
loading.current = true;
const layer = canvas.layers.add(CANVAS_MINIGAME_LAYER_NAME, new Container());
if (!layer) {
console.error("Failed to create UI layer for minigame");
return;
}
let cancelled = false;
(props?.onStart ?? (async () => {}))().then(() => {
if (cancelled) return;
loading.current = false;
game(layer);
});
return () => {
cancelled = true;
canvas.layers.remove(CANVAS_MINIGAME_LAYER_NAME);
props?.onExit?.(layer);
};
}, [game]);
return { loading };
}
```
Usage inside the minigame's own route file:
```tsx title="src/routes/minigame/example.tsx"
import { Layer } from "@drincs/pixi-vn";
import { Graphics, Ticker } from "@drincs/pixi-vn/pixi.js";
import { createFileRoute } from "@tanstack/react-router";
import { useCallback, useMemo, useState } from "react";
import { useHotkey } from "@tanstack/react-hotkeys";
import useMinigame from "@/lib/hooks/minigame-hooks";
export const Route = createFileRoute("/minigame/example")({
component: MiniGame,
});
function MiniGame() {
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(false);
const ticker = useMemo(() => {
const ticker = new Ticker();
ticker.add(({ deltaMS }) => {
// update game logic; call setGameOver(true) + ticker.stop() to end
});
return ticker;
}, []);
useHotkey(
"ArrowUp",
() => {
/* handle input */
},
{ enabled: !gameOver },
);
const game = useCallback(
(layer: Layer) => {
ticker.start();
},
[ticker], // must NOT change during the game or it restarts
);
const options = useMemo(
() => ({
onExit() {
ticker.stop();
ticker.destroy();
},
}),
[ticker], // must NOT change during the game or it restarts
);
useMinigame(game, options);
return (
<>{/* score HUD, game-over overlay, control buttons — regular UI */}>
);
}
```
- **`game(layer)`** is where you build the actual PixiJS content (`Graphics`, `Sprite`, `Text`, a
`Ticker`) and add it to the given `layer`. It runs once, after `onStart` resolves.
- **`onExit(layer)`** is where you tear down anything `game` started — stop and `destroy()` any
`Ticker`, remove event listeners. `useMinigame` already removes the layer itself.
- **`game` and the `options` object (specifically its `onExit`) must keep a stable identity** across
re-renders (`useCallback`/`useMemo` with the right deps) — since they're the effect's dependency,
changing identity mid-game tears down and restarts the whole minigame.
## Controls and rendering
- Use **hotkeys** for keyboard input (see `pixi-vn-ui`/[hotkeys](https://pixi-vn.com/start/hotkeys))
rather than raw `window.addEventListener("keydown", ...)`, and gate them with `enabled: !gameOver`
so input stops mounting a listener that outlives the round.
- Build the actual gameplay visuals with PixiJS directly — either the `pixi.js` package or the
`@drincs/pixi-vn/pixi.js` re-export — using `Graphics`/`Sprite`/`Text` and a `Ticker` for the game
loop, same primitives as `pixi-vn-canvas` but on the dedicated minigame layer instead of
`gameLayer`.
- On-screen touch controls, score displays, and game-over messages should still be plain UI
components (buttons, divs) layered over the canvas with `pointerEvents: "auto"` where they need to
be clickable — see `pixi-vn-ui`'s "Enabling interaction" section.
## Gotchas
- **Adding game content to `canvas.gameLayer` instead of a dedicated layer** makes it save-able and
mixes it with narrative elements — use `canvas.layers.add` for the minigame instead (`pixi-vn-canvas`
has the full `gameLayer` vs. layer distinction).
- **An unstable `game`/`onExit` reference restarts the minigame on every render** — see the
`useCallback`/`useMemo` note above; this is the single most common bug when porting the pattern.
- **Nothing about minigame progress is captured by `Game.exportGameState()`** — a mid-round save/
reload will silently drop it unless you persist state yourself through game storage
(`pixi-vn-storage`/`pixi-vn-saves`).
- **Forgetting to stop/destroy the `Ticker` in `onExit`** leaves the game loop running (and
potentially erroring against removed objects) after the player navigates away.
## Related skills
- pixi-vn-canvas: the PixiJS rendering primitives (`Graphics`, `Sprite`, `Text`, tickers,
`canvas.animate`) a minigame is built from, and the `gameLayer`/layer distinction.
- pixi-vn-ui: `canvas.layers.add`/`htmlLayers.add`, hotkeys, and navigating between UI screens/routes —
how the minigame's HUD and route linkage actually work.
- pixi-vn-storage: persisting minigame progress (score, board state) in game storage if it needs to
survive a save/reload.
- pixi-vn-saves: confirms `Game.exportGameState()` has no built-in notion of in-progress minigame
state.