pixi-vn-minigames
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePixi'VN Minigames
Pixi'VN 小游戏
Official docs: 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.
官方文档: pixi-vn.com/start/minigames。
Pixi'VN 没有专门的小游戏 API——小游戏本质上就是 PixiJS 代码(其 HUD/UI 使用常规框架 UI),并通过与其他地方相同的基础机制接入游戏:画布图层、热键和导航。本 skill 讲解的是如何专门围绕小游戏组合这些部分,而不是引入新的 API。
When to use this skill
何时使用此 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 () or for
general app UI screens unrelated to a minigame ().
pixi-vn-canvaspixi-vn-ui只要任务涉及构建或接入小游戏,就应加载此 skill:贪吃蛇/街机风格游戏、计时/反应挑战、益智游戏、问答模式,或任何玩家脱离主叙事来游玩的内容。不适用于叙事画布本身(),也不适用于与小游戏无关的常规应用 UI 页面()。
pixi-vn-canvaspixi-vn-uiCore mental model
核心思维模型
- A minigame renders on its own PixiJS layer, not on .
canvas.gameLayeris the save-able narrative scene graph (backgrounds, characters — seegameLayer); a minigame's moving pieces (snake segments, falling blocks, ...) are transient and don't belong there. Usepixi-vn-canvasto get a separate, non-save-able layer for the minigame, exactly like a PixiJS UI layer (canvas.layers.add(name, new Container())) — the official template even reserves a constant for it,pixi-vn-ui.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 (), positioned over the canvas the same way any other UI is.
pixi-vn-ui - 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. ) and navigate to it from a step, then navigate back when it ends — see
/minigame/snake's "Navigating between UI screens" section and interface-navigate. Every minigame example below is written as a route file for this reason, not a bare component.pixi-vn-ui - Saving and restoring the minigame's current state (score, board, position, ...) is entirely the
developer's responsibility. does not know about a minigame in progress — see
Game.exportGameState(). If a minigame's progress must survive a save/reload, persist it yourself (e.g. via game storage, seepixi-vn-saves).pixi-vn-storage
- 小游戏应渲染在独立的 PixiJS 图层上,而不是 。
canvas.gameLayer是可保存的叙事场景图(背景、角色——参见gameLayer);小游戏中的移动元素(贪吃蛇身体、下落的方块等)都是临时性的,不应放在其中。使用pixi-vn-canvas创建一个独立且不可保存的小游戏图层,其方式与 PixiJS UI 图层(canvas.layers.add(name, new Container()))完全相同——官方模板甚至为此预留了常量pixi-vn-ui。CANVAS_MINIGAME_LAYER_NAME - 小游戏的 HUD/菜单(分数、游戏结束遮罩、控制按钮)应使用常规 UI,而不是 PixiJS——使用项目已有的 React/Vue 等 UI 技术栈构建(),并像其他 UI 一样叠加在画布上。
pixi-vn-ui - 小游戏从叙事中启动,结束后玩家再返回叙事。 推荐做法是为小游戏关联一个独立路由(例如 ),从一个步骤导航进入,并在小游戏结束后导航返回——参见
/minigame/snake的“在 UI 页面之间导航”章节以及 interface-navigate。出于这个原因,下面的每个小游戏示例都写成了路由文件,而不是单独的组件。pixi-vn-ui - 保存和恢复小游戏当前状态(分数、棋盘、位置等)完全由开发者负责。 并不知道正在运行的小游戏——参见
Game.exportGameState()。如果小游戏进度必须在保存/重新加载后仍然存在,请自行持久化(例如通过 game storage,参见pixi-vn-saves)。pixi-vn-storage
Routing structure: one route per minigame, plus a shared layout route
路由结构:每个小游戏一个路由,再加一个共享布局路由
With file-based routing (e.g. TanStack Router, used by the
official templates), each minigame gets its own file under , so a project with
several minigames ends up with , , etc.:
src/routes/minigame//minigame/snake/minigame/quiztsx
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, , that renders an for whichever
minigame route is active — the same pattern the official React template already uses for
( wraps every screen with +
+ ):
src/routes/minigame.tsx<Outlet />/gamesrc/routes/game.tsx/game/*usePauseGameWhenMenuIsOpenuseGameHotkeys<Outlet />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 <Outlet />;
}What actually goes in 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:
src/routes/minigame.tsx- 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, has nothing to do — leave it empty (just the
src/routes/minigame.tsx) or skip creating it at all and let each minigame route stand alone.<Outlet /> - 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 centralizes
src/routes/game.tsxandusePauseGameWhenMenuIsOpenfor everyuseGameHotkeysscreen./game/* - Should the same menus as (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:
src/routes/game.tsxknows 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 (Game.exportGameState()) so the save is still meaningful. Don't wire in the game's menus unchanged and assume they "just work" inside a minigame.pixi-vn-storage
在基于文件的路由下(例如官方模板使用的 TanStack Router),每个小游戏在 下都有自己的文件,因此包含多个小游戏的项目最终会有 、 等路由:
src/routes/minigame//minigame/snake/minigame/quiztsx
import { createFileRoute } from "@tanstack/react-router";
import SnakeGame from "@/screens/minigames/SnakeGame";
export const Route = createFileRoute("/minigame/snake")({
component: SnakeGame,
});所有小游戏共享的内容(小游戏打开时暂停叙事/音频、通用热键、共享 HUD 包装组件)不应复制粘贴到这些文件中的每一个里。将其提取到一个无路径布局路由 ,由它为当前激活的小游戏路由渲染 。官方 React 模板对 已经采用相同模式( 使用 + + 包装所有 页面):
src/routes/minigame.tsx<Outlet />/gamesrc/routes/game.tsxusePauseGameWhenMenuIsOpenuseGameHotkeys<Outlet />/game/*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 <Outlet />;
}src/routes/minigame.tsx- 小游戏之间是否确实存在共享内容? 如果小游戏打开时无法访问游戏设置菜单、暂停菜单或快捷操作轮盘,而且没有每个小游戏都需要使用的通用热键、HUD 或 provider,那么 就无事可做——让它保持为空(仅包含
src/routes/minigame.tsx),或者干脆不创建它,让每个小游戏路由独立存在。<Outlet /> - 是否存在仅供小游戏使用的菜单? 例如小游戏暂停菜单、“退出小游戏并返回故事”确认框或小游戏专属设置面板。如果有,这些正是应该放入共享布局的内容,与 为所有
src/routes/game.tsx页面集中管理/game/*和usePauseGameWhenMenuIsOpen的方式相同。useGameHotkeys - 是否应该让玩家在小游戏内访问与 相同的菜单(设置、保存/读取、快速操作)? 如果应该,不要假定它们的行为完全一致——需要逐项检查每个菜单在小游戏进行中是否仍然合理。保存功能是最明显的例子:
src/routes/game.tsx完全不知道正在进行的小游戏状态(参见下面的“常见陷阱”章节),因此,从小游戏内触发的保存操作要么需要在其中禁用/隐藏,要么小游戏必须先持久化自己的状态(Game.exportGameState()),以确保保存仍然具有意义。不要直接接入游戏的原有菜单,并假定它们在小游戏内也能“直接可用”。pixi-vn-storage
The useMinigame
lifecycle hook
useMinigameuseMinigame
生命周期 hook
useMinigameEvery official template ships a hook (e.g. ) that
handles the repetitive part of wiring a minigame into a layer: creating the layer once, running an
optional async before the game begins, calling back into your function, and
cleaning the layer up (plus your ) when the screen unmounts.
useMinigamesrc/lib/hooks/minigame-hooks.tsonStartgame(layer)onExitts
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<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 };
}Usage inside the minigame's own route file:
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 */}</>
);
}- is where you build the actual PixiJS content (
game(layer),Graphics,Sprite, aText) and add it to the givenTicker. It runs once, afterlayerresolves.onStart - is where you tear down anything
onExit(layer)started — stop andgameanydestroy(), remove event listeners.Tickeralready removes the layer itself.useMinigame - and the
gameobject (specifically itsoptions) must keep a stable identity across re-renders (onExit/useCallbackwith the right deps) — since they're the effect's dependency, changing identity mid-game tears down and restarts the whole minigame.useMemo
每个官方模板都会提供一个 hook(例如 ),用于处理将小游戏接入图层的重复工作:只创建一次图层、在游戏开始前运行可选的异步 、回调你的 函数,并在屏幕卸载时清理图层以及调用 。
useMinigamesrc/lib/hooks/minigame-hooks.tsonStartgame(layer)onExitts
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<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 };
}在小游戏自己的路由文件中的用法:
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 */}</>
);
}- 是构建实际 PixiJS 内容(
game(layer)、Graphics、Sprite、Text)并将其添加到指定Ticker的位置。它会在layerresolve 后运行一次。onStart - 用于销毁
onExit(layer)启动的所有内容——停止并game任何destroy()、移除事件监听器。Ticker已经负责移除图层本身。useMinigame - 和
game对象(尤其是其中的options)必须在重新渲染期间保持引用身份稳定——使用带有正确依赖项的onExit/useCallback——因为它们是 effect 的依赖项,在游戏过程中改变引用身份会销毁并重启整个小游戏。useMemo
Controls and rendering
控制与渲染
- Use hotkeys for keyboard input (see /hotkeys) rather than raw
pixi-vn-ui, and gate them withwindow.addEventListener("keydown", ...)so input stops mounting a listener that outlives the round.enabled: !gameOver - Build the actual gameplay visuals with PixiJS directly — either the package or the
pixi.jsre-export — using@drincs/pixi-vn/pixi.js/Graphics/Spriteand aTextfor the game loop, same primitives asTickerbut on the dedicated minigame layer instead ofpixi-vn-canvas.gameLayer - On-screen touch controls, score displays, and game-over messages should still be plain UI
components (buttons, divs) layered over the canvas with where they need to be clickable — see
pointerEvents: "auto"'s "Enabling interaction" section.pixi-vn-ui
- 使用热键处理键盘输入(参见 /hotkeys),而不是直接使用
pixi-vn-ui,并通过window.addEventListener("keydown", ...)对其进行门控,确保回合结束后输入监听器不会继续生效。enabled: !gameOver - 使用 PixiJS 直接构建实际的游戏视觉内容——可以选择 包,或使用
pixi.js重新导出的 API——通过@drincs/pixi-vn/pixi.js/Graphics/Sprite和Text实现游戏循环。其基础机制与Ticker相同,只是应使用专用的小游戏图层,而不是pixi-vn-canvas。gameLayer - 屏幕触控控制、分数显示和游戏结束消息仍应使用普通 UI 组件(按钮、div),叠加在画布上;需要点击的位置应设置 ——参见
pointerEvents: "auto"的“启用交互”章节。pixi-vn-ui
Gotchas
常见陷阱
- Adding game content to instead of a dedicated layer makes it save-able and mixes it with narrative elements — use
canvas.gameLayerfor the minigame instead (canvas.layers.addhas the fullpixi-vn-canvasvs. layer distinction).gameLayer - An unstable /
gamereference restarts the minigame on every render — see theonExit/useCallbacknote above; this is the single most common bug when porting the pattern.useMemo - Nothing about minigame progress is captured by — a mid-round save/ reload will silently drop it unless you persist state yourself through game storage (
Game.exportGameState()/pixi-vn-storage).pixi-vn-saves - Forgetting to stop/destroy the in
Tickerleaves the game loop running (and potentially erroring against removed objects) after the player navigates away.onExit
- 将游戏内容添加到 而不是专用图层会使其成为可保存状态,并与叙事元素混在一起——小游戏应使用
canvas.gameLayer(canvas.layers.add详细介绍了pixi-vn-canvas与其他图层的区别)。gameLayer - 不稳定的 /
game引用会在每次渲染时重启小游戏——参见上方关于onExit/useCallback的说明;这是迁移该模式时最常见的错误。useMemo - 不会捕获任何小游戏进度——如果未通过 game storage(
Game.exportGameState()/pixi-vn-storage)自行持久化状态,那么在回合中途保存或重新加载会静默丢失进度。pixi-vn-saves - **忘记在 中停止/销毁
onExit**会导致游戏循环在玩家离开后继续运行,并可能因访问已移除的对象而报错。Ticker
Related skills
相关 skill
- pixi-vn-canvas: the PixiJS rendering primitives (,
Graphics,Sprite, tickers,Text) a minigame is built from, and thecanvas.animate/layer distinction.gameLayer - pixi-vn-ui: /
canvas.layers.add, hotkeys, and navigating between UI screens/routes — how the minigame's HUD and route linkage actually work.htmlLayers.add - pixi-vn-storage: persisting minigame progress (score, board state) in game storage if it needs to survive a save/reload.
- pixi-vn-saves: confirms has no built-in notion of in-progress minigame state.
Game.exportGameState()
- pixi-vn-canvas:构建小游戏所用的 PixiJS 渲染基础机制(、
Graphics、Sprite、Text、Ticker),以及canvas.animate/图层之间的区别。gameLayer - pixi-vn-ui:/
canvas.layers.add、热键,以及在 UI 页面/路由之间导航——小游戏 HUD 和路由关联的实际工作方式。htmlLayers.add - pixi-vn-storage:如果小游戏进度需要在保存/重新加载后仍然存在,可通过 game storage 持久化小游戏进度(分数、棋盘状态)。
- pixi-vn-saves:确认 没有内置的“正在进行的小游戏状态”概念。
Game.exportGameState()