游戏存档压缩的常用手段

07/0262 浏览开发心得
游戏存档通常会随着玩家进度、背包、装备、任务、活动和日志数据增长而变大。存档过大会带来云同步慢、保存耗时长、网络失败率上升、加载卡顿和设备存储占用增加等问题。
下面整理一套通用的存档压缩思路,适用于本地存档、云存档、JSON 存档、二进制存档等场景。
1.只保存必要状态 最重要的原则是:能通过配置、公式或运行时重建的数据,不要保存。
常见可省略字段:
道具名称、描述、图标路径
品质名称、颜色、排序权重
技能文本、词条显示文本
默认值字段,例如 level = 1、locked = false、star = 0 空数组、空表、空字符串
可由 ID 查配置表恢复的静态字段
示例:
不推荐 { "id": "sword_001", "name": "铁剑", "icon": "sword.png", "quality": "common", "level": 1, "locked": false }
推荐 { "id": "sword_001" } 读取存档后,再通过配置表恢复显示信息。
2.使用短字段名
JSON 存档中字段名会重复出现。背包、装备、符文、宠物等大量列表数据里,长字段名会显著增加体积。
可以在保存层做字段映射:
运行时结构 { "itemId": 123, "qualityId": "red", "level": 50, "experience": 12000 }
存档结构 { "i": 123, "q": "red", "l": 50, "e": 12000 } 注意:
运行时代码不一定要使用短字段名。
推荐只在序列化/反序列化层转换,避免降低业务代码可读性。 需要保留版本兼容,旧字段和新字段都能读取。
3. 省略默认值
如果某个字段等于默认值,保存时可以直接省略。
常见默认值:
false 0 1 空字符串 空数组 空表 初始容量 初始等级 示例:
不推荐 { "level": 1, "star": 0, "locked": false, "equipped": false }
推荐 {} 反序列化时统一补默认值:
level = saved.level or 1 star = saved.star or 0 locked = saved.locked == true
4. 列表用数组代替对象
如果字段顺序固定,可以用数组压缩对象。
对象形式 { "id": "atk_pct", "value": 0.08, "locked": true }
数组形式 ["atk_pct", 0.08, 1] 优点:体积更小。 缺点:可读性差,字段变更时更容易出错。
建议:
数据规模很大时再使用数组形式。 保留清晰的编码/解码函数。 在代码中集中定义字段顺序。
5. 拆分存档分组
不要把所有数据都放进一个巨大的存档对象。可以按变更频率和功能拆分。
常见分组:
核心进度:等级、关卡、解锁状态
货币资源:金币、钻石、材料
装备背包:装备、符文、道具
活动数据:限时活动、签到、任务
设置数据:音量、画质、偏好 好处:
只上传变化的分组。 降低单次保存体积。 某个分组损坏时不一定影响全部数据。 高频变化数据可以单独节流保存。
6. 脏标记与延迟保存
不是每次数据变化都立刻完整保存。
推荐做法:
数据变化时只标记 dirty。
定时合并保存,例如每 10~30 秒保存一次。
关键操作立即保存,例如付费购买、抽卡、消耗稀有资源。
普通操作延迟保存,例如 UI 排序、临时选择、普通战斗进度。 这样可以减少云存档请求次数和重复写入。
7.避免保存临时运行时状态
很多状态只在当前会话有意义,不应该进入长期存档。
常见不应保存的字段:
UI 当前选中项
滚动条位置
动画计时器
战斗中的临时
buff 计时 粒子、飘字、特效状态
缓存字段
已格式化文本
临时排序结果
调试信息
如果必须保存中途战斗进度,也应单独设计一个轻量的战斗快照,而不是把完整运行时对象直接落盘。
8.对重复结构做模板化
大量对象如果结构相似,可以保存模板 ID + 差异。
示例:
不推荐:每个怪物保存完整属性 { "id": "goblin", "hp": 100, "atk": 20, "speed": 1.2, "drop": "coin" }
推荐:保存模板 ID 和必要差异 { "id": "goblin", "lv": 12, "hpRate": 0.7 } 配置表负责提供基础属性,存档只保存玩家造成的差异状态。
9.数值量化与精度控制
很多小数没有必要保存完整精度。
例如:
百分比保留 4 位小数
坐标保留 2 位小数
时间戳用整数秒,不用毫秒
战斗统计只保存整数 json
不推荐 { "critRate": 0.123456789 }
推荐 { "critRate": 0.1235 } 注意不要压缩会影响公平性的核心数值,尤其是竞技、排行榜、付费相关数据。
10.使用位集或数字编码布尔状态
大量布尔值可以用 bitset 压缩。
适用场景:
成就是否领取
关卡是否通关
图鉴是否解锁
每日奖励是否领取 json
不推荐 { "claimed1": true, "claimed2": false, "claimed3": true }
可选 { "claimedBits": 5 } 5 的二进制是 101,表示第 1 和第 3 项已领取。
缺点是可读性下降,建议只用于数量较大的布尔集合。
11.使用增量记录替代完整快照
某些系统可以只保存变更记录,而不是保存完整状态。
例如:
已领取奖励 ID 列表
已购买商品 ID 和次数
已解锁图鉴 ID
已完成任务 ID
如果完整状态能从配置表 + 变更记录推导出来,就不需要保存完整状态。
12.定期清理过期数据
活动、邮件、临时兑换、历史战报等数据容易长期残留。
建议:
活动结束后清理活动临时货币和进度。
过期邮件只保留摘要或直接删除。
战报只保留最近 N 条。
日志不进入玩家存档。
临时补偿标记在确认无用后迁移清理。
13. 使用压缩算法
如果存档后端允许,可以对 JSON 做压缩,例如:
gzip zlib lz4 zstd
优点:
对重复字段名和大量 JSON 文本非常有效。
缺点:
增加 CPU 开销。
调试不方便。
需要后端和客户端都支持。
小存档压缩收益有限。
通常建议先做结构压缩,再考虑通用压缩算法。
14.二进制存档
二进制格式比 JSON 更紧凑,例如:
protobuf flatbuffers msgpack
自定义二进制格式
优点:体积小,解析快。
缺点:开发和维护成本更高,排查线上问题不如 JSON 方便。
适合数据量大、结构稳定、对性能要求高的项目。
15.版本迁移机制
所有压缩都必须配合版本迁移。
建议存档包含:
{ "saveVersion": 3 }
读取时:
判断版本。
旧格式转新格式。
补默认字段。
修复非法值。
再交给业务逻辑使用。
兼容策略:
新读取器尽量同时支持旧字段和新字段。
不要一次性删除旧格式支持。
高风险迁移前保留备份或灰度。
16. 保存前校验和裁剪
保存前可以做一次 sanitize:
删除 nil、非法类型、函数、userdata。
限制数组最大长度。
限制字符串最大长度。
修正 NaN、Infinity。
裁剪超过上限的缓存数据。
这可以避免异常数据把云存档撑爆或导致 JSON 编码失败。
17.常见优先级建议 如果要快速降低存档体积,建议按以下顺序做:
删除不该保存的运行时状态。
省略默认值和空字段。
删除可由配置恢复的静态字段。
大列表字段名缩短。
拆分存档分组,只保存 dirty 分组。
清理过期活动和历史记录。
对超大列表使用数组编码或 bitset。
最后再考虑 gzip/protobuf 等底层格式。
18. 风险控制清单 上线存档压缩前,应检查:
新存档能否正常读写。
旧存档能否正常迁移。
断网、保存失败、云端返回空表时是否保护本地数据。
压缩后关键字段是否丢失。
付费、抽卡、排行榜相关数据是否保持准确。
是否有回滚方案。 是否有日志记录迁移异常。
总结
存档压缩的核心不是简单地把文件 gzip,而是区分“必须保存的玩家状态”和“可以从配置或运行时恢复的数据”。
优先做结构级优化:
少存字段
短字段名
省略默认值
拆分分组
清理过期数据
延迟保存
这些方式通常风险低、收益稳定,也更容易调试和维护。
猜你想搜
taptap 制造存档压缩
2
4