创建你的第一个视觉小说
使用 Pixi’VN 创建视觉小说的分步指南,涵盖项目设置、叙事、资源和交互性。
本教程将引导你完成创建第一个视觉小说的整个过程。
什么是视觉小说? 视觉小说(VN)是一种数字交互式虚构作品形式。 视觉小说通常与电子游戏媒介相关联,但并不总是被明确归类为游戏。 它结合了文本叙事、静态或动画插图,以及不同程度的交互性。 这种形式有时也被称为「novel game」,这是对和制英语 noberu gēmu(ノベルゲーム)的转写,而这一称呼在日本更为常见。
出于测试目的,在本指南中我们将使用 Pixi’VN 复刻视觉小说 Breakdown。 Breakdown 是一个短篇故事,具备视觉小说应有的所有特征。 Breakdown 的作者 Josh Powlison 已授权我们将其叙事内容用于教学目的 ❤️。
由于 Pixi’VN 允许你通过选择一个或多个 可用的叙事语言 来编写自己的叙事内容,因此在每个开发阶段,都会提供当前所有可用语言的示例。
创建新项目
第一步是创建一个新项目。 有关如何从模板创建新项目的更多信息,请参阅 这里。 本指南将使用 “Visual Novel - React” 模板。
Visual Novel -> React
项目创建完成后,务必阅读位于项目根目录中的 README.md 文件。 该文件包含有关项目的重要信息以及使用说明。
在本例中,启动项目只需执行所提供的相关命令即可:
npm install
npm start创建角色
现在我们将定义这个故事中的角色。 为此,我们会在 /values/characters.ts 文件中定义要使用的角色。 有关如何创建和使用角色的更多信息,请参阅:Characters
mc 表示什么? mc 是 “Main Character(主角)” 的常见缩写。 在视觉小说中,通常使用 mc 作为主角的名称。
import { RegisteredCharacters } from "@drincs/pixi-vn";
import Character from "../models/Character";
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]);叙事的初稿
Markup
所有模板都支持 Markdown 和 Tailwind CSS,因此我们将使用它们来编写叙事内容。
现在,我们可以开始编写视觉小说 叙事 的「初稿」。
我们将创建第一个名为 start 的 label,它将作为游戏的起点。 随后,就可以编写在视觉小说中展开的 dialogues。
下面是一个示例:
=== start ===
james: You're my roommate's replacement, huh?
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!
mc: ...
He thrusts out his hand.
james: James!
mc: ...Peter.
I take his hand and shake.
james: Ooh, Peter! Nice, firm handshake! The last quy always gave me the dead fish. I already think we'r gonna get along fine.
james: Come on in and...
james: ...
james: I know you're both watching, come on out already!
sly: I just wanted to see what the new guy was like. Hey, you, Peter- be nice to our little brother, or you'll have to deal with *us*.
mc: ...
james: Peter, this is Sly. Yes, that is her real name.
I put out my hand.
sly: I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.
mc: Fair enough, I'm a pretty scary guy, or so l've been told.
james: The redhead behind her is Stephanie.
// Example of using Tailwind CSS
steph: <span class="inline-block motion-translate-y-loop-25">Hey</span>! Everyone calls me Steph. I'll shake your hand.
// ...
-> DONE将叙事拆分为 labels
即使是线性的视觉小说,也不建议创建过长的 labels。相反,更推荐创建多个较小的 labels,并在需要时通过 叙事流程控制功能 来调用它们。
因此,即便在本例中故事是线性的,也会被拆分为多个 labels,并使用 start 与 second_part。
下面是一个示例:
=== start ===
james: You're my roommate's replacement, huh?
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!
mc: ...
He thrusts out his hand.
james: James!
mc: ...Peter.
// ...
-> second_part
=== second_part ===
She enters my room before I'VE even had a chance to. \\n\\n...I could've just come back and gotten the platter later...
She sets it on a desk. I throw my two paper bags down beside the empty bed.
steph: They got you a new mattress, right? That last guy was a druggie, did James tell you that?
sly: *We're* the reason he got expelled!
steph: Sly! If word gets out about that... well, actually, it wouldn't matter, *he's* the one who shot himself up.
I'm fumbling for a new subject.
// ...
-> DONE在 dialogues 中使用 “glue” 功能
在视觉小说中,经常需要将文本粘贴到当前的 dialogue 中。 例如,暂停一段对话,并在后续的 step 中继续。 为此,可以使用 glue 功能。
下面是一个示例:
=== start ===
// ...
james: Ooh, [mc]! Nice, firm handshake!
<>The last guy always gave me the dead fish.
<>I already think we're gonna get along fine.
james: Come on in and...
// ...
-> DONE选择菜单
接下来,我们将询问玩家是否希望继续推进视觉小说的后续内容。
为此,我们将使用 选择菜单。
下面是一个示例:
=== start ===
// ...
You want continue to the next part?
* Yes, I want to continue
-> second_part
* No, I want to stop here
-> END
=== second_part ===
// ...
-> DONE编辑角色信息并将其作为变量使用
接下来,我们将允许玩家修改 mc 的名字。
为此,我们会让玩家通过 Pixi’VN 提供的功能 在输入框中填写内容。
在获取输入的值之后,可以使用该值来 设置角色名称。
下面是一个示例:
=== start ===
// ...
He thrusts out his hand.
# request input type string default Peter
What is your name?
# rename mc { _input_value_ }
// ...
-> DONE随后,就可以在对话中 使用角色名称。
下面是一个示例:
VAR steph_fullname = "Stephanie"
=== start ===
// ...
sly: I just wanted to see what the new guy was like. Hey, you, [mc]- be nice to our little brother, or you'll have to deal with *us*.
mc: ...
james: [mc], this is [sly]. Yes, that is her real name.
I put out my hand.
sly: I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.
mc: Fair enough, I'm a pretty scary guy, or so l've been told.
james: The redhead behind her is [steph_fullname].
steph: Hey! Everyone calls me [steph]. I'll shake your hand.
She puts out her hand, and I take it.
mc: Thanks, good to meet you, [steph_fullname].
steph: WOW, that is, like, the most perfect handshake I've ever had! Firm, but also gentle. [sly], you *gotta* shake his hand!
// ...
-> DONE定义并加载资源
要加载和操作资源(图片、GIF、视频等), 需要使用 Assets。 Assets 是一个功能丰富的类,来自 PixiJS 库。如需更多信息,请参阅 这里。
第一步之一是选择用于保存视觉小说资源的位置。 在此示例中,我们将资源保存到 Firebase Storage(一种托管服务)。 你也可以使用任意你喜欢的托管服务,或者将资源本地保存在项目中。更多信息请参阅 这里。
在使用资源之前,强烈建议先 初始化资源矩阵。
默认情况下,如 assets/manifest.ts 文件所示,onLoadingLabel 中的所有模板都会尝试在后台加载别名等于当前 label id 的 “bundle assets”。 因此,建议在 manifest 中为每个 label 添加一个与该 label id 同名的 “bundle assets”,并包含该 label 中使用的图片。
下面是一个示例:
import { AssetsManifest } from "@drincs/pixi-vn";
import { MAIN_MENU_ROUTE } from "../constans";
/**
* Manifest for the assets used in the game.
* You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets
*/
const manifest: AssetsManifest = {
bundles: [
// screens
{
name: MAIN_MENU_ROUTE,
assets: [
{
alias: "background_main_menu",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/main-menu.png",
},
],
},
// labels
{
name: "start",
assets: [
{
alias: "bg01-hallway",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/bg01-hallway.webp",
},
],
},
{
name: "second_part",
assets: [
{
alias: "bg02-dorm",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/bg02-dorm.webp",
},
],
},
// characters
{
name: "fm01",
assets: [
{
alias: "fm01-body",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-body.webp",
},
{
alias: "fm01-eyes-grin",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-eyes-grin.webp",
},
{
alias: "fm01-eyes-smile",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-eyes-smile.webp",
},
{
alias: "fm01-eyes-soft",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-eyes-soft.webp",
},
{
alias: "fm01-eyes-upset",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-eyes-upset.webp",
},
{
alias: "fm01-eyes-wow",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-eyes-wow.webp",
},
{
alias: "fm01-mouth-grin00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-grin00.webp",
},
{
alias: "fm01-mouth-serious00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-serious00.webp",
},
{
alias: "fm01-mouth-serious01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-serious01.webp",
},
{
alias: "fm01-mouth-smile00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-smile00.webp",
},
{
alias: "fm01-mouth-smile01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-smile01.webp",
},
{
alias: "fm01-mouth-soft00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-soft00.webp",
},
{
alias: "fm01-mouth-soft01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-soft01.webp",
},
{
alias: "fm01-mouth-upset00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-upset00.webp",
},
{
alias: "fm01-mouth-upset01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-upset01.webp",
},
{
alias: "fm01-mouth-wow01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm01/fm01-mouth-wow01.webp",
},
],
},
{
name: "fm02",
assets: [
{
alias: "fm02-body",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-body.webp",
},
{
alias: "fm02-eyes-bawl",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-bawl.webp",
},
{
alias: "fm02-eyes-joy",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-joy.webp",
},
{
alias: "fm02-eyes-nervous",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-nervous.webp",
},
{
alias: "fm02-eyes-smile",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-smile.webp",
},
{
alias: "fm02-eyes-upset",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-upset.webp",
},
{
alias: "fm02-eyes-wow",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-eyes-wow.webp",
},
{
alias: "fm02-mouth-cry01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-cry01.webp",
},
{
alias: "fm02-mouth-nervous00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-nervous00.webp",
},
{
alias: "fm02-mouth-nervous01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-nervous01.webp",
},
{
alias: "fm02-mouth-smile00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-smile00.webp",
},
{
alias: "fm02-mouth-smile01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-smile01.webp",
},
{
alias: "fm02-mouth-upset00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-upset00.webp",
},
{
alias: "fm02-mouth-upset01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-upset01.webp",
},
{
alias: "fm02-mouth-wow01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/fm02/fm02-mouth-wow01.webp",
},
],
},
{
name: "m01",
assets: [
{
alias: "m01-body",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp",
},
{
alias: "m01-eyes-annoy",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-annoy.webp",
},
{
alias: "m01-eyes-concern",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-concern.webp",
},
{
alias: "m01-eyes-cry",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-cry.webp",
},
{
alias: "m01-eyes-grin",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-grin.webp",
},
{
alias: "m01-eyes-smile",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp",
},
{
alias: "m01-eyes-wow",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-wow.webp",
},
{
alias: "m01-mouth-annoy00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-annoy00.webp",
},
{
alias: "m01-mouth-annoy01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-annoy01.webp",
},
{
alias: "m01-mouth-concern00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-concern00.webp",
},
{
alias: "m01-mouth-concern01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-concern01.webp",
},
{
alias: "m01-mouth-cry00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-cry00.webp",
},
{
alias: "m01-mouth-cry01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-cry01.webp",
},
{
alias: "m01-mouth-grin00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-grin00.webp",
},
{
alias: "m01-mouth-neutral00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-neutral00.webp",
},
{
alias: "m01-mouth-neutral01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-neutral01.webp",
},
{
alias: "m01-mouth-smile00",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp",
},
{
alias: "m01-mouth-smile01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile01.webp",
},
{
alias: "m01-mouth-wow01",
src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-wow01.webp",
},
],
},
],
};
export default manifest;添加背景和角色图片
现在是时候考虑视觉部分了。 我们将背景和角色精灵添加到视觉小说的画布中。
什么是精灵(sprite)? 在计算机图形学中,精灵是一个二维位图,通常被集成到更大的场景中,最常见于 2D 视频游戏。
在本例中,角色精灵由 3 张图片组成:身体、眼睛和嘴巴。 然后我们使用 ImageContainer 来组合角色。 有关添加画布组件的更多信息,请参阅 此文档。
下面是一个示例:
=== start ===
# show image bg bg01-hallway
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1
james: You're my roommate's replacement, huh?
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
mc: ...
// ...
-> DONE智能资源加载
在本例中,我们将游戏图片保存在托管服务(Firebase)上。 因此,资源加载并不是即时的。
为了避免玩家感知到过多的加载过程,我们应当在游戏的特定阶段对资源进行分组加载。 在此示例中,我会在 label 开始时加载最常用的图片。
有关如何管理加载的更多信息,请参阅 这里。
下面是一个示例:
=== start ===
# lazyload bundle m01 fm01 fm02
# show image bg bg01-hallway
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right speed 300
james: You're my roommate's replacement, huh?
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
mc: ...
// ...
-> DONE使用过渡效果
为了让视觉小说更加生动,可以在显示图片时使用过渡效果。 有关如何使用过渡效果的更多信息,请参阅 这里。
下面是一个示例:
=== start ===
// ...
# show image bg bg01-hallway
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right speed 300
james: You're my roommate's replacement, huh?
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
mc: ...
// ...
-> DONE构建动画
为了让视觉小说更加生动,可以使用动画。 有关如何使用动画的更多信息,请参阅 这里。
如果需要设置大量属性,建议使用 TypeScript,因为它可以提供更强的控制能力、更多功能以及类型反馈。
在此示例中,动画会将 steph 移出场景,并在下一个 step 中重新插入。 同时,我还会在 x 轴方向对她进行镜像处理,以确保角色朝向正确。
为了将 steph 移出/移入场景,我会使用 moveOut 和 moveIn 函数。 镜像效果则通过 canvas.animate 函数实现。
首先,我使用 canvas.animate 创建一个动画,将 scaleX 属性从 -1(镜像)变化到 1,并设置 autoplay: false,使其不会立即播放。
然后,我使用 moveIn 函数将 steph 移入场景,并传入该动画的 tickerId,以便在过渡完成后继续播放动画。
我使用 completeOnContinue: true 选项来确保动画在执行下一个 step 之前完成。 对于作为过渡的 moveIn,completeOnContinue 默认就是 true。
由于该动画使用 TypeScript 编写,我为其创建了一个独立的 label,以便也可以从其他语言中调用。
import { canvas, ImageContainer, moveIn, newLabel } from "@drincs/pixi-vn";
export const animation01 = newLabel("animation_01", [
async () => {
let tickerId = canvas.animate<ImageContainer>(
"steph",
{
scaleX: 1,
},
{ autoplay: false, completeOnContinue: true }
);
await moveIn(
"steph",
{
value: ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile01"],
options: { xAlign: 0.8, yAlign: 1, scale: { y: 1, x: -1 }, anchor: 0.5 },
},
{ direction: "right", ease: "easeInOut", tickerIdToResume: tickerId }
);
},
]);ink
如 这里 所述,可以从 ink 调用用 JS/TS 编写的 labels,反之亦然。
现在,你可以从主 label(start)中调用这个 label(animation_01)。
=== start ===
// ...
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-grin00]
# show imagecontainer sly [fm01-body fm01-eyes-smile fm01-mouth-smile00]
# show imagecontainer steph [fm02-body fm02-eyes-upset fm02-mouth-nervous00]
# remove image steph with moveout direction left speed 300
[steph_fullname] goes through the opposite door,
# call animation_01
<> and returns with a HUGE tinfoil-covered platter.
// ...
-> DONESounds and music
To add sounds and music to your visual novel, you can use the sound utility. You can find more information about how to use it here.
The first step is to define the audio assets in the manifest. Then, you can load them in the background at the start of the game, so they will be available when needed without blocking the game start.
import { Assets, sound } from "@drincs/pixi-vn";
import manifest from "../assets/manifest";
/**
* Define all the assets that will be used in the game.
* This function will be called before the game starts.
* You can read more about assets management in the documentation: https://pixi-vn.web.app/start/assets-management.html
*/
export async function defineAssets() {
await Assets.init({ manifest });
// The audio bundle will be loaded in the background, so it will be available when needed, but it won't block the game start.
sound.backgroundLoadBundle("audio");
}You can also define sound channels to manage different types of sounds (e.g., BGM, SFX) and control their volume, pause, resume, etc. It is recommended to define the channels at the start of the game, for example in the then callback of the Game.init function.
Into this example, I define two channels: one for the background music (BGM) and one for the sound effects (SFX). I also set the defaultChannelAlias to the SFX channel, so if I don't specify a channel when playing a sound, it will be played in the SFX channel by default. To define the background channel, I set the background property to true, so unlike the other channels the sounds will not be stopped at the end of each narrative step, but they will continue until they are paused or stopped.
import { Game, sound } from "@drincs/pixi-vn";
import {
BGM_CHANNEL_NAME,
SFX_CHANNEL_NAME,
} from "./constans";
Game.init(body, {
// ...
}).then(() => {
sound.addChannel(BGM_CHANNEL_NAME, { background: true });
sound.addChannel(SFX_CHANNEL_NAME);
sound.defaultChannelAlias = SFX_CHANNEL_NAME;
});Finally, you can play the sounds and music in the labels using the sound.play function. You can also use channels to manage different types of sounds (e.g., BGM, SFX) and control their volume, pause, resume, etc.
=== start ===
# show image bg bg01-hallway
# play sound sfx_whoosh delay 0.1
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right ease circInOut type spring
james: You're my roommate's replacement, huh?
# play sound sfx_whoosh channel bgm loop true
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
mc: ...
// ...
# pause all sounds
# show imagecontainer steph [fm02-body fm02-eyes-smile fm02-mouth-smile00]
# play sound sfx_whoosh delay 0.1
# remove image james with moveout direction right ease circInOut type spring duration 0.5 delay 0.05
# remove image sly with moveout direction right ease anticipate duration 0.5
# remove image steph with moveout direction left ease easeInOut duration 0.5 delay 0.1
You want continue to the next part?<># continue
* Yes, I want to continue
-> second_part
* No, I want to stop here
-> END
=== second_part ===
# show text bg "(A few minutes later...)" style \{ fontFamily: "Arial", dropShadow: \{ alpha: 0.8, angle: 2.1, blur: 4, color: "0x111111", distance: 10, \}, fill: "\#ffffff", stroke: \{ color: "\#004620", width: 12, join: "round" \}, fontSize: 60, fontWeight: "lighter" \} with fade
# edit text bg align 0.5
# pause
# resume all sounds
# show image bg bg02-dorm align 0 with fade
# play sound sfx_whoosh delay 0.4
// ...
She enters my room before I'VE even had a chance to.
// ...
-> DONE结论
现在,你已经了解了如何使用 Pixi’VN 创建视觉小说。 能力越大,责任越大,请明智地使用它,创作一个精彩的故事吧! 🚀
下面是一个使用最小 UI(HTML)的交互式示例。 向下滚动后,你可以看到使用完整 UI(React 模板)实现的相同结果。