LogoPixi’VN
Создай свой первый проект

Создание вашего первого визуального романа

Пошаговое руководство по созданию визуального романа с использованием Pixi’VN, включая настройку проекта, нарратив, ассеты и интерактивность.

Этот туториал проведёт вас через процесс создания вашего первого визуального романа.

Что такое визуальный роман? Визуальный роман (VN) — это форма цифровой интерактивной художественной литературы. Визуальные романы часто ассоциируются с видеоиграми, однако не всегда классифицируются именно как игры. Они объединяют текстовый нарратив со статичными или анимированными иллюстрациями и различной степенью интерактивности. Этот формат также иногда называют «novel game», что является транскрипцией японского термина noberu gēmu (ノベルゲーム), который в Японии используется значительно чаще.

В целях тестирования в этом руководстве мы будем воссоздавать визуальный роман Breakdown с использованием Pixi’VN. Breakdown — это короткая история, содержащая все элементы, которыми должен обладать визуальный роман. Джош Поулисон, создатель Breakdown, дал нам разрешение использовать его повествование в образовательных целях ❤️.

Поскольку 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, поэтому они будут использоваться для нарратива.

Теперь можно приступить к написанию «первого черновика» нарратива визуального романа. Мы создадим первый label с именем start, который будет являться началом игры. После этого можно писать dialogues, которые будут развиваться в визуальном романе.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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

Меню выбора

Теперь мы предложим игроку решить, хочет ли он продолжить визуальный роман и перейти к следующей части истории.

Для этого будет использовано меню выбора.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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.

После этого станет возможным использование имён персонажей внутри диалогов.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== start ===
// ...

He thrusts out his hand.
# request input type string default Peter
What is your name?
# rename mc { _input_value_ }

// ...
-> DONE

Теперь мы можем использовать имена персонажей в диалогах.

Ниже приведён пример:

file_type_ink
ink/start.ink
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

Использование функции «glue» в dialogues

В визуальных новеллах часто бывает полезно вставлять текст в текущий dialogue. Например, чтобы приостановить разговор и продолжить его в последующем step. Для этого можно использовать функцию glue.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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

Определение и загрузка ассетов

Для загрузки и управления ассетами (изображениями, GIF, видео и т. д.) необходимо использовать Assets. Assets — это класс с множеством возможностей, предоставляемый библиотекой PixiJS. Подробнее см. здесь.

Одним из первых шагов является выбор места для хранения ассетов визуальной новеллы. В этом случае ассеты будут сохранены в Firebase Storage (хостинг-сервис). Вы можете использовать любой другой хостинг или сохранить ассеты локально в проекте. Подробнее читайте здесь.

Перед использованием ассетов настоятельно рекомендуется инициализировать матрицу ассетов.

По умолчанию, как видно в файле assets/manifest.ts, все шаблоны в onLoadingLabel пытаются загружать в фоновом режиме «bundle assets» с алиасом, равным текущему id label. Поэтому рекомендуется добавлять в manifest «bundle assets» для каждого label с алиасом, совпадающим с id label, и содержащим изображения, используемые в этом 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;

Добавление фоновых изображений и изображений персонажей

Теперь пришло время подумать и о визуальной части. Мы добавим фоновые изображения и спрайты персонажей на canvas визуальной новеллы.

Что такое спрайт? В компьютерной графике спрайт — это двумерное растровое изображение, встроенное в более крупную сцену, чаще всего используемое в 2D-видеоиграх.

В нашем случае спрайты персонажей состоят из 3 изображений: тела, глаз и рта. Затем мы используем ImageContainer для сборки персонажа. Дополнительную информацию о добавлении canvas-компонентов можно найти в этой документации.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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.

Подробнее о управлении загрузками можно узнать здесь.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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

Использование переходов

Чтобы сделать визуальную новеллу более динамичной, можно использовать переходы при отображении изображений. Подробнее об использовании переходов см. здесь.

Ниже приведён пример:

file_type_ink
ink/start.ink
=== 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 анимации, чтобы она могла быть продолжена после завершения перехода.

Опция forceCompleteBeforeNext: true используется для гарантии завершения анимации перед выполнением следующего step. Для moveIn, как для перехода, forceCompleteBeforeNext по умолчанию равно true.

Поскольку эта анимация реализована на TypeScript, для неё был создан отдельный label, чтобы её можно было вызывать и из других языков.

labels/animation01.ts
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, forceCompleteBeforeNext: 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 можно вызывать labels, написанные на JS/TS, и наоборот.

Теперь вы можете вызывать этот label (animation_01) из основного label (start).

file_type_ink
ink/start.ink
=== 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.

// ...
-> DONE

Заключение

Теперь вы знаете, как создать визуальную новеллу с помощью Pixi’VN. С большой силой приходит большая ответственность — используйте её с умом и создайте отличную историю! 🚀

Ниже приведён интерактивный пример с минимальным UI (HTML). Прокрутив страницу вниз, вы увидите тот же результат с использованием полноценного UI (шаблон React).