` that becomes (or contains) the element passed to `Game.init`, and loads the entry point as an ES module:
```html title="index.html"
Game
```
Paired with a `styles.css` that makes the canvas fill the viewport with no scrollbars or margin:
```css title="styles.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](https://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](https://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.
## The Vite plugin: `@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.
## Real-world project layout (official React template)
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.
### 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.
### Full `main.tsx` wiring
```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(
);
});
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.
### Auto-importing narrative content: `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`.)
### Augmenting `StepLabelProps` for custom per-step context
`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
; // 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;
}
}
```
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.
## 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](../narration/SKILL.md#optional-runtime-ai-drincspixi-vn-ai).
- `@drincs/pixi-vn-live2d`: Live2D models. Register `Live2DPlugin` before initializing the
canvas; see [canvas plugin setup](../canvas/SKILL.md#external-rendering-plugins).
- `@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](../canvas/SKILL.md#external-rendering-plugins).
Check each plugin's installed peer dependencies against the project's engine/PixiJS version.
## Related 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.