最近这两个月,在好几个技术群里反复被同一个问题刷屏:Unity 的存档到底用什么方式落地最稳?配表读取、主城数据、设置项存盘,到底该 PlayerPrefs 一把梭还是手写文件解析?我的答复一直很统一:用 Json 做序列化,写到 Application.persistentDataPath 下,这是绝大多数 Unity 项目里性价比最高、也最不容易出问题的数据可持续化方案。
这个结论不是拍脑袋来的。我做过小体量的独立游戏,也接过需要热更和 SDK 接入的商业项目,Json 在 Unity 里承担的任务从一行设置项保存,到几个 MB 的玩家存档,再到服务器下发的配置表,几乎都有覆盖。它不像二进制那么难调试,也不像 XML 那样冗余得让人头皮发麻。这篇文章就把我踩过的坑、总结的模板,以及排查问题的一套思路完整写出来,希望能让正在为存档发愁的朋友少走弯路。
1. 为什么我推荐用 Json 做 Unity 持久化
1.1 三种主流持久化方案的横向对比
很多新手一上来就喜欢问“哪个方案最好”,其实这类问题没有标准答案,只有最合适。真正落到 Unity 项目里,绕不开的无非是三种:PlayerPrefs、二进制序列化、Json 文本序列化。我先用自己的经验把这三种方案摊开对比一下。
| 方案 | 上手难度 | 调试友好度 | 可读性 | 版本兼容性 | 性能 | 典型场景 |
|---|---|---|---|---|---|---|
| PlayerPrefs | 极低 | 低 | 中等 | 差,键多了难迁移 | 高 | 设置项、音量、画质选项 |
| 二进制 | 中 | 极差 | 极差 | 很差,结构一变就废 | 最高 | 核心战斗数据、超大数据量存档 |
| Json | 低 | 高 | 高 | 好,可加版本号迁移 | 中上 | 玩家存档、配表、服务器通信 |
PlayerPrefs 的本质是平台层的键值存储,它在 Windows 上写注册表、在 Android 上写 XML 文件,一旦键数量膨胀,查找和整理都是灾难。而且它的定位是“轻量偏好”,塞大段字符串进去虽然能跑,但以后想迁移到别的平台或服务器,数据拿不出来,这是很痛的。
二进制方案我最早也迷恋过,序列化快、体积小,但调试起来真的会崩溃。线上玩家反馈存档损坏,你拉回来一个 .bytes 文件,用十六进制工具打开,完全是天书。更麻烦的是二进制布局和运行时类型强绑定,版本一迭代,老存档几乎不可读。Json 恰好在这两者之间达到了一个大多数人能接受的平衡点。
1.2 Json 方案的适用边界
我之前负责一个中轻度卡牌项目,玩家存档里包含角色等级、背包物品、任务进度、设置项和少量统计信息。如果把整套数据用 PlayerPrefs 拆成键值对,大概要维护上百个键,想想就头大。后来我统一用一个存档类包住所有数据,直接 ToJson 成字符串存本地文件,读取时 FromJson 还原,代码量骤减,后来接后端时 JSON 结构还能直接复用,相当于省了一轮模型层重写。
但要注意,Json 并不是万能的。如果你的存档每帧产生大量浮点数组,或者有几十 MB 的关卡录像类数据,Json 的文本膨胀和解析开销就不太划算。这种场景我建议核心数据走二进制,元数据、可读配置、玩家可见信息走 Json,两者可以共存,没必要一棵树吊死。
还有一种场景是“高度反作弊敏感”的存档,比如排行榜分数、竞速类计时器。Json 明文文件玩家拿记事本就能改,这是它的天然弱点。此时必须叠加加密和完整性校验,而不是指望格式本身提供安全。
1.3 选第三方 Json 库前必须想清楚的事
Unity 自带 JsonUtility,但它的限制不少,后面我会详细说。很多人一开始就上 LitJson 或 Newtonsoft.Json,这当然可以,但选型前有四个问题一定要先问自己:目标平台是否需要 IL2CPP 打包?包体大小敏感不敏感?数据结构里有没有 Dictionary、多态这类 JsonUtility 不支持的类型?服务器和客户端是否共用同一套模型?
IL2CPP 是最大的坑。Newtonsoft.Json 在 IL2CPP 下如果用到了反射创建类型,可能会被代码裁剪干掉,必须配 link.xml 或在类上打 preserve 标记。这个问题网上问了无数遍,本质上不是库坏了,而是裁剪规则没写好。如果只是简单存档,JsonUtility 其实完全够用;一旦涉及复杂结构或者要和服务端统一模型,再引入第三方库不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JsonUtility 核心用法与限制盘点
2.1 基础序列化和反序列化
JsonUtility 是 UnityEngine 命名空间下的工具类,不需要额外引入第三方包。它的基础用法非常清爽:类上标记 [System.Serializable],然后 ToJson 和 FromJson 来回转。
csharp复制[System.Serializable]
public class PlayerData
{
public string playerName;
public int level;
public float hp;
public bool isNewPlayer;
}
// 序列化
PlayerData data = new PlayerData();
data.playerName = "小明";
data.level = 10;
data.hp = 100f;
data.isNewPlayer = false;
string json = JsonUtility.ToJson(data, true);
// true 表示格式化输出,带缩进,方便查日志
Debug.Log(json);
// 反序列化
PlayerData loaded = JsonUtility.FromJson<PlayerData>(json);
Debug.Log(loaded.playerName + " " + loaded.level);
我建议在开发期保留第二个参数 true,输出带缩进的格式化字符串,日志里能直接看清字段层级。正式发布版为了省一点存储空间,可以传 false,或者直接不传直接 ToJson(data),默认就是紧凑模式。这个参数对性能影响很小,但对你调试时的眼睛影响很大。
这里还有个隐藏知识点:FromJson 传入空字符串或 null 时并不会抛异常,而是返回一个默认实例。字段会保持默认值。所以如果你发现读取后数据全是 0 或 null,别急着怪 Json,先查是否文件本身是空的,或者路径根本没写对。
2.2 字段类型支持和不支持的情况
JsonUtility 的序列化规则和 Unity 的 Inspector 规则高度一致,这一点是好事也是坏事。好事是你熟悉了序列化面板后,基本能猜出哪些字段会被 Json 处理;坏事是很多从 Java 或 C# 原生 JSON 库转过来的人,会对它的“不处理 Dictionary、不处理多态”非常不适应。
支持的情况相对比较简单:基础类型 int、float、string、bool,一维数组、List,以及标记了 [Serializable] 的嵌套类、struct、enum,还有 Vector2、Vector3 这类 Unity 原生结构。只要你把字段声明为 public,或者标记了 [S
