剧情+对话完善

This commit is contained in:
SoulliesOfficial
2026-07-21 15:24:42 -04:00
parent 8f230831e9
commit 810d019619
161 changed files with 7271 additions and 1893 deletions

View File

@@ -1,7 +1,7 @@
# 存档、剧情进度与选曲缓存:当前状态说明
> **适用项目**`ichni Official`
> **最后更新**2026-07-18
> **最后更新**2026-07-21
> **状态**:已完成 Core-002 的存档与选曲缓存收尾,以及 Core-003 的统一 Offline 内容解锁模块。
本文记录当前已落地的运行时存档机制、数据边界、字段语义和维护规则。它不是面向玩家的说明;以后修改存档、谱面、剧情或选曲逻辑时,应先阅读本文。
@@ -18,9 +18,9 @@
| 数据域 | 主要职责 | ES3 文件 | 当前 Schema | 启动加载顺序 |
|---|---|---|---|---|
| 游戏记录 | 歌曲完成状态、每个谱面的成绩、Chart Revision | `GameSaves/SongSaves.json` | Song Save v1 | 先加载 |
| 游戏记录 | 歌曲尝试/完成状态、每个谱面的成绩、Chart Revision | `GameSaves/SongSaves.json` | Song Save v1 | 先加载 |
| 内容解锁 | 已授予的章节/歌曲/教程等内容访问 Key | `GameSaves/UnlockKeys.json` | Unlock Save v1 | 游戏记录后、菜单显示前加载 |
| 剧情记录 | 各章节剧情树、选项状态、全局 Yarn 变量 | `StorySaves/*.json`(由 `StorySaveModule` 管理) | Story Save v1 | 游戏记录后加载 |
| 剧情记录 | 各章节剧情树、选项状态、章节 Yarn 变量和回滚快照 | `StorySaves/*.json`(由 `StorySaveModule` 管理) | Story Save v1 | 按章节加载 |
| 选曲缓存 | 当前运行期间各章节最后选择的歌曲与难度 | 仅内存 | 不适用 | 按需创建 |
`GameSaveManager.Start()` 先初始化 `SongSaveModule` 并加载游戏记录、剧情解锁 Key再初始化 `StorySaveModule`。菜单和章节 UI 因而可在首次展示前读取成绩和解锁状态。
@@ -45,6 +45,7 @@
| 字段 | 所属层级 | 含义与更新规则 |
|---|---|---|
| `SongStatusSave.isTried` | 歌曲 | 玩家曾确认进入该歌曲的 `GameScene`Story SongBlock 以此作为完成依据,即使中途退出也保持为 `true`。 |
| `SongStatusSave.isCompleted` | 歌曲 | 任意一个有效游玩结束后置为 `true`;不会因为较差成绩回退。 |
| `SongStatusSave.additionalInfo` | 歌曲 | 预留信息;当前保持非空字符串,未承载游戏规则。 |
| `BeatmapSave.accuracy` | 谱面 | 当前 `Chart Revision` 下的最高 Accuracy。 |
@@ -84,7 +85,7 @@
## 5. 剧情存档
剧情存档由 `Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs` 负责。它独立保存章节剧情树、选项/节点状态和全局 Yarn 变量,并有自己的 Story Save Schema v1。
剧情存档由 `Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs` 负责。它按章节独立保存剧情树、选项、章节永久变量和 Timeline 回滚快照,并有自己的 Story Save Schema v1。
剧情存档不得承担歌曲 Accuracy、Combo、FC/AP 等游玩数据;反过来,游戏记录也不得复制 Yarn 变量或剧情节点状态。剧情通过 Yarn 命令授予内容解锁 Key由独立 `UnlockSaveModule` 决定章节和歌曲是否开放。
@@ -122,7 +123,7 @@
- 玩家重新进入同一次运行中的同一章节时UI 会优先恢复该选择。
- 若缓存歌曲已不在章节内、难度索引失效、歌曲没有可用难度,系统会安全回退,不会把空难度交给进入游戏流程。
### 6.1 跨歌曲的难度回退规则
### 7.1 跨歌曲的难度回退规则
当玩家从一首歌拖动到另一首歌,而目标歌曲没有当前难度时,系统会选择目标歌曲中“更接近”的可用难度;距离相同则优先较低的列表序号。例如当前选择难度索引为 `2`,目标歌曲只有 `0``1``3` 可用时选择 `1`

View File

@@ -0,0 +1,533 @@
# 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
-> StoryTimelineControllerMarker 投影、进度、回滚入口)
-> 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 文件中的 `<<declare>>``<<set $...>>` 旧语法。
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. 横向拖动 StoryTreeMarker 和 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发布后结构变化必须编写迁移不能继续直接删档。

View File

@@ -0,0 +1,459 @@
# 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: 先从这里开始。
<<jump C0_Intro_Continue>>
===
title: C0_Intro_Continue
---
Asahi: 我们继续吧。
===
```
`<<jump NodeName>>` 会转到另一个 Node。目标必须存在名称必须完全一致。
`<<stop>>` 会立即结束当前 Yarn 对话。只有确认这就是本 TextBlock 的正常终点时才使用;正常结束会提交事务并完成 Block。
### 5.2 玩家选项
```yarn
SLS: 你打算怎样处理信号?
-> 记录信号的变化
<<set_int "c0_signal_route" 1>>
<<jump C0_SignalCheckpoint_Record>>
-> 优先保护观测装置
<<set_int "c0_signal_route" 2>>
<<jump C0_SignalCheckpoint_Protect>>
```
注意:
- 选项正文必须缩进。
- 当前 `DialogUIPage` 只有 4 个 `ChoiceButton`,一个选择组最多写 4 个可显示选项;需要更多时先与程序员确认 UI 扩容。
- 选项的 Line ID 会被保存。重新排序选项不会改变记忆;增加/删除选项或更换 Line ID 会形成新的选项组,玩家可能需要重新选择。
- 已保存的选项如果在新版台本中消失或变为不可用,系统会删除旧记录并重新显示选择页面。
### 5.3 条件选项
```yarn
-> 询问朝日的过去 <<if get_bool("c0_asahi_origin_known")>>
Asahi: 我现在可以谈起那段记忆了。
```
条件不满足时,该选项由 Yarn 标记为不可用。不要用 Yarn 原生 `$variable` 作为条件。
## 6. 条件语法
```yarn
<<if get_int("c0_signal_route") == 1>>
SLS: 你选择了观测路线。
<<elseif get_int("c0_signal_route") == 2>>
SLS: 你选择了保护路线。
<<else>>
SLS: 目前还没有选择路线。
<<endif>>
```
常用运算:
- 相等/不等:`==``!=`
- 数值比较:`>``>=``<``<=`
- 逻辑组合:`and``or``not`
- Bool 函数可直接作为条件:`<<if get_bool("c0_intro_read")>>`
复杂条件应优先拆成可读的事实变量。不要把大量业务规则全部塞在一行 Yarn 表达式中;对应 Block 的开放和禁用仍应由程序员在 `StoryData` 条件树中复核。
## 7. 永久章节变量:全部命令与函数
新台本禁止使用:
```yarn
<<declare $temporary = false>>
<<set $temporary = true>>
<<if $temporary>>
```
这些变量不受 ichni Official 的章节存档、对话事务和 Timeline 回滚管理。
### 7.1 Bool
```yarn
<<set_bool "c0_intro_read" true>>
<<if get_bool("c0_intro_read")>>
SLS: 你已经读过开场。
<<endif>>
```
| 接口 | 用途 |
|---|---|
| `<<set_bool "key" true>>` | 写入 Bool |
| `get_bool("key")` | 读取 Bool用于 `if` 或表达式 |
### 7.2 Int
```yarn
<<set_int "c0_signal_route" 1>>
<<add_int "c0_resonance" 2>>
<<if get_int("c0_resonance") >= 3>>
SLS: 共鸣已经足够强烈。
<<endif>>
```
| 接口 | 用途 |
|---|---|
| `<<set_int "key" 1>>` | 写入整数语义的数值 |
| `<<add_int "key" 1>>` | 在当前值上增加整数 |
| `get_int("key")` | 读取整数 |
### 7.3 Float
```yarn
<<set_float "c0_signal_strength" 0.5>>
<<add_float "c0_signal_strength" 0.25>>
<<if get_float("c0_signal_strength") >= 0.75>>
SLS: 信号强度已达到阈值。
<<endif>>
```
| 接口 | 用途 |
|---|---|
| `<<set_float "key" 0.5>>` | 写入小数 |
| `<<add_float "key" 0.25>>` | 在当前值上增加小数 |
| `get_float("key")` | 读取小数 |
### 7.4 String
```yarn
<<set_string "c0_signal_source" "moon">>
<<if get_string("c0_signal_source") == "moon">>
Asahi: 信号来自月面。
<<endif>>
```
| 接口 | 用途 |
|---|---|
| `<<set_string "key" "value">>` | 写入字符串 |
| `get_string("key")` | 读取字符串 |
### 7.5 通用变量操作
| 接口 | 用途 |
|---|---|
| `has_variable("key")` | 存档中有该变量,或 `StoryData.Initial Variables` 声明了该 Key 时返回 `true` |
| `<<remove_variable "key">>` | 删除存档覆盖值;若 `Initial Variables` 有同名定义,之后读取会重新得到默认值 |
所有变量只属于当前章节。不要依赖 Chapter 0 的变量直接解锁 Chapter 1跨章节内容开放应使用 `Unlock Key`
## 8. 立绘命令
立绘坐标采用推荐范围 `-100``100``(0, 0)` 为舞台中心;角色自己的 `CharacterData.positionOffset` 会额外叠加。当前代码不会强制 Clamp超出范围可能让立绘离开画面。
| 命令 | 作用 |
|---|---|
| `<<set_portrait "Asahi" 30 0 "normal">>` | 角色不在场时创建;已在场时设置位置和表情 |
| `<<set_portrait_current 30 0 "happy">>` | 操作当前说话者;必须已经显示过一行带角色名的台词 |
| `<<set_portrait_position "Asahi" -30 0>>` | 只改位置;角色不在场时以 `normal` 创建 |
| `<<set_portrait_emotion "Asahi" "angry">>` | 只改表情;角色必须已在场 |
| `<<hide_portrait "Asahi">>` | 隐藏并移除指定角色 |
| `<<hide_all_portraits>>` | 清空全部立绘 |
| `<<move_portrait "Asahi" 0 0 0.5>>` | 在指定秒数内平滑移动 |
| `<<jump_portrait "Asahi" 5 2 0.8>>` | 跳跃;参数为高度、次数、总时长 |
| `<<shake_portrait "Asahi" 0.5 2 10>>` | 横向震动;参数为时长、强度、频率 |
动画命令启动 DOTween 后会立即让 Yarn 继续执行,不会自动等待动画完成。若下一句必须等动画结束,可以使用 Yarn Spinner 内置命令:
```yarn
<<move_portrait "Asahi" 0 0 0.5>>
<<wait 0.5>>
Asahi: 我到了。
```
建议每个独立 TextBlock 开头明确建立所需立绘,结束前按设计调用 `hide_portrait``hide_all_portraits`,不要隐式依赖上一次对话留下的舞台状态。
## 9. 解锁、提示和调试命令
| 命令 | 作用 | 是否等待对话正常提交 |
|---|---|---|
| `<<grant_unlock "story_ch0_prologue_completed">>` | 授予通用 Offline 内容 Key不自动弹提示 | 是 |
| `<<unlock_song "story_ch0_song_space_rain_available">>` | 兼容旧流程:授予 Key并在首次授予时显示歌曲提示 | 是 |
| `<<show_message "title_key" "content_key" "Message">>` | 从指定 Unity String Table 解析并显示 MessageBox | 是 |
| `<<log "checkpoint reached" "info">>` | 输出 `info``warning``error` 日志 | 否;只影响 Console |
推荐把通用解锁和玩家提示明确拆开:
```yarn
<<grant_unlock "story_ch0_prologue_completed">>
<<show_message "story_c0_unlock_title" "story_c0_unlock_space_rain" "Message">>
```
`unlock_song` 主要用于兼容现有内容。它当前的专用提示仍可能显示内部 Key正式文本优先使用 `grant_unlock + show_message`
## 10. 完整分支案例
### 10.1 第一段主线
```yarn
title: C0_SignalIntro
---
<<set_portrait "Asahi" 30 -5 "normal">>
SLS: 记录开始。异常信号将在一分钟后抵达。
Asahi: 我们要观察它,还是先保护装置?
<<set_bool "c0_signal_intro_read" true>>
<<add_int "c0_signal_resonance" 1>>
===
```
### 10.2 带 Timeline Marker 的重要选择
```yarn
title: C0_SignalCheckpoint
---
<<if get_bool("c0_signal_intro_read")>>
SLS: 前置记录已经同步。现在决定路线。
<<else>>
SLS: 前置记录缺失,请程序员检查 StoryData 连接。
<<endif>>
-> 继续观测
<<set_int "c0_signal_route" 1>>
<<jump C0_SignalCheckpoint_Observe>>
-> 保护装置
<<set_int "c0_signal_route" 2>>
<<jump C0_SignalCheckpoint_Protect>>
===
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
---
<<if get_int("c0_signal_route") == 1>>
SLS: 观测结果已经归档。
<<set_bool "c0_signal_observed" true>>
<<add_int "c0_signal_resonance" 2>>
<<endif>>
===
```
保护分支:
```yarn
title: C0_SignalProtect
---
<<if get_int("c0_signal_route") == 2>>
SLS: 保护程序已经完成。
<<set_bool "c0_signal_protected" true>>
<<add_int "c0_signal_resonance" 1>>
<<endif>>
===
```
程序员应同时配置:
- 观测 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
---
<<if get_bool("c0_signal_observed")>>
SLS: 你从时间的回声中取得了答案。
<<elseif get_bool("c0_signal_protected")>>
SLS: 你为下一次观测保留了机会。
<<else>>
SLS: 没有有效路线记录,请检查剧情配置。
<<endif>>
<<set_bool "c0_signal_convergence_read" true>>
<<grant_unlock "story_ch0_signal_convergence_completed">>
<<show_message "story_c0_signal_complete_title" "story_c0_signal_complete_body" "Message">>
<<hide_all_portraits>>
===
```
仓库中还有一组可直接参考的 `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. 没有 `<<declare>>``<<set $...>>``$variable`
5. 所有剧情变量使用 `set_*` / `get_*`,并附带类型与默认值说明。
6. 互斥路线同时说明 Unlock 和 Forbidden 规则。
7. 重要选择说明是否需要 Timeline Marker。
8. 立绘 Character ID 与 `CharacterData.characterId` 完全一致。
9. 需要等待的立绘动画显式添加 `<<wait>>`
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 是否被重建 |