Touhou: Fading Illusion

Touhou: Fading Illusion

Not enough ratings
모드 제작 가이드
By GiMark
해당 가이드에서는 간단한 모드를 만들기 위한 정보를 열람할 수 있습니다.
   
Award
Favorite
Favorited
Unfavorite
서문
환영합니다, 모더님들! 이번 가이드는 동방조몽록의 모드 제작을 원하는 유저분들을 위한 글입니다. 하지만, 먼저 동방 프로젝트의 공식 외 활동에 대한 규칙을 준수해 주세요.

주의: 모딩은 현재 윈도우 OS를 사용 중이신 유저분들께 한정되어 있습니다.

참고: "_game_resources" 안에 있는 자료들에 앞으로의 전개에 대한 스포일러가 포함될 수 있습니다.
창작물 규정
오리지널 창작물의 창작자에 대한 규정

동방조몽록은 동ㅂ방 프로젝트의 2차 창작물인 만큼, 반드시 이하의 내용이 들어간 콘텐츠를 제작해서는 안 됩니다:
  • 동방 프로젝트 자체의 평판에 위해를 가하는 작품.

  • 타인의 저작권을 침해하는 작품.

  • 2차 창작물을 공식이라 착각할 내용을 담은 작품.

  • 동방 프로젝트 원작에서 소스를 따 온 작품.

  • 동방 프로젝트 원작의 엔드 일러스트를 사용한 작품.

  • 개인의 믿음에 대한 자유를 침해하는 작품.

  • 다른 2차 창작물의 저작권을 침해한 작품.

  • 불법적이고 과도한 성적 묘사가 들어간 작품.

  • 게인이나 단체에 대한 혐오가 담긴 작품.

  • ZUN의 허락 없이 그의 사진을 사용한 작품.
2차 창작 가이드라인 공식의 전문은 여기에서 볼 수 있습니다.[touhou-project.news].

또한, 모드를 구성하는 코드에는 어떠한 악성 코드가 담겨서는 안 되며, 이를 위해 소스 코드를 공개적으로 내보이기 싫은 모드의 경우 저희 팀에게 검수를 요청받아야 합니다.

이외의 제안 사항이 있다면 디스코드 서버[discord.gg]에 문의해 주세요.
가이드
이제 본격적으로 모드 제작의 가이드를 시작하겠습니다. 먼저 주로 사용할 텍스트 에디터를 깔아 주세요. .rpy 파일을 다루기 위해, 기본적인 메모장보다는 VS Code 또는 노트패드 등의 툴을 추천합니다.

그 다음, 동방조몽록이 컴퓨터에 깔렸는지 확인하고, 공식 모드를 깔아 모드 폴더의 구조를 파악하세요.
모드 폴더 생성
"steamapps\workshop\content\2132480\3565758917" 로 들어가 동방조몽록 안의 폴더를 확인하세요.

폴더명 대신 보이는 숫자는 스팀의 식별 방식이니, 너무 걱정 마시길 바랍니다.


공식 모드 폴더 내부에서, "_upload_app" 를 찾은 후 실행 파일을 실행합니다. 그 뒤, 제작하실 모드의 이름을 좌측 메뉴에 적고 "Create Item" 버튼을 눌러 주세요. 그러면 WorkshopContent 폴더에 *모드 이름*.workshop.json 라는 파일이 생성될 겁니다. 이 파일을 미리 백업해 두시는 걸 추천드립니다.


해당 파일을 열고, "publishedfileid"에 있는 값을 복사해 "steamapps\workshop\content\2132480" 경로에 똑같은 이름의 폴더를 만들어 둡니다. 그리고 폴더 안에서 mod_info.rpyscript.rpy라는 파일을 만들고, mod_info.rpy 파일을 텍스트 에디터로 열어 줍니다.

모드 정보
mod_info.rpy 안에 들어가면 이런 코드가 보입니다.

init 99 python:
    tfi_official_mod_path = os.path.join(os.path.dirname(os.path.dirname(renpy.config.basedir)), "workshop/content/2132480/3565758917").replace("\\","/")
    global_game_mods["tfi_official_mod"] = {
      "start_scene": "tfi_official_mod_scene1",
      "description": _("This is a tutorial mod that also serves as a template for creating other mods. Open the folder steam/steamapps/workshop/content/2132480/3565758917 to look at the code and images in more detail."),
      "name": _("Tutorial mod"),
      "image": os.path.join(tfi_official_mod_path, "images/preview.png").replace("\\","/"),
    }

이 코드를 mod_info.rpy에 붙여넣은 후 아래와 같이 수정합니다.

  • global_game_mods["tfi_official_mod"] 안의 tfi_official_mod 라는 파일을 알파벳으로 된 무작위 이름으로 교체합니다. 이 파일이 곧 고유 식별 파일의 역할을 하며, 다른 모드와의 충돌을 피하게 해 줍니다.

  • start_scene의 값을 바꿉니다. 이것이 모드를 연 후 플레이어가 이동할 첫 장면입니다. 예를 들어, 위에 적어둔 모드의 이름을 복사해 뒷편에 "_scene1"을 붙여 주는 식입니다. 각 장면의 이름 또한 고유해야 합니다.

  • descriptionname은 모드의 다운 창에서 유저들에게 비칠 기본적인 정보입니다. 다만, 글자수 제한이 있다는 점은 유의 부탁드립니다. {size} 태그를 조정해 글자 크기를 조정할 수 있습니다.
    "{size=+2}큰 텍스트.{/size}"
    "{size=-2}작은 텍스트.{/size}"

  • image 값은 모드의 대표 이미지를 불러오는 값입니다. 없으면 없는 대로 두셔도 무방합니다.

결론적으로 아래와 같은 형태의 구조가 잡히게 됩니다:


mod_info.rpy의 수정이 끝났다면, script.rpy의 코드를 복사해 봅니다.
"init:" 블록
이 섹션에서는 모드에 대해 정의하고 싶은 변수를 포함합니다. 예를 들어, "define tfi_official_mod_dev = Character(_('TFI Main Dev'))""TFI Main Dev"라는 새로운 캐릭터를 만듭니다. 원하는 만큼 캐릭터를 만들 수 있지만, 변수 이름이 고유해야 하며, 다른 모드나 기본 게임의 변수와 충돌하지 않도록 해야 합니다. 또한, 이름 앞에 고유한 접두사를 사용하는 것이 좋습니다.

캐릭터가 상황에 따라 달라지는 경우, 변수를 정의하는 대신 캐릭터의 이름을 직접 사용하여 대사를 작성할 수 있습니다.
"TFI Main Dev" "Hello!"
"TFI Player" "Hey!"
The table below lists the variables currently used in our game, which you can reference as needed. As new characters are introduced to the story, we will add new variables.

Official Game Series Characters

Name
Tag
Name
Tag
Name
Tag
Name
Tag
Name
Tag
Reimu
rei
Marisa
mar
Byakuren
byak
Rin
orin
Rinnosuke
korin
Yukari
yuk
Mokou
moko
Kanako
kan
Youmu
youmu
Sanae
san
Kasen
kas
Clownpiece
clown
Eirin
eirin
Reisen
reisen
Nazrin
naz
Satori
sat
Utsuho
okuu
Suwako
suwa
Komachi
koma
Eiki
eiki
Chen
chen
Ran
ran
Aya
aya
Yuugi
yuugi
Mima
mima
Mamizou
mami
Sumireko
sum
Meiling
mei
Kokoro
kokoro
Sakuya
sak
Koishi
koi
Remilia
remi
Mizuchi
mizu
Seija
seija
Flandre
flan
Hecatia
heca

Secondary or Original Characters

Name
Tag
Name
Tag
Name
Tag
Name
Tag
Name
Tag
Girl
girl
Hitomu
hito
Bake-danuki
bake
Rabbit
usausa
Tengu
tengu
Kappa
kappa
Yamaraja
yamaraja
Itime-kozo
itime

"tfi_official_mod_path" 변수는 모드 폴더의 경로를 단축시킬 때 사용됩니다. 커스텀 이미지를 사용하는 경우 이 변수가 유용합니다. 변수 이름에서 "tfi_official_mod"를 모드 이름으로 바꾸고, 마지막 폴더 이름을 "workshop/content/2132480/3565758917"로 바꾸면 됩니다. 기본 게임의 리소스를 사용하는 경우, "3565758917/_game_resourses" 폴더에서 파일을 참조할 수 있지만, 고유한 파일을 참조할 경우 이 변수를 사용해야 합니다.

define mod_name_path = os.path.join(os.path.dirname(os.path.dirname(renpy.config.basedir)), "workshop/content/2132480/your_folder").replace("\\","/")
show image("[mod_name_path]/images/your_image1.png")
show image(Transform("images/backgrounds/hakurei_shrine_inter_d1.jpg", zoom=0.5))
장면과 대사
다음은 장면의 시작 부분입니다. label로 정의하며, 고유한 이름을 붙입니다. 앞에서 시작 장면을 "your mod_scene1"로 지정했으니, 여기서도 같은 이름으로 바꿔 주세요.

이제 필요한 건 줄거리를 짜고 대사를 적는 일뿐입니다. 텍스트는 대화창에 맞춰 나눠서 표시해야 합니다. 대사 앞에 캐릭터 식별자가 있으면 이름이 같이 뜨고, 없으면 텍스트만 표시됩니다.

두세 캐릭터가 동시에 대사할 수도 있습니다. 이럴 땐 대사 끝에 (multiple = 2)(multiple = 3)를 붙여 주면 됩니다.
rei "Good luck." (multiple = 2)
mar "See ya." (multiple = 2)
이미지
대사를 썼으면 이미지도 넣어야겠죠. 다소 복잡합니다. 아래 예시로 해결이 안 되면 RenPy 문서의 관련 항목[www.renpy.org]을 참고해 보세요.

배경과 정적 이미지

이미지를 띄우려면 show image("이미지 경로") 명령을 씁니다. 게임 해상도는 1920x1080이지만, 보통 원본 이미지 크기가 더 크므로 Transform()로 축소해야 합니다. 배경이나 CG는 0.5, 스프라이트는 0.6 비율을 썼지만 원하는 값으로 조절 가능합니다. 장면 시작 시에는 scene 명령으로 모든 이미지를 교체하는 게 좋습니다.

scene image(Transform("images/backgrounds/human_village1_m.jpg", zoom=0.5))
show image(Transform(tfi_official_mod_reimu_happy, zoom=0.6))

Dynamic Sprites
동방조몽록에서는 RenPy 기본 방식 외에도 최적화와 확장을 위해 새로운 레이어를 추가했습니다. 캐릭터 스프라이트를 조합해 표정이나 팔 움직임 등을 쉽게 바꿀 수 있게 한 겁니다.

공식 모드에서 아래와 같은 코드를 보실 수 있습니다:
$ chars = 2
$ elems = 13
$ scale = 0.6

$ pict_ = [
["none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png",],
["none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png","none.png",],
]

$ pict_1 = [
[
"none.png",
"none.png",
"none.png",
"sprites/Sakuya/sak_body1.png",
"sprites/Sakuya/sak_body1_brow1.png",
"sprites/Sakuya/sak_body1_eyes1_d.png",
"sprites/Sakuya/sak_body1_mouth2.png",
"sprites/Sakuya/sak_body1_leftarm2.png",
"sprites/Sakuya/sak_body1_rightarm6.png",
"none.png",
"none.png",
"none.png",
"none.png",
],
[
"none.png",
"none.png",
"none.png",
"sprites/Nazrin/naz_body1.png",
"sprites/Nazrin/naz_body1_brow1.png",
"sprites/Nazrin/naz_body1_eyes5_f.png",
"sprites/Nazrin/naz_body1_mouth18.png",
"sprites/Nazrin/naz_body1_arms1.png",
"none.png",
"none.png",
"none.png",
"none.png",
"none.png",
],
]

$ xp = [111000, 111000, 111000]
$ yp = [-130, -130, -130]
$ copy()
show screen _Pers_ onlayer add_l
여기서는 사쿠야와 나즈린의 스프라이트가 코드에서 어떻게 조립되는지 볼 수 있습니다. 레이어 방식을 써서 표정이나 팔 동작을 자유롭게 바꿀 수 있습니다. 특정 스프라이트 요소를 지정하기 전에 chars(캐릭터 수), elems(요소 수), scale(스프라이트 크기 비율) 변수를 정의해야 하며, "none.png"로 채운 배열을 pict_ 변수에 할당해야 합니다.

스프라이트 위치는 xpyp 두 변수로 조정합니다. 각각 가로·세로 위치를 담당하며, xp[1] = … 식으로 개별 설정하거나 xp = […, …, …]처럼 전체 배열로 지정할 수도 있습니다.

또한 이 방식은 스프라이트를 별도의 레이어에 표시합니다: "show screen Pers onlayer add_l". 이 레이어는 기본 이미지 레이어보다 앞에 있으므로 항상 다른 이미지 위에 보입니다. 반대로 스프라이트 앞에 이미지를 표시하려면 "onlayer add_l"를 붙여야 합니다.

하지만 필수는 아닙니다. 여러 레이어를 미리 합쳐 단일 스프라이트로 만들어 pict_1 변수에 넣고 쓰거나, RenPy의 기본 기능을 써도 무방합니다.

아래 예제는 스프라이트 요소를 바꾸거나, 모드 파일에 있는 완성된 레이무 스프라이트를 불러오는 방식입니다.
$ pict_[0][4] = "sprites/Sakuya/sak_body1_brow3.png" # 4
$ pict_[0][5] = "sprites/Sakuya/sak_body1_eyes3_f.png" # 5
$ pict_[0][6] = "sprites/Sakuya/sak_body1_mouth6.png" # 6
$ pict_[0][7] = "sprites/Sakuya/sak_body1_leftarm3.png" # 7
$ pict_[0][8] = "sprites/Sakuya/sak_body1_rightarm1.png" # 8

$ pict_[1][4] = "sprites/Nazrin/naz_body1_brow3.png" # 4
$ pict_[1][5] = "sprites/Nazrin/naz_body1_eyes3_f.png" # 5
$ pict_[1][6] = "sprites/Nazrin/naz_body1_mouth10.png" # 6
$ pict_[1][7] = "sprites/Nazrin/naz_body1_leftarm1.png" # 7
$ pict_[1][8] = "sprites/Nazrin/naz_body1_rightarm4.png" # 8

hide image(Transform(tfi_official_mod_reimu_happy, zoom=0.6))

show image(Transform(tfi_official_mod_reimu_sad, zoom=0.6)):
    xalign 0.8
    yalign -0.1
with dissolve

색상의 변경

이미지의 색상 구성을 바꿀 수도 있습니다. blur() 함수에 hue/b,brightness,saturation/b 라는 세 가지 값을 넣고, blur(image, 320, 120, 100)은 색조 320, 밝기 120, 채도 100으로 바꾸는 식입니다.
$ pict_[0][3] = blur("sprites/Sakuya/sak_body1.png", -10, -0.22, 0.9) # 3
$ pict_[0][4] = blur("sprites/Sakuya/sak_body1_brow3.png", -10, -0.22, 0.9) # 4
$ pict_[0][5] = blur("sprites/Sakuya/sak_body1_eyes3_f.png", -10, -0.22, 0.9) # 5
$ pict_[0][6] = blur("sprites/Sakuya/sak_body1_mouth6.png", -10, -0.22, 0.9) # 6
$ pict_[0][7] = blur("sprites/Sakuya/sak_body1_leftarm3.png", -10, -0.22, 0.9) # 7
$ pict_[0][8] = blur("sprites/Sakuya/sak_body1_rightarm1.png", -10, -0.22, 0.9) # 8

효과
RenPy는 이미지가 나타나거나 사라질 때 다양한 시각 효과를 지원합니다. 예시 코드에서 "with dissolve"로 끝나는 명령을 많이 보셨을 겁니다. 이는 이미지를 바로 띄우는 대신 1초 동안 전환 효과로 보여 주는 방식입니다.이외에도 dissolve_f라는 내장 버전이 있는데, 0.25초 동안 지속됩니다. 주로 스프라이트 요소 교체 시, 게임 템포가 늘어지지 않게 쓰입니다. 직접 변수를 정의해서 새로운 전환 효과도 만들 수 있습니다:
define dissolve_five = Dissolve(5) # a 5-second transition
define dissolve_five = Dissolve(5) # 5초 전환 효과
보통은 해당 효과를 중심적으로 쓰지만, 비슷한 방식으로 다른 효과도 적용할 수 있습니다. 더 많은 효과는 RenPy 문서[www.renpy.org]를 참고하세요.
음악과 음향
음악은 renpy.music.play, renpy.music.stop 라는 명령어로 재생·정지할 수 있습니다. 파일 경로를 지정한 뒤, 어떤 채널에서 재생할지와 시작·종료 시의 효과를 설정할 수 있습니다.
$ renpy.music.play("sounds/ambience/crowd_medium1.ogg", channel="music_2", synchro_start=True, fadein=2.0)
$ renpy.music.stop(channel="music_2", fadeout=1.5)
음악 메뉴에는 music, music_1, music_2 라는 세 가지 채널이 있고, 일회성 효과음을 위한 sound 채널이 따로 있습니다.
선택지
게임을 만들 당시 기본 선택지 메뉴를 수정해 두었기에, 플레이어에게 선택을 종용하는 문구까지 함께 보여주도록 바꿨습니다. 질문을 표시하려면 menu_question 변수를 설정해야 합니다.
$ menu_question = _("Do you need an extra round?")

$ quick_menu = False
window hide dissolve

menu:
    with dissolve
    "Yes, please!":
      $ quick_menu = True
      window show dissolve
      jump tfi_official_mod_scene1
    "Nah, I got it.":
      $ quick_menu = True
      window show dissolve
변수 사용
플레이어의 선택을 기록하려면 변수를 쓸 수 있습니다. 변수는 문자열(= "one"), 숫자(= 1), 논리값(= True) 등으로 만들 수 있고, iffor 같은 조건문으로 다룰 수 있습니다. 자세한 내용은 RenPy 문서[www.renpy.org]에서 확인하세요.
$ menu_question = _("One or two?")

menu:
    with dissolve
    "One!":
      $ mod_name_player_choice = 1
      jump mod_name_scene_player_pick_one
    "Two!":
      $ mod_name_player_choice = 2
      jump mod_name_scene_player_pick_two
if your_mod_condition == 2:
    jump destitute1_scene2_5
else:
    jump destitute1_scene1_5
번역
참고 사항: 저희 팀원들 중 몇 분은 본편 외에도 마음에 드는 모드가 있다면 직접 번역할 의향이 있다고 합니다. 언제나 디스코드 서버[discord.gg]로 연락해 주세요.

모드를 여러 언어로 제작하려면 번역 파일을 생성해야 합니다. 이를 위해 RenPy 8.3.7[www.renpy.org]을 내려받아 게임 경로를 설정하고 원하는 언어로 번역 파일을 생성해 주세요.


번역은 대사뿐만 아니라 변수나 캐릭터 이름에도 적용할 수 있습니다. 이때는 변수 선언 시 텍스트를 (...) 안에 넣으면 됩니다.
define tfi_official_mod_dev = Character(_('TFI Main Dev'))
단, 일부 변수는 오류를 낼 수 있으므로 대사 번역 파일만 생성하는 걸 권장합니다. 대사 파일은 번역 식별자가 필요하고, 변수 텍스트 자체가 식별자로 쓰이기 때문입니다.

# translatable line
translate english prologue_scene_0_89ef4bf6:
    "Humanity has always been afraid of the unknown."

# translatable variable
translate english strings:
    old "Продолжение следует..."
    new "To be continued..."
생성이 완료되었다면 tl 폴더를 모드 폴더로 옮기고 새 파일로 채우시면 됩니다.

유저들을 위해 모드의 시작 지점에 지원 가능한 언어를 표시하기를 권장합니다.

menu:
    with dissolve
    "{font=fonts/Merriweather-Regular.ttf}Я хочу играть на Русском{/font}":
      $ renpy.change_language("russian")
    "{font=fonts/Merriweather-Regular.ttf}I want to play in English{/font}":
      $ renpy.change_language("english")
    "{font=fonts/SourceHanSansCN-Regular.otf}{size=26}以简体中文开始{/size}{/font}":
      $ renpy.change_language("schinese")
    "{font=fonts/NotoSansJP-Regular.ttf}{size=26}日本語でプレイする{/size}{/font}":
      $ renpy.change_language("japanese")
현재 이 게임은 9개 언어를 지원합니다: 러시아어 (russian), 영어 (english), 프랑스어 (french), 스페인어 (spanish), 브라질 포르투갈어 (portuguese), 번체 중국어 (tchinese), 간체 중국어 (schinese), 한국어 (korean), 일본어 (japanese). 만약 이 외의 언어를 쓰려면 새로운 폰트를 모드에 포함해야 합니다.
부가 기능
카메라 활용

캐릭터는 별도의 화면에 표시되므로, 확대/축소를 쓰려면 기본 카메라 레이어와 동시에 움직여야 합니다.

camera:
    perspective True
    gl_depth True
    zpos -1100 xpos -375 ypos -380
    easein 3.0 zpos 0 xpos 0 ypos 0
show layer add_l:
    perspective True
    gl_depth True
    zpos -1100 xpos -375 ypos -380
    easein 3.0 zpos 0 xpos 0 ypos 0

날짜와 시간
이야기가 날짜나 시간 단위로 나뉘는 경우, 게임에 내장된 화면을 활용해 장소·날짜·시간을 표시할 수 있습니다.

$ date_place = _("Muenzuka")
$ date_date = _("April 17, 2018")
$ date_time = _("Evening, a few hours later")
show screen date with dissolve

로딩 화면
스프라이트 재조립이나 색상 변경이 많으면 처리 시간이 조금 걸릴 수 있습니다. 이를 덜 눈에 띄게 하기 위해 "Girls are praying…" 문구가 뜨는 전환 화면을 만들었습니다. show screen loadinghide screen loading 순으로 호출하면 됩니다. 대화창은 다음 예시처럼 숨겼다가 다시 보여 주면 전환이 자연스럽습니다.

$ quick_menu = False
window hide dissolve

$ quick_menu = True
window show dissolve
모드 테스트
창작마당 업로드 전에 모드가 잘 동작하는지 확인해야겠죠. 개발자 모드를 쓰면 편리합니다. init 블록에 아래 코드를 넣어 주세요:
define config.developer = True
define config.default_developer = True
이제 Shift + R을 누르면 파일 수정 후 게임이 같은 지점에서 다시 로드됩니다. 이로서 코드를 고치고 바로 결과를 확인할 수 있습니다. 다만 업로드 전에는 꼭 이 코드를 지워 주세요.
모드 업로드
마지막으로, 정말 모든 준비가 끝났다면:

1. 업로드 실행 파일의 모드 폴더 안에 모드 파일을 넣습니다. 미리보기 이미지는 정사각형, 최소 500x500 해상도여야 합니다.

2. 필요한 정보를 입력합니다. _upload_app 폴더 전체를 2132480 폴더 밖으로 옮겨 두면 다른 모드와의 충돌을 피할 수 있습니다.

3. 설명을 적고 Submit 버튼을 누릅니다. 입력한 내용은 .workshop.json 파일에 저장됩니다.

혹여나 더 궁금하신 점이 있다면, 이 쪽에서 가이드 내용을 보충해드리도록 하겠습니다. 성공적인 모드 제작을 기원하며, 좋은 작품을 만드실 수 있기를 바랍니다.