下载 App

”模块自治“的存档系统 设计指南

修改于05/11360 浏览开发交流 疑似 AI 合成内容
可结合下方方案一起使用
大部分游戏的存档系统是这样诞生的:项目初期,有人写了一个 SaveGame() 函数,把几个变量塞进 JSON 存到本地。
能跑。
后来系统越来越多——背包、成就、任务、好友、赛季——每个系统都往这个函数里加几行。
半年后 SaveGame() 变成了三千行的巨型函数,碰一行就可能把整个存档搞坏,没有人敢重构。

这篇文章讲的是:一个存档系统从第一天开始就该长什么样

一个完整的存档系统由四层构成,每一层建立在上一层的基础上:调度、所有权、契约、效率
大部分存档系统只有第一层。
后面三层做不做、做到什么程度,取决于项目规模和团队纪律。
但如果你在第一天就知道这四层的存在,很多设计选择会自然地往正确的方向走。

调度:每个模块管自己

存档框架的核心是一个注册表。
每个子系统在自己的文件里注册三样东西:怎么存、怎么读、默认值是什么。
框架按注册顺序依次调用,用 pcall 隔离——一个模块出错,其他模块不受影响,出错的模块回退到默认值。
新增一个子系统只需要加一个注册调用,不需要碰框架代码,不需要碰其他模块。
order 字段解决加载顺序依赖——如果背包依赖配置表先加载完,配置表的 order 设小一点就行。
这一层做好了,你就不会有三千行的 SaveGame()。但光有调度还不够。

所有权:数据是模块的局部变量

光有调度还不够。
如果数据仍然挂在一个全局对象上,全代码库任何位置都能直接改,那存档逻辑虽然分散了,数据的混乱程度没变——运行时临时数据会被无脑存进去,变更无法追踪,脏标记无从做起。
数据应该是模块自己的局部变量。
Lua 的 local 变量文件外看不到,不是靠团队规范"请不要直接改全局数据",而是语言层面不让你改。
外部只能通过模块暴露的 API 来读写。

这带来三个好处。

第一,serialize 显式列出要存的字段,模块内部的临时变量不会意外被存进去。
第二,所有写操作都经过模块的 API 入口,可以在入口调 MarkDirty 标脏,存档系统能知道"这个模块的数据变了",这是后面做增量保存的前提。
第三,数据变更可追踪,可以加日志、加断言、加校验。
所有权这一层越早建立越好。
一旦"直接改全局对象"的写法扩散开,再改就要找几十个文件、几百个调用点。
新模块从第一天就把数据放自己手里,旧模块大改的时候顺手迁移,不追求一步到位。

契约:一份 Schema 定义一切

光有 Register 的话,每个模块要自己写防御性检查,自己维护默认值——initDefault 里写一次,deserialize 的 else 分支里再写一次。
模块越多,这些手写防御的写法越不统一,默认值越容易两处不同步。
想知道一个模块的存档格式有哪些字段,得翻三个函数合并。
在 Register 之上封装一层 Define,用一份 schema 声明把这些全替代掉。
每个模块声明自己有哪些字段、每个字段什么类型、默认值是什么。
框架根据 schema 自动做四件事。

默认值填充

——存档里缺字段,用 schema 里的 default 补上,模块拿到的数据一定是完整的。
新玩家初始化时也用同一份 default,不需要单独维护 initDefault,不存在两处默认值不一致的问题。

类型检查

——存档里的某个字段被 bug 写成了错误类型,框架打一行警告并用 default 替换,模块不用自己写防御代码。

版本迁移

——每个模块有自己的 version 号,存档里记录着各模块的版本。
加载时框架比对版本号,自动链式执行迁移函数直到追平。
背包改了格式不需要碰全局版本号,不需要通知其他模块。
新增一个带 default 的字段不需要升版本号(schema 自动补全),字段重命名或结构变更才需要写 migration。

可检视

——schema 本身就是存档格式的文档。
任何人想知道这个模块存了什么,看 schema 就行。
Define 在内部调用 Register,和已有的 Register 模块完全共存。
早期快速试错的模块可以用 Register,格式稳定后迁移到 Define,新模块从一开始就用 Define。
两者共存,不强制一步到位。

效率:只存变了的部分

每 30 秒自动保存一次,不需要每次都全量序列化所有模块。
本地存档差别不大,但云端存档的网络上传远比序列化贵——放置挂机时几十个模块里可能只有货币在涨,其他纹丝不动。
两个层次的优化可以互补使用。

脏标记减少序列化次数。

数据被修改时打标记,保存时只序列化有标记的模块,其他复用上次的快照。
脏标记的前提是数据所有权——所有写操作都经过模块 API,在入口调 MarkDirty。
数据挂在全局对象上,任何代码都能直接改,MarkDirty 就会漏掉。
第二层(所有权)是第四层(效率)的地基。
如果所有权还没到位,脏标记不可靠,跳过这一步,直接做快照对比。

快照对比减少上传次数。

把这次编码的 JSON 和上次成功上传的比较,一样就跳过。
代价是每次保存仍然需要全量序列化(要编码出来才能比较),但对于"序列化快、上传慢"的云端存档场景,省掉网络上传的收益远大于序列化的开销。
如果存档按功能分了几个分组,可以按分组粒度做快照对比,挂机时可能一半的分组都能跳过。
脏标记是代码级粗筛,快照对比是数据级细筛。
脏标记可能误报(标了脏但数据实际没变,比如加了 10 又减了 10),快照对比负责兜底。
两者配合效果最好,但快照对比可以独立工作。

存档浏览器

有 schema 之后,可以自动生成结构化的存档查看工具——每个模块按 schema 结构化展示,标注版本号和分组,遇到异常数据自动告警。
比肉眼翻几千行 JSON 快一个数量级。
更进一步,两个存档之间的 diff:一眼看出什么变了、什么丢了。
开发成本不高——schema 提供结构信息,剩下的就是遍历和比较。
团队小不想维护工具的话,把存档 JSON 和 schema 一起丢给 AI 分析也行,成本接近零。

取舍

这四层不是都必须做。
Schema 有成本,要求先定义数据结构再写业务。
快速试错阶段 schema 会频繁变动,每次变动都要写 migration。
折中方案是早期 Register,稳定后 Define,两者共存。
脏标记有前提,需要数据所有权到位。
代码库里大量直接操作全局对象的话,引入脏标记之前要先解决所有权。
解决不了就用快照对比替代。
模块级版本号有心智负担,几十个模块几十个版本号,每次改字段都要判断要不要升版本号。
判断本身不难(加字段不需要,改结构才需要),但分散在每个模块的开发者头上。
Jam 和原型做到调度就够了。
小团队长线项目做到调度加所有权。
中型项目四层都值得做。
大型项目四层必须做,而且还需要更多——分布式存档、事务性保存、回滚机制等等。

第一天的清单

如果从零开始,按优先级排:
注册式框架,数据归模块所有,
分组策略(云端存档提前规划),
Schema(新模块用 Define),
快照对比,脏标记(等所有权到位后加),
存档浏览器(有余力再做)。

最后

一个好的存档系统不是一个复杂的系统。它是一个边界清晰的系统。
数据归谁管,清晰。
每个模块守好自己的局部变量,通过 API 暴露访问。
数据长什么样,清晰。
一份 schema 声明字段、类型、默认值,不靠代码逻辑隐式定义。
什么时候需要存,清晰。
知道哪些模块变了、哪些分组动了,只碰需要碰的部分。
存档系统是所有子系统里最不容许"先上线再重构"的。
有了线上玩家,每一次格式变更都要写 migration,每一次重构都要向后兼容。
地基越早打好,后面的日子越轻松。
猜你想搜
taptap 制造模块自治存档系统
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
68 赞
21 回复
03:06
为了不被斩杀,我只能做到这些了截图
为了不被斩杀,我只能做到这些了
之前一直申请不到计划3,之前说太花哨,后面说体量完成度不足,在我一番小作文下,编辑终于给了我解答,什么原生质感,玩家开局体验啥的。 本来计划2也够用,准备以后再优化了,结果计划2赠送的积分要砍半了,学生党实在消费不起,于是只能再冲一波计划3了 从周六中午到现在一天半时间,优化了一遍角色卡,战斗卡,还有抽卡界面,优化了新手教程,该说不说确实比之前要舒服些。 前半部分为新版,后半部分为旧版。 明天再次
33 赞
6 回复
01:27
《大二暑假,我用AI做了一款属于自己的游戏》截图
《大二暑假,我用AI做了一款属于自己的游戏》
本人自制游戏现已加入taptap封闭测试(仅适配安卓机型)欢迎大家前来尝试(八月十号可以正式开始游玩) 如果有建议或者bug反馈可以加入qq群928501443 #发现好游戏 #游戏日常 #浅评一下 #今天游戏圈发生了啥
3 赞
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
122 赞
44 回复
【游戏上新】商业帝国大亨截图
【游戏上新】商业帝国大亨
无名小卒还是名扬天下。 从临时工到称霸宇宙。 只需点击屏幕,就能获得财富,利用财富购置产业,获取更多财富。
6 赞
4 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
开发心得-界面布局心得截图
开发心得-界面布局心得
大家好,我正在用 TapTap 制造制作一款2D 治愈林间徒步旅行游戏《徒步荒野》。 我分享一下我的开发心得,希望能有用,下面是一些界面布局的心得。 第一步:定版式、操作 先确定游戏是横版还是竖版、需要哪些操作按钮,敲定整体玩法思路。 我的玩法灵感参考《边境之旅》,主打散步看风景、沿途触发随机事件。 原版是 3D,考虑到自己是新手,选择2D 横版街机式来做,制作更简单、好上手。 第二步:敲定全局美
6 赞
3 回复
永恒虚空持续开发日记1——关于游戏中的新名词:场频截图
永恒虚空持续开发日记1——关于游戏中的新名词:场频
写在前边:首先感谢TapTap制造版主大大@汪崽 对于作品潜力的肯定,给与了我积分计划二的权限[表情_不好意思]。这边也在加紧开发改进各种功能中,期望尽快给大家带来成熟的版本。[表情_猫咪举手] 大家好,我是永恒虚空的制作人黑猫不详,大家叫我黑猫就好。从今天开始我将以后置开发日记的形式给大家分享我在TapTap制造中制作的游戏永恒虚空的开发过程,新人希望大家多多关照! 今天先给大家介绍我作品中最重
投票
3 赞
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复
UI 组件速查指南-TapMaker大学习截图
UI 组件速查指南-TapMaker大学习
在设计游戏UI的过程中,你是否存在和ai讨论UI设计时AI出现指东打西的红温时刻。本文致力于介绍常见的UI组件,让你在开发过程中可以做到精确制导! 这是一份UI 组件的速查手册。 新手用 AI 做游戏 UI 最大的卡点,不是"AI 写不出来",而是——你说不出那个东西叫什么。 为了让你能准确的说出你需要的组件的名字,我整理出了这篇UI组件指南。 每个组件我都写了三件事:它是什么、它长什么样、它在游
精华
36 赞
6 回复