pixi-vn-characters

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pixi'VN Characters

Pixi'VN 角色

When to use this skill

何时使用此 skill

Use this skill whenever a task involves creating the characters that appear in a Pixi'VN game (name, surname, age, color, icon, and any custom fields), storing them so the engine and narration code can find them by id, or hooking a character into a
narration.dialogue
line. It does not cover how dialogue/text itself is displayed (see the narration skill) or how variables are generally saved (see the storage skill).
ts
import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn";
(Also available from the narrower
@drincs/pixi-vn/characters
subpath — see
pixi-vn-getting-started
for when to prefer that.)
只要任务涉及创建 Pixi'VN 游戏中出现的角色(姓名、姓氏、年龄、颜色、图标和任何自定义字段)、将角色存储起来以便引擎和旁白代码可通过 id 找到它们,或将角色接入
narration.dialogue
行,就应使用此 skill。它不涉及对话/文本本身的显示方式(参见 narration skill),也不涉及变量通常如何保存(参见 storage skill)。
ts
import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn";
(也可从范围更窄的
@drincs/pixi-vn/characters
子路径导入——何时优先使用该路径,请参阅
pixi-vn-getting-started
。)

1. Defining a character

1. 定义角色

CharacterBaseModel
is the built-in character class. Construct it with a unique string
id
and a props object (
name
,
surname
,
age
,
icon
,
color
— all optional):
ts
import { CharacterBaseModel } from "@drincs/pixi-vn";

export const liam = new CharacterBaseModel("liam", {
  name: "Liam",
  surname: "Smith",
  age: 25,
  icon: "https://example.com/liam.png",
  color: "#9e2e12",
});

export const emma = new CharacterBaseModel("emma", {
  name: "Emma",
  surname: "Johnson",
  age: 23,
  icon: "https://example.com/emma.png",
  color: "#c23b7f",
});
The
id
is the value used everywhere else in the game (dialogue, lookups, history) to reference this character, so it must be unique and stable across saves.
name
/
surname
/
age
are actually get/set accessors backed by the game's storage (see
CharacterStoredClass
, which
CharacterBaseModel
extends, and the stored classes doc): the value passed in the constructor becomes the default, but setting
liam.name = "..."
at runtime persists an override in storage that survives save/load and takes priority over the default.
icon
and
color
are plain readonly fields, not stored/overridable at runtime.
ts
import { liam } from "@/content/characters";

console.log(liam.name); // "Liam"
liam.name = "Liam Smith";
console.log(liam.name); // "Liam Smith" — now persisted in game storage
If a character's
id
is ever changed between game versions, the engine does not migrate the storage data from the old id to the new one — treat ids as permanent once shipped.
CharacterBaseModel
是内置角色类。使用唯一的字符串
id
和一个 props 对象(
name
、
surname
、
age
、
icon
、
color
——全部为可选)来构造它:
ts
import { CharacterBaseModel } from "@drincs/pixi-vn";

export const liam = new CharacterBaseModel("liam", {
  name: "Liam",
  surname: "Smith",
  age: 25,
  icon: "https://example.com/liam.png",
  color: "#9e2e12",
});

export const emma = new CharacterBaseModel("emma", {
  name: "Emma",
  surname: "Johnson",
  age: 23,
  icon: "https://example.com/emma.png",
  color: "#c23b7f",
});
id
是游戏中其他所有位置(对话、查找、历史记录)用于引用该角色的值,因此它必须在各存档之间保持唯一且稳定。
name
/
surname
/
age
实际上是 get/set 访问器,其底层由游戏存储支持(参见
CharacterStoredClass
(
CharacterBaseModel
扩展的类)以及 stored classes 文档):传递给构造函数的值将成为_默认值_,但在运行时设置
liam.name = "..."
会在存储中持久保存一个覆盖值,该值在存档/加载后仍然有效,并且优先于默认值。
icon
和
color
是普通只读字段,在运行时不可存储或覆盖。
ts
import { liam } from "@/content/characters";

console.log(liam.name); // "Liam"
liam.name = "Liam Smith";
console.log(liam.name); // "Liam Smith" — now persisted in game storage
如果角色的
id
在游戏版本之间发生更改,引擎不会将存储数据从旧 id 迁移到新 id——一旦发布,就应将 id 视为永久固定。

Custom character classes with extra fields

包含额外字段的自定义角色类

If you need fields beyond
name
/
surname
/
age
/
icon
/
color
(e.g. a
sprite
id or a relationship stat), this is the recommended, docs-endorsed approach (project templates already ship it as
models/Character.ts
):
  1. Create a class
    Character extends CharacterStoredClass implements CharacterInterface
    .
  2. Augment the (otherwise empty)
    CharacterInterface
    in a
    pixi-vn.d.ts
    file via
    declare module "@drincs/pixi-vn"
    so every
    CharacterInterface
    consumer in the project — including
    RegisteredCharacters.get()
    — sees the richer, project-specific type.
  3. For any field that must persist across saves, back it with a getter/setter that calls
    this.getStorageProperty()
    /
    this.setStorageProperty()
    (inherited from
    CharacterStoredClass
    ), the same pattern
    CharacterBaseModel
    uses internally for
    name
    /
    surname
    /
    age
    .
This is exactly what the official
pixi-vn-react-template
(the "TS narration
  • React" project
    npm create pixi-vn@latest
    scaffolds) ships as
    src/models/Character.ts
    — stored getters/setters for
    name
    ,
    surname
    and
    age
    , and plain (non-stored) fields for
    icon
    /
    color
    :
ts
// models/Character.ts
import { type CharacterInterface, CharacterStoredClass } from "@drincs/pixi-vn";

export default class Character
  extends CharacterStoredClass
  implements CharacterInterface
{
  constructor(
    id: string | { id: string; emotion: string },
    props: CharacterProps,
  ) {
    super(
      typeof id === "string" ? id : id.id,
      typeof id === "string" ? "" : id.emotion,
    );
    this.defaultName = props.name;
    this.defaultSurname = props.surname;
    this.defaultAge = props.age;
    this.icon = props.icon;
    this.color = props.color;
  }

  // stored — persists across save/load, same pattern as CharacterBaseModel
  private defaultName?: string;
  get name(): string {
    return (
      this.getStorageProperty<string>("name") || this.defaultName || this.id
    );
  }
  set name(value: string | undefined) {
    this.setStorageProperty<string>("name", value);
  }

  private defaultSurname?: string;
  get surname(): string | undefined {
    return this.getStorageProperty<string>("surname") || this.defaultSurname;
  }
  set surname(value: string | undefined) {
    this.setStorageProperty<string>("surname", value);
  }

  private defaultAge?: number;
  get age(): number | undefined {
    return this.getStorageProperty<number>("age") || this.defaultAge;
  }
  set age(value: number | undefined) {
    this.setStorageProperty<number>("age", value);
  }

  // not stored
  readonly icon?: string;
  readonly color?: string;
}

interface CharacterProps {
  name?: string;
  surname?: string;
  age?: number;
  icon?: string;
  color?: string;
}
ts
// pixi-vn.d.ts
declare module "@drincs/pixi-vn" {
  interface CharacterInterface {
    name: string;
    surname?: string;
    age?: number;
    readonly icon?: string;
    readonly color?: string;
  }
}
如果需要
name
/
surname
/
age
/
icon
/
color
之外的字段(例如
sprite
id 或关系值),建议采用官方文档认可的方法(项目模板已经以
models/Character.ts
的形式提供该方法):
  1. 创建一个类
    Character extends CharacterStoredClass implements CharacterInterface
    。
  2. 在
    pixi-vn.d.ts
    文件中,通过
    declare module "@drincs/pixi-vn"
    扩充(原本为空的)
    CharacterInterface
    ,使项目中每个
    CharacterInterface
    使用方——包括
    RegisteredCharacters.get()
    ——都能看到更丰富的、项目专属类型。
  3. 对于必须在存档/加载后保留的任何字段,使用调用
    this.getStorageProperty()
    /
    this.setStorageProperty()
    的 getter/setter 作为其后端(这些方法继承自
    CharacterStoredClass
    ),与
    CharacterBaseModel
    内部为
    name
    /
    surname
    /
    age
    使用的模式相同。
官方
pixi-vn-react-template
(即
npm create pixi-vn@latest
脚手架生成的“TS narration + React”项目)正是以
src/models/Character.ts
的形式提供这一实现——
name
、
surname
和
age
使用可存储的 getter/setter,而
icon
/
color
使用普通(非存储)字段:
ts
// models/Character.ts
import { type CharacterInterface, CharacterStoredClass } from "@drincs/pixi-vn";

export default class Character
  extends CharacterStoredClass
  implements CharacterInterface
{
  constructor(
    id: string | { id: string; emotion: string },
    props: CharacterProps,
  ) {
    super(
      typeof id === "string" ? id : id.id,
      typeof id === "string" ? "" : id.emotion,
    );
    this.defaultName = props.name;
    this.defaultSurname = props.surname;
    this.defaultAge = props.age;
    this.icon = props.icon;
    this.color = props.color;
  }

  // stored — persists across save/load, same pattern as CharacterBaseModel
  private defaultName?: string;
  get name(): string {
    return (
      this.getStorageProperty<string>("name") || this.defaultName || this.id
    );
  }
  set name(value: string | undefined) {
    this.setStorageProperty<string>("name", value);
  }

  private defaultSurname?: string;
  get surname(): string | undefined {
    return this.getStorageProperty<string>("surname") || this.defaultSurname;
  }
  set surname(value: string | undefined) {
    this.setStorageProperty<string>("surname", value);
  }

  private defaultAge?: number;
  get age(): number | undefined {
    return this.getStorageProperty<number>("age") || this.defaultAge;
  }
  set age(value: number | undefined) {
    this.setStorageProperty<number>("age", value);
  }

  // not stored
  readonly icon?: string;
  readonly color?: string;
}

interface CharacterProps {
  name?: string;
  surname?: string;
  age?: number;
  icon?: string;
  color?: string;
}
ts
// pixi-vn.d.ts
declare module "@drincs/pixi-vn" {
  interface CharacterInterface {
    name: string;
    surname?: string;
    age?: number;
    readonly icon?: string;
    readonly color?: string;
  }
}

2. Registering and looking up characters

2. 注册和查找角色

Characters are looked up at runtime by id through the
RegisteredCharacters
registry (a namespace, not a decorator despite its file name). Calling
RegisteredCharacters.add(...)
is required — a
CharacterBaseModel
(or custom class) instance that is only exported/constructed but never passed to
add
will not be findable by id and will not work in dialogue lookups after a save/load. Register every character you define once, at module load time (recommended: import them all at project startup), then reference it by id elsewhere:
ts
import { RegisteredCharacters } from "@drincs/pixi-vn";

RegisteredCharacters.add(liam, emma);
// or with arrays:
RegisteredCharacters.add([liam, emma]);
Other
RegisteredCharacters
functions:
  • RegisteredCharacters.get(id)
    — returns the character (or
    undefined
    , logging a warning if it was never registered).
  • RegisteredCharacters.has(id)
    — checks whether an id is registered.
  • RegisteredCharacters.values()
    — returns all registered characters.
  • RegisteredCharacters.keys()
    — returns all registered ids.
  • RegisteredCharacters.clear()
    — removes all registered characters (mainly useful in tests).
ts
const liam = RegisteredCharacters.get("liam");
Re-adding an id that's already registered overwrites the previous character (a log message notes this), so registration order matters if two files define the same id.
运行时通过
RegisteredCharacters
注册表按 id 查找角色(尽管其文件名如此,它是一个 namespace,而不是 decorator)。调用
RegisteredCharacters.add(...)
是必需的——仅导出或构造、但从未传给
add
的
CharacterBaseModel
(或自定义类)实例无法按 id 找到,并且在存档/加载后无法用于对话查找。将每个定义的角色在模块加载时注册一次(建议在项目启动时导入所有角色),然后在其他位置通过 id 引用它:
ts
import { RegisteredCharacters } from "@drincs/pixi-vn";

RegisteredCharacters.add(liam, emma);
// or with arrays:
RegisteredCharacters.add([liam, emma]);
RegisteredCharacters
的其他函数:
  • RegisteredCharacters.get(id)
    ——返回角色(如果从未注册,则返回
    undefined
    并记录警告)。
  • RegisteredCharacters.has(id)
    ——检查某个 id 是否已注册。
  • RegisteredCharacters.values()
    ——返回所有已注册的角色。
  • RegisteredCharacters.keys()
    ——返回所有已注册的 id。
  • RegisteredCharacters.clear()
    ——移除所有已注册的角色(主要用于测试)。
ts
const liam = RegisteredCharacters.get("liam");
重新注册已存在的 id 会覆盖之前的角色(系统会记录一条日志消息),因此如果两个文件定义了相同的 id,注册顺序就会产生影响。

Real-world project convention (official React template)

实际项目约定(官方 React 模板)

The official
pixi-vn-react-template
(what
npm create pixi-vn@latest
scaffolds for the "TS narration + React" setup) defines its characters as
export const
instances in
src/content/characters.ts
, then registers all of them with a single
RegisteredCharacters.add([...])
call at module scope:
ts
// src/content/characters.ts
import Character from "@/models/Character";
import { RegisteredCharacters } from "@drincs/pixi-vn";

export const mc = new Character("mc", { name: "Me" });
export const james = new Character("james", {
  name: "James",
  color: "#0084ac",
});
export const steph = new Character("steph", {
  name: "Steph",
  color: "#ac5900",
});
export const sly = new Character("sly", { name: "Sly", color: "#6d00ac" });

RegisteredCharacters.add([mc, james, steph, sly]);
This works with no manual import list to maintain because
src/content/index.ts
uses
import.meta.glob
to import every file under
content/
for its side effects at app startup — so the module-scope
RegisteredCharacters.add(...)
call above always runs. That means you never need to remember to import each new character file yourself (dropping it anywhere under
content/
is enough), but conversely, a character defined (and
.add
ed) in a file outside
content/
that nothing else imports never gets its
add
call executed, and stays silently unregistered.
This is the official template's convention, not a hard requirement of the library —
RegisteredCharacters.add
can be called from any file, as long as it's guaranteed to run before the character is looked up. Projects without this glob-import setup must import each character file explicitly instead (e.g. from a top-level
content/index.ts
or the app's entry point).
官方
pixi-vn-react-template
(
npm create pixi-vn@latest
为“TS narration + React”设置生成的模板)将角色定义为
src/content/characters.ts
中的
export const
实例,然后在模块作用域通过单次
RegisteredCharacters.add([...])
调用注册所有角色:
ts
// src/content/characters.ts
import Character from "@/models/Character";
import { RegisteredCharacters } from "@drincs/pixi-vn";

export const mc = new Character("mc", { name: "Me" });
export const james = new Character("james", {
  name: "James",
  color: "#0084ac",
});
export const steph = new Character("steph", {
  name: "Steph",
  color: "#ac5900",
});
export const sly = new Character("sly", { name: "Sly", color: "#6d00ac" });

RegisteredCharacters.add([mc, james, steph, sly]);
这样无需手动维护 import 列表,因为
src/content/index.ts
使用
import.meta.glob
导入
content/
下的每个文件,以便在应用启动时执行其副作用——因此,上述模块作用域中的
RegisteredCharacters.add(...)
调用总会执行。这意味着你永远不必记住自行导入每个新角色文件(将其放在
content/
下的任意位置即可);但反过来,在
content/
之外 定义(并
.add
)且没有其他任何内容导入的文件,其
add
调用永远不会执行,角色会悄无声息地保持未注册状态。
这是官方模板的约定,而不是库的硬性要求——可以从任何文件调用
RegisteredCharacters.add
,只要能保证它在角色被查找之前执行即可。不具备这种 glob 导入设置的项目,必须改为显式导入每个角色文件(例如从顶层
content/index.ts
或应用入口点导入)。

Typed ids (optional)

类型化 id(可选)

Character ids are typed as
CharacterIdType
, which is
string
by default. If a project augments the empty
PixivnCharacterIds
interface (typically auto-generated by the
vitePluginPixivn
Vite plugin's
typeFilePath
option),
CharacterIdType
narrows to a union of known ids and typos become compile errors. Don't assume this augmentation exists unless you see a generated
pixi-vn.gen.d.ts
/similar file in the project — if not, treat ids as plain strings.
角色 id 的类型为
CharacterIdType
,默认是
string
。如果项目扩充了空的
PixivnCharacterIds
interface(通常由
vitePluginPixivn
Vite 插件的
typeFilePath
选项自动生成),
CharacterIdType
就会收窄为已知 id 的联合类型,拼写错误将变成编译错误。除非在项目中看到生成的
pixi-vn.gen.d.ts
或类似文件,否则不要假定存在这种扩充;如果没有,则将 id 视为普通字符串。

3. Emotions

3. Emotion

A character id may optionally be paired with an emotion by passing an object instead of a plain string as the constructor's first argument:
ts
export const alice = new CharacterBaseModel("alice", {
  name: "Alice",
  icon: "https://example.com/alice.png",
  color: "#9e2e12",
});

export const angryAlice = new CharacterBaseModel(
  { id: "alice", emotion: "angry" },
  { icon: "https://example.com/angryAlice.png" },
);

RegisteredCharacters.add([alice, angryAlice]);
Internally this stores the character's
name
/
surname
/
age
under a separate storage key (
"<id>@<emotion>"
) while still falling back to the base id's stored/default values if an emotion-specific value isn't set — the base id and its emotion variants share the same underlying character data except where the emotion variant overrides it:
ts
console.log(alice.name); // "Alice"

alice.name = "Eleonora";
console.log(alice.name); // "Eleonora"
console.log(angryAlice.name); // "Eleonora" — falls back to the base id's value

angryAlice.name = "Angry Eleonora";
console.log(alice.name); // "Eleonora" — unaffected
console.log(angryAlice.name); // "Angry Eleonora" — its own override now set
Use this only if you need per-emotion overrides of a character's stored fields; for swapping the character's on-screen sprite/image per emotion, that's driven by the canvas/image APIs, not this module.
角色 id 可以选择通过在构造函数第一个参数中传入对象而非普通字符串来与 emotion 配对:
ts
export const alice = new CharacterBaseModel("alice", {
  name: "Alice",
  icon: "https://example.com/alice.png",
  color: "#9e2e12",
});

export const angryAlice = new CharacterBaseModel(
  { id: "alice", emotion: "angry" },
  { icon: "https://example.com/angryAlice.png" },
);

RegisteredCharacters.add([alice, angryAlice]);
在内部,这会将角色的
name
/
surname
/
age
存储在单独的存储键(
"<id>@<emotion>"
)下,同时仍会回退到基础 id 的已存储值或默认值(如果未设置 emotion 专属值)——基础 id 和它的 emotion 变体共享相同的底层角色数据,只有 emotion 变体明确覆盖的内容会有所不同:
ts
console.log(alice.name); // "Alice"

alice.name = "Eleonora";
console.log(alice.name); // "Eleonora"
console.log(angryAlice.name); // "Eleonora" — falls back to the base id's value

angryAlice.name = "Angry Eleonora";
console.log(alice.name); // "Eleonora" — unaffected
console.log(angryAlice.name); // "Angry Eleonora" — its own override now set
仅当需要针对每种 emotion 覆盖角色存储字段时使用此功能;对于按 emotion 替换角色屏幕上的 sprite/图像,应使用 canvas/image API,而不是本模块。

4. Using a character in dialogue

4. 在对话中使用角色

Once registered, pass the character instance (or its id string) as the
character
field of a dialogue, e.g. inside a label step:
ts
narration.dialogue = {
  character: liam,
  text: "Which test do you want to perform?",
};
// or by id:
narration.dialogue = { character: "liam", text: "..." };
Which form to prefer depends on whether the project's
@drincs/pixi-vn/vite
plugin generates typed character ids (the
typeFilePath
option covered in
pixi-vn-getting-started
, which narrows
CharacterIdType
from plain
string
to a union of the actually-registered ids):
  • Without it, prefer passing the instance (
    character: liam
    ) — a bare string id has no compile-time typo protection, while the instance is a real variable reference the compiler already checks.
  • With it, prefer the string-id form (
    character: "liam"
    ) — it now gets the same compile-time safety (a typo like
    "liem"
    fails to typecheck against the generated
    CharacterIdType
    union) without needing to import the character module wherever a dialogue line is written, which keeps label files decoupled from character-definition files.
DialogueInterface.character
accepts
CharacterInterface | string
; when a string id is stored, the engine resolves it back to the registered character object (via
RegisteredCharacters.get
) when the dialogue is read back — so a character must already be registered by the time its dialogue line runs. Reading/rendering the resolved dialogue is covered by the narration skill.
注册后,将角色实例(或其 id 字符串)作为 dialogue 的
character
字段传入,例如在 label 步骤中:
ts
narration.dialogue = {
  character: liam,
  text: "Which test do you want to perform?",
};
// or by id:
narration.dialogue = { character: "liam", text: "..." };
使用哪种形式取决于项目的
@drincs/pixi-vn/vite
插件是否生成类型化角色 id(
typeFilePath
选项在
pixi-vn-getting-started
中有介绍,它会将
CharacterIdType
从普通
string
收窄为实际已注册 id 的联合类型):
  • 未启用时,优先传入实例(
    character: liam
    )——普通字符串 id 没有编译期拼写错误保护,而实例是编译器已经检查过的真实变量引用。
  • 启用时,优先使用字符串 id 形式(
    character: "liam"
    )——它现在具备相同的编译期安全性(例如
    "liem"
    这样的拼写错误无法通过生成的
    CharacterIdType
    联合类型检查),并且无需在编写每条 dialogue 的地方都导入角色模块,从而让 label 文件与角色定义文件保持解耦。
DialogueInterface.character
接受
CharacterInterface | string
;当存储的是字符串 id 时,引擎会在读取 dialogue 时通过
RegisteredCharacters.get
将其解析回已注册的角色对象——因此,在 dialogue 行运行之前,角色必须已经注册。读取和渲染已解析 dialogue 的内容由 narration skill 覆盖。

Related skills

相关 skill

pixi-vn-getting-started, pixi-vn-narration, pixi-vn-storage
pixi-vn-getting-started, pixi-vn-narration, pixi-vn-storage