剧情+对话完善
This commit is contained in:
@@ -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`。
|
||||
|
||||
|
||||
533
docs/story-programmer-integration-guide.md
Normal file
533
docs/story-programmer-integration-guide.md
Normal 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
|
||||
-> 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 文件中的 `<<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. 横向拖动 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;发布后结构变化必须编写迁移,不能继续直接删档。
|
||||
459
docs/story-writer-yarn-guide.md
Normal file
459
docs/story-writer-yarn-guide.md
Normal 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 是否被重建 |
|
||||
Reference in New Issue
Block a user