docs: 补充存档系统设计文档并更新 CLAUDE.md

This commit is contained in:
2026-06-03 19:58:49 +08:00
parent 2c1e5d8fa7
commit 2cd3a1a7df
3 changed files with 564 additions and 2 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ Modular framework with Kit-pattern utilities:
### Key Game Systems
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `StorageSystem` (save + Yarn variables), `SceneLoader`, `ChapterController`, `TalkSceneSO`
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
@@ -139,7 +139,7 @@ Modular framework with Kit-pattern utilities:
## Important Notes
- Narrative-focused game — dialogue and story take precedence
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
- `StorageSystem` serves as both save system and Yarn variable storage
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
## Reference Documents
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
+481
View File
@@ -0,0 +1,481 @@
# 存档系统设计方案
> 本文档为**实现层**设计文档,承接 [存档系统需求](存档系统需求.md)。
> 记录拆分方案、各部分实现思路,以及已确定的关键决策。需求未定项见文末 TODO。
## . 文档状态
- 需求来源:`Docs/存档系统需求.md`
- 当前阶段:**P1 快照层已实现**;原 `StorageSystem` 已重命名为 `YarnVariableStorage` 并完成职责拆分。
- 最近更新:2026-06-02(明确读档还原编排终态与 P1 Provider 临时形态)。
---
## 一. 前提:旧实现的三档归类
需求§一明确:新方案**不沿用**旧实现的**恢复锚点、存档文件组织**等不可靠部分;旧系统中**仍可靠的底层能力**在后续实现阶段**再评估复用**。据此分三档:
| 档别 | 含义 | 内容 |
| --- | --- | --- |
| **A. 废弃 / 重做** | 被点名的不可靠部分,不沿用旧做法 | 恢复锚点;存档文件组织;依附其上的「无条件写盘触发」 |
| **B. 候选,待评估复用** | 旧系统中仍可靠的底层能力,到对应阶段再决定是否复用 | Yarn 变量存取能力;承接深度维修状态的数据容器(`IData`/`DataContainer` |
| **C. 全新构建** | 旧系统无对应实现 | 快照模型、槽位/落盘、可存点判定、复原编排、存读档 UI、横切项 |
> B 类只是「候选」,不作为既定地基;是否复用、复用多少,在 P1/P5 设计时单独决策。
---
## 二. 关键决策记录
| 编号 | 决策 | 取值 | 影响 |
| --- | --- | --- | --- |
| D1 | 表现类复原方式 | **逐项还原** | P1 快照内容、P4 复原方式 |
| D1.1 | Timeline 等时间序列项的复原粒度 | 复原到**结尾(或开头)状态**即可,**不**按时间轴逐帧复原 | P1/P4 |
| D2 | 自动档策略 | **单覆盖**(始终保留一份,新档覆盖旧档) | P2 |
| D3 | 槽位界面是否截图 | **需要截图** | P0/P2 |
| D4 | 「继续游戏」指向 | **最近保存的存档**(自动 / 手动中时间最新的一份) | P6 |
| D5 | 新游戏与既有档关系 | **覆盖自动存档,不影响其他(手动)存档** | P6 |
| D6 | 读档还原编排 | **Phase + Barrier**:少数异步边界 `yield`,其余同步批量还原 | P4 编排、`ISnapshotProvider` 契约 |
### 决策展开
- **D1 逐项还原**:表现类(角色动画、环境、Timeline、UI 遮罩等)在读档时由快照中记录的状态逐项恢复,而非「重进节点重演」。
- **D1.1 时间序列粒度**:Timeline 等只需复原到终态或初态,不记录播放进度。
- **D6 Phase + Barrier**:读档不是「每个 Provider 串行 `yield return` 的协程链」。编排层按阶段推进,仅在**必须等待**的边界上挂起(场景加载、锚点重进、P4 淡入淡出、P5 深度维修状态机等);同阶段内的 sync 还原应连续调用,不必逐步 yield。详见 §4.1。
- **D2~D5**:见上表;P2/P6 实现时使用。
---
## 三. 两个被点名不可靠点的全新设计(A 档)
### 1. 恢复锚点(全新)
- **锚点定义**YarnProject 标识 + 节点名 + 进入该节点那一刻的宏观阶段。
- **落点时机**:P3 可存点判定通过后(当前 P1:`SnapshotCapture` 在进入节点写档时捕获 `GetCurrentNodeContext()`)。
- **重入方式**:读档后 `SnapshotRestore.RestoreAnchor``StartDialogue(节点名)`
### 2. 存档文件组织(全新)
- 槽位制、sidecar、原子写、Steam 目录——**P2 实现**P1 仅用 `SnapshotPersistence` 写固定测试路径。
---
## 四. 代码架构:职责拆分(已实现)
`StorageSystem`(现 `YarnVariableStorage`)曾同时承担 Yarn 变量、深度维修容器、快照 I/O、读档编排,现已拆成以下类。**禁止再往 `YarnVariableStorage` 上堆存档逻辑。**
```
┌─────────────────────┐
│ YarnVariableStorage │ Yarn 运行时变量(VariableStorageBehaviour
└──────────┬──────────┘
│ GetAllVariables / SetAllVariables
┌──────────▼──────────┐
│ SnapshotService │ Capture() / Restore() — 纯内存快照
└──────────┬──────────┘
┌───────────────────┼───────────────────┐
│ │ │
┌──────────▼─────────┐ ┌───────▼────────┐ ┌───────▼──────────────────┐
│ SnapshotPersistence │ │ ISnapshotProvider × N │ DeepRepairDataRegistry │
│ 文件读写(P2 接管) │ │ scene/macro/env/… │ 深度维修 IDataP5) │
└──────────┬─────────┘ └──────────────────────┘ └────────────────────────┘
┌──────────▼─────────────────┐
│ SaveRestoreOrchestrator │ 存读档流程:UI 反馈、落盘、还原、旧档兼容
└────────────────────────────┘
```
| 类 | 路径 | 职责 |
| --- | --- | --- |
| `YarnVariableStorage` | `Game Loop/YarnVariableStorage.cs` | 仅 Yarn `$` / `$global_` 变量读写 |
| `SnapshotService` | `SaveSystem/SnapshotService.cs` | `Capture()` / `Restore()`,不涉及文件 |
| `SnapshotPersistence` | `SaveSystem/SnapshotPersistence.cs` | JSON 读写;P1 测试路径;旧档格式检测 |
| `SaveRestoreOrchestrator` | `SaveSystem/SaveRestoreOrchestrator.cs` | `SaveToTestPath()` / `RestoreFromFile()`;保存 UIlegacy 分支 |
| `DeepRepairDataRegistry` | `SaveSystem/DeepRepairDataRegistry.cs` | `RegisterData` / `LoadByJson`FixSystem 等 P5 前临时) |
| `SnapshotCapture` / `SnapshotRestore` | `SaveSystem/` | 快照组装与逐项 provider 还原 |
| `SnapshotSerializer` | `SaveSystem/` | schemaVersion、稳定 SaveId key |
| `SnapshotRegistry` + `Providers/*` | `SaveSystem/` | 7 个表现类 / 宏观 provider |
### 调用约定
| 场景 | 入口 |
| --- | --- |
| `<<auto_save>>` | `SaveRestoreOrchestrator.SaveToTestPath()` |
| 读档 | `SaveRestoreOrchestrator.RestoreFromFile(path)` |
| 仅捕获内存快照(P2 手动档固化) | `SnapshotService.Capture()` |
| 深度维修注册 | `DeepRepairDataRegistry.RegisterData(...)` |
| Yarn 变量 | `YarnVariableStorage.Instance.SetValue / TryGetValue` |
### P1 测试落盘路径
`persistentDataPath/AllOurBrokenParts/snapshot_test/latest_snapshot.json``ConstRef.SnapshotTestPath`
### 4.1 读档还原编排与 Provider 契约(重要)
> **勿将 P1 代码形态当作终态架构。** 当前 `ISnapshotProvider.Restore` 统一返回 `IEnumerator`,且 `SnapshotRestore` 对每个 Provider 做 `yield return`,这是受旧 `IData.Load()` 影响的**临时 scaffolding**,容易误导后续 P4/P5 规划。本节描述终态模型;P4 实施前应据此 refactor 编排层与 Provider 契约。
#### 终态模型:Phase + Barrier,而非协程链
读档还原由 `SnapshotRestore` / `SaveRestoreOrchestrator`(P4 扩展)按**阶段**编排,阶段之间用 **Barrier(必须等完再继续)** 分隔:
```
Phase 0 同步预备
└─ Yarn 变量 SetAllVariables
Phase 1 Barrier(异步)
└─ 场景加载(Addressables LoadSceneAsync
Phase 2 同步批量还原(同帧连续调用,不逐步 yield)
└─ macro / env / actor / audio / timeline / screen …
Phase 2 可选 Barrier(异步,按需)
└─ Timeline Addressable 加载、其他须上报的 async 还原
Phase 3 Barrier(异步)
└─ RestoreAnchorStop + StartDialogue
Phase 4 BarrierP4:淡入淡出等演出时序)
└─ 黑屏 / Loading / FadeIn …
```
- **Barrier**:该步完成前不进入下一阶段(如场景未加载完不还原 actorYarn 未 LoadDialog 不 StartDialogue)。
- **同步批量**:Phase 2 内各子系统在主线程上连续调用即可,**不需要** Provider 之间逐步 `yield return`;这与 D1「逐项还原」不矛盾——「逐项」指各子系统各自写回状态,不是指每步之间必须挂起协程。
- **并行**:Unity 主线程上不做多线程并行;此处「不必串行 yield」指**不必为 sync 还原逐步挂协程**,而非 CPU 多线程并行。
#### 哪些步骤属于 Barrier(异步边界)
| 步骤 | 是否 Barrier | 说明 |
| --- | --- | --- |
| Yarn 变量 | 否(Phase 0 sync | `SetAllVariables` |
| 场景加载 | **是** | 唯一 P1 中 Provider 层真正需要等待的 async |
| macro / env / actor / audio / screen | 否(Phase 2 sync | 当前实现均为同步写回 |
| timeline(本地终态) | 否(Phase 2 sync | `RestoreAtEndLocal` 等 |
| timelineAddressable | **可选 Barrier** | 若要求进节点前 Timeline 终态就绪,须显式等待;**禁止** fire-and-forget(见下) |
| RestoreAnchor | **是** | `StartDialogue` 为 Yarn 异步 API |
| P4 淡入淡出 | **是** | 演出时序 |
| P5 深度维修 | **部分 Barrier** | 如 `FixSystemData.Load()``SwitchState` 须等待;不宜硬套「与 scene 同款的单一 Provider 协程」 |
#### `RestoreOrder` 的语义(终态)
- 表示**同 Phase 内的建议顺序或软依赖**(如 scene 必须先于 env/actor),**不是**「每步之间必须 `yield return`」。
- 硬依赖应通过 **Phase 划分 + Barrier** 表达,而不是无限细化 RestoreOrder 数字。
- scene10)必须在 Phase 2 之前完成;macro 必须在 RestoreAnchor 之前完成;env / actor / audio / timeline / screen 之间目前无硬依赖,Phase 2 内一批执行即可。
#### Provider 契约:终态 vs P1 临时形态
| | P1 临时形态(当前代码) | 终态(P4 前 refactor 目标) |
| --- | --- | --- |
| 同步还原 | `IEnumerator Restore` + `yield break` | `void Restore(object dto)` |
| 异步还原 | 同上(仅 scene 真正 yield | `IAsyncSnapshotRestore.RestoreAsync(object dto)` 或等价显式接口 |
| Manager 层 | 部分 `IEnumerator RestoreSnapshot``yield break` | 默认 `void RestoreSnapshot`;仅真有 async 处保留 `IEnumerator` |
| 编排层 | `foreach` 逐步 `yield return provider.Restore` | Phase 编排 + 仅对 Barrier 步骤 `yield` |
新增可存子系统时:**默认实现 sync `Restore`**;仅当存在必须等待的加载/状态切换时,才实现 async 接口并向编排层**上报**可等待句柄,不得在 Provider 内私自 `StartCoroutine` 而不纳入 Barrier。
#### 已知偏离(tech debt,非推荐 pattern
- `DirectorHandler.RestoreSnapshotEntry` 在 Addressable 路径下内部 `StartCoroutine(RestoreAtEndFromAddressable)`,Provider 已返回,编排层无法感知完成——与 D6 冲突。P4 编排时应改为**可等待**的还原路径,或纳入 Phase 2 Barrier。
- 各 Manager 的 `RestoreSnapshot` 声明为 `IEnumerator` 但内部仅 `yield break`——属 `IData.Load()` 惯性,终态应收回到 `void`
#### 对后续阶段的影响
- **P2 / P3**:可不改动 `SnapshotRestore`;但新增代码**不应**再复制「全 Provider 协程链」模式。
- **P4**:在 `SaveRestoreOrchestrator` / `SnapshotRestore` 上按本节 Phase 模型重写编排;顺带 refactor Provider 契约。P4 是「加 fade UI」+「修正编排模型」,而非在现有 foreach 链首尾叠 UI。
- **P5**:深度维修还原更接近旧 `DataContainer` + `LoadIndex` + 部分 `IData.Load()` async**不应**假设「再注册一个 Provider、继续逐步 yield」即可;是否独立 Phase、哪些模块 Barrier,在 P5 单独定案。
---
## 五. 拆分方案(分层 × 分阶段)
```
P0 契约定义 ── 已完成
P1 快照层 ── 已实现(含原 StorageSystem 职责拆分与重命名)
┌────┴───────────────┐
P2 槽位/落盘层 P3 可存点判定层
└────┬───────────────┘
P4 复原编排层(按 §4.1 Phase+Barrier 重写 SnapshotRestore,并扩展淡入淡出)
┌────┴────┐
P5 深度维修阶段存档 P6 存/读档 UI & 流程
P7 横切
```
### P1 快照层(已实现)
- `SaveSnapshot` 纯数据结构 + `ISnapshotProvider` 逐项还原(D1/D1.1)。
- Provider`scene` / `macro` / `env` / `actor` / `audio` / `timeline` / `screen`
- Yarn 变量经 `YarnVariableStorage` 纳入快照;`$data.*` 死路径已移除。
- `DeepRepair` 段可空占位(`DeepRepairSavePolicy`),P5 再定。
- 深度维修旧 `IData` 仍经 `DeepRepairDataRegistry` 运行,**尚未**纳入新快照。
- **注意**P1 中 `Restore` 统一为协程、逐步 `yield return` 属临时实现,终态见 **§4.1 / D6**;后续阶段勿在此基础上堆逻辑。
### P2 槽位/落盘层(待做)
- 接管 `SnapshotPersistence`:槽位抽象、原子写、sidecar、截图(D2/D3)。
- 手动档 = 拷贝最近自动快照。
### P3~P7
- P4:按 **§4.1** 将编排从「Provider 协程链」改为 Phase + Barrierrefactor `ISnapshotProvider` 为 sync 默认 + 显式 async;在 `SaveRestoreOrchestrator` 接入淡入淡出。不再回到 `YarnVariableStorage`
- P5:深度维修还原单独定案,不默认套用 P1 Provider 协程模式(见 §4.1)。
- P6 / P7:见需求文档与上文决策表。
---
## 六. 推荐落地顺序
**P0 → P1(完成)→ P2 → P3 → P4 →(P5/P6)→ P7**
---
## 七. 仍待确定(TODO
- 手动档数量上限、覆盖 / 删除 / 重命名规则。
- 游戏进行中读档是否允许、是否二次确认。
- 自动存档是否需要可见反馈(当前 Orchestrator 保留旧 loading UI)。
- 各深度维修是否支持阶段存档的逐个清单(P5)。
- 全局 / 跨周目数据与单局存档的边界划分。
- 截图的具体规格(P2)。
+81
View File
@@ -0,0 +1,81 @@
# 存档系统需求
> 本文档为**需求层**文档,只描述"存档系统要做成什么样、对玩家承诺什么",不涉及具体实现与数据结构。实现方案、数据结构等待需求确认后另行讨论。
## 一. 背景
本游戏是一个赛博医生题材的视觉小说游戏。游戏中不同患者有公共的插件检修和各种定制的检修玩法。
本项目中使用 YarnSpinner 作为剧情插件,已更新到 3.2 版本。
项目中原来有存档系统,仅有自动存档;读取时从章节选择进入,且仅能读到最新的一份。这套存档系统是很早期设计的,现在问题已经非常大、无法正常使用。**新方案不沿用旧实现的恢复锚点、存档文件组织等不可靠部分**;旧系统中仍可靠的底层能力(Yarn 变量存取、用于承接深度维修状态的数据容器等)在后续实现阶段再评估复用。
## 二. 设计总则
这几条是贯穿整套存档系统的约定,作为后续所有细节设计的前提。
1. **所见即所存**:读档后玩家看到的画面与状态,等于"**进入该可存节点那一刻**"的画面与状态。注意基准是"进入节点那一刻",而**不是**玩家退出游戏那一刻——玩家不能精确回到退出瞬间。
2. **存档点是离散的、节点级的**:进度只在"可存节点"上落点。两个存档点之间的内容(一段对话播放途中、一段演出途中、一段深度维修途中)若退出,则会丢失,下次从上一个存档点重新开始。这是明确的体验约定,需在设计中被接受、而非视作缺陷。
3. **落盘可靠性**:自动存档在"进入可存节点"时即完成写盘,保证崩溃 / 断电情况下最多只丢失到上一个存档点。
## 三. 存档
### 1. 存档时机
由于 YarnSpinner 机制限制,无法从某一行直接开始;另外由于检修玩法复杂,很难随时记录状态并恢复。因此采用**进入节点时自动保存**。
手动存档不单独记录运行时状态,而是**保存最近一次自动保存的那份存档**(即把最近的自动存档点固化到一个手动档位)。
自动存档过程中是否给玩家可见反馈(保存图标 / 提示),**待定**。
### 2. 可存档范围
分情况讨论:
1. **诊所内维修**:仅在诊所中的对话和插线维修时保存;每个角色各自的深度维修过程中**不保存**。但深度维修可能会记录一些**阶段终点状态**,以处理"多段深度维修中间穿插诊所对话"的情况。
2. **诊所外对话**(天桥、酒吧、诊室外等):每个节点都保存。
3. **梦境状态**:情况较复杂,此时 Yarn 结构遵循 [Yarn 节点类型规范](Yarn节点类型规范.md)。在这种情况下,`hub` 节点与 `linear` 节点保存,`detour` 节点与 `function` 节点不保存。
> 说明:上述各情况"如何在运行时被判定"属于实现细节,后续讨论。需求层只约定"哪些时机算可存点"。
### 3. 存档内容
存档恢复遵循总则"所见即所存"。下面按**技术分类**列出存档应覆盖的内容范围(每一类的具体数据结构与还原方式后续单独讨论):
1. **宏观阶段 / 状态**:包括但不限于场景、YarnProject、(部分情况下)FixState 等标志着当前游戏重要阶段的内容。
2. **表现类**:包括但不限于 UI 层的演出工具(UI 遮罩等)、环境状态、角色动画、Timeline 状态等。这些是大部分情况下都会出现在场景里的东西,内容和结构相对稳定,且严重影响当前状态的视觉展现。
3. **深度维修状态**:深度维修不需要做到每一刻都能保存或复原;只要能做到"一个大阶段完成后可以保存和复原"即可。部分深度维修**可以完全不做保存**(读档时回到进入该维修之前的存档点)。哪些深度维修支持阶段存档、哪些不支持,**待定(需逐个梳理清单)**。
4. **YarnSpinner 变量**:较清晰,可直接保存。
> 表现类状态究竟是"逐项还原"还是"读档时由该节点重新演出来还原",属于数据结构层面的讨论,留待后续;本节只确定"所见即所存"这一对外承诺。
### 4. 档位设置
初步打算是**一个自动保存档位 + 多个手动保存档位**。
以下细节**待定**:自动档是单份覆盖还是保留多份滚动备份;手动档数量上限;手动档能否覆盖 / 删除 / 重命名;存档槽位在选择界面展示哪些信息(截图、章节标题、进度描述、真实时间、版本号等)。
### 5. 读档
可以从"继续游戏"读取最近的存档,也可以在存档选择界面选择自动存档或某个手动存档来读档。
以下细节**待定**:"继续游戏"具体指向哪一份(最近自动档,还是最近的自动 / 手动档);游戏进行中读档是否允许、是否二次确认。
## 四. 其他需求
1. **不可存时段与保存按钮置灰**:当前不处于可存档状态时,手动保存按钮置灰。典型为**深度维修过程中**(按设计这部分不能存档)。注意:**首次启动不置灰**——刚启动就会进入第一个节点,此时一般已经完成了自动保存,因此正常情况下总有可用的自动存档。
2. **设置数据与存档分离**:音量、画质、分辨率、语言、对话推进模式(自动 / 快进)等全局设置**不进入存档槽**,独立持久化,不随读档回滚。
3. **本地化跟随当前语言**:存档记录的是行 ID 等本地化引用而非最终文本,读档后对话文本 / 语音按**当前语言设置**显示,而不是存档时的语言。
4. **Steam 云存档**:需要支持 Steam 云存档 / 多机同步(存档目录规划与文件组织需为此预留)。
5. **不做防篡改**:作为叙事向单机游戏,存档不做加密 / 校验,允许玩家修改存档。
## 五. 待后续确定的事项(TODO)
- 自动档 / 手动档的数量、覆盖、删除、重命名规则。
- 存档槽位选择界面展示的字段(含是否截图)。
- "继续游戏"指向哪一份存档;游戏中读档是否允许及确认流程。
- 新游戏与既有存档的关系(是否覆盖 / 清空、是否需要覆盖确认)。
- 全局 / 跨周目数据(章节解锁、已读、画廊、成就等)与单局存档的边界划分。
- 各深度维修是否支持阶段存档的逐个清单。
- 自动存档是否需要可见反馈。
- 表现类状态的还原方式(逐项还原 vs 重进节点重演)——并入数据结构讨论。