pixi-vn-getting-started

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pixi'VN project initialization and the Game API

Pixi'VN 项目初始化与 Game API

Official docs: pixi-vn.com/start. For a full machine-readable index of every Pixi'VN doc page — useful whenever a task needs more detail than this skill set covers — see pixi-vn.com/llms.txt.
官方文档:pixi-vn.com/start。如需获取所有 Pixi'VN 文档页面的完整机器可读索引——当任务需要比本 skill 集更详细的信息时,该索引会很有用——请参阅 pixi-vn.com/llms.txt。

When to use this skill

何时使用此 skill

Use this skill whenever you are bootstrapping a Pixi'VN project from scratch, wiring up
src/main.ts
, or need to understand what
Game.init
/
Game.start
actually do before wiring narration, canvas, sound, storage, or history features. Every other Pixi'VN skill assumes
Game.init
has already run.
在从头引导 Pixi'VN 项目、配置
src/main.ts
,或需要在配置 narration、canvas、sound、storage 或 history 功能之前了解
Game.init
/
Game.start
的实际作用时,使用此 skill。本集合中的每个其他 Pixi'VN skill 都假定
Game.init
已经运行。

Installing the library

安装库

Two paths, depending on whether a project already exists:
  • New project — scaffold from an official template (recommended for anything from scratch):
    bash
    npm create pixi-vn@latest
    This generates a working project (index.html, entry point, bundler config) already wired to call
    Game.init
    , with pre-built menu/narration screens, VS Code launch configs, and (since template v2.0.0) TanStack Router for navigation. See pixi-vn.com/start/templates for the full generated project structure, available templates/demos, and how to run/customize one.
    The wizard asks for a narrative language and defaults/suggests Ink + TypeScript (it's listed first and pre-selected in the prompt for the Visual Novel, Text-based Story, and Point & Click Adventure project types) — plain TypeScript narration (what every skill in this set documents) is the other option, selectable in the same prompt. Ink support is a separate narrative language with its own syntax and its own future skill; nothing in this skill set requires it.
    The wizard's first question is the project/game type; the docs have a dedicated getting-started walkthrough for some of them — pixi-vn.com/start/make-visual-novel for "Visual Novel", pixi-vn.com/nqtr/make-point-and-click for "Point & Click Adventure" — worth pointing a developer to the matching one for their project type, in addition to the general pixi-vn.com/start/templates page above.
  • Existing JS/TS project — add the package directly:
    bash
    npm install @drincs/pixi-vn
根据项目是否已经存在,有两种安装路径:
  • 新项目——基于官方模板搭建脚手架(对于任何从零开始的项目均推荐):
    bash
    npm create pixi-vn@latest
    这会生成一个可运行的项目(index.html、入口点和 bundler 配置),并已配置为调用
    Game.init
    ,同时包含预构建的菜单/narration 屏幕、VS Code 启动配置,以及(自模板 v2.0.0 起)用于导航的 TanStack Router。请参阅 pixi-vn.com/start/templates,了解生成项目的完整结构、可用模板/演示,以及如何运行或自定义模板。
    向导的第一个问题是项目/游戏类型;文档为其中一些类型提供了专门的入门教程——pixi-vn.com/start/make-visual-novel 对应“Visual Novel”,pixi-vn.com/nqtr/make-point-and-click 对应“Point & Click Adventure”——除了一般的 pixi-vn.com/start/templates 页面外,还应根据开发者的项目类型将其引导至匹配的教程。
    向导会询问叙事语言,并默认/建议使用 Ink + TypeScript(在 Visual Novel、Text-based Story 和 Point & Click Adventure 项目类型的提示中,它都列在第一位并被预先选中)——纯 TypeScript narration(也是本 skill 集合中每个 skill 所记录的内容)是另一个选项,可在同一提示中选择。Ink 支持是一种独立的叙事语言,拥有自己的语法和未来的专属 skill;本 skill 集合不要求使用它。
  • 现有 JS/TS 项目——直接添加该包:
    bash
    npm install @drincs/pixi-vn

Required peer dependencies

必需的 peer dependencies

@drincs/pixi-vn
does not bundle its own PixiJS/audio/animation stack — it declares compatible versions as peer dependencies (per
package.json
):
PackageRequired version
pixi.js
>=8.17.0
pixi-filters
>=6.1.0
(PixiJS's extended filter collection, usable by canvas filter-based transitions/effects)
tone
>=15.1.0
(audio engine used by the sound module)
motion
>=12.38.0
(used internally for canvas animations, e.g.
Game.GameUnifier.animate
)
@pixi/devtools
>=2.0.0
(enables the PixiJS Devtools browser extension integration)
vite
>=5.0.0
— optional peer dependency (only needed if you use Vite as your bundler; the official templates do)
With npm 7+,
npm install @drincs/pixi-vn
auto-installs these peer dependencies too — no separate install step is needed in the common case. If a developer is on an older npm, or uses a package manager that doesn't auto-install peers (e.g. pnpm, or yarn in strict mode), they'll need to install them explicitly (
npm install pixi.js pixi-filters tone motion @pixi/devtools
). If a developer reports errors about missing modules like
pixi.js
,
tone
, or
motion
, check their package manager/version and whether these ended up installed before looking elsewhere.
@drincs/pixi-vn
不捆绑自己的 PixiJS/音频/动画技术栈——它会在
package.json
中将兼容版本声明为 peer dependencies:
包所需版本
pixi.js
>=8.17.0
pixi-filters
>=6.1.0
(PixiJS 的扩展滤镜集合,可用于基于 canvas 滤镜的转场/效果)
tone
>=15.1.0
(sound 模块使用的音频引擎)
motion
>=12.38.0
(在内部用于 canvas 动画,例如
Game.GameUnifier.animate
)
@pixi/devtools
>=2.0.0
(启用 PixiJS Devtools 浏览器扩展集成)
vite
>=5.0.0
——可选的 peer dependency(仅在使用 Vite 作为 bundler 时需要;官方模板使用 Vite)
在 npm 7+ 中,
npm install @drincs/pixi-vn
也会自动安装这些 peer dependencies——常见情况下无需单独执行安装步骤。如果开发者使用较旧的 npm,或使用不会自动安装 peer dependencies 的包管理器(例如 pnpm,或 strict mode 下的 yarn),则需要显式安装它们(
npm install pixi.js pixi-filters tone motion @pixi/devtools
)。如果开发者报告缺少
pixi.js
、
tone
或
motion
等模块的错误,应先检查其包管理器/版本,以及这些包是否实际安装完成,再排查其他问题。

Import paths:
@drincs/pixi-vn
vs subpaths

导入路径:
@drincs/pixi-vn
与子路径

Every module (canvas, characters, history, narration, sound, storage, ...) is available both from the package root and from its own subpath, e.g.
Game
/
canvas
/
sound
all from
@drincs/pixi-vn
, or
canvas
alone from
@drincs/pixi-vn/canvas
. Default to importing from the root
@drincs/pixi-vn
— that's what every official doc example and template does:
ts
import { canvas, Game, narration, sound, storage } from "@drincs/pixi-vn";
Reach for a narrower subpath (
@drincs/pixi-vn/canvas
,
/sound
,
/characters
,
/storage
,
/narration
,
/core
,
/pixi.js
,
/motion
) only when there's a concrete reason to avoid pulling in the whole package surface — e.g. a headless/server-side environment where canvas or sound isn't available or permitted, a size-constrained bundle target, or code that genuinely only ever touches one module. This applies to every skill in this set; it isn't repeated per-module.
每个模块(canvas、characters、history、narration、sound、storage 等)都可以从包根目录导入,也可以从各自的子路径导入,例如
Game
/
canvas
/
sound
都可从
@drincs/pixi-vn
导入,或仅从
@drincs/pixi-vn/canvas
导入
canvas
。默认从根路径
@drincs/pixi-vn
导入
——所有官方文档示例和模板都采用这种方式:
ts
import { canvas, Game, narration, sound, storage } from "@drincs/pixi-vn";
只有在存在避免引入整个包 API 的明确理由时,才应使用更窄的子路径(
@drincs/pixi-vn/canvas
、
/sound
、
/characters
、
/storage
、
/narration
、
/core
、
/pixi.js
、
/motion
)——例如 canvas 或 sound 不可用或不允许使用的 headless/server-side 环境、对包体积有严格限制的构建目标,或代码确实只涉及一个模块。这条规则适用于本集合中的每个 skill,因此不会在各模块中重复说明。

Initializing the game:
Game.init

初始化游戏:
Game.init

Game.init
must be called before any other Pixi'VN function (narration, canvas, sound, storage, history). It has two forms:
  1. Full initialization — sets up the internal
    GameUnifier
    state manager and creates the PixiJS
    Application
    plus the canvas' interface
    <div>
    , attached to an HTML element:
    ts
    export async function init(
      element: HTMLElement,
      options: Partial<ApplicationOptions> & {
        id?: string; // default: "pixi-vn-canvas"
        navigate?: (path: string) => void | Promise<void>;
        resizeMode?: "contain" | "none"; // default: "contain"
      },
      devtoolsOptions?: Devtools,
    ): Promise<void>;
    • options
      is
      Partial<ApplicationOptions>
      — i.e. every option accepted by a PixiJS
      Application
      (
      width
      ,
      height
      ,
      backgroundColor
      ,
      antialias
      , ...) — plus three Pixi'VN-specific extras:
      • id
        : id of the generated canvas element.
      • navigate
        : the app's router navigation function (can also be set later with
        Game.onNavigate
        ).
      • resizeMode
        :
        "contain"
        (fit within the parent, keep aspect ratio — default) or
        "none"
        .
    • devtoolsOptions
      configures the PixiJS Devtools integration.
  2. Headless initialization —
    Game.init()
    with no arguments. This initializes only the internal
    GameUnifier
    (storage, narration, history bookkeeping) without creating a PixiJS
    Application
    or canvas. Useful if you only need save/load, storage, or narration logic without rendering. If you call the 3-argument form but omit
    element
    /
    options
    ,
    Game.init
    logs a warning and skips canvas setup the same way.
Game.init
必须在调用任何其他 Pixi'VN 函数(narration、canvas、sound、storage、history)之前调用。它有两种形式:
  1. 完整初始化——设置内部
    GameUnifier
    状态管理器_并_创建 PixiJS
    Application
    以及 canvas 的界面
    <div>
    ,再将其附加到 HTML 元素:
    ts
    export async function init(
      element: HTMLElement,
      options: Partial<ApplicationOptions> & {
        id?: string; // default: "pixi-vn-canvas"
        navigate?: (path: string) => void | Promise<void>;
        resizeMode?: "contain" | "none"; // default: "contain"
      },
      devtoolsOptions?: Devtools,
    ): Promise<void>;
    • options
      为
      Partial<ApplicationOptions>
      ——即 PixiJS
      Application
      接受的所有选项(
      width
      、
      height
      、
      backgroundColor
      、
      antialias
      等),另外还有三个 Pixi'VN 专属扩展项:
      • id
        :生成的 canvas 元素的 id。
      • navigate
        :应用的路由导航函数(也可稍后通过
        Game.onNavigate
        设置)。
      • resizeMode
        :
        "contain"
        (适配父容器并保持宽高比——默认值)或
        "none"
        。
    • devtoolsOptions
      用于配置 PixiJS Devtools 集成。
  2. Headless 初始化——不带参数调用
    Game.init()
    。这只会初始化内部
    GameUnifier
    (用于 storage、narration、history 状态管理),不会创建 PixiJS
    Application
    或 canvas。如果调用三参数形式但省略
    element
    /
    options
    ,
    Game.init
    会记录警告并同样跳过 canvas 设置。

Complete example

完整示例

The official docs' inline example calls
.then()
and wires
Game.onNavigate
as a separate statement (see pixi-vn.com/start); both that style and
await
are valid, and the
navigate
option can be passed inline instead of via a separate
Game.onNavigate
call. Here is the shape matching the current signature in
src/index.ts
:
ts
// src/main.ts
import { Game } from "@drincs/pixi-vn";

const body = document.body;
if (!body) {
  throw new Error("body element not found");
}

await Game.init(body, {
  width: 1920,
  height: 1080,
  backgroundColor: "#303030",
  resizeMode: "contain",
  navigate: (path) => {
    // e.g. window.history.pushState({}, "", path)
  },
});

Game.onEnd(async (props) => {
  Game.clear();
  props.navigate("/end");
});

Game.addOnError((error, props) => {
  console.error("Error occurred", error);
});

await Game.start("start", {});
Game.start
internally calls
Game.clear()
first, so there is no need to call
Game.clear()
before it yourself.
props.navigate
above is not a built-in Pixi'VN prop — it exists only because the project augmented
StepLabelProps
via declaration merging (typically in a
pixi-vn.d.ts
file, as every official template does). The same
StepLabelProps
type is used for the props passed to
Game.onEnd
,
Game.addOnError
, and every narration step, so whatever you add there (a
navigate
function, or any other custom prop) becomes available in all of them:
ts
declare module "@drincs/pixi-vn" {
  interface StepLabelProps {
    navigate: (route: string) => void;
  }
}
See also: pixi-vn.com/start/interface-navigate for the full navigation pattern (including the
# navigate
ink hashtag command and blocking the browser back/forward buttons). For the full set of fields the official React template actually adds this way (router
navigate
, i18n,
toast
, interface invalidation), see "Real-world project layout" below.
官方文档中的内联示例使用
.then()
,并将
Game.onNavigate
作为一条独立语句进行连接(见 pixi-vn.com/start);这种写法和
await
都有效,也可以在调用
Game.init
时内联传入
navigate
选项,以代替单独调用
Game.onNavigate
。下面是与当前
src/index.ts
签名一致的形式:
ts
// src/main.ts
import { Game } from "@drincs/pixi-vn";

const body = document.body;
if (!body) {
  throw new Error("body element not found");
}

await Game.init(body, {
  width: 1920,
  height: 1080,
  backgroundColor: "#303030",
  resizeMode: "contain",
  navigate: (path) => {
    // e.g. window.history.pushState({}, "", path)
  },
});

Game.onEnd(async (props) => {
  Game.clear();
  props.navigate("/end");
});

Game.addOnError((error, props) => {
  console.error("Error occurred", error);
});

await Game.start("start", {});
Game.start
会在内部先调用
Game.clear()
,因此无需在调用它之前自行调用
Game.clear()
。
上面的
props.navigate
并不是 Pixi'VN 内置 prop——它之所以存在,是因为项目通过声明合并(declaration merging)扩展了
StepLabelProps
(通常放在
pixi-vn.d.ts
文件中,所有官方模板都采用这种方式)。传给
Game.onEnd
、
Game.addOnError
以及每个 narration step 的 props 也使用同一个
StepLabelProps
类型,因此无论在其中添加什么(
navigate
函数或任何其他自定义 prop),都会在上述所有位置可用:
ts
declare module "@drincs/pixi-vn" {
  interface StepLabelProps {
    navigate: (route: string) => void;
  }
}
另请参阅 pixi-vn.com/start/interface-navigate,了解完整的导航模式(包括
# navigate
ink hashtag 命令,以及阻止浏览器前进/后退按钮)。若要了解官方 React 模板实际以这种方式添加的完整字段集合(router
navigate
、i18n、
toast
、interface invalidation),请参阅下方的“实际项目结构”。

The
Game.*
functions

Game.*
函数

  • Game.init(element?, options?, devtoolsOptions?)
    — must be called first; see above. Returns a
    Promise<void>
    .
  • Game.start<T>(label, props)
    — clears all game data (
    Game.clear()
    ) and begins narration from the given label (a
    LabelIdType
    string, or a
    LabelAbstract
    instance). Throws if the label isn't found.
    ts
    await Game.start("start", {});
  • Game.clear()
    — resets everything: storage, canvas (if initialized), sound, narration, and history. Used to reset the game (e.g. returning to the main menu).
  • Game.onEnd(value: StepLabelType)
    — registers the function run when all
    steps
    of all
    labels
    have been executed (no more steps/labels to continue into). Set it once during initialization. The docs note three common ending strategies: the game simply ends here; the game has no real end and this handler restarts/loops back into a label; or the game ends earlier at a specific point (e.g. game over) without ever relying on this handler.
    ts
    // end the game and navigate to an end screen
    Game.onEnd(async (props) => {
      props.navigate("/end");
    });
    ts
    // no real end: loop back into the start label instead
    Game.onEnd(async (props) => {
      narration.call(startLabel, props);
    });
  • Game.addOnError(handler: OnErrorHandler)
    — registers an error handler; multiple handlers can be added and run in registration order (sync or async). Returns a function that removes the handler. Check
    error instanceof PixiError
    (from
    @drincs/pixi-vn/core
    ) to special-case Pixi'VN's own errors (e.g. missing assets). A companion helper,
    drawCanvasErrorHandler()
    (from
    @drincs/pixi-vn/canvas
    ), returns a ready-made handler that draws a red placeholder box on the canvas when a
    PixiError
    carries canvas element info — useful during development to visualize broken canvas elements instead of silently failing.
    ts
    Game.addOnError((error, props) => {
      if (error instanceof PixiError) {
        // Pixi'VN-specific error (has error.code, maybe error.canvasElementInfo)
      }
      // any extra field on `props` (e.g. a `toast` helper for showing an error
      // notification) is only available if you added it to `StepLabelProps`
      // yourself — see the `navigate` example above, and the fuller
      // `toast`/`invalidateInterfaceData` example later in this skill.
    });
    Game.removeOnError(handler)
    unregisters a previously added handler.
  • Game.onNavigate(value: (path: string) => void | Promise<void>)
    — registers (or replaces) the app's routing function, used whenever Pixi'VN needs to navigate (e.g. on
    Game.onEnd
    , or restoring a save). Equivalent to passing
    navigate
    inline to
    Game.init
    .
    ts
    Game.onNavigate((path) => router.push(path));
  • Game.exportGameState()
    /
    Game.restoreGameState(data)
    /
    Game.jsonToGameState(json)
    — the save/load trio. Full detail, including the official template's save-slot/quick-save/IndexedDB convention, is covered by
    pixi-vn-saves
    .
  • Narration lifecycle hooks —
    Game.onStepStart
    ,
    Game.onStepEnd
    ,
    Game.onLoadingLabel
    (fires on step 0 and on save-load — the hook a label uses to load/background-load its own asset bundle, see
    pixi-vn-assets
    ),
    Game.onLabelStarting
    , and
    Game.onLabelClosing
    let you intercept/defer label transitions (e.g. to run a UI transition before the next label actually starts). These are advanced hooks — most projects only need
    onEnd
    .
  • Game.addOnPreContinue(handler)
    /
    Game.removeOnPreContinue(handler)
    — register handlers that run immediately before every narration "continue" step (e.g. auto-save on each step).
  • Game.init(element?, options?, devtoolsOptions?)
    ——必须最先调用;见上文。返回
    Promise<void>
    。
  • Game.start<T>(label, props)
    ——清除所有游戏数据(
    Game.clear()
    ),并从给定 label(一个
    LabelIdType
    字符串,或一个
    LabelAbstract
    实例)开始 narration。如果找不到该 label,则会抛出异常。
    ts
    await Game.start("start", {});
  • Game.clear()
    ——重置所有内容:storage、canvas(如果已初始化)、sound、narration 和 history。用于重置游戏(例如返回主菜单)。
  • Game.onEnd(value: StepLabelType)
    ——注册当所有
    labels
    的所有
    steps
    执行完毕时运行的函数(没有更多 steps/labels 可继续执行)。应在初始化期间设置一次。文档列出了三种常见的结束策略:游戏就在此处结束;游戏没有真正的结局,此处理器重新启动/循环回到某个 label;或者游戏在更早的特定位置结束(例如 game over),完全不依赖此处理器。
    ts
    // end the game and navigate to an end screen
    Game.onEnd(async (props) => {
      props.navigate("/end");
    });
    ts
    // no real end: loop back into the start label instead
    Game.onEnd(async (props) => {
      narration.call(startLabel, props);
    });
  • Game.addOnError(handler: OnErrorHandler)
    ——注册错误处理器;可以添加多个处理器,并按注册顺序运行(同步或异步)。返回一个用于移除该处理器的函数。可通过检查
    error instanceof PixiError
    (来自
    @drincs/pixi-vn/core
    )来单独处理 Pixi'VN 自身的错误(例如缺少 assets)。配套辅助函数
    drawCanvasErrorHandler()
    (来自
    @drincs/pixi-vn/canvas
    )返回一个现成的 handler:当
    PixiError
    带有 canvas element info 时,它会在 canvas 上绘制一个红色占位框——这在开发期间很有用,可以将损坏的 canvas 元素可视化,而不是静默失败。
    ts
    Game.addOnError((error, props) => {
      if (error instanceof PixiError) {
        // Pixi'VN-specific error (has error.code, maybe error.canvasElementInfo)
      }
      // any extra field on `props` (e.g. a `toast` helper for showing an error
      // notification) is only available if you added it to `StepLabelProps`
      // yourself — see the `navigate` example above, and the fuller
      // `toast`/`invalidateInterfaceData` example later in this skill.
    });
    Game.removeOnError(handler)
    用于注销之前添加的 handler。
  • Game.onNavigate(value: (path: string) => void | Promise<void>)
    ——注册(或替换)应用的路由函数,Pixi'VN 需要进行导航时会调用它(例如在
    Game.onEnd
    时或恢复存档时)。这等同于在
    Game.init
    中内联传入
    navigate
    。
    ts
    Game.onNavigate((path) => router.push(path));
  • Game.exportGameState()
    /
    Game.restoreGameState(data)
    /
    Game.jsonToGameState(json)
    ——存档/读档三件套。完整细节,包括官方模板的存档槽/快速存档/IndexedDB 约定,均由
    pixi-vn-saves
    涵盖。
  • Narration 生命周期 hooks——
    Game.onStepStart
    、
    Game.onStepEnd
    、
    Game.onLoadingLabel
    (在 step 0 和存档/读档时触发;label 可通过此 hook 加载/后台加载自己的 asset bundle,参见
    pixi-vn-assets
    )、
    Game.onLabelStarting
    和
    Game.onLabelClosing
    允许拦截或延迟 label 转换(例如在下一个 label 实际开始前运行 UI 转场)。这些属于高级 hook——大多数项目只需要
    onEnd
    。
  • Game.addOnPreContinue(handler)
    /
    Game.removeOnPreContinue(handler)
    ——注册处理器,在每个 narration “continue” step 之前立即运行(例如在每一步执行 autosave)。

Project structure

项目结构

A typical Pixi'VN project (as generated by
npm create pixi-vn@latest
, or as set up manually):
index.html          # loads /src/main.ts as an ES module
src/main.ts          # calls Game.init(), Game.onEnd(), Game.addOnError(), Game.onNavigate(), Game.start()
styles.css           # full-viewport, no scroll/margin, dark background behind the canvas
The canonical minimal
index.html
mounts a root
<div id="root">
that becomes (or contains) the element passed to
Game.init
, and loads the entry point as an ES module:
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Game</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
Paired with a
styles.css
that makes the canvas fill the viewport with no scrollbars or margin:
css
html,
body {
  background-color: #242424;
  height: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  overflow: hidden;
}
Real templates generated by
npm create pixi-vn@latest
are richer than this minimal sketch: narrative content lives under
ink/
(
.ink
scripts) and
src/content/
(
labels/*.label.ts
,
characters.ts
), UI screens under
src/components/
/
src/routes/
(TanStack Router), and
src/pixi-vn.d.ts
is exactly where the
StepLabelProps
augmentation shown above lives. Full breakdown: pixi-vn.com/start/templates#project-structure.
Note also that Pixi'VN deliberately has no built-in UI components — buttons, menus, and HUD are built with a regular JS framework (React/Vue/PixiJS) layered over the canvas via
canvas.htmlLayers.add
/
canvas.layers.add
. Canvas state is included in saves; UI state is not, so persist any UI-only state yourself. See pixi-vn.com/start/interface.
A few constants worth knowing about (exported from
@drincs/pixi-vn
, defined in
src/constants.ts
):
PIXIVN_VERSION
(the installed library version, embedded in
GameState.pixivn_version
for save-file compatibility checks) and
CANVAS_APP_GAME_LAYER_ALIAS
(the alias of the root canvas layer).
SYSTEM_RESERVED_STORAGE_KEYS
lists storage keys Pixi'VN reserves internally (dialogue, choices, input, characters, etc.) — avoid reusing these names for custom storage variables.
典型的 Pixi'VN 项目(由
npm create pixi-vn@latest
生成,或手动搭建):
index.html          # loads /src/main.ts as an ES module
src/main.ts          # calls Game.init(), Game.onEnd(), Game.addOnError(), Game.onNavigate(), Game.start()
styles.css           # full-viewport, no scroll/margin, dark background behind the canvas
规范的最小
index.html
会挂载根
<div id="root">
,该元素将成为(或包含)传给
Game.init
的元素,并将入口点作为 ES module 加载:
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Game</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
再配上一份让 canvas 填满视口且不出现滚动条或边距的
styles.css
:
css
html,
body {
  background-color: #242424;
  height: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  overflow: hidden;
}
由
npm create pixi-vn@latest
生成的实际模板比上述最小草图丰富:叙事内容位于
ink/
(
.ink
脚本)和
src/content/
(
labels/*.label.ts
、
characters.ts
)下,UI 屏幕位于
src/components/
/
src/routes/
(TanStack Router),而
src/pixi-vn.d.ts
正是上文
StepLabelProps
扩展所在的文件。完整说明请参阅 pixi-vn.com/start/templates#project-structure。
还请注意,Pixi'VN 有意不提供内置 UI 组件——按钮、菜单和 HUD 使用常规 JS 框架(React/Vue/PixiJS)构建,并通过
canvas.htmlLayers.add
/
canvas.layers.add
叠加在 canvas 上。Canvas 状态会纳入存档;UI 状态不会,因此任何仅属于 UI 的状态都需要自行持久化。参见 pixi-vn.com/start/interface。
还有一些值得了解的常量(从
@drincs/pixi-vn
导出,定义于
src/constants.ts
):
PIXIVN_VERSION
(已安装的库版本,会嵌入
GameState.pixivn_version
,用于检查存档文件兼容性)以及
CANVAS_APP_GAME_LAYER_ALIAS
(根 canvas layer 的别名)。
SYSTEM_RESERVED_STORAGE_KEYS
列出了 Pixi'VN 内部保留的 storage key(dialogue、choices、input、characters 等)——不要将这些名称用于自定义 storage 变量。

The Vite plugin:
@drincs/pixi-vn/vite

Vite 插件:
@drincs/pixi-vn/vite

Every official template wires up
vitePluginPixivn
in
vite.config.ts
— for any Vite-based project, this plugin is the backbone that makes content (characters/labels) and asset manifests available consistently in dev and build, and it is what generates the string-literal ID types (
CharacterIdType
,
LabelIdType
, ...) used everywhere else in this skill set.
Reach for it whenever a developer's
narration.call("someLabel")
isn't getting autocomplete/type-checking on the label id, or asks how characters/labels "just work" without a manual import list.
ts
// vite.config.ts
import { vitePluginPixivn } from "@drincs/pixi-vn/vite";

export default defineConfig({
  plugins: [
    vitePluginPixivn({
      content: "./src/content/index.ts",
      characters: "./src/content/characters.ts",
      labels: "./src/content/labels/*.label.ts",
      typeFilePath: "./src/pixi-vn.keys.gen.ts",
      assetsManifest: async (ssrLoadModule) => {
        const mod = await ssrLoadModule("./src/assets/index.ts");
        return mod.manifest;
      },
    }),
  ],
});
What it actually does:
  • Loads content before anything else needs it. The
    content
    /
    characters
    /
    labels
    glob options are executed server-side (via Vite SSR, in dev and during
    vite build
    ) at startup, so
    RegisteredCharacters
    /
    RegisteredLabels
    are fully populated before any other plugin (notably an ink compiler) or the app itself runs. Without this, whether your content has actually registered by the time something reads it would depend on unpredictable import ordering.
  • Generates type-safe ID unions. When
    typeFilePath
    is set, the plugin (re)writes that file on every startup and every content hot-reload with
    declare module
    augmentations that narrow
    CharacterIdType
    ,
    LabelIdType
    ,
    BundleIdType
    , and
    AssetAliasIdType
    from plain
    string
    to a union of the actual known literals — so
    narration.call("typo_label")
    becomes a compile error instead of a runtime one. The same file also exports plain
    as const
    arrays/enums (
    characterIds
    ,
    labelIds
    ,
    bundleIds
    ,
    assetAliasIds
    ) for runtime validation (e.g.
    z.enum(characterIdsEnum)
    ). This generated file is excluded from HMR, so regenerating it never triggers a full page reload.
  • Bridges the PixiJS assets manifest. The
    assetsManifest
    option (a value or an async function, typically resolving to the
    manifest
    built in
    src/assets/index.ts
    — see
    pixi-vn-assets
    for how that file is authored) feeds bundle/asset-alias ids into the same generated file, and seeds a dev-server endpoint (
    GET /__pixi-vn/assets/manifest
    ) so devtools/other tooling can read the current manifest without the client having to push it first.
  • Hot-reloads content correctly. Editing a watched label/character file clears the relevant registry and reloads just those files, regenerating the keys file — without a full page reload.
Don't hand-configure the underlying
GET/POST /__pixi-vn/*
dev-server endpoints or
api.setExternalLabels
/
api.setAssetsManifest
unless building a plugin that integrates with
vitePluginPixivn
(e.g. an ink compiler) — for an app, the four options above (
content
,
characters
,
labels
,
typeFilePath
,
assetsManifest
) are the whole interface that matters.
A related, smaller export,
@drincs/pixi-vn/vite-listener
, provides
setupPixivnViteData()
— a client-side, dev-only function that POSTs the runtime assets manifest and current canvas size back to the same dev-server endpoints. Call it once after content is loaded (
await setupPixivnViteData()
) only as a fallback for cases where the manifest truly isn't knowable at
vite.config.ts
time; the official templates prefer the
assetsManifest
function option above and don't need it.
每个官方模板都会在
vite.config.ts
中配置
vitePluginPixivn
——对于任何基于 Vite 的项目,该插件都是核心骨干,可让 content(characters/labels)与 assets manifest 在开发和构建期间始终一致地可用;它还负责生成本 skill 集合其他各处使用的字符串字面量 ID 类型(
CharacterIdType
、
LabelIdType
等)。当开发者的
narration.call("someLabel")
无法为 label id 提供自动补全/类型检查,或询问 characters/labels 如何在无需手动维护导入列表的情况下“正常工作”时,应使用该插件。
ts
// vite.config.ts
import { vitePluginPixivn } from "@drincs/pixi-vn/vite";

export default defineConfig({
  plugins: [
    vitePluginPixivn({
      content: "./src/content/index.ts",
      characters: "./src/content/characters.ts",
      labels: "./src/content/labels/*.label.ts",
      typeFilePath: "./src/pixi-vn.keys.gen.ts",
      assetsManifest: async (ssrLoadModule) => {
        const mod = await ssrLoadModule("./src/assets/index.ts");
        return mod.manifest;
      },
    }),
  ],
});
它实际执行的工作如下:
  • 在任何其他内容需要之前加载 content。
    content
    /
    characters
    /
    labels
    glob 选项会在启动时通过 Vite SSR 在服务端执行(开发和
    vite build
    期间皆然),因此在任何其他插件(尤其是 ink compiler)或应用本身运行之前,
    RegisteredCharacters
    /
    RegisteredLabels
    就已完全填充。否则,内容在读取时是否已注册将取决于不可预测的 import 顺序。
  • 生成类型安全的 ID 联合类型。 设置
    typeFilePath
    后,插件会在每次启动和每次 content 热重载时通过
    declare module
    扩展重写该文件,将
    CharacterIdType
    、
    LabelIdType
    、
    BundleIdType
    和
    AssetAliasIdType
    从普通
    string
    收窄为已知字面量的联合类型——因此
    narration.call("typo_label")
    会在编译期而非运行时报错。同一文件还导出普通的
    as const
    数组/枚举(
    characterIds
    、
    labelIds
    、
    bundleIds
    、
    assetAliasIds
    ),用于运行时验证(例如
    z.enum(characterIdsEnum)
    )。这个生成的文件不参与 HMR,因此重新生成它不会触发整页刷新。
  • 衔接 PixiJS assets manifest。
    assetsManifest
    选项(一个值或 async 函数,通常解析为
    src/assets/index.ts
    中构建的
    manifest
    ——参见
    pixi-vn-assets
    了解该文件的编写方式)会将 bundle/asset-alias id 注入同一个生成文件,并预置一个 dev-server endpoint(
    GET /__pixi-vn/assets/manifest
    ),使 devtools 或其他工具无需等待客户端主动推送即可读取当前 manifest。
  • 正确热重载 content。 编辑受监视的 label/character 文件时,会清除相应 registry,只重新加载这些文件并重新生成 keys file——无需整页刷新。
不要手动配置底层的
GET/POST /__pixi-vn/*
dev-server endpoint 或
api.setExternalLabels
/
api.setAssetsManifest
,除非正在构建一个与
vitePluginPixivn
集成的插件
(例如 ink compiler)——对于应用而言,上述四个选项(
content
、
characters
、
labels
、
typeFilePath
、
assetsManifest
)才是需要关注的完整接口。
另一个相关但更小的导出
@drincs/pixi-vn/vite-listener
提供了
setupPixivnViteData()
——一个仅限客户端、开发环境使用的函数,将运行时 assets manifest 和当前 canvas 尺寸通过 POST 发送回同一组 dev-server endpoint。仅当 manifest 确实无法在
vite.config.ts
运行时确定时,才在 content 加载完成后调用一次(
await setupPixivnViteData()
)作为兜底;官方模板更推荐上述
assetsManifest
函数选项,不需要此方法。

Real-world project layout (official React template)

实际项目结构(官方 React 模板)

Everything below reflects the official
pixi-vn-react-template
's convention
(what
npm create pixi-vn@latest
generates for the "TS narration + React" option) — it is one working way to organize a project, not a rule the library enforces. Other official/community templates (ink narration, non-React UI, headless) are free to lay things out differently; only
Game.init
running first is actually required.
以下所有内容都反映官方
pixi-vn-react-template
的约定
(即
npm create pixi-vn@latest
为“TS narration + React”选项生成的内容)——它是一种可行的项目组织方式,并非库强制规定的规则。其他官方/社区模板(ink narration、非 React UI、headless)可以自由采用不同布局;实际唯一的要求是
Game.init
最先运行。

Named constants instead of hardcoded strings

使用命名常量而不是硬编码字符串

The template defines layer/channel ids as constants in
src/constants.ts
instead of inlining string literals at each call site:
ts
// src/constants.ts
export const CANVAS_UI_LAYER_NAME = "ui";
export const HTML_UI_LAYER_NAME = "ui";
export const HTML_CANVAS_LAYER_NAME = "canvas";
export const BGM_CHANNEL_NAME = "bgm";
export const SFX_CHANNEL_NAME = "sfx";
This keeps the
canvas.layers.add(...)
,
canvas.htmlLayers.add(...)
, and
sound.channels.add(...)
calls below typo-proof and greppable.
模板在
src/constants.ts
中将 layer/channel id 定义为常量,而不是在每个调用点直接写入字符串字面量:
ts
// src/constants.ts
export const CANVAS_UI_LAYER_NAME = "ui";
export const HTML_UI_LAYER_NAME = "ui";
export const HTML_CANVAS_LAYER_NAME = "canvas";
export const BGM_CHANNEL_NAME = "bgm";
export const SFX_CHANNEL_NAME = "sfx";
这样可以防止下方
canvas.layers.add(...)
、
canvas.htmlLayers.add(...)
和
sound.channels.add(...)
调用出现拼写错误,也更便于 grep。

Full
main.tsx
wiring

完整的
main.tsx
连接

tsx
Game.init(body, {
  id: HTML_CANVAS_LAYER_NAME,
  height: 1080,
  width: 1920,
  backgroundColor: "#303030",
  resizeMode: "contain",
}).then(() => {
  // A PixiJS-rendered layer for in-canvas UI elements
  canvas.layers.add(CANVAS_UI_LAYER_NAME, new Container());

  // One background music channel, one default channel for one-off sfx
  sound.channels.add(BGM_CHANNEL_NAME, { background: true });
  sound.channels.add(SFX_CHANNEL_NAME);
  sound.defaultChannelAlias = SFX_CHANNEL_NAME;

  // Mount a UI framework (React here) INSIDE the PixiJS canvas tree
  const root = document.getElementById("root")!;
  const htmlLayout = canvas.htmlLayers.add(HTML_UI_LAYER_NAME, root);
  if (!htmlLayout) throw new Error("htmlLayout not found");
  createRoot(htmlLayout).render(<App />);
});

Game.onEnd(async ({ navigate }) => {
  Game.clear();
  navigate({ to: "/" });
});

Game.addOnError(drawCanvasErrorHandler());
Game.addOnError((error, { toast, uiTransition }) => {
  toast?.error(uiTransition?.("allert_error_occurred"));
  console.error("Error occurred", error);
});

Game.onLoadingLabel((_stepId, { id }) => Assets.backgroundLoadBundle(id));
Key points:
  • canvas.layers.add
    adds a PixiJS container drawn directly in the canvas render tree;
    canvas.htmlLayers.add
    instead hands back a DOM node kept in sync with a canvas-tracked layer, and is where any HTML/React/Vue UI gets mounted. This is the concrete mechanism behind "Pixi'VN has no built-in UI, bring your own framework" (see
    pixi-vn-ui
    ).
  • Multiple error handlers can be stacked: the library's own
    drawCanvasErrorHandler()
    (visualizes broken canvas elements) runs alongside a project-specific handler that shows a toast and logs to console.
  • Game.onLoadingLabel
    fires as a label is about to run; the template uses it to kick off
    Assets.backgroundLoadBundle(id)
    , pre-warming an asset bundle named after the label id so images referenced by that label are already cached when needed.
tsx
Game.init(body, {
  id: HTML_CANVAS_LAYER_NAME,
  height: 1080,
  width: 1920,
  backgroundColor: "#303030",
  resizeMode: "contain",
}).then(() => {
  // A PixiJS-rendered layer for in-canvas UI elements
  canvas.layers.add(CANVAS_UI_LAYER_NAME, new Container());

  // One background music channel, one default channel for one-off sfx
  sound.channels.add(BGM_CHANNEL_NAME, { background: true });
  sound.channels.add(SFX_CHANNEL_NAME);
  sound.defaultChannelAlias = SFX_CHANNEL_NAME;

  // Mount a UI framework (React here) INSIDE the PixiJS canvas tree
  const root = document.getElementById("root")!;
  const htmlLayout = canvas.htmlLayers.add(HTML_UI_LAYER_NAME, root);
  if (!htmlLayout) throw new Error("htmlLayout not found");
  createRoot(htmlLayout).render(<App />);
});

Game.onEnd(async ({ navigate }) => {
  Game.clear();
  navigate({ to: "/" });
});

Game.addOnError(drawCanvasErrorHandler());
Game.addOnError((error, { toast, uiTransition }) => {
  toast?.error(uiTransition?.("allert_error_occurred"));
  console.error("Error occurred", error);
});

Game.onLoadingLabel((_stepId, { id }) => Assets.backgroundLoadBundle(id));
要点:
  • canvas.layers.add
    会添加一个直接绘制在 canvas 渲染树中的 PixiJS container;相比之下,
    canvas.htmlLayers.add
    会返回一个 DOM 节点,该节点会与由 canvas 跟踪的 layer 保持同步,HTML/React/Vue UI 就挂载在这里。这正是“Pixi'VN 没有内置 UI,请自带框架”的具体实现机制(参见
    pixi-vn-ui
    )。
  • 可以叠加多个 error handler:库自带的
    drawCanvasErrorHandler()
    (用于可视化损坏的 canvas 元素)会与项目自定义 handler 并行运行;后者显示 toast 并记录到 console。
  • Game.onLoadingLabel
    会在 label 即将运行前触发;模板利用它启动
    Assets.backgroundLoadBundle(id)
    ,预热以 label id 命名的 asset bundle,使该 label 引用的图片在需要时已经完成缓存。

Auto-importing narrative content:
content/index.ts

自动导入叙事内容:
content/index.ts

Instead of hand-maintaining an import list for every label/character file (and remembering to update it each time one is added), the template eagerly imports everything under
content/
purely for side effects, using Vite's
import.meta.glob
:
ts
// src/content/index.ts
void import.meta.glob(["./**/*.ts", "./**/*.tsx", "!./index.ts"], {
  eager: true,
});
main.tsx
/
App.tsx
then does a single
import "@/content"
. This works because defining a
Character
or calling
newLabel(...)
registers it as a side effect of the module executing — the glob guarantees every file under
content/
runs at startup with no per-file import to remember. (Bundlers other than Vite need their own equivalent, e.g. webpack's
require.context
.)
模板使用 Vite 的
import.meta.glob
,仅为副作用而提前导入
content/
下的所有内容,而不是手动维护每个 label/character 文件的导入列表(也不必在每次新增文件后记得更新列表):
ts
// src/content/index.ts
void import.meta.glob(["./**/*.ts", "./**/*.tsx", "!./index.ts"], {
  eager: true,
});
随后
main.tsx
/
App.tsx
只需执行一次
import "@/content"
。之所以可行,是因为定义
Character
或调用
newLabel(...)
都会在模块执行时注册自身——glob 保证
content/
下的每个文件都会在启动时运行,无需逐个导入。(webpack 等其他 bundler 需要自己的等效方案,例如
require.context
。)

Augmenting
StepLabelProps
for custom per-step context

扩展
StepLabelProps
以提供自定义的逐步上下文

src/pixi-vn.d.ts
is where the template applies the declaration-merging pattern shown earlier in this skill (the
navigate
example) to add its actual project-specific fields to
StepLabelProps
:
ts
// src/pixi-vn.d.ts
declare module "@drincs/pixi-vn" {
  interface StepLabelProps {
    navigate: UseNavigateResult<string>; // TanStack Router
    t: TFunction<[string], undefined>; // i18next: narration text
    uiTransition: TFunction<[string], undefined>; // i18next: UI chrome text
    toast: typeof toast; // sonner's toast function
    invalidateInterfaceData: (delay?: number) => Promise<void> | void;
  }
}
Whatever is added here becomes available on the
props
argument of every narration step,
Game.onEnd
, and
Game.addOnError
alike — a single place to inject router/i18n/toast/anything-else a project's narration layer needs.
t
vs
uiTransition
splits narration-text translations from UI-chrome translations;
invalidateInterfaceData
is a template-specific hook that forces UI data (e.g. a React Query cache) to refetch after a step changes something the interface depends on.
src/pixi-vn.d.ts
是模板应用前文所示 declaration-merging 模式的位置,用于向
StepLabelProps
添加项目实际使用的字段:
ts
// src/pixi-vn.d.ts
declare module "@drincs/pixi-vn" {
  interface StepLabelProps {
    navigate: UseNavigateResult<string>; // TanStack Router
    t: TFunction<[string], undefined>; // i18next: narration text
    uiTransition: TFunction<[string], undefined>; // i18next: UI chrome text
    toast: typeof toast; // sonner's toast function
    invalidateInterfaceData: (delay?: number) => Promise<void> | void;
  }
}
在这里添加的任何内容都会出现在每个 narration step、
Game.onEnd
和
Game.addOnError
的
props
参数中——只需一个位置,即可注入项目 narration 层所需的 router/i18n/toast 或其他内容。
t
与
uiTransition
用于区分 narration 文本翻译和 UI chrome 翻译;
invalidateInterfaceData
是模板专属 hook,用于在某一步更改了界面所依赖的数据后,强制 UI 数据(例如 React Query cache)重新获取。

Optional external plugins

可选的外部插件

Install only the integrations the game uses; these are separate npm packages, not exports of
@drincs/pixi-vn
:
  • @drincs/pixi-vn-ai
    : runtime dialogue/image generation. Await its
    ai.init(...)
    during startup; see narration's AI notes.
  • @drincs/pixi-vn-live2d
    : Live2D models. Register
    Live2DPlugin
    before initializing the canvas; see canvas plugin setup.
  • @drincs/pixi-vn-spine
    : Spine skeletal animation. Import the package in the app entry point so saved components can be restored even before lazy scenes load; see canvas plugin setup.
Check each plugin's installed peer dependencies against the project's engine/PixiJS version.
仅安装游戏实际使用的集成;它们是独立的 npm 包,而不是
@drincs/pixi-vn
的导出:
  • @drincs/pixi-vn-ai
    :运行时 dialogue/image 生成。在启动期间
    await
    其
    ai.init(...)
    ;参见 narration 的 AI 说明。
  • @drincs/pixi-vn-live2d
    :Live2D models。在初始化 canvas 前注册
    Live2DPlugin
    ;参见 canvas 插件设置。
  • @drincs/pixi-vn-spine
    :Spine skeletal animation。在应用入口点导入该包,以便即使在 lazy scenes 加载之前也能恢复已保存的 components;参见 canvas 插件设置。
检查每个插件所安装的 peer dependencies 是否与项目的 engine/PixiJS 版本匹配。

Related skills

相关 skills

  • pixi-vn-assets — registering local/online assets, the manifest/bundle/alias system, and when to load them.
  • pixi-vn-canvas — adding/animating images, sprites, text, video and containers on the PixiJS canvas after
    Game.init
    .
  • pixi-vn-characters — defining and registering
    Character
    instances used in dialogue.
  • pixi-vn-history — the step history system (
    stepHistory
    ), going back/forward through played steps, and save-checkpoint behavior.
  • pixi-vn-narration — labels, steps, dialogue, choices, and the
    narration
    object driving the story.
  • pixi-vn-saves —
    Game.exportGameState
    /
    restoreGameState
    and the official save-slot/quick-save convention.
  • pixi-vn-sound — playing music and sound effects, channels, and volume control.
  • pixi-vn-storage — reading/writing persistent and temporary game variables and flags.
  • pixi-vn-ui — building/mounting UI layers (HTML or PixiJS) on top of the canvas, screen navigation, theming, and connecting UI to storage.
  • pixi-vn-migration — upgrading an existing project to the current version.
  • pixi-vn-assets——注册本地/在线 assets、manifest/bundle/alias system,以及加载 assets 的时机。
  • pixi-vn-canvas——在
    Game.init
    之后向 PixiJS canvas 添加/动画化 images、sprites、text、video 和 containers。
  • pixi-vn-characters——定义并注册 dialogue 中使用的
    Character
    instances。
  • pixi-vn-history——step history system(
    stepHistory
    ),在已播放的 steps 之间前进/后退,以及 save-checkpoint 行为。
  • pixi-vn-narration——labels、steps、dialogue、choices,以及驱动故事的
    narration
    object。
  • pixi-vn-saves——
    Game.exportGameState
    /
    restoreGameState
    和官方 save-slot/quick-save 约定。
  • pixi-vn-sound——播放音乐和音效、管理 channels 及控制音量。
  • pixi-vn-storage——读取/写入持久和临时 game variables 与 flags。
  • pixi-vn-ui——在 canvas 上构建/挂载 UI layers(HTML 或 PixiJS)、屏幕导航、主题,以及将 UI 连接到 storage。
  • pixi-vn-migration——将现有项目升级到当前版本。