# ichni Official 剧本编写与 Yarn Script 制作手册 > 适用对象:剧本编写者、叙事设计师、负责初步拆分剧情 Block 的策划 > 适用项目:`ichni Official` > 剧情工具:Yarn Spinner `3.2.1` + Unity Localization > 最后更新:2026-07-21 本文说明如何把一段剧情写成可交付给程序员的 Yarn Scripts。本文只描述当前项目已经存在的语法和运行规则;所有玩家可见文本使用 Unity Localization,也不要使用 Yarn 原生临时变量保存本项目的剧情进度。 ## 1. 先理解游戏中的剧情结构 一个章节由一份 `StoryData` 定义。`StoryData` 中预先排好完整剧情路线,运行时根据玩家进度把每个 Block 显示为以下状态之一: - `Locked`:前置流程或额外条件尚未满足,不能点击。 - `Current`:当前可进入。 - `Completed`:已经完成,仍可点击回顾。 - `Forbidden`:玩家的分支选择已经排除了这条路线;Block 保留显示,但不能点击。 Block 有三种类型: | 类型 | 用途 | 剧本编写者需要提供的内容 | |---|---|---| | `TextBlock` | 播放 Yarn 对话 | Yarn 文件、入口 Node、Block 标题、重要性、变量与分支说明 | | `SongBlock` | 进入指定歌曲的选曲界面 | 目标歌曲及它在叙事中的意义;不需要单独写 Yarn | | `TutorialBlock` | 弹出“游玩/跳过教程”的选择 | 教程前后文案与目标教程;不需要单独写 Yarn | `TextBlock` 又分为: - `Important`:主线转折、重要选择、适合作为 Timeline 回滚点的内容。 - `Secondary`:补充信息、支线或非关键叙事。 剧情图不是运行时自动排版。策划和程序员会把所有可能路线预先放入 `StoryData`;玩家的选择通过章节变量、`Unlock Condition` 和 `Forbidden Condition` 动态开放或禁用对应 Block。 ## 2. 存档与对话事务规则 每次进入一个 `TextBlock`,系统会建立一份“本次对话事务”: 1. 进入前复制当前章节剧情存档。 2. 对话中的 `set_*`、选项记录和 Block 进度先只修改内存。 3. 对话正常走到结束时,一次性提交本次变化并把 TextBlock 标记为 `Completed`。 4. 玩家中途按 `Back`,或 Yarn 启动失败时,恢复进入前的章节快照。 因此: - 没有正常结束的对话不会保存其中的选项和剧情变量。 - 再次进入已完成的 TextBlock 时,已经记忆的选项会自动采用,不再显示选择页面。 - Timeline 回滚到某个 Marker 时,会恢复该列之前的 Block、选项和章节变量。 - Timeline 回滚不会撤销歌曲成绩、`isTried`、设置、Offline 解锁 Key 或其它章节。 - `grant_unlock`、`unlock_song` 和 `show_message` 会等到对话正常提交后才执行;中途退出不会留下全局解锁或孤立弹窗。 ## 3. 命名规范 稳定 ID 一旦进入测试存档就不要随意重命名。玩家可见文本必须进入 Unity Localization;稳定 ID 不翻译。 | 对象 | 规范 | 示例 | |---|---|---| | Chapter Index | 必须与 `ChapterSelectionUnit`、`StoryData`、`StoryManager` 完全一致 | `Chapter0` | | StoryData | `StoryData_Chapter{n}` | `StoryData_Chapter0` | | Yarn Project | `Chapter{n}.yarnproject` | `Chapter0.yarnproject` | | Yarn 文件 | `C{n}_{主题}.yarn` | `C0_SignalCheckpoint.yarn` | | Yarn 入口 Node | `C{n}_{Block用途}`,在整个 Yarn Project 内唯一 | `C0_SignalCheckpoint` | | Yarn 内部 Node | 在入口名后增加语义后缀 | `C0_SignalCheckpoint_Record` | | Block ID | 小写英文、数字、下划线;建议包含章节、类型和用途 | `c0_text_signal_checkpoint` | | 章节变量 | `c{n}_{范围}_{事实}`,使用小写英文、数字和下划线 | `c0_signal_route` | | Timeline Marker ID | `c{n}_marker_{时间或事件}` | `c0_marker_day_01` | | Block 标题本地化 Key | `story_c{n}_block_{用途}_title` | `story_c0_block_signal_title` | | Timeline 文本 Key | `story_c{n}_timeline_{时间或事件}` | `story_c0_timeline_day_01` | | Character ID | 与 Yarn 台词冒号前的名字完全一致,区分大小写 | `Asahi` | | Character Name Key | `character_{角色}_name` | `character_asahi_name` | | Unlock Key | 遵守 Offline Key 规范:小写英文、数字、下划线,以字母开头 | `story_ch0_prologue_completed` | 不要使用 `Record`、`Start`、`End` 这类过短的 Yarn Node 名。Node 名在同一 Yarn Project 内不是文件局部名称,多个文件出现同名 Node 会冲突。 变量应描述已经发生的事实,而不是按钮动作: - 推荐:`c0_signal_observed`、`c0_signal_route`、`c0_intro_read`。 - 不推荐:`clicked_left`、`do_next`、`temp`、`choice1`。 Bool 变量尽量使用正向语义,例如 `c0_tutorial_resolved`,避免 `not_skipped` 这类双重否定。Int 路线编码必须在交付说明中列出每个数字的含义,例如 `0 = 未选择,1 = 观测,2 = 保护`。 ## 4. Yarn 文件的基本结构 最小可运行 Node: ```yarn title: C0_Intro --- SLS: 记录开始。 Asahi: 我已经准备好了。 === ``` 规则: - `title:` 后是 Node 名,必须在当前 Yarn Project 内唯一。 - `---` 表示正文开始。 - `===` 表示 Node 结束。 - `//` 是只给作者和程序员看的注释。 - `Character: 台词` 中冒号前的名称会成为 `CharacterName`。 - 没有角色名前缀的文本按旁白处理。 - 不要手工删除或复用 `#line:xxxxxxx`。这些 Line ID 由 Yarn 工具添加,是本地化和选项记忆的稳定身份。 一个文件可以包含多个 Node,但建议“一份入口剧情 + 它的内部小分段”放在同一文件;跨 Block 的入口 Node 则分文件保存。 ## 5. 流程、跳转与选项 ### 5.1 跳转 ```yarn title: C0_Intro --- SLS: 先从这里开始。 <> === title: C0_Intro_Continue --- Asahi: 我们继续吧。 === ``` `<>` 会转到另一个 Node。目标必须存在,名称必须完全一致。 `<>` 会立即结束当前 Yarn 对话。只有确认这就是本 TextBlock 的正常终点时才使用;正常结束会提交事务并完成 Block。 ### 5.2 玩家选项 ```yarn SLS: 你打算怎样处理信号? -> 记录信号的变化 <> <> -> 优先保护观测装置 <> <> ``` 注意: - 选项正文必须缩进。 - 当前 `DialogUIPage` 只有 4 个 `ChoiceButton`,一个选择组最多写 4 个可显示选项;需要更多时先与程序员确认 UI 扩容。 - 选项的 Line ID 会被保存。重新排序选项不会改变记忆;增加/删除选项或更换 Line ID 会形成新的选项组,玩家可能需要重新选择。 - 已保存的选项如果在新版台本中消失或变为不可用,系统会删除旧记录并重新显示选择页面。 ### 5.3 条件选项 ```yarn -> 询问朝日的过去 <> Asahi: 我现在可以谈起那段记忆了。 ``` 条件不满足时,该选项由 Yarn 标记为不可用。不要用 Yarn 原生 `$variable` 作为条件。 ## 6. 条件语法 ```yarn <> SLS: 你选择了观测路线。 <> SLS: 你选择了保护路线。 <> SLS: 目前还没有选择路线。 <> ``` 常用运算: - 相等/不等:`==`、`!=` - 数值比较:`>`、`>=`、`<`、`<=` - 逻辑组合:`and`、`or`、`not` - Bool 函数可直接作为条件:`<>` 复杂条件应优先拆成可读的事实变量。不要把大量业务规则全部塞在一行 Yarn 表达式中;对应 Block 的开放和禁用仍应由程序员在 `StoryData` 条件树中复核。 ## 7. 永久章节变量:全部命令与函数 新台本禁止使用: ```yarn <> <> <> ``` 这些变量不受 ichni Official 的章节存档、对话事务和 Timeline 回滚管理。 ### 7.1 Bool ```yarn <> <> SLS: 你已经读过开场。 <> ``` | 接口 | 用途 | |---|---| | `<>` | 写入 Bool | | `get_bool("key")` | 读取 Bool;用于 `if` 或表达式 | ### 7.2 Int ```yarn <> <> <= 3>> SLS: 共鸣已经足够强烈。 <> ``` | 接口 | 用途 | |---|---| | `<>` | 写入整数语义的数值 | | `<>` | 在当前值上增加整数 | | `get_int("key")` | 读取整数 | ### 7.3 Float ```yarn <> <> <= 0.75>> SLS: 信号强度已达到阈值。 <> ``` | 接口 | 用途 | |---|---| | `<>` | 写入小数 | | `<>` | 在当前值上增加小数 | | `get_float("key")` | 读取小数 | ### 7.4 String ```yarn <> <> Asahi: 信号来自月面。 <> ``` | 接口 | 用途 | |---|---| | `<>` | 写入字符串 | | `get_string("key")` | 读取字符串 | ### 7.5 通用变量操作 | 接口 | 用途 | |---|---| | `has_variable("key")` | 存档中有该变量,或 `StoryData.Initial Variables` 声明了该 Key 时返回 `true` | | `<>` | 删除存档覆盖值;若 `Initial Variables` 有同名定义,之后读取会重新得到默认值 | 所有变量只属于当前章节。不要依赖 Chapter 0 的变量直接解锁 Chapter 1;跨章节内容开放应使用 `Unlock Key`。 ## 8. 立绘命令 立绘坐标采用推荐范围 `-100` 到 `100`,`(0, 0)` 为舞台中心;角色自己的 `CharacterData.positionOffset` 会额外叠加。当前代码不会强制 Clamp,超出范围可能让立绘离开画面。 | 命令 | 作用 | |---|---| | `<>` | 角色不在场时创建;已在场时设置位置和表情 | | `<>` | 操作当前说话者;必须已经显示过一行带角色名的台词 | | `<>` | 只改位置;角色不在场时以 `normal` 创建 | | `<>` | 只改表情;角色必须已在场 | | `<>` | 隐藏并移除指定角色 | | `<>` | 清空全部立绘 | | `<>` | 在指定秒数内平滑移动 | | `<>` | 跳跃;参数为高度、次数、总时长 | | `<>` | 横向震动;参数为时长、强度、频率 | 动画命令启动 DOTween 后会立即让 Yarn 继续执行,不会自动等待动画完成。若下一句必须等动画结束,可以使用 Yarn Spinner 内置命令: ```yarn <> <> Asahi: 我到了。 ``` 建议每个独立 TextBlock 开头明确建立所需立绘,结束前按设计调用 `hide_portrait` 或 `hide_all_portraits`,不要隐式依赖上一次对话留下的舞台状态。 ## 9. 解锁、提示和调试命令 | 命令 | 作用 | 是否等待对话正常提交 | |---|---|---| | `<>` | 授予通用 Offline 内容 Key,不自动弹提示 | 是 | | `<>` | 兼容旧流程:授予 Key,并在首次授予时显示歌曲提示 | 是 | | `<>` | 从指定 Unity String Table 解析并显示 MessageBox | 是 | | `<>` | 输出 `info`、`warning` 或 `error` 日志 | 否;只影响 Console | 推荐把通用解锁和玩家提示明确拆开: ```yarn <> <> ``` `unlock_song` 主要用于兼容现有内容。它当前的专用提示仍可能显示内部 Key,正式文本优先使用 `grant_unlock + show_message`。 ## 10. 完整分支案例 ### 10.1 第一段主线 ```yarn title: C0_SignalIntro --- <> SLS: 记录开始。异常信号将在一分钟后抵达。 Asahi: 我们要观察它,还是先保护装置? <> <> === ``` ### 10.2 带 Timeline Marker 的重要选择 ```yarn title: C0_SignalCheckpoint --- <> SLS: 前置记录已经同步。现在决定路线。 <> SLS: 前置记录缺失,请程序员检查 StoryData 连接。 <> -> 继续观测 <> <> -> 保护装置 <> <> === title: C0_SignalCheckpoint_Observe --- Asahi: 我会记录每一次波形变化。 === title: C0_SignalCheckpoint_Protect --- Asahi: 我先切断装置与外界的连接。 === ``` 程序员会把这个 Important TextBlock 配置为 Timeline Marker 的 Anchor。第一次完成后再次进入,会自动采用第一次保存的选项;回滚到该 Marker 后,选项和 `c0_signal_route` 都恢复到该列之前。 ### 10.3 两条互斥分支 观测分支: ```yarn title: C0_SignalObserve --- <> SLS: 观测结果已经归档。 <> <> <> === ``` 保护分支: ```yarn title: C0_SignalProtect --- <> SLS: 保护程序已经完成。 <> <> <> === ``` 程序员应同时配置: - 观测 Block:`Unlock c0_signal_route == 1`,`Forbidden c0_signal_route == 2`。 - 保护 Block:`Unlock c0_signal_route == 2`,`Forbidden c0_signal_route == 1`。 ### 10.4 汇合段与内容解锁 ```yarn title: C0_SignalConvergence --- <> SLS: 你从时间的回声中取得了答案。 <> SLS: 你为下一次观测保留了机会。 <> SLS: 没有有效路线记录,请检查剧情配置。 <> <> <> <> <> === ``` 仓库中还有一组可直接参考的 `1-1-2-1` 示例: - `Assets/Story/Chapter0/C0_RollbackDemo_Intro.yarn` - `Assets/Story/Chapter0/C0_RollbackDemo_Checkpoint.yarn` - `Assets/Story/Chapter0/C0_RollbackDemo_Observe.yarn` - `Assets/Story/Chapter0/C0_RollbackDemo_Protect.yarn` - `Assets/Story/Chapter0/C0_RollbackDemo_Convergence.yarn` ## 11. 本地化交付规则 Yarn 台词使用 Unity Localization。当前 `Chapter0.yarnproject` 的 `baseLanguage` 是 `en`,因此正式生产有两种选择: 1. Yarn 源文件直接写英文,翻译到 `zh-CN`、`ja`、`ko`、`vi-VN`。 2. 如果团队以中文为原稿,程序员必须在批量生产前把 Yarn Project 的 Base Language 一次性改为 `zh-CN`,再建立其它语言翻译。 不要在已经大量翻译后频繁切换 Base Language,也不要让“配置为 English 的项目”长期保存中文源文本。 交付给程序员时,每个 Yarn 文件还应附带: - 所属 Chapter 和目标 Block ID。 - 入口 Node 名。 - Important / Secondary。 - 新增变量清单、类型、默认值和语义。 - 路线编号含义。 - 哪些 Block 应被开放或 Forbidden。 - 是否需要 Timeline Marker,以及显示的时间文本。 - 需要授予的 Unlock Key 和 Message 文案 Key。 - 新角色、表情和立绘资源需求。 翻译人员只修改 String Table 中的玩家可见文本,不修改 Line ID、Block ID、Node 名、变量 Key、Character ID 或 Unlock Key。 ## 12. 剧本自检清单 提交前逐项确认: 1. 每个文件和 Node 都遵守命名规范,Node 在当前 Yarn Project 内唯一。 2. 每个入口 Node 都有 `---` 和 `===`,所有 `if` 都有 `endif`。 3. 选项缩进正确,单组选项不超过当前 UI 的 4 个按钮。 4. 没有 `<>`、`<>` 或 `$variable`。 5. 所有剧情变量使用 `set_*` / `get_*`,并附带类型与默认值说明。 6. 互斥路线同时说明 Unlock 和 Forbidden 规则。 7. 重要选择说明是否需要 Timeline Marker。 8. 立绘 Character ID 与 `CharacterData.characterId` 完全一致。 9. 需要等待的立绘动画显式添加 `<>`。 10. 玩家可见标题、提示和时间文本提供 Unity Localization Key。 11. Line ID 由 Yarn 工具生成后不再手工改动。 12. 对话正常结束路径、Back 中断路径和已记忆选项回顾路径都能成立。 ## 13. 常见错误 | 现象 | 常见原因 | |---|---| | TextBlock 点击后没有对话 | `Yarn Node` 为空、Node 名拼错、Yarn Project 编译失败 | | 变量在重开后没有恢复 | 使用了 `$variable`、`declare` 或原生 `set` | | 第二次进入仍显示选项 | Option Line ID 变化、选项组结构变化、上次对话中途退出 | | 某条分支仍可点击 | 只写了 Yarn `if`,没有在 StoryData 配置 Forbidden Condition | | 立绘命令找不到角色 | Character ID 大小写不一致,或 CharacterRegistry 未登记 | | `set_portrait_current` 无效果 | 命令执行前还没有显示任何带角色名的台词 | | Marker 回滚按钮不可用 | 该 Marker 的“列之前快照”尚未创建,或 Marker 配置错误 | | 翻译后选项记忆错乱 | 不应发生;系统按 Line ID 记忆。若发生,优先检查 Line ID 是否被重建 |