# Make your first Visual Novel (/start/make-visual-novel)





This tutorial will guide you through the process of creating your first Visual Novel.

**What is a Visual Novel?** A visual novel (VN) is a form of digital interactive fiction. Visual novels are often associated with the medium of video games, but are not always labeled as such themselves. They combine a textual narrative with static or animated illustrations and a varying degree of interactivity. The format is more rarely referred to as novel game, a retranscription of the wasei-eigo term noberu gēmu (ノベルゲーム), which is more often used in Japanese.

For testing purposes, in this guide we will be recreating the visual novel [Breakdown](https://joshpowlison.itch.io/breakdown) using Pixi’VN. Breakdown is a short story that has all the features that a visual novel should have. Josh Powlison, the creator of Breakdown, has given us permission to use his narration for educational purposes❤️.

Since Pixi’VN gives you the ability to write your own narration by choosing one or more <DynamicLink href="/start/narration">available narrative languages</DynamicLink>, examples will be made for each currently available language at each development step.

Create a new project [#create-a-new-project]

The first step is to create a new project. You can find more information on how to create a new project starting from a template <DynamicLink href="/start#project-initialization">here</DynamicLink>. We will use the template "Visual Novel - React".

`Visual Novel -> React`

After the creation is complete, it is very important to read the `README.md` file that is in the root of the project. This file contains important information about the project and how to use it.

In our case, to start the project we will simply need to execute the following commands:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install
    npm start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm install
    pnpm start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn install
    yarn start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun install
    bun start
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Characters creation [#characters-creation]

Now we will define the characters of this story. To do this, we will define in the `/values/characters.ts` file the characters that we will be using. For more information on how to create and use characters you can consult: <DynamicLink href="/start/character">Characters</DynamicLink>

What does `mc` mean? `mc` is a common abbreviation for "Main Character". It is a common practice in visual novels to use `mc` as the main character's name.

<CodeBlockTabs defaultValue="values/characters.ts">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="values/characters.ts">
      values/characters.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="App.tsx">
      App.tsx
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="values/characters.ts">
    ```ts
    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]);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="App.tsx">
    ```ts
    // Remember to import the character file at least once into your project. // [!code focus]
    import "./values/characters"; // [!code focus]

    export default function App() {
        return; // ...
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

First draft of the narrative [#first-draft-of-the-narrative]

<Callout title="Markup" type="info">
  All templates have <DynamicLink href="/start/markup-markdown">Markdown</DynamicLink> and <DynamicLink href="/start/markup-tailwindcss">Tailwind CSS</DynamicLink> support, so we will use it for our narration.
</Callout>

Now we can start writing the "first draft" of the <DynamicLink href="/start/narration">narration</DynamicLink> of the visual novel.
We will create the first <DynamicLink href="/start/labels">`label`</DynamicLink> called `start`, which will be the beginning of the game. After that we can write the <DynamicLink href="/start/dialogue">dialogues</DynamicLink> that will follow in our visual novel.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        () =>
            (narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () => {
            narration.dialogue = "He thrusts out his hand.";
        },
        () => (narration.dialogue = { character: james, text: `James!` }),
        () => (narration.dialogue = { character: mc, text: `...Peter.` }),
        () => {
            narration.dialogue = "I take his hand and shake.";
        },
        () =>
            (narration.dialogue = {
                character: james,
                text: `Ooh, Peter! Nice, firm handshake! The last quy always gave me the dead fish. I already think we'r gonna get along fine.`,
            }),
        () =>
            (narration.dialogue = { character: james, text: `Come on in and...` }),
        () => (narration.dialogue = { character: james, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `I know you're both watching, come on out already!`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `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*.`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Peter, this is Sly. Yes, that is her real name.`,
            }),
        () => {
            narration.dialogue = "I put out my hand.";
        },
        () =>
            (narration.dialogue = {
                character: james,
                text: `I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.`,
            }),
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Fair enough, I'm a pretty scary guy, or so l've been told.`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `The redhead behind her is Stephanie.`,
            }),
        // Example of using Tailwind CSS
        () =>
            (narration.dialogue = {
                character: steph,
                text: `<span class="inline-block motion-translate-y-loop-25">Hey</span>! Everyone calls me Steph. I'll shake your hand.`,
            }),
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Split the narrative into `labels` [#split-the-narrative-into-labels]

It is not advisable to create very long `labels` (even for linear visual novels), but it is advisable to create multiple small `labels` and "call" them when needed with the <DynamicLink href="/start/labels-flow">narration flow control features</DynamicLink>.

For this reason, even if in our case our story is linear, it will be divided into two `labels`, the first will be the one we just created (`start`), while the second will be called `second_part`.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        () =>
            (narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () => {
            narration.dialogue = "He thrusts out his hand.";
        },
        () => (narration.dialogue = { character: james, text: `James!` }),
        () => (narration.dialogue = { character: mc, text: `...Peter.` }),
        // ...
        async (props) => await narration.jump(secondPart, props),
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        () => {
            narration.dialogue = `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...`;
        },
        () => {
            narration.dialogue = `She sets it on a desk. I throw my two paper bags down beside the empty bed.`;
        },
        () =>
            (narration.dialogue = {
                character: steph,
                text: `They got you a new mattress, right? That last guy was a druggie, did James tell you that?`,
            }),
        () =>
            (narration.dialogue = {
                character: sly,
                text: `*We're* the reason he got expelled!`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `Sly! If word gets out about that... well, actually, it wouldn't matter, *he's* the one who shot himself up.`,
            }),
        () => {
            narration.dialogue = `I'm fumbling for a new subject.`;
        },
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Use the "glue" feature of dialogues [#use-the-glue-feature-of-dialogues]

In visual novels, it is often useful to paste text into the current dialogue. For example, to pause a conversation and have it continue in a subsequent `step`. To do this, we can use the <DynamicLink href="/start/dialogue#dialogue-glue">glue functionality</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        async () =>
            (narration.dialogue = {
                character: james,
                text: `Ooh, ${mc.name}! Nice, firm handshake!`,
            }),
        async () => {
            narration.dialogGlue = true;
            narration.dialogue = `The last guy always gave me the dead fish.`;
        },
        async () => {
            narration.dialogGlue = true;
            narration.dialogue = `I already think we're gonna get along fine.`;
        },
        async () =>
            (narration.dialogue = { character: james, text: `Come on in and...` }),
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Choice menus [#choice-menus]

Now we will ask the player if he wants to continue with the second part of the visual novel.

To do this, we will use the <DynamicLink href="/start/choices">choice menu</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        async () => {
            narration.dialogue = `You want continue to the next part?`;
            narration.choices = [
                newChoiceOption(
                    "Yes, I want to continue",
                    secondPart,
                    {},
                    { type: "jump" },
                ),
                newCloseChoiceOption("No, I want to stop here"),
            ];
        },
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Edit character information and use it as a variable [#edit-character-information-and-use-it-as-a-variable]

Now I will give the player the ability to change the name of the `mc`.

To do this, I will ask the player to <DynamicLink href="/start/input">complete an input box using Pixi’VN's features</DynamicLink>.

After getting the input value, you can <DynamicLink href="/start/character#edit">set the character name</DynamicLink> using the obtained value.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    // ...

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

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        () => {
            narration.dialogue = `He thrusts out his hand.`;
        },
        () => {
            narration.requestInput({ type: "string" }, "Peter");
            narration.dialogue = `What is your name?`;
        },
        () => {
            mc.name = narration.inputValue as string;
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Now we could <DynamicLink href="/start/character#use">use character names</DynamicLink> within dialogues.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const steph_fullname = "Stephanie";

    const startLabel = newLabel("start", [
        // ...
        () =>
            (narration.dialogue = {
                character: sly,
                text: `I just wanted to see what the new guy was like. Hey, you, ${mc.name}- be nice to our little brother, or you'll have to deal with *us*.`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `${mc.name}, this is ${sly.name}. Yes, that is her real name.`,
            }),
        () => {
            narration.dialogue = `I put out my hand.`;
        },
        () =>
            (narration.dialogue = {
                character: sly,
                text: `I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.`,
            }),
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Fair enough, I'm a pretty scary guy, or so I've been told.`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `The redhead behind her is ${steph_fullname}.`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `Hey! Everyone calls me ${steph.name}. I'll shake your hand.`,
            }),
        () => {
            narration.dialogue = `She puts out her hand, and I take it.`;
        },
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Thanks, good to meet you, ${steph_fullname}.`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `WOW, that is, like, the most perfect handshake I've ever had! Firm, but also gentle. ${sly.name}, you *gotta* shake his hand!`,
            }),
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Define assets and load them [#define-assets-and-load-them]

<Callout type="info">
  To load and manipulate assets (images, gifs, videos...) you will need to use `Assets`. `Assets` is a class with many features and comes from the PixiJS library, if you want more information read [here](https://pixijs.com/8.x/guides/components/assets).
</Callout>

One of the first steps is choosing where to save your visual novel assets. In this case, we will save the assets in the Firebase storage (a hosting service). You can use any hosting service you prefer or even save the assets locally in the project, read more about it <DynamicLink href="/start/assets">here</DynamicLink>.

Before using an asset it is highly recommended to <DynamicLink href="/start/assets-management#initialize-the-asset-matrix-at-project-start">initialize the asset matrix</DynamicLink>.

By default, as you can see in the `assets/manifest.ts` file, all templates in the `onLoadingLabel` try to load in the background the "bundle assets" with the alias equal to the current `label` id. So it is recommended to add, in the `manifest`, a "bundle assets" for each `label` with the alias equal to the `label` id and containing the images used in that `label`.

This is the example:

<CodeBlockTabs defaultValue="assets/manifest.ts">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="assets/manifest.ts">
      assets/manifest.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="utils/defineAssets.ts">
      utils/defineAssets.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="main.ts">
      main.ts
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="assets/manifest.ts">
    ```ts
    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;
    ```
  </CodeBlockTab>

  <CodeBlockTab value="utils/defineAssets.ts">
    ```ts
    import { Assets } 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.com/start/assets-management.html
     */
    export async function defineAssets() {
        await Assets.init({ manifest });

        // The game will not start until these asserts are loaded.
        await Assets.loadBundle("main_menu");

        // The game will start immediately, but these asserts will be loaded in the background.
        // Assets.backgroundLoadBundle("main_menu");
        // Assets.backgroundLoad("background_main_menu");
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="main.ts">
    ```ts
    import { Assets, canvas, Container, Game } from "@drincs/pixi-vn";
    import { createRoot } from "react-dom/client";
    import App from "./App";
    import { CANVAS_UI_LAYER_NAME } from "./constans";
    import "./index.css";
    import "./values/characters";

    // ...

    Game.onLoadingLabel((_stepId, { id }) => {
        // [!code focus]
        Assets.backgroundLoadBundle(id); // [!code focus]
    }); // [!code focus]
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Add background and character images [#add-background-and-character-images]

Now it's time to think about the visual part too. We will add the background and character sprites to the visual novel canvas.

**What is a sprite?** In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene, most often in a 2D video game.

In our case the character sprites are composed of 3 images: the body, the eyes and the mouth. Then we use <DynamicLink href="/start/canvas-image-container">ImageContainer</DynamicLink> to compose the character. You can find more information on how to add canvas components in <DynamicLink href="/start/canvas-components">this documentation</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        async () => {
            await showImage("bg", "bg01-hallway");
            await showImageContainer(
                "james",
                ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                { xAlign: 0.5, yAlign: 1 },
            );
            narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-smile01",
            ]);
            narration.dialogue = {
                character: james,
                text: `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!`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-smile",
                "m01-mouth-grin00",
            ]);
            narration.dialogue = { character: mc, text: `...` };
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Smart asset loading [#smart-asset-loading]

In our case we saved the game images on a hosting service (Firebase). For this reason the asset loading is not timely.

In order for the player not to perceive too many loadings we should group them in certain phases of the game. In my case I will load the most used images at the start of the `label`.

You can find more information on how to manage the loadings <DynamicLink href="/start/assets-management">here</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    # lazyload bundle m01 fm01 fm02 // [!code focus]

    # 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel(
        "start",
        [
            // ...
        ],
        {
            onLoadingLabel: () => {
                // [!code focus]
                Assets.backgroundLoadBundle(["fm01", "fm02", "m01"]); // [!code focus]
            }, // [!code focus]
        },
    );
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Use transitions [#use-transitions]

To make the visual novel more dynamic, you can use transitions to show images. You can find more information about using transitions <DynamicLink href="/start/canvas-transition">here</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        async () => {
            await showImage("bg", "bg01-hallway");
            await moveIn(
                "james",
                {
                    value: ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                    options: { xAlign: 0.5, yAlign: 1 },
                },
                { direction: "right", ease: "circInOut", type: "spring" },
            );
            narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-smile01",
            ]);
            narration.dialogue = {
                character: james,
                text: `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!`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-smile",
                "m01-mouth-grin00",
            ]);
            narration.dialogue = { character: mc, text: `...` };
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Building an animation [#building-an-animation]

To make the visual novel more dynamic, you can use animations. For more information about how to use animations, see <DynamicLink href="/start/canvas-animations-effects">here</DynamicLink>.

I recommend using TypeScript if you need to set many properties, as this gives you more control, more functionality, and type feedback.

In this example, my animation will take `steph` out of the scene and reinsert her in the next `step`. I'll also mirror her on the x-axis to make sure she's facing the right way.

To take `steph` out/in, I will use the `moveOut` and `moveIn` functions. For the mirror effect, I will use the `canvas.animate` function.

First, I use the `canvas.animate` function to create an animation that sets the `scaleX` property from `-1` (mirrored) to `1`, with `autoplay: false` so it does not start immediately.

Then, I use the `moveIn` function to move `steph` into the scene, passing the `tickerId` of the animation so it can be resumed after the transition is complete.

I use the `completeOnContinue: true` option to ensure the animation completes before the next `step` is executed. For `moveIn`, being a transition, `completeOnContinue` is true by default.

Since I use TypeScript for this animation, I created a `label` for it, so it can be called from other languages as well.

```ts title="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, 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,
            },
        );
    },
]);
```

<Callout type="info" title="ink">
  As explained <DynamicLink href="/ink/labels#use-the-call-script">here</DynamicLink>, from *ink* you can call `labels` written in JS/TS and vice versa.
</Callout>

Now you can call this `label` (`animation_01`) from the main `label` (`start`).

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-grin00",
            ]);
            await showImageContainer("sly", [
                "fm01-body",
                "fm01-eyes-smile",
                "fm01-mouth-smile00",
            ]);
            await showImageContainer("steph", [
                "fm02-body",
                "fm02-eyes-upset",
                "fm02-mouth-nervous00",
            ]);
            moveOut("steph", { direction: "left", speed: 300 });
            narration.dialogue = `${steph_fullname} goes through the opposite door,`;
        },
        async (props) => {
            narration.dialogGlue = true;
            narration.dialogue = ` and returns with a HUGE tinfoil-covered platter.`;
            await narration.call(animation01, props);
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Sounds and music [#sounds-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 <DynamicLink href="/start/sound">here</DynamicLink>.

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.

<CodeBlockTabs defaultValue="utils/assets-utility.ts">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="utils/assets-utility.ts">
      utils/assets-utility.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="assets/manifest.ts">
      assets/manifest.ts
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="utils/assets-utility.ts">
    ```ts
    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.com/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. // [!code focus]
        sound.backgroundLoadBundle("audio"); // [!code focus]
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="assets/manifest.ts">
    ```ts
    import { AssetsManifest } from "@drincs/pixi-vn";

    /**
     * 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: [
            {
                name: "audio",
                assets: [
                    {
                        alias: "bgm_cheerful",
                        src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/audio/bgm_cheerful.wav",
                    },
                    {
                        alias: "sfx_whoosh",
                        src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/audio/sfx_whoosh.wav",
                    },
                ],
            },
        ],
    };
    export default manifest;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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.

```ts title="main.ts"
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.

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === 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

    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        async () => {
            await showImage("bg", "bg01-hallway");
            await sound.play("sfx_whoosh", { delay: 0.1 });
            await moveIn("james", {
                value: ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                options: { xAlign: 0.5, yAlign: 1 },
            });
            narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            };
        },
        async () => {
            await sound.play("bgm_cheerful", {
                channel: BGM_CHANNEL_NAME,
                loop: true,
            });
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-smile01",
            ]);
            narration.dialogue = {
                character: james,
                text: `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!`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-smile",
                "m01-mouth-grin00",
            ]);
            narration.dialogue = { character: mc, text: `...` };
        },
        // ...
        async () => {
            sound.pauseAll();
            await showImageContainer("steph", [
                "fm02-body",
                "fm02-eyes-smile",
                "fm02-mouth-smile00",
            ]);
            await sound.play("sfx_whoosh", { delay: 0.1 });
            moveOut("james", {
                direction: "right",
                ease: "circInOut",
                type: "spring",
                duration: 0.5,
                delay: 0.05,
            });
            moveOut("sly", {
                direction: "right",
                ease: "anticipate",
                duration: 0.5,
            });
            moveOut("steph", {
                direction: "left",
                ease: "easeInOut",
                duration: 0.5,
                delay: 0.1,
            });
            narration.dialogue = `You want continue to the next part?`;
            narration.choices = [
                newChoiceOption(
                    "Yes, I want to continue",
                    secondPart,
                    {},
                    { type: "jump" },
                ),
                newCloseChoiceOption("No, I want to stop here"),
            ];
        },
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        async ({ t }) => {
            narration.dialogue = undefined;
            const text = new Text({
                text: t(`(A few minutes later...)`),
            });
            await showWithFade("bg", text);
            text.align = 0.5;
        },
        async () => {
            sound.resumeAll();
            await showWithFade("bg", "bg02-dorm");
            await sound.play("sfx_whoosh", { delay: 0.4 });
            // ...
            narration.dialogue = `She enters my room before I'VE even had a chance to.`;
        },
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Conclusion [#conclusion]

Well, now you know how to create a visual novel with Pixi’VN. With great power comes great responsibility, so use it wisely and create a great story! 🚀

Here is an interactive example with a minimal UI (HTML). Scrolling down you can see the same result using a complete UI (React template).

<Tabs items="[&#x22;ink&#x22;, &#x22;Typescript&#x22;]" groupId="narrative_language">
  <Tab>
    <InkVisualNovelExample />
  </Tab>

  <Tab>
    <TSVisualNovelExample />
  </Tab>
</Tabs>

<iframe
  src="https://pixi-vn-react-template.web.app/demo"
  title="Visual Novel Demo"
  style="{
      width: &#x22;100%&#x22;,
      height: &#x22;400px&#x22;,
      border: &#x22;0&#x22;,
      borderRadius: &#x22;4px&#x22;,
      overflow: &#x22;hidden&#x22;,
  }"
  allowFullScreen="true"
/>
