存档系统设计指南
精华修改于03/17360 浏览开发心得
多槽位云端存档系统设计
适用于 WASM/移动端场景的云端优先、本地缓存的多槽位存档方案。
1. 设计背景与约束
| 约束 | 说明 |
| --------------------- | ---------------------------------------------- |
| WASM 无持久存储 | 每次启动本地存储为空,不能依赖本地文件做主存储 |
| 云变量单 key 大小限制 | 单个 key 的 value 不能超过约 10KB |
| 存档数据会持续膨胀 | 随游戏进度增加,总数据量可能达到几十 KB |
| 需要多存档槽位 | 玩家可管理多个独立存档 |
2. 整体架构
┌────────────────────────────────────┐
│ 业务层 (游戏逻辑) │
│ Save() / SaveNow() / MarkDirty() │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ SlotSaveSystem (唯一接口) │
│ 序列化 · 分片 · 云端读写 · 迁移 │
└───────┬──────────────┬─────────────┘
│ │
▼ ▼
┌─────────┐ ┌────────────┐
│本地文件 │ │ 云变量 API │
│(同步写) │ │ (异步读写) │
└─────────┘ └────────────┘
核心原则:
云端是权威数据源,启动只从云端加载
本地是运行时缓存,先同步写本地保证不丢,再异步上传云端
SlotSaveSystem 是唯一入口,业务层不直接接触云变量或本地文件
3. 云端 Key 布局
cloud keys:
├── save_meta # 全局概要 (所有槽位的摘要信息)
│
├── s_1_head # 槽位1 分片头 (索引 + 校验)
├── s_1_groupA # 槽位1 数据组A
├── s_1_groupB # 槽位1 数据组B
│
├── s_2_head # 槽位2 (同上结构)
├── s_2_groupA
│ ...
└── s_N_... # 槽位N
3.1 save_meta
存储所有槽位的摘要信息,用于存档选择界面快速展示:
lua
save_meta = {
version = 1,
activeSlot = 2, -- 上次使用的槽位
slots = {
["1"] = { -- 每个槽位的摘要
label1 = ...,
playTime = 36000,
timestamp = 1710590400,
createdAt = 1710000000,
},
["2"] = { ... },
},
}
3.2 分片 head
每个槽位有一个 head key,记录该槽位所有数据组的索引和校验信息:
lua
s_N_head = {
format = 1, -- 分片格式版本号
version = 6, -- 存档数据版本号
timestamp = 1710590400,
slotId = N,
keys = {
groupA = {
cs = 123456, -- 校验码
len = 2048, -- 长度
},
groupB = {
cs = 789012,
len = 3072,
},
},
}
4. 数据分组
将完整存档数据拆分为多个功能组,每组独立编码为 JSON 并存到单独的 key:
lua
local function SplitIntoGroups(saveData)
return {
core = { ... }, -- 核心进度 (等级/关卡等)
currency = { ... }, -- 货币类数据
items = { ... }, -- 物品/装备
skills = { ... }, -- 技能/天赋
misc = { ... }, -- 杂项配置
}
end
5. 校验机制
每个分组在编码时计算 DJB2 哈希,写入 head。读取时重新计算并比对,确保数据完整性。
6. 生命周期流程
6.1 初始化 (Init)
从云端读取 save_meta
有 meta → 回调 (meta, isNewPlayer=false)
无 meta → 检查旧格式 key,执行迁移
网络错误 → 指数退避重试,全部失败则回调错误
6.2 加载存档 (LoadSlot)
读取 sNhead
从 head.keys 收集所有分组 key
批量读取所有分组
每组: 校验 DJB2 → JSON 解码
合并所有组 → 完整 saveData
版本迁移 → 反序列化到运行时状态
计算离线时长 → 设置 saveConfirmed = true
6.3 保存 (Save / SaveNow / MarkDirty)
Serialize() → 收集运行时状态为 saveData
SaveLocal(saveData) → 原子写入本地文件
SplitIntoGroups(saveData) → 拆分为功能组
每组: JSON 编码 → 计算校验码
异步云端批量写入 (BatchSet): head + 所有分组 key
更新 save_meta
6.4 新建存档 (CreateNewSlot)
重置运行时状态到初始值
序列化 → 本地保存 → 云端保存
更新 save_meta(添加新槽位摘要)
6.5 删除存档 (DeleteSlot)
校验: 不能删除当前活跃存档
从 save_meta 移除该槽位
云端批量删除该槽位的所有 key
7. 自动保存循环
在每帧 Update 中管理:
累计游戏时长
云端重试计时器(保存失败后的重试)
自动保存计时器:每 SAVE_INTERVAL 秒触发 Save()
脏标记计时器:MarkDirty() 后 DIRTY_DELAY 秒触发 Save()
8. 版本迁移
存档数据包含 version
字段。加载时通过迁移链逐级升级:
```lua
local MIGRATIONS = {
[1] = function(data) ... end, -- v1 → v2
[2] = function(data) ... end, -- v2 → v3
}
local function RunVersionMigrations(data)
while data.version < CURRENT_VERSION do
local migrateFn = MIGRATIONS[data.version]
if migrateFn then
migrateFn(data)
data.version = data.version + 1
else
break
end
end
end
```
9. 容错策略
| 场景 | 处理 |
| ---------------- | ----------------------------------------- |
| 云端读取失败 | 指数退避重试,最多 N 次 |
| 分片校验不匹配 | 回退到旧格式单 key 读取 |
| 云端保存失败 | 本地已保存不阻塞游戏,后台定时重试 |
| 本地写入失败 | 仅打印警告,依赖云端 |
10. 接口设计
```lua
-- 初始化 (启动时调用一次)
SlotSaveSystem.Init(onMetaReady)
-- 存档操作
SlotSaveSystem.LoadSlot(slotId, onComplete)
SlotSaveSystem.CreateNewSlot(slotId, onComplete)
SlotSaveSystem.DeleteSlot(slotId, onComplete)
SlotSaveSystem.CopyToSlot(targetSlot, onComplete)
SlotSaveSystem.SaveAndUnload(onComplete)
-- 保存触发
SlotSaveSystem.Save() -- 常规保存
SlotSaveSystem.SaveNow() -- 立即保存
SlotSaveSystem.MarkDirty() -- 标记脏数据, 延迟合并
-- 每帧调用
SlotSaveSystem.Update(dt)
-- 查询
SlotSaveSystem.GetMeta() -- 返回 save_meta
SlotSaveSystem.GetActiveSlot() -- 当前槽位号
SlotSaveSystem.IsSaveHealthy() -- 存档健康状态
```
11. 关键设计决策
| 决策 | 理由 |
| ---------------------------- | ---------------------------------------------- |
| 云端优先,本地缓存 | WASM 每次启动本地为空,必须以云端为权威 |
| 先写本地,再异步上云 | 保证运行中不丢数据,云端失败不阻塞游戏 |
| 数据分组 | 绕过单 key 大小限制,且各组独立互不影响 |
| DJB2 校验 | 确保数据完整性 |
| 迁移链逐级升级 | 任意旧版本存档都能加载 |
| 原子本地写入 (.tmp + rename) | 防止写入中断导致文件损坏 |
| MarkDirty 合并机制 | 高频小改动不触发高频保存,延迟合并后一次性写入 |
12. 业务层需实现的扩展点
| 扩展点 | 说明 |
| ----------------------------- | ---------------------------------- |
| SplitIntoGroups(saveData)
| 定义分组策略,将完整数据拆为命名组 |
| MergeGroups(groups)
| 将各组合并还原为完整数据 |
| Serialize()
| 从运行时状态收集数据 |
| Deserialize(data)
| 将数据写入运行时状态 |
| BuildMetaSlot()
| 构建当前槽位的摘要信息 |
| MIGRATIONS[version]
| 版本迁移函数 |
总结
云端优先的设计,确保 WASM 环境下的数据持久化
数据分组解决单 key 大小限制
本地缓存保证数据安全
多槽位管理满足玩家需求
完善的容错机制确保系统稳定
通过 SlotSaveSystem 作为唯一接口,业务层可以专注于游戏逻辑,无需关心底层存储细节。



