# ichni Official 剧情系统整合、配置与测试手册 > 适用对象:程序员、Technical Designer、负责 Unity 配置和剧情 QA 的成员 > 适用项目:`ichni Official` > 当前实现:Yarn Spinner `3.2.1`、Unity Localization、ES3、DOTween > 最后更新:2026-07-21 本文说明如何把剧本编写者交付的 Yarn Scripts 整合进游戏,并配置 StoryData、角色、Timeline、Helper、存档和测试流程。CreatorStudio 不参与本套任务;只有未来任务明确触及节奏游戏元素、共享序列化结构或 Creator 导入流程时,才需要重新评估跨项目同步。 ## 1. 系统边界与数据流 ```text StoryData(静态剧情图、条件、Marker、默认变量) -> StoryTreeController(生成 Block、推导状态、处理回滚) -> TextBlockView -> StoryDialogueController -> DialogueRunner -> VNDialoguePresenter / VNPortraitStage / DialogUIPage -> StoryProgress -> StorySaveModule -> ES3 -> SongBlockView -> SongSaves.isTried -> TutorialBlockView -> TutorialFlowController -> StoryTimelineController(Marker 投影、进度、回滚入口) -> StoryHelperController(全局 HelperData、条件对话池) ``` 三套持久化数据必须保持隔离: | 数据域 | 主要内容 | 回滚本章时是否变化 | |---|---|---| | `SongSaveModule` | `isTried`、`isCompleted`、Accuracy、MaxCombo、FC/AP、Chart Revision | 否 | | `StorySaveModule` | Completed Blocks、章节变量、选项、Marker 快照 | 是,仅当前章节 | | `UnlockSaveModule` | Offline 内容 Key | 否 | ## 2. 主要代码和资产 | 路径 | 职责 | |---|---| | `Assets/Scripts/NewStorySystem/Data/StoryData.cs` | 运行时章节数据与查询 | | `Assets/Scripts/NewStorySystem/Data/StoryData.Editor.cs` | StoryData Validation 与 Inspector Preview | | `Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs` | Block 类型、布局、条件和类型专属字段 | | `Assets/Scripts/NewStorySystem/Data/StoryConditionNode.cs` | AND / OR / NOT / Variable / BlockCompleted 条件树 | | `Assets/Scripts/NewStorySystem/Tree/StoryTreeController.cs` | 生成、状态推导、连接、完成、回滚 | | `Assets/Scripts/NewStorySystem/Dialogue/StoryDialogueController.cs` | TextBlock 对话事务与 Yarn Project 切换 | | `Assets/Scripts/NewStorySystem/Dialogue/VNDialoguePresenter.cs` | 台词、选项、快进、记录 | | `Assets/Scripts/NewStorySystem/Dialogue/VNPortraitStage.cs` | 动态立绘实例和动画入口 | | `Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs` | 按章节 ES3 存档与事务 | | `Assets/Scripts/NewStorySystem/Timeline/StoryTimelineController.cs` | Marker、刻度同步、进度和确认回滚 | | `Assets/Scripts/NewStorySystem/Helper/StoryHelperController.cs` | 全局 Helper、加权随机台词和气泡 | | `Assets/Scripts/NewStorySystem/YarnFunctions/` | 项目专有 Yarn Commands / Functions | | `Assets/Story/Chapter0/` | Chapter 0 StoryData、Yarn Project、Yarn Scripts、CharacterRegistry | | `Assets/Localization/Story/` | 当前 Yarn 台词 Unity String Table Collection | ## 3. 新章节整合流程 ### 3.1 确定稳定 Chapter Index `ChapterSelectionUnit.chapterIndex`、`StoryData.chapterIndex`、`StoryManager.storyDatas` 的 Dictionary Key 必须完全一致,包括大小写。 当前 Chapter 0 使用: ```text Chapter0 ``` `StoryTreeController.BuildChapter` 会在运行时报告三者不一致,但不会擅自改写资产。 ### 3.2 创建章节目录和 Yarn Project 建议结构: ```text Assets/Story/Chapter1/ Chapter1.yarnproject StoryData_Chapter1.asset CharacterRegistry.asset(也可改为全局 Registry,需统一引用) C1_Intro.yarn C1_Checkpoint.yarn ... ``` Yarn Project 的 `Source Yarn Scripts` 建议使用 `**/*.yarn`,并确保不会把其它章节目录意外包含进来。Node 名必须在该 Project 内唯一。 ### 3.3 创建 StoryData 使用: ```text Create > Ichni > Story > New > StoryData ``` 配置: 1. `Chapter Index`:与章节资产完全一致。 2. `Yarn Project`:拖入本章节 Yarn Project。 3. `Initial Variables`:登记章节变量的默认值和类型。 4. `Blocks`:配置完整静态剧情图。 5. `Timeline Markers`:配置重要时间点。 6. `Exclusive Route Groups`:只用于 Validation,登记互斥终点组。 最后在场景 `StoryManager.storyDatas` 中添加: ```text Key: Chapter1 Value: StoryData_Chapter1 ``` ## 4. Initial Variables 每个变量定义包含: - `Key` - `Type`:Bool / Int / Float / String - 对应类型的 `Default` 默认值不写入玩家存档。只有 Yarn 或其它剧情入口第一次写入同名变量时,才产生存档覆盖值。`remove_variable` 会删除覆盖值,使读取重新回到 StoryData 默认值。 建议所有 Yarn 会读写的变量都在 `Initial Variables` 登记,即使运行时允许首次写入未声明变量。这样能让策划、程序员和 Validation 审阅时看到章节完整变量表。 ## 5. Block 通用配置 ### 5.1 Identity 与 Layout - `ID`:稳定唯一,连接、存档、选项和 Marker 都依赖它。 - `Type`:Text / Song / Tutorial。 - `Column`:叙事时间向右推进;列坐标代表 Block 中心点。 - `Row`:路线位置,正数向下、负数向上。 运行时默认步距: - `columnStep = 720` - `rowStep = 525` - `helperReservedLeftSpace = 400` Prefab 自己决定尺寸,控制器不覆盖 `sizeDelta`: | Block | 当前设计尺寸 | |---|---| | Important Text | `600 x 375` | | Secondary Text | `400 x 200` | | Song | `400 x 100` | | Tutorial | `400 x 100` | 如果未来修改 Prefab 尺寸,还要同步 `StoryData.Editor.cs` 中的 Preview 尺寸常量。 ### 5.2 Next Blocks 与 Connected Rule `Next Blocks` 同时承担: - 运行时连接线。 - 目标 Block 的自动前置条件来源。 目标 Block 的 `Connected Rule`: | 规则 | 语义 | |---|---| | `Require All Direct Predecessors` | 所有直接前置完成后可用;默认值 | | `Require Any Direct Predecessor` | 任一直接前置完成即可;用于互斥分支汇合 | | `Ignore Connections` | 连线只作视觉用途,不参与解锁 | 自动连接条件与手写 `Unlock Condition` 按 AND 合并。没有前置连接的入口 Block 不受自动条件限制。 ### 5.3 Unlock 与 Forbidden 两者共用 `StoryCondition`: - `All Of (AND)` - `Any Of (OR)` - `Not` - `Block Completed` - `Variable Compare` 状态优先级: ```text Forbidden > Completed > Current > Locked ``` 互斥路线应同时配置 Unlock 和 Forbidden。例如: ```text Route A Unlock: c0_signal_route == 1 Route A Forbidden: c0_signal_route == 2 Route B Unlock: c0_signal_route == 2 Route B Forbidden: c0_signal_route == 1 ``` 不要为相同语义再增加一套 Route Binding 字段;运行时路线始终以通用条件树为准。 ## 6. TextBlock 配置 字段: - `Yarn Node`:点击时启动的入口 Node。 - `Title Key`:Block 标题的 Unity Localization Entry Key。 - `Importance`:Important / Secondary。 当前限制:`TextBlockView` 仍直接把 `titleKey` 字符串显示到 TMP,没有解析 Unity Localization。这是发布前本地化待办;配置时仍应从现在开始填写正式 Key,避免后续迁移时重新清点内容。 TextBlock 运行流程: 1. `StoryDialogueController` 检查 Runner、章节上下文和 Yarn Node 是否存在。 2. 复制 `ChapterStorySave` 并开始事务。 3. `StoryProgress.BeginBlockMutation` 捕获同列 Marker 快照。 4. Yarn 运行,选项和变量只修改内存。 5. 正常结束:完成 Block、提交 ES3、执行排队的解锁/Message。 6. Back 或启动失败:恢复进入前快照并刷新现有 StoryTree。 ## 7. SongBlock 配置 字段: - `Song ID`:必须等于目标 `SongItemData.songName`,不能使用 `displaySongName`。 - `Illustration Y`:范围 `-74.25` 到 `74.25`。 插画和曲名资料由 `ChapterSelectionUnit` 中的 `SongItemData` 提供,不在 StoryData 重复维护。 完成规则: - 玩家确认进入目标歌曲时,`InformationTransistor.PrepareGameLaunch` 调用 `SongSaveModule.MarkSongTried`。 - `SongStatusSave.isTried = true` 是 SongBlock 的唯一持久化完成事实。 - 即使玩家进入后中途退出,也算尝试并完成对应 SongBlock。 - Timeline 回滚不会把 `isTried` 改回 false;重新构建剧情树会再次把该 SongBlock 同步为完成。 同时仍要通过 `UnlockSaveModule.CanEnterSong` 检查章节和歌曲授权,SongBlock 不能绕过普通选曲的锁定规则。 ## 8. TutorialBlock 配置 字段: - `Display Name` - `Tutorial Key`:`TutorialCollection` 的稳定 Key。 - `Progress Variable`:玩家选择游玩或跳过后写入 `true` 的章节变量。 - `Difficulty Save ID`:目标 `DifficultyData.saveDifficultyId`,不是列表下标。 玩家每次点击 Completed TutorialBlock 都可以重新选择游玩或跳过。第一次选择任一项时会立刻完成 TutorialBlock 并解锁后续;游玩教程不等待结算。 当前 `TutorialFlowController` 的“游玩教程/跳过教程”文本仍是硬编码中文,发布前需要迁移到 Unity Localization。 ## 9. Timeline Marker 每个 Marker 配置: - `Marker ID`:章节内唯一。 - `Anchor`:必须是 TextBlock ID。 - `Label Key`:时间或特殊叙事文本的 Unity Localization Key。 - `Main Progress`:是否参与主线百分比。 - `Value`:完成 Anchor 后贡献的 `0..1` 进度;互斥结局都可以是 `1`。 显示规则: - Anchor 为 `Current` 或 `Completed`:显示。 - Anchor 为 `Locked` 或 `Forbidden`:隐藏。 位置直接读取 Anchor TextBlock 的实际视觉中心,并监听 Story ScrollRect;装饰刻度使用一张 `Image Type = Tiled` 的 Repeat Image 跟随同一横向位移。 点击 Marker 只有在对应“列之前快照”已经创建后才可交互。确认回滚会重置目标列及右侧的 Block、选项和章节变量,并删除失效的后续 Marker 快照。 当前 Timeline 回滚确认框仍有硬编码中文;发布前需要迁移到 Unity Localization。 ## 10. Exclusive Route Groups 该列表只用于编辑器 Validation,不参与 Runtime。 一个 Group 应包含同一章节中互斥的两个或多个终点 Block。Validation 会检查: - Group ID 是否有效和重复。 - 终点 Block 是否存在和重复。 - 终点是否仍有 Next Block。 - 互斥终点是否配置了 Forbidden Condition。 通用条件树可以表达任意复杂条件,Validation 无法数学证明两条路线真正互斥,最终仍需人工测试。 ## 11. Yarn 与 Unity Localization 所有新剧情文本只使用 Unity Localization。 ### 11.1 Yarn Project Inspector 选中 `.yarnproject` 后检查: 1. `Source Yarn Scripts` 覆盖正确目录。 2. `Base Language` 与 Yarn 源文本语言一致。 3. 开启 `Use Unity Localisation System`。 4. `String Table Collection` 指向目标 Unity String Table Collection。 5. 点击 `Add Line Tags to Yarn Scripts`,为缺少 ID 的台词和选项生成 `#line:`。 6. 保存并重新导入,确认 Yarn Project 没有编译错误。 当前 Chapter 0 使用 `Chapter0_Lines`,而 `StoryDialogueController` 已预留统一 `yarnLinesTable`。在 Chapter 1 大量接入前,推荐把各章节 Yarn 台词统一到一个 `YarnLines` String Table Collection;这样场景中的单个 `UnityLocalisedLineProvider` 在切换 Yarn Project 时无需同时切换表。 若暂时保留 `Chapter0_Lines`: - `UnityLocalisedLineProvider.stringsTable` 必须指向 `Chapter0_Lines`。 - `StoryDialogueController.yarnLinesTable` 留空时不会覆盖现有 Provider 配置。 ### 11.2 Locale 首发至少验证: - English:`en` - Simplified Chinese:`zh-CN` - Japanese:`ja` - Korean:`ko` - Vietnamese:`vi-VN` 新增 Yarn 行后,确认五张 String Table 都有同一 Line ID。不要通过删除并重新生成 Line Tag 的方式“整理”ID,否则会同时影响本地化和已保存选项。 ### 11.3 非 Yarn 文本 建议按用途拆表: | 内容 | 建议 String Table | |---|---| | Yarn 台词和选项 | `YarnLines` | | TextBlock 标题、Timeline、章节标题 | `StoryUI` | | Helper 名称和气泡 | `StoryHelper` | | MessageBox / SelectionBox | `Message` | 当前场景中 `StoryTimelineController.localizationTable` 和 `StoryHelperData.localizationTable` 仍为空;完成正式文本接入时必须补齐。 ## 12. 角色与立绘配置 ### 12.1 CharacterData 创建: ```text Create > Ichni > Story > New > CharacterData ``` 字段: - `Character ID`:与 Yarn 的 `CharacterName` 完全一致,例如 `Asahi`。 - `Display Name Key`:角色显示名的 Unity Localization Key。 - `Portrait Style`:Sprite / Live2D / Spine。 - `Position Offset`:归一化舞台偏移。 - `Emotion Sprites`:`normal`、`happy`、`angry` 等表情到 Sprite 的映射。 - `Dynamic Portrait Prefab`:未来 Live2D / Spine 使用,必须实现 `IPortraitRenderer`。 把 CharacterData 登记到 `CharacterRegistry.characters`,再把 Registry 赋给场景 `StoryManager.characterRegistry`。 当前限制:`VNDialoguePresenter` 的 Speaker TMP 仍直接显示 Yarn 的 `CharacterName`,尚未通过 `CharacterData.displayNameKey` 本地化。该字段先按规范配置,发布前应补上显示名解析。 ### 12.2 VNPortraitStage 场景需配置: - `_spritePortraitPrefab` - `_portraitContainer` Sprite Prefab 必须包含 `SpritePortraitRenderer`;Live2D / Spine Prefab 必须有实现 `IPortraitRenderer` 的组件。 当前舞台按需 Instantiate,`hide_portrait` 时 Destroy。对话数量增大后若发现频繁分配,应再评估池化;现在不要为了未知规模提前重构。 ## 13. Dialog 场景装配 `StoryDialogueRoot` 应包含: - `DialogueRunner` - `UnityLocalisedLineProvider` - `VNDialoguePresenter` - `StoryDialogueController` `StoryDialogueController`: - `dialogueRunner` - `storyUIPage` - 可选的统一 `yarnLinesTable` `DialogUIPage`: - `portraitStage` - `dialogueText` - `speakerContainer` / `speakerText` - `advanceButton` - `fastForwardButton` - `backButton` - `showHistoryButton` - `dialogueHistoryPage` - `choiceFrame` - `choiceButtons` `DialogueHistoryPage` 必须有全屏可接收 Raycast 的覆盖层;打开时要阻断底层 Dialog 输入。 当前 `MenuScene` 仍启用了调试用 `ConsoleLinePresenter`。它会额外把每行本地化结果输出到 Console;发布构建前应从 DialogueRunner Presenter 列表移除或禁用,只保留 `VNDialoguePresenter`。 ## 14. Helper 配置 Helper 使用全局 `StoryHelperData`,不属于单个 StoryData。 字段: - `Helper ID` - `Portrait Sprite` - `Localization Table` - `Display Name Key` - `Fallback Dialogue Key` - `Dialogue Pool` 每条候选包含: - `ID` - `Text Key` - `Availability`:复用 StoryCondition;留空时始终可用。 - `Weight` 运行时会从满足条件的候选中加权随机抽取,并尽量避免连续重复。没有候选时使用 Fallback。`StoryHelperController` 会动态实例化独立 `HelperTalk` Prefab,不使用 MessageBox。 当前 `Assets/Story/StoryHelperData.asset` 的 Localization Table 为空且 Dialogue Pool 为空,只有 Fallback Key;正式 Helper 文本测试前需要配置对应 String Table 和至少一条候选。 ## 15. StoryData Validation 在 StoryData Inspector 点击 `Validate Story Data`。Console 现在会输出: - 一条摘要。 - 每个问题一条独立 Warning 或 Error。 不要再把整份报告拼到同一条多行日志;Unity Console 折叠行不会按内嵌换行扩展高度,会造成文字重叠。 当前 Validation 检查: - Chapter Index 和 Yarn Project。 - Initial Variable Key 的格式和重复。 - Block ID 重复、Next Block 引用、类型必要字段。 - Yarn Node 是否存在及是否被多个 TextBlock 共用。 - 入口、不可达 Block、环路、非向前连接和终点。 - Unlock / Forbidden 条件树的空节点和无效 Block 引用。 - Timeline Marker ID、Anchor、Label 和重复 Anchor。 - Exclusive Route Groups。 - Yarn 文件中的 `<>` 和 `<>` 旧语法。 Validation 不检查: - Scene / Prefab 引用是否齐全。 - Song ID 是否真的存在于 ChapterSelectionUnit。 - TutorialCollection 内容。 - Unity Localization Entry 是否齐全。 - 互斥条件是否在逻辑上百分之百正确。 - 所有语言的排版和溢出。 因此 Validation 通过不等于剧情已经达到发布质量。 ## 16. 当前 Chapter 0 的已知配置状态 审阅时确认: - `StoryData_Chapter0.chapterIndex` 已修正为 `Chapter0`,与 ChapterSelectionUnit 和 StoryManager Key 对齐。 - `Chapter0.yarnproject` 的 Base Language 是 `en`,但旧 `C0_A0.yarn` 含中文原文;正式生产前应确定基础语言并统一。 - `C0_A0.yarn` 仍包含废弃的 `declare / set $` 示例,Validation 会逐条报告;此前已经明确该旧台本不作为新系统规范。 - 五份 `C0_RollbackDemo_*.yarn` 是当前永久变量、选项记忆和 Timeline 回滚的测试样例。 - `StoryTimelineController.localizationTable`、`StoryHelperData.localizationTable` 尚未配置。 - TextBlock 标题、Speaker 显示名、Tutorial 选择和 Timeline 回滚确认仍有待完成 Unity Localization 接入。 ## 17. 推荐测试顺序 ### 17.1 静态检查 1. Yarn Project 无编译错误。 2. StoryData Validation 没有 Error。 3. 所有 Warning 已确认是临时内容还是需要修复。 4. StoryData Preview 中 Block 尺寸、中心列和连线符合预期。 ### 17.2 新档主流程 1. 清除当前章节 StorySave。 2. 打开章节,确认只有入口 Block 为 Current。 3. 完成 TextBlock,确认后继解锁并在重启后保持。 4. 在重要选择中选择 Route A,确认 Route B 变为 Forbidden。 5. 再次进入已完成 TextBlock,确认选项自动采用。 ### 17.3 对话按钮 1. `Advance` 第一次补全文字、第二次下一句。 2. `FastForward` 只影响当前一次 Dialog。 3. FastForward 可跨过已记忆选项,但不会越过首次选择页面。 4. `History` 只显示当前一次对话,能滚动并阻断底层输入。 5. `Back` 中途退出后,变量、选项、Block 完成状态全部恢复。 ### 17.4 Timeline 1. Marker 只在 Anchor Current / Completed 时显示。 2. 横向拖动 StoryTree,Marker 和 Repeat Tick 同步移动。 3. 完成 Marker 所在列后按钮可点击。 4. 回滚后目标列及右侧重新开放,目标列之前保持不变。 5. Song 成绩、`isTried`、Unlock Key、Settings 和其它章节不变。 ### 17.5 Song 与 Tutorial 1. SongBlock 自动选中目标歌曲。 2. 只浏览选曲再返回,不写 `isTried`。 3. 确认进入目标歌曲后,即使中途退出,SongBlock 仍完成。 4. Tutorial 选择游玩或跳过都完成 Block。 5. Completed Tutorial 仍可再次点击回顾。 ### 17.6 本地化 逐一切换 `en`、`zh-CN`、`ja`、`ko`、`vi-VN`: 1. Yarn 台词与选项正确。 2. Character 名称正确。 3. TextBlock 标题正确。 4. Timeline 文本和回滚确认正确。 5. Helper 名称与气泡正确。 6. MessageBox / SelectionBox 正确。 7. 长文本不溢出,CJK 与越南语字体包含所需字形。 ## 18. 常见故障定位 | 现象 | 检查顺序 | |---|---| | 点击 TextBlock 无反应 | Block State -> Button -> StoryDialogueController -> Yarn Node -> Yarn Project | | Yarn Node 不存在 | StoryData 的 Node 名、Yarn Project source glob、编译诊断 | | 对话退出后变量仍存在 | 是否使用项目 `set_*`;全局副作用是否经过 Commit Queue | | 选项被意外跳过 | Option Line ID 是否沿用;是否已有 Choice Record;是否错误修改选项组 | | Marker 不移动 | Story ScrollRect 引用、Marker Container 坐标转换、Anchor View 是否存在 | | SongBlock 不完成 | `PrepareGameLaunch` 是否调用、Song ID 是否匹配、`SongStatusSave.isTried` | | SongBlock 错误完成 | 是否有多个章节/歌曲复用了同一个 `songName`;该字段必须全局稳定 | | Helper 只显示 Key | StoryHelperData Localization Table 或 Entry 缺失 | | Speaker 显示内部 ID | 当前显示名本地化尚未接入,不能只配置 displayNameKey 就视为完成 | | Console 出现多份台词日志 | 调试用 ConsoleLinePresenter 仍启用 | ## 19. 发布前剧情系统 Gate 满足以下条件后,剧情和对话系统才可进入 Release 验收: 1. Chapter 0 所有正式 Yarn 台本不再使用废弃临时变量语法。 2. Yarn Base Language 与源文本一致。 3. 五种首发语言的 Yarn、Story UI、Helper、Message 文本齐全。 4. TextBlock 标题、Speaker、Tutorial、Timeline 确认框全部完成 Unity Localization。 5. ConsoleLinePresenter 从发布配置移除。 6. StoryData Validation 无 Error,所有 Warning 有明确处理结论。 7. 两个互斥结局、支线 Forbidden、选项记忆和 Timeline 回滚完成回归测试。 8. SongBlock `isTried`、Tutorial 回顾、Back 回滚、FastForward 会话边界均通过测试。 9. Android、iOS、PC 分别完成长文本、输入和场景往返测试。 10. 正式发布前冻结 Story Save v1;发布后结构变化必须编写迁移,不能继续直接删档。