Touhou: Fading Illusion

Touhou: Fading Illusion

Not enough ratings
模组制作指南
By GiMark
本指南包含了创建一个简易模组所需的全部信息。
   
Award
Favorite
Favorited
Unfavorite
前言
模组爱好者们好!本指南包含了创建模组所需的基本信息。在开始制作模组之前,我们建议你先了解东方同人规约。

注意:目前模组上传工具仅支持Windows。

注意:_game_resources文件夹中的图片以及本指南的部分内容可能包含剧透。

创作限制
东方Project原作使用规定

《东方凋梦录》是一部东方同人作品,因此你的模组必须遵循原作者ZUN的要求。具体而言,你的模组不能包含以下内容:

  • 任何有损东方Project声誉的内容;

  • 任何侵犯其他知识产权的内容;

  • 任何意图将你的作品与官方作品混淆的内容;

  • 任何从东方Project官作中提取的内容;

  • 东方Project官作的结局场景;

  • 任何意图在虚构范围外宣传个人信仰的内容;

  • 未经创作者许可的其他东方同人内容;

  • 非法色情内容;

  • 任何煽动对个人或群体仇恨的内容;

  • 未经ZUN许可的本人照片。
你可以在此处[thwiki.cc]查阅完整的东方同人规约。

此外,你的模组不得包含任何恶意代码。你的模组必须包含源代码(.rpy文件)以供审查。如果你不希望分发源码,请联系我们,我们将在私下审查源码后允许你发布模组。开始创建模组即表示你已阅读并同意此条款。如你的模组违反此条款,我们保留隐藏或屏蔽你的模组的权利。

如果你有任何问题,请在QQ群882402531或Discord服务器[discord.gg]与我们联系。
快速开始
如果你同意上述内容,那我们就开始吧。首先,你需要一个方便的文本编辑器,因为接下来需要编写大量的文本和代码。“.rpy”脚本文件可以用系统自带的记事本打开,但我们推荐更高级的工具,比如VSCode。VSCode是免费使用的。

之后,确保你已安装东方凋梦录并订阅官方模组。官方模组可帮助你理解模组开发过程并提供模组上传工具。
创建模组文件夹
打开《东方凋梦录》安装盘上的Steam或SteamLibrary文件夹,找到路径为steamapps\workshop\content\2132480\3565758917的文件夹。这个文件夹应该在下载完官方模组后出现。
数字没有特别之处,这些是Steam的标识符,当你创建自己的模组时你也会得到一个标识符。

在模组文件夹里,打开_upload_app文件夹并启动SteamWorkshopUploader.exe。在左侧菜单中输入你的模组名称,然后点击右下角的Create Item按钮。之后,一个存放你模组内容的文件夹将出现在WorkshopContent文件夹中,其中还有一个名称为你的模组名.workshop.json的文件。这就是我们需要的文件。(我们建议你填写完毕后将它保存到某个地方)
打开该文件,复制publishedfileid字段的值,并在steamapps\workshop\content\2132480文件夹下创建一个名为publishedfileid的文件夹。玩家将下载你的模组到这个相同路径,因此推荐在这里开发。在你的模组文件夹中创建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!"
下表列出了我们游戏目前使用的变量,你可以按需引用。随着新角色加入,我们将添加新的变量。

官作角色

名称
标签
名称
标签
名称
标签
名称
标签
名称
标签
博丽灵梦
rei
雾雨魔理沙
mar
圣白莲
byak
火焰猫燐
orin
森近霖之助
korin
八云紫
yuk
藤原妹红
moko
八坂神奈子
kan
魂魄妖梦
youmu
东风谷早苗
san
茨木华扇
kas
克劳恩皮丝
clown
八意永琳
eirin
铃仙·优昙华院·因幡
reisen
娜兹玲
naz
古明地觉
sat
灵乌路空
okuu
洩矢诹访子
suwa
小野冢小町
koma
四季映姬·夜摩仙那度
eiki
chen
八云蓝
ran
射命丸文
aya
星熊勇仪
yuugi
魅魔
mima
二岩猯藏
mami
宇佐见堇子
sum
红美铃
mei
秦心
kokoro
十六夜咲夜
sak
古明地恋
koi
蕾米莉亚·斯卡蕾特
remi
宫出口瑞灵
mizu
鬼人正邪
seija
芙兰朵露·斯卡蕾特
flan
赫卡提亚·拉碧斯拉祖利
heca

次要或原创角色

名称
标签
名称
标签
名称
标签
名称
标签
名称
T标签ag
少女
girl
幺梦
hito
妖怪狸
bake
兔子
usausa
天狗
tengu
河童
kappa
阎魔
yamaraja
一目小僧
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语句标示了一个场景的开始。你需要为场景指定唯一名称。之前你已将初始场景设置为你的模组名_scene1,所以现在将label的名称替换为初始场景名。

一切就绪后,剩下的就是构思一个情节并写下来。文本应分段显示在屏幕上以适应对话窗口。如果在一行台词前给定了角色标识符,角色名将与文本一同显示,否则只显示文本。

游戏还支持两或三个角色同时说话。你只需要在台词右侧相应添加(multiple = 2)(multiple = 3)
rei "Good luck." (multiple = 2)
mar "See ya." (multiple = 2)
图像
假设你写好了对话,但怎么添加图片?这个问题要复杂一些。如果你在下面的示例中找不到问题的答案,我们建议你查阅Ren'Py文档的相应部分[doc.renpy.cn]

背景和静态图像
你可以使用show image("图片路径")语句在屏幕上显示一张图片。由于游戏的目标分辨率是1920x1080,而许多图片有更高的分辨率,因此在显示前需要调用Transform()函数进行缩放。对于背景和插图,我们使用的缩放系数是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))

动态立绘
在《东方凋梦录》中,我们使用了非标准的Ren'Py方法来优化游戏和添加演出。具体地,我们为图像显示系统添加了一个新的图层,用于显示角色分层立绘。
在我们的模组中,你可以看到以下代码:
$ 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(立绘缩放系数),并为变量pict_分配与立绘类似的数组,其中每个图像均用"none.png"

立绘的位置由两个变量决定:xpyp,分布控制水平和垂直位置。这两个变量可以为每个立绘单独设置,例如xp[1] = ...,或一次性为所有立绘设置,如xp = [..., ..., ...]

此外,我们使用一个单独的图层来显示立绘:show screen _Pers_ onlayer add_l。该图层位于标准图像图层的前方,因此立绘将始终显示在其他图像的前面。如果你想在立绘前面显示图像,你需要使用onlayer add_l

以上所有都是非必需的。你可以预先将一个完整的立绘从各图层中组合好,以类似的方式赋值给变量pict_1,然后切换到其他立绘。或者使用标准Ren'Py方法。

下面的代码展示了更改立绘元素以及从模组文件组合“完整”灵梦立绘的示例。
$ 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()函数更改图像的色调。该函数有四个入参,后3个分别是hue(色相)、brightness(亮度)、saturation(饱和度)。例如,blur(image, 320, 120, 100)将把image的色相改为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
特效
Ren'Py支持多种图像出现或消失的视觉效果。你可能已经注意到,我们提供的许多示例语句都以with dissolve结尾。这会使图像出现或消失时带1秒的过渡效果。还有一个内置版本dissolve_f,它持续0.25秒,更改立绘元素时可避免特效过长。你可以使用类似的变量创建自己的过渡效果:
define dissolve_five = Dissolve(5) # a 5-second transition
我们基本上只使用这些特效。类似地,你也可以使用其他特效,请参考Ren'Py文档中关于特效的更多信息[doc.renpy.cn]
音乐和音效
你可以使用renpy.music.playrenpy.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)
有三个音乐通道:musicmusic_1music_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关键字来操作变量。你可以在Ren'Py文档的相关部分[doc.renpy.cn]了解更多关于变量的信息。
$ 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
翻译
注意:我们团队的部分成员表示愿意翻译喜欢的模组。如果你有兴趣,请在QQ群882402531或Discord服务器[discord.gg]与我们联系。

如果你计划创建一个支持多语言的模组,你需要生成翻译文件。要生成它们,你需要下载Ren'Py 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

加载界面
如果立绘的“重组”或颜色变化很大,可能会导致游戏出现轻微延迟,因为Ren'Py需要时间来处理并将所有图像单独放置在屏幕上。为了减缓体感延迟,我们创建了一个带有“少女祈祷中”的过渡界面。要使用它,你需要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文件中。


如果你有任何问题,请在本指南的评论区中反馈,以便我们更新信息。祝你好运!