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

선택 메뉴

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

이를 위해 narration.choicesnewChoiceOption(내러티브 노드 (label)로 점프하거나 호출하기 위함)과 newCloseChoiceOption(아무 곳으로도 이동하지 않고 선택지 메뉴를 닫기 위함)으로 생성한 선택 옵션 배열로 설정합니다.

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

다음은 그 예시입니다.

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으로 비주얼 노벨을 만드는 방법을 알게 되었습니다. 큰 힘에는 큰 책임이 따르는 법이니, 현명하게 사용하여 멋진 이야기를 만들어보세요! 🚀

이 페이지에서