存档系统设计指南

精华修改于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 作为唯一接口,业务层可以专注于游戏逻辑,无需关心底层存储细节。
猜你想搜
taptap 制造云端存档设计
12
18
7