LogoPixi’VN
첫걸음을 내딛으세요

첫 번째 Visual Novel 만들기

프로젝트 설정, 내러티브, 에셋, 상호작용을 다루는 Pixi’VN으로 비주얼 노벨을 만드는 단계별 가이드.

이 튜토리얼에서는 첫 번째 Visual Novel을 만드는 과정을 안내합니다.

이 가이드에서는 테스트 목적으로 Pixi’VN을 사용하여 비주얼 노벨 Breakdown을 재현해 볼 것입니다. Breakdown은 비주얼 노벨이 갖춰야 할 모든 기능을 갖춘 짧은 이야기입니다. Breakdown의 제작자인 Josh Powlison은 교육 목적으로 그의 내러티브를 사용할 수 있도록 허락해 주었습니다❤️.

Pixi’VN은 하나 이상의 사용 가능한 내러티브 언어를 선택하여 자신만의 내러티브를 작성할 수 있는 기능을 제공하므로, 개발 단계마다 현재 사용 가능한 각 언어에 대한 예시가 제시됩니다.

새 프로젝트 만들기

시작하기 전에 사전 준비물을 설치했는지 확인하세요.

첫 번째 단계는 새 프로젝트를 만드는 것입니다. 다음을 실행하여 만들 수 있습니다.

npm create pixi-vn@latest

그리고 Visual Novel 템플릿을 선택합니다.

VS Code

VS Code로 프로젝트를 열면, 일부 작업 공간 작업(workspace task)을 실행하는 것과 프로젝트에 권장되는 확장 프로그램을 설치하는 것에 동의하라는 요청을 받게 됩니다. 두 가지 모두 수락하는 것을 강력히 권장합니다.

템플릿, 작업(task), 확장 프로그램에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

캐릭터 생성

이제 이 이야기의 캐릭터를 정의하겠습니다. 이를 위해 /content/characters.ts 파일에 사용할 캐릭터를 정의합니다.

캐릭터를 만들고 사용하는 방법에 대한 자세한 정보는 다음을 참고하세요: Characters

content/characters.ts
import Character from "@/models/Character";
import { RegisteredCharacters } from "@drincs/pixi-vn";

export const mc = new Character("mc", {
    name: "Me",
});

export const james = new Character("james", {
    name: "James",
    color: "#0084ac",
});

export const steph = new Character("steph", {
    name: "Steph",
    color: "#ac5900",
});

export const sly = new Character("sly", {
    name: "Sly",
    color: "#6d00ac",
});

RegisteredCharacters.add([mc, james, steph, sly]);

내러티브 초안 작성

Markup

모든 템플릿은 MarkdownTailwind CSS를 지원하므로, 우리는 내러티브 작성에 이를 사용할 것입니다.

이제 비주얼 노벨의 내러티브 "초안"을 작성해 보겠습니다. 게임의 시작이 될 start라는 이름의 첫 번째 내러티브 노드 (label)를 만들 것입니다. 그 다음 비주얼 노벨에서 이어질 대사를 작성할 수 있습니다.

다음은 그 예시입니다.

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

내러티브를 내러티브 노드 (label)로 나누기

(선형적인 비주얼 노벨이라 하더라도) 매우 긴 내러티브 노드 (label)를 만드는 것은 권장되지 않으며, 대신 여러 개의 작은 내러티브 노드 (label)를 만들고 필요할 때 내러티브 흐름 제어 기능으로 "호출"하는 것이 권장됩니다.

이러한 이유로, 우리의 이야기가 선형적이라 하더라도 두 개의 내러티브 노드 (label)로 나눌 것입니다. 첫 번째는 방금 만든 것(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

선택 메뉴

이제 플레이어에게 비주얼 노벨의 두 번째 부분을 계속할 것인지 물어보겠습니다.

To do this, we will set narration.choices to an array of choice options created with newChoiceOption (to jump or call a label) and newCloseChoiceOption (to close the choice menu without navigating anywhere).

선택지 메뉴에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

다음은 그 예시입니다.

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의 기능을 사용하여 입력 상자를 완성하도록 요청하겠습니다.

입력값을 받은 후에는 얻은 값을 사용하여 캐릭터 이름을 설정할 수 있습니다.

_ink_에서 _input_value_는 플레이어가 입력한 값을 담고 있는 특수 변수입니다. 이에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

다음은 그 예시입니다.

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

이제 대사 안에서 캐릭터 이름을 사용할 수 있습니다.

_ink_에서는 대괄호(예: [sly])를 사용하여 텍스트 치환 메커니즘을 활용할 수 있습니다. 기본적으로 템플릿은 대괄호 안의 텍스트가 캐릭터 id와 일치하는지 확인하고, 일치하면 해당 캐릭터의 이름으로 치환합니다.

JS/TS에서는 ${}를 사용한 템플릿 리터럴로 동일한 결과를 얻을 수 있습니다(예: `${sly.name}`).

다음은 그 예시입니다.

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

에셋 정의 및 로드

에셋(이미지, GIF, 동영상 등) 을 로드하고 조작하려면 Assets를 사용해야 합니다. Assets는 다양한 기능을 갖춘 클래스로 PixiJS 라이브러리에서 제공됩니다. 자세한 정보를 원하시면 여기를 읽어보세요.

첫 번째 단계 중 하나는 비주얼 노벨 에셋을 저장할 위치를 선택하는 것입니다. 온라인 호스팅 서비스를 사용하거나 프로젝트 내에 로컬로 저장할 수 있습니다. 두 옵션에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

에셋을 로컬에 저장하기로 했다면, src/assets 안에 배치하기만 하면 됩니다. 템플릿은 AssetPack을 사용하여 파일을 자동으로 분석하고 src/assets/manifest.gen.json에 정의합니다. 대신 온라인에 보관하기로 했다면, assets/index.ts를 수동으로 편집하여 등록해야 합니다.

두 경우 모두, 각 에셋에는 하나 이상의 별칭 (alias)이 할당되며, 이후 코드에서 이를 사용하여 해당 에셋을 참조할 수 있습니다.

icon.png
manifest.gen.json
index.ts
assets/manifest.gen.json
{
  "bundles": [
    {
      "name": "images",
      "assets": [
        {
          "alias": [
            "images_icon",
            "icon"
          ],
          "src": [
            "images/[email protected]",
            "images/icon-VA-NtA.webp",
            "images/[email protected]",
            "images/icon-E_ZC2g.png"
          ],
          "data": {
            "tags": {}
          }
        }
      ]
    }
  ]
}

배경 및 캐릭터 이미지 추가하기

이제 비주얼 요소에 대해서도 생각해 볼 차례입니다. 배경과 캐릭터 이미지를 비주얼 노벨 캔버스에 추가하겠습니다.

이 경우, 각 캐릭터는 몸, 눈, 입이라는 3개의 스프라이트로 구성됩니다. Sprite 자식들을 그룹화하는 ContainerImageContainer를 사용하여 이들을 함께 묶어 캐릭터를 구성합니다. 캔버스 컴포넌트를 추가하는 방법에 대한 자세한 정보는 이 문서에서 확인할 수 있습니다.

다음은 그 예시입니다.

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

사운드와 음악

비주얼 노벨에 사운드와 음악을 추가하려면 sound 유틸리티를 사용할 수 있습니다.

다양한 유형의 사운드(예: BGM, SFX)를 관리하고 볼륨, 일시정지, 재개 등을 제어하기 위해 사운드 채널을 정의할 수 있습니다. 채널은 게임 시작 시, 예를 들어 Game.init 함수의 then 콜백에서 정의하는 것이 권장됩니다.

이 예시에서는 두 개의 채널을 정의합니다. 하나는 배경 음악(BGM)용이고, 다른 하나는 효과음(SFX)용입니다. 또한 defaultChannelAlias를 SFX 채널로 설정하여, 사운드를 재생할 때 채널을 지정하지 않으면 기본적으로 SFX 채널에서 재생되도록 합니다. 배경 채널을 정의하기 위해 background 속성을 true로 설정합니다. 이렇게 하면 다른 채널과 달리 각 내러티브 단계 (step)가 끝날 때 사운드가 정지되지 않고, 일시정지되거나 정지될 때까지 계속됩니다.

main.ts
import { Game, sound } from "@drincs/pixi-vn";
import { BGM_CHANNEL_NAME, SFX_CHANNEL_NAME } from "@/constans";

Game.init(body, {
    // ...
}).then(() => {
    sound.channels.add(BGM_CHANNEL_NAME, { background: true });
    sound.channels.add(SFX_CHANNEL_NAME);
    sound.defaultChannelAlias = SFX_CHANNEL_NAME;
});

마지막으로, 내러티브 노드 (label) 안에서 sound.play 함수를 사용하여 사운드와 음악을 재생할 수 있습니다. 또한 채널을 사용하여 다양한 유형의 사운드(예: BGM, SFX)를 관리하고 볼륨, 일시정지, 재개 등을 제어할 수 있습니다.

사용 방법에 대한 자세한 정보는 여기에서 확인할 수 있습니다.

file_type_ink
ink/start.ink
=== start ===
# show image bg bg01-hallway
# play sound sfx_whoosh delay 0.1
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right ease circInOut type spring
james: You're my roommate's replacement, huh?
# play sound sfx_whoosh channel bgm loop true
# show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
# show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
mc: ...

// ...

# pause all sounds
# show imagecontainer steph [fm02-body fm02-eyes-smile fm02-mouth-smile00]
# play sound sfx_whoosh delay 0.1
# remove image james with moveout direction right ease circInOut type spring duration 0.5 delay 0.05
# remove image sly with moveout direction right ease anticipate duration 0.5
# remove image steph with moveout direction left ease easeInOut duration 0.5 delay 0.1

You want continue to the next part?<># continue
* Yes, I want to continue
-> second_part
* No, I want to stop here
-> END

=== second_part ===
# show text bg "(A few minutes later...)" style { fontFamily: "Arial", dropShadow: { alpha: 0.8, angle: 2.1, blur: 4, color: "0x111111", distance: 10, }, fill: "\#ffffff", stroke: { color: "\#004620", width: 12, join: "round" }, fontSize: 60, fontWeight: "lighter" } with fade
# edit text bg align 0.5
# pause

# resume all sounds
# show image bg bg02-dorm align 0 with fade
# play sound sfx_whoosh delay 0.4
// ...
She enters my room before I'VE even had a chance to.

// ...

-> DONE

결론

자, 이제 Pixi’VN으로 비주얼 노벨을 만드는 방법을 알게 되었습니다. 큰 힘에는 큰 책임이 따르는 법이니, 현명하게 사용하여 멋진 이야기를 만들어보세요! 🚀

이 페이지에서