LogoPixi’VN
Crea tu primer proyecto

Crea tu primer visual novel

Guía paso a paso para crear un visual novel con Pixi’VN, que cubre la configuración del proyecto, la narrativa, los recursos y la interactividad.

Este tutorial te guiará a través del proceso de creación de tu primer visual novel.

¿Qué es un visual novel? Un visual novel (VN) es una forma de ficción interactiva digital. Los visual novels suelen asociarse con el medio de los videojuegos, pero no siempre se clasifican como tales. Combinan una narrativa textual con ilustraciones estáticas o animadas y un grado variable de interactividad. Este formato también recibe en ocasiones el nombre de «novel game», una retranscripción del término wasei-eigo noberu gēmu(ノベルゲーム), que se utiliza con mayor frecuencia en Japón.

Con fines de prueba, en esta guía recrearemos el visual novel Breakdown utilizando Pixi’VN. Breakdown es una historia corta que contiene todas las características que debería tener un visual novel. Josh Powlison, el creador de Breakdown, nos ha dado permiso para usar su narración con fines educativos ❤️.

Dado que Pixi’VN te permite escribir tu propia narrativa eligiendo uno o varios lenguajes narrativos disponibles, se proporcionarán ejemplos para cada lenguaje disponible actualmente en cada etapa del desarrollo.

Crear un nuevo proyecto

El primer paso consiste en crear un nuevo proyecto. Puedes encontrar más información sobre cómo crear un proyecto a partir de una plantilla aquí. En esta guía utilizaremos la plantilla «Visual Novel - React».

Visual Novel -> React

Una vez completada la creación, es muy importante leer el archivo README.md que se encuentra en la raíz del proyecto. Este archivo contiene información importante sobre el proyecto y sobre cómo utilizarlo.

En nuestro caso, para iniciar el proyecto solo será necesario ejecutar los comandos indicados:

npm install
npm start

Creación de personajes

Ahora definiremos los personajes de esta historia. Para ello, los personajes que se utilizarán se definirán en el archivo /values/characters.ts. Para obtener más información sobre cómo crear y utilizar personajes, puedes consultar: Characters

¿Qué significa mc? mc es una abreviatura común de «Main Character». En los visual novels es una práctica habitual usar mc como nombre del personaje principal.

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]);

Primer borrador de la narrativa

Markup

Todas las plantillas admiten Markdown y Tailwind CSS, por lo que los utilizaremos para la narrativa.

Ahora podemos comenzar a escribir el «primer borrador» de la narrativa del visual novel. Crearemos el primer label llamado start, que será el inicio del juego. Después de eso, podremos escribir los dialogues que se desarrollarán en el visual novel.

Este es un ejemplo:

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

Dividir la narrativa en labels

No es recomendable crear labels muy largos, incluso en visual novels lineales. En su lugar, es aconsejable crear varios labels pequeños y llamarlos cuando sea necesario mediante las funcionalidades de control del flujo narrativo.

Por esta razón, aunque en este caso la historia sea lineal, se dividirá en varios labels, utilizando start y second_part.

Este es un ejemplo:

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

Menús de elección

Ahora preguntaremos al jugador si desea continuar con la parte siguiente del visual novel.

Para ello, utilizaremos el menú de elecciones.

Este es un ejemplo:

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

Editar la información del personaje y usarla como variable

Ahora daremos al jugador la posibilidad de cambiar el nombre de mc.

Para ello, pediremos al jugador que complete un campo de entrada utilizando las funcionalidades de Pixi’VN.

Después de obtener el valor introducido, será posible establecer el nombre del personaje utilizando dicho valor.

Este es un ejemplo:

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

A continuación, será posible usar los nombres de los personajes dentro de los diálogos.

Este es un ejemplo:

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

Usar la funcionalidad “glue” de dialogues

En las novelas visuales, a menudo es útil pegar texto en el dialogue actual. Por ejemplo, para pausar una conversación y continuarla en un step posterior. Para hacerlo, podemos usar la funcionalidad glue.

Este es un ejemplo:

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

Definir y cargar los assets

Para cargar y manipular assets (imágenes, Gif, vídeos, etc...) es necesario usar Assets. Assets es una clase con muchas funcionalidades y proviene de la biblioteca PixiJS. Si deseas más información, lee aquí.

Uno de los primeros pasos es elegir dónde guardar los assets de tu novela visual. En este caso, guardaremos los assets en Firebase Storage (un servicio de hosting). Puedes usar cualquier servicio de hosting que prefieras o incluso guardar los assets localmente en el proyecto. Lee más sobre esto aquí.

Antes de usar un asset, se recomienda encarecidamente inicializar la matriz de assets.

De forma predeterminada, como puedes ver en el archivo assets/manifest.ts, todas las plantillas en onLoadingLabel intentan cargar en segundo plano los “bundle assets” con un alias igual al id del label actual. Por lo tanto, se recomienda añadir en el manifest un “bundle assets” para cada label, con un alias igual al id del label y que contenga las imágenes utilizadas en ese label.

Este es un ejemplo:

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;

Añadir imágenes de fondo y de personajes

Ahora es el momento de pensar también en la parte visual. Añadiremos las imágenes de fondo y los sprites de los personajes al canvas de la novela visual.

¿Qué es un sprite? En gráficos por ordenador, un sprite es un mapa de bits bidimensional que se integra en una escena más grande, con mayor frecuencia en un videojuego 2D.

En nuestro caso, los sprites de los personajes están compuestos por 3 imágenes: el cuerpo, los ojos y la boca. Luego usamos ImageContainer para componer el personaje. Puedes encontrar más información sobre cómo añadir componentes al canvas en esta documentación.

Este es un ejemplo:

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

Carga inteligente de assets

En nuestro caso, guardamos las imágenes del juego en un servicio de hosting (Firebase). Por esta razón, la carga de assets no es inmediata.

Para que el jugador no perciba demasiadas cargas, deberíamos agruparlas en ciertas fases del juego. En este ejemplo, cargaré las imágenes más utilizadas al inicio del label.

Puedes encontrar más información sobre cómo gestionar las cargas aquí.

Este es un ejemplo:

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

Usar transiciones

Para hacer la novela visual más dinámica, puedes usar transiciones para mostrar imágenes. Puedes encontrar más información sobre el uso de transiciones aquí.

Este es un ejemplo:

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

Construir una animación

Para hacer la novela visual más dinámica, puedes usar animaciones. Para más información sobre cómo usar animaciones, consulta aquí.

Recomiendo usar TypeScript si necesitas configurar muchas propiedades, ya que ofrece más control, más funcionalidades y retroalimentación de tipos.

En este ejemplo, mi animación sacará a steph de la escena y la volverá a insertar en el siguiente step. También la reflejaré en el eje X para asegurarme de que esté mirando en la dirección correcta.

Para sacar/introducir a steph, usaré las funciones moveOut y moveIn. Para el efecto espejo, usaré la función canvas.animate.

Primero, uso la función canvas.animate para crear una animación que establece la propiedad scaleX de -1 (espejado) a 1, con autoplay: false para que no se inicie inmediatamente.

Luego, uso la función moveIn para mover a steph dentro de la escena, pasando el tickerId de la animación para que pueda reanudarse después de que la transición haya finalizado.

Uso la opción forceCompleteBeforeNext: true para asegurar que la animación se complete antes de que se ejecute el siguiente step. Para moveIn, al ser una transición, forceCompleteBeforeNext es true por defecto.

Dado que uso TypeScript para esta animación, creé un label para ella, de modo que también pueda ser llamada desde otros lenguajes.

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

Como se explica aquí, desde ink puedes llamar a labels escritos en JS/TS y viceversa.

Ahora puedes llamar a este label (animation_01) desde el label principal (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

Conclusión

Ahora ya sabes cómo crear una novela visual con Pixi’VN. ¡Un gran poder conlleva una gran responsabilidad, así que úsalo sabiamente y crea una gran historia! 🚀

Aquí tienes un ejemplo interactivo con una UI mínima (HTML). Al desplazarte hacia abajo, puedes ver el mismo resultado usando una UI completa (plantilla React).