存档系统概述
Fink Framework 的存档系统以 SaveManager 为统一入口,为游戏提供强类型数据保存、读取、槽位管理、全局存档、备份恢复和自动存档能力。
当前实现将职责拆成三层:
| 组件 | 职责 |
|---|---|
SaveManager | 管理槽位/全局作用域、串行队列、异步 API、恢复顺序和自动存档 |
SaveSchema | 校验存档类型、创建默认实例、处理成员重命名兼容 |
SaveFileStore | 序列化容器、临时文件、回读校验、原子替换、备份轮换和文件解析 |
运行时命名空间为 FinkFramework.Runtime.Save。SaveManager 是不继承 MonoBehaviour 的普通 C# 单例,不需要在场景中挂载对象;但它的构造和 Unity 资源配置读取仍应在 Unity 主线程完成。
项目配置入口:
Edit → Project Settings → Fink Framework → Save System1. 系统能力
| 能力 | 当前行为 |
|---|---|
| 强类型存档 | 通过 SaveAsync<T> / LoadAsync<T> 使用明确的根数据类型 |
| JSON 与 Binary | JSON 保存为可读裸 JSON;Binary 使用 FSV1 容器和 Odin Payload |
| 单槽位与多槽位 | 单槽位固定使用 Slot 1;多槽位支持选择、创建、枚举和删除 |
| 全局存档 | 独立于玩家槽位,拥有自己的主档、备份和历史链 |
| 快照保存 | 保存调用在第一次异步等待前完成 Schema 校验和序列化,冻结本次数据 |
| 串行文件队列 | 同一主文件的保存、加载、删除和回档按顺序执行;不同目标互不阻塞 |
| 安全提交 | 临时文件写入、刷新、回读校验后才替换主档 |
| 自动恢复 | 主档失败后依次尝试即时备份和编号历史备份 |
| 历史回档 | 将选中的历史文件读取后按正常保存流程提交为新一代主档 |
| Schema 演进 | 新增成员使用默认值,删除成员自动忽略,重命名可声明旧名称 |
| 自动存档 | 主线程抓取数据,文件保存复用普通保存队列 |
2. 一次保存发生了什么
游戏对象 / 运行时状态
↓ 业务层 Capture 为纯数据对象
SaveManager.SaveAsync<T>()
↓ Schema 校验
↓ 序列化并冻结快照
等待同一目标的保存队列
↓ 后台写入临时文件并 Flush
↓ 回读临时文件并校验
↓ 轮换即时备份和可选历史备份
↓ 原子替换主档
SaveResultSaveAsync 不会把业务对象本身直接交给后台线程长期读取。Schema 校验和序列化在方法第一次让出线程前完成,因此调用方随后修改原对象,不会影响已经进入队列的这一笔保存。
文件进入原子替换阶段后不会响应中途取消,以避免出现半提交的主档;取消主要作用于排队等待和提交前工作。
3. 加载与自动恢复
加载一个目标时,系统按以下顺序寻找可用数据:
主档
↓ 失败
即时备份 _bak
↓ 失败
编号历史备份 _bak1、_bak2……
↓ 全部失败
返回失败结果如果目标文件和所有备份都不存在,系统会创建当前版本的默认实例:
Status为FileNotFound;Source为Default;Data为当前类型的新实例;LoadResult<T>.Succeeded仍为true。
因此首次启动可以直接使用 result.Data;如果需要区分首次启动、主档损坏或从备份恢复,应检查 Status、Source、UsedDefault 和 Recovered。
如果主档存在但主档、即时备份和历史备份全部读取失败,系统不会静默伪造一份看似成功的数据,而是返回具体失败状态及最后一次读取异常。
4. 槽位数据与全局数据
槽位存档
槽位数据适合保存:
- 玩家进度;
- 关卡和任务状态;
- 角色、背包和装备;
- 与某个存档槽绑定的游戏设置。
无 slotId 参数的 API 使用 CurrentSlotId。单槽位模式下它固定为 1;多槽位模式下可通过 SelectSlot 修改。
全局存档
全局存档不受当前槽位影响,适合保存:
- 音量、画质和辅助功能设置;
- 图鉴、全局解锁和账号级进度;
- 最近使用的槽位或启动偏好。
槽位和全局数据使用独立的目标锁、主档、即时备份和历史备份链。
5. 当前文件布局
存档根目录为:
Application.persistentDataPath/FinkFramework_Save以默认 Binary 后缀 .sav 为例,当前布局为:
FinkFramework_Save/
├─ global_save.sav
├─ global_save_bak.sav
├─ global_save_bak1.sav
└─ slot/
├─ slot_01.sav
├─ slot_01_bak.sav
├─ slot_01_bak1.sav
└─ slot_02.sav文件含义:
global_save/slot_01:当前主档;_bak:最近一次覆盖主档前保留的即时备份,只用于自动恢复;_bak1、_bak2:可枚举、可手动回档的编号历史备份。
加载、槽位枚举、历史枚举和删除仍兼容旧版 Slots/Slot_N/main.save、Global/global.save 等目录结构,但新保存会使用当前扁平布局。
6. JSON 与 Binary
| 项目 | JSON | Binary |
|---|---|---|
| 文件内容 | DataUtil 生成的 UTF-8 裸 JSON | FSV1 容器 + Odin Binary Payload |
| 默认后缀 | .json | .sav |
| 人工可读性 | 高,可直接检查 | 不面向人工编辑 |
| AES | 始终关闭 | 复用全局 AES 配置,可选 |
| GZip | 始终关闭 | 可通过存档配置启用 |
| 元数据 | 主要依赖 JSON 解析和文件时间 | 类型、格式、代数、时间、标志和 SHA-256 校验 |
JSON 模式为了保持可读性,不套用 FSV1 外层容器,也不会启用压缩或加密。Binary 模式会保存容器头、数据类型、提交代数、UTC 时间、压缩/加密标志和 Payload 校验值。
开发和调试阶段可以选择 JSON 方便检查;正式单机项目通常根据体积、完整性校验和内容隐藏需求选择 Binary。AES 只能提高直接读取门槛,客户端内置密钥不能替代服务端校验或防作弊机制。
7. 业务层应该负责什么
存档系统负责文件和数据生命周期,业务层仍需要负责:
- 将当前游戏状态转换为独立的纯数据对象;
- 设计稳定的存档根类型和默认值;
- 不把
GameObject、组件或其他UnityEngine.Object引用写入存档; - 根据
LoadResult<T>的来源决定是否提示玩家或记录恢复日志; - 在删除槽位前进行 UI 二次确认;
- 在格式、加密密码或数据结构变更时规划迁移方案。

