19 KiB
存档系统设计方案
本文档为实现层设计文档,承接 存档系统需求。 记录拆分方案、各部分实现思路,以及已确定的关键决策。需求未定项见文末 TODO。
〇. 文档状态
-
需求来源:
Docs/存档系统需求.md -
当前阶段:P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版已落地。原
StorageSystem已重命名为YarnVariableStorage并完成职责拆分;FixStateMachine 接口与 DTO 已接入;自动档 / 手动档槽位、sidecar、缩略图、原子写、latest_slot 与 P1 测试档迁移已接入;YarnonNodeStart已触发自动存档判定与写盘;读档已切换为 Provider sync/async 契约 + Phase/Barrier 编排。 -
最近更新:2026-07-17(读档入口收敛到
GameManager;恢复拆分为预检与执行;Legacy 旧档停止运行时恢复)。
一. 前提:旧实现的三档归类
需求§一明确:新方案不沿用旧实现的恢复锚点、存档文件组织等不可靠部分;旧系统中仍可靠的底层能力在后续实现阶段再评估复用。据此分三档:
| 档别 | 含义 | 内容 |
|---|---|---|
| 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 契约 |
| D7 | 维修子模块存取 | 统一 sections + Provider,不单独设 deepRepair 根字段 |
P1 快照结构、新增维修模块方式 |
决策展开
-
D1 逐项还原:表现类(角色动画、环境、Timeline、UI 遮罩等)在读档时由快照中记录的状态逐项恢复,而非「重进节点重演」。
-
D1.1 时间序列粒度:Timeline 等只需复原到终态或初态,不记录播放进度。
-
D6 Phase + Barrier:读档不是「每个 Provider 串行
yield return的协程链」。编排层按阶段推进,仅在必须等待的边界上挂起(场景加载、锚点重进、P4 淡入淡出、部分 async Provider 还原);同阶段内的 sync 还原应连续调用,不必逐步 yield。详见 §4.1。 -
D7 统一 sections:BlockPuzzle / Memory / Cutting 等维修子模块与 env / actor / fix 一样,各建 DTO + Provider 写入
sections,用RestoreOrder表达还原顺序。不再维护独立的SaveSnapshot.deepRepair段或DeepRepairSavePolicy。 -
D2~D5:见上表;P2/P6 实现时使用。
三. 两个被点名不可靠点的全新设计(A 档)
1. 恢复锚点(全新)
-
锚点定义:YarnProject 标识 + 节点名 + 进入该节点那一刻的宏观阶段。
-
落点时机:P3 可存点判定通过后(当前代码由
DialogController.OnNodeStart传入触发节点名,SnapshotCapture以该节点作为 anchor)。 -
瞬跳节点(规范层,待脚本排查 + 实现补强):凡进入后不向玩家停留、仅作路由/阶段切换的节点,均不得作为存档边界。维修流程中
center明确属于瞬跳;start/init/event/end等 tag 已在SavePointEvaluator黑名单。部分content瞬跳节点(如UF检查状态)须 Yarn 侧标no_save,详见 Yarn 维修节点类型规范 §8。另:若判定通过后在 settle 帧内 Yarn 已连跳,锚点 nodeName 可能漂移——属实现层已知问题,与瞬跳规范一并处理。 -
重入方式:读档后
SnapshotRestore.RestoreAnchor→StartDialogue(节点名)。
2. 存档文件组织(全新)
- 槽位制、sidecar、原子写、截图缩略图——P2 基础实现已落地,统一由
SlotManager/SlotDirectory写入persistentDataPath/AllOurBrokenParts/demo_saves/slot_x/。Steam 云存档目前仅保留CloudSaveManager/ICloudSaveBackend扩展点,尚未接真实平台后端。
四. 代码架构:职责拆分(已实现)
旧 StorageSystem(现 YarnVariableStorage)曾同时承担 Yarn 变量、深度维修容器、快照 I/O、读档编排,现已拆成以下类。禁止再往 YarnVariableStorage 上堆存档逻辑。
┌─────────────────────┐
│ YarnVariableStorage │ Yarn 运行时变量(VariableStorageBehaviour)
└──────────┬──────────┘
│ GetAllVariables / SetAllVariables
┌──────────▼──────────┐
│ SnapshotService │ Capture() / Restore() — 纯内存快照
└──────────┬──────────┘
┌───────────────────┼───────────────────┐
│ │ │
┌──────────▼─────────┐ ┌───────▼────────┐
│ SnapshotPersistence │ │ ISnapshotProvider × N │
│ 文件读写(P2 接管) │ │ env/actor/fix/… │
└──────────┬─────────┘ └──────────────────────┘
│
┌──────────▼─────────────────┐
│ SaveRestoreOrchestrator │ 存读档流程:UI 反馈、落盘、还原、旧档兼容
└────────────────────────────┘
| 类 | 路径 | 职责 |
|---|---|---|
YarnVariableStorage |
Game Loop/YarnVariableStorage.cs |
仅 Yarn $ / $global_ 变量读写 |
SnapshotService |
SaveSystem/SnapshotService.cs |
Capture(),不涉及文件 |
SnapshotPersistence |
SaveSystem/SnapshotPersistence.cs |
JSON 序列化 / 反序列化;P1 测试路径兼容;旧档格式检测 |
SlotManager / SlotDirectory |
SaveSystem/SlotManager.cs / SaveSystem/SlotFileSystem.cs |
P2 槽位业务、sidecar、缩略图、latest_slot、原子写、P1 测试档迁移 |
SaveRestoreOrchestrator |
SaveSystem/SaveRestoreOrchestrator.cs |
保存入口;读档预检与 Snapshot 内部执行阶段;不拥有会话状态 |
SnapshotCapture / SnapshotRestore |
SaveSystem/ |
快照组装与逐项 provider 还原 |
SnapshotSerializer |
SaveSystem/ |
schemaVersion、稳定 SaveId key |
SnapshotRegistry + Providers/* |
SaveSystem/ |
表现层 + 维修子模块 Provider(env / actor / fix / bodyModule …) |
调用约定
| 场景 | 入口 |
|---|---|
Yarn onNodeStart 自动存档 |
SavePointEvaluator.CanAutoSave() → SaveRestoreOrchestrator.AutoSaveRoutine(nodeName) |
| 手动存档 | SaveRestoreOrchestrator.CreateManualSlot(slotIndex)(复制最近自动档) |
| 槽位读档 | GameManager.Instance.TryRestoreSlot(slotIndex, completed) |
| 文件读档(调试) | GameManager.Instance.TryRestoreFile(path, options, completed) |
| 仅捕获内存快照 | SnapshotService.Capture() |
| Yarn 变量 | YarnVariableStorage.Instance.SetValue / TryGetValue |
P1 测试落盘路径
persistentDataPath/AllOurBrokenParts/snapshot_test/latest_snapshot.json(ConstRef.SnapshotTestPath)
4.1 读档还原编排与 Provider 契约(重要)
当前实现已经采用同步 / 异步 Provider 显式契约,并由 GameManager 在会话命令外层调用 PrepareRestore 与 ExecutePreparedRestore。
终态模型:Phase + Barrier,而非协程链
读档还原由 SnapshotRestore / SaveRestoreOrchestrator(P4 扩展)按阶段编排,阶段之间用 Barrier(必须等完再继续) 分隔:
Phase 0 同步预备
└─ Yarn 变量 SetAllVariables
│
Phase 1 Barrier(异步)
└─ 场景加载(Addressables LoadSceneAsync)
│
Phase 2 同步批量还原(同帧连续调用,不逐步 yield)
└─ macro / env / actor / audio / timeline / fix / screen …
│
Phase 2′ 可选 Barrier(异步,按需)
└─ Timeline Addressable 加载、其他须上报的 async 还原
│
Phase 3 Barrier(异步)
└─ RestoreAnchor(Stop + StartDialogue)
│
Phase 4 Barrier(P4:淡入淡出等演出时序)
└─ 黑屏 / Loading / FadeIn …
- Barrier:该步完成前不进入下一阶段(如场景未加载完不还原 actor;Yarn 未 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 / fix / screen | 否(Phase 2 sync) | 当前实现均为同步写回;fix 的 EnterImmediate 默认 yield break |
| timeline(本地终态) | 否(Phase 2 sync) | RestoreAtEndLocal 等 |
| timeline(Addressable) | 可选 Barrier | 若要求进节点前 Timeline 终态就绪,须显式等待;禁止 fire-and-forget(见下) |
| RestoreAnchor | 是 | StartDialogue 为 Yarn 异步 API |
| P4 淡入淡出 | 是 | 演出时序 |
| P5 深度维修 | 部分 Barrier | 如 Fix cue EnterImmediate、async Provider 还原须等待;与其他 section 同走 Provider 编排 |
RestoreOrder 的语义(终态)
- 表示同 Phase 内的建议顺序或软依赖(如 scene 必须先于 env/actor),不是「每步之间必须
yield return」。 - 硬依赖应通过 Phase 划分 + Barrier 表达,而不是无限细化 RestoreOrder 数字。
- scene 加载(Phase 1 Barrier)必须在 Phase 2 之前完成;目标章节已在 PrepareRestore 中解析并放入
SnapshotRestoreContext;env / actor / audio / timeline / fix / screen 之间目前无硬依赖,Phase 2 内一批执行即可。
Provider 契约(当前实现)
| 类型 | 契约 | 使用规则 |
|---|---|---|
| 同步还原 | ISyncSnapshotProvider.Restore(object dto, context) |
同一 Phase 内按 RestoreOrder 连续调用 |
| 异步还原 | IAsyncSnapshotProvider.RestoreAsync(object dto, context) |
必须把完整等待过程交给编排层 |
| 编排层 | SnapshotRestore + SaveRestoreOrchestrator |
Phase 编排,仅对异步 Provider、Scene、Readiness、Anchor 等 Barrier yield |
新增可存子系统时:默认实现 sync Restore;仅当存在必须等待的加载/状态切换时,才实现 async 接口并向编排层上报可等待句柄,不得在 Provider 内私自 StartCoroutine 而不纳入 Barrier。
对后续阶段的影响
- P2 / P3:新增代码不得复制「全 Provider 协程链」模式。
- P4:当前已完成 Prepare / Execute 边界、Provider 契约、SceneReadiness 与 GameManager 会话编排;后续主要补 Play Mode 存档样本验证。
- P5:深度维修子模块与其他子系统一样经 sections Provider 扩展,不应再假设独立 Phase 或
deepRepair根字段;是否独立 Phase 仅由具体 Provider 的 async 需求决定。
五. 拆分方案(分层 × 分阶段)
P0 契约定义 ── 已完成
│
P1 快照层 ── 已实现(含原 StorageSystem 职责拆分与重命名)
│
┌────┴───────────────┐
P2 槽位/落盘层基础版 P3 可存点判定基础版
已落地:slot_0/slot_1..8、sidecar、缩略图、原子写、latest_slot、onNodeStart 门控
└────┬───────────────┘
│
P4 复原编排层基础版(Provider 契约 + Phase/Barrier + 抑制自动存档)── 已落地
│
┌────┴────┐
P5 维修子模块 section 扩展 P6 存/读档 UI & 流程
│
P7 横切
P1 快照层(已实现)
SaveSnapshot纯数据结构 +ISnapshotProvider逐项还原(D1/D1.1)。- Provider:
env/actor/audio/timeline/subway/fix/screen;维修子模块punchTape(63) /bodyModule(66) /eye(67) 等,后续 BlockPuzzle / Memory 等同理扩展。 - Yarn 变量经
YarnVariableStorage纳入快照;$data.*死路径已移除。 - 原五类 IData 已迁入
sectionsProvider;IData/DataContainer/LoadIndex已移除。 - 注意:P1 中
Restore统一为协程、逐步yield return属临时实现,终态见 §4.1 / D6;后续阶段勿在此基础上堆逻辑。
P2 槽位/落盘层(基础版已落地)
SlotIndex:slot_0为自动档,slot_1..slot_8为手动档。SlotDirectory/SlotAtomicWriter:槽位目录、snapshot.json/meta.json/thumbnail.png、latest_slot.json与原子写。SlotManager:自动档覆盖写入、手动档复制、删除槽位、读取快照 / meta / 缩略图、UI view model、P1 测试档迁移。SlotThumbnailCapture:当前使用Camera.main渲染 640×360 PNG;失败时返回 null,不阻断存档。CloudSaveManager/ICloudSaveBackend:仅保留云存档扩展点,未接 Steam 后端与冲突处理。- 手动档 = 拷贝最近自动快照,符合需求约定。
当前接入:
SavesPanel/SaveFileUIController使用SlotManager.GetSlotViewModels(),并通过GameManager.TryRestoreSlot()发起恢复。- 「继续游戏」使用
SlotManager.GetLatestSlotIndex(),由ChapterController转交GameManager.TryRestoreSlot()。 - 缩略图规格暂按 640×360 实装,是否满足正式 UI 仍需 P6 验证。
P3 可存点判定层(基础版已落地)
DialogController.OnNodeStart更新当前 node/tags 后调用SavePointEvaluator.OnNodeStartForAutoSave(),通过后执行SaveRestoreOrchestrator.AutoSaveRoutine(nodeName)。- Fresh 进入 vs detour 续跑:
SavePointEvaluator按yarnProjectId维护InProgressNodes;节点尚未onNodeComplete时再次onNodeStart(Yarn detour 返回父节点)→DetourResume,跳过自动存。OnNodeComplete移出集合;切换 YarnProject /StopDialog重置;读档 anchor 重进仅OnRestoreEnterNode标记,不写盘。 - tag 规则:
hub/linear/content默认允许;start/init/function/detour/center/performance/event/end默认禁止;no_save覆盖一切并禁止 OnNodeStart 自动存。 - 自动档边界 = Fresh OnNodeStart 且 tag 判定通过且非
DetourResume(不区分 hub / content / linear 的额外终身去重)。 - Yarn
<<save>>(SaveYarnCommand)走CanExplicitSave:仅全局门控,默认 omit anchor;用于节点末尾进入无对话 / Fix 交互等边界。 SaveRestoreOrchestrator.IsRestoring与GameManager.Session.IsPaused会阻止自动保存与显式 save,避免读档中覆盖自动档。CreateManualSlot复用CanManualSave:slot_0存在且通过全局门控即可复制(含显式<<save>>写盘后的手动档)。
仍待补齐:
- 维修 Yarn 的瞬跳
content节点需要继续排查并补no_save,优先处理验证工具中暴露的误存节点。 - 深度维修过程中是否允许
<<save>>的需求边界待确认(与可存点判定一并定案)。 - 目前验证工具主要覆盖存盘行为;复杂节点 tags 边界和读档后落点尚未形成自动化验证。
P4~P7
- P4:已落地。
ISnapshotProvider已拆为捕获契约 +ISyncSnapshotProvider/IAsyncSnapshotProvider;读档由GameManager管理会话、遮罩和失败决策,SaveRestoreOrchestrator负责预检与 Snapshot 内部 Phase + Barrier;Provider 后增加 SceneReadiness;所有文件统一按当前 schema 预检,不保留旧格式专用分支。 - P5(部分已落地):
punchTape/fix/bodyModule/eye已迁入 sections;BlockPuzzle / Memory / Cutting 等待新增 Provider;isSystemOn/currentRepairSystemTypeRestore 仍延后。 - P6:接入正式存 / 读档 UI、继续游戏、新游戏覆盖自动档、游戏中读档确认等玩家流程。
- P7:Steam 云存档真实后端、版本兼容策略、全局 / 跨周目数据边界等横切项。
六. 推荐落地顺序
P0 → P1(完成)→ P2/P3 基础版(已落地,待补边界与 UI 接入)→ P4 基础版(完成,待 Play Mode 验证)→(P5/P6)→ P7
七. 仍待确定(TODO)
- 手动档数量上限、覆盖 / 删除 / 重命名规则。
- 游戏进行中读档是否允许、是否二次确认。
- 自动存档是否需要正式可见反馈(当前
SaveRestoreOrchestrator使用InfoPanel.ShowSaveLoading()/HideSaveLoading(),是否作为最终 UX 待定)。 - 各维修子模块是否支持阶段存档的逐个清单(新增 Provider 前定案)。
- 维修 Yarn 瞬跳
content节点:按 Yarn 维修节点类型规范 §8.4 全项目补no_save(PeipeiUF检查状态等优先)。 - 全局 / 跨周目数据与单局存档的边界划分。
- 缩略图最终规格与 UI 展示方式(当前 P2 基础版为 640×360 PNG)。