Loading...
Loading...
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.
npx skill4agent add drincs-productions/pixi-vn pixi-vn-minigamespixi-vn-canvaspixi-vn-uicanvas.gameLayergameLayerpixi-vn-canvascanvas.layers.add(name, new Container())pixi-vn-uiCANVAS_MINIGAME_LAYER_NAMEpixi-vn-ui/minigame/snakepixi-vn-uiGame.exportGameState()pixi-vn-savespixi-vn-storagesrc/routes/minigame//minigame/snake/minigame/quizimport { createFileRoute } from "@tanstack/react-router";
import SnakeGame from "@/screens/minigames/SnakeGame";
export const Route = createFileRoute("/minigame/snake")({
component: SnakeGame,
});src/routes/minigame.tsx<Outlet />/gamesrc/routes/game.tsx/game/*usePauseGameWhenMenuIsOpenuseGameHotkeys<Outlet />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 <Outlet />;
}src/routes/minigame.tsxsrc/routes/minigame.tsx<Outlet />src/routes/game.tsxusePauseGameWhenMenuIsOpenuseGameHotkeys/game/*src/routes/game.tsxGame.exportGameState()pixi-vn-storageuseMinigameuseMinigamesrc/lib/hooks/minigame-hooks.tsonStartgame(layer)onExitimport { 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<void>;
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 };
}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)GraphicsSpriteTextTickerlayeronStartonExit(layer)gamedestroy()TickeruseMinigamegameoptionsonExituseCallbackuseMemopixi-vn-uiwindow.addEventListener("keydown", ...)enabled: !gameOverpixi.js@drincs/pixi-vn/pixi.jsGraphicsSpriteTextTickerpixi-vn-canvasgameLayerpointerEvents: "auto"pixi-vn-uicanvas.gameLayercanvas.layers.addpixi-vn-canvasgameLayergameonExituseCallbackuseMemoGame.exportGameState()pixi-vn-storagepixi-vn-savesTickeronExitGraphicsSpriteTextcanvas.animategameLayercanvas.layers.addhtmlLayers.addGame.exportGameState()