在Unity开发里,“数据可持续化”这个词听起来挺唬人的,翻译成人话其实就是:把游戏里要长期保留的数据(存档、设置、排行榜、玩家进度)从内存里搬到硬盘上,下次启动还能读回来。而Json,就是这中间最常用、最顺手、也最值得你花时间搞明白的载体格式。
我做Unity项目这么多年,经手过单机RPG的存档、联网游戏的本地缓存、甚至数字孪生项目里的配置热更新,凡是和数据落地打交道的地方,十有八九最后都回归到Json。你可以不写服务器,可以不接数据库,但只要你做的是单机游戏、工具类App、或者需要离线可用的应用,Json这套组合拳你就躲不开。
这篇文章我不打算给你抄一段官方文档就完事,而是把我自己在实际项目里踩过的坑、试过的方案、优化过的写法,全部摊开来讲。内容适合刚接触Unity、对数据存取还一头雾水的新手,也适合已经会调用PlayerPrefs但想更进一步、把存档系统做得更规范的中级开发者。你把这篇文章看完,应该能做到:用Json写出一个健壮的、可扩展的、抗异常的本机数据持久化层。
1. 内容整体设计与思路拆解
1.1 为什么是Json,而不是PlayerPrefs、XML或二进制
很多人一上手Unity,第一个接触的持久化API是PlayerPrefs。它简单啊,一行代码存一个值,再一行代码读回来。但它的局限性你在项目稍微大一点之后就会立刻感受到:它本质是个键值对存储,你很难把一个完整的、嵌套的、带数组的对象结构直接丢进去。就算你硬塞,也得自己拼字符串、自己解析,绕一大圈回到原点。
XML是严肃的老牌格式,它的优点是严谨、有Schema校验,但缺点也很直接:标签冗余太多了。同样一条数据,XML写出来占用空间大概是Json的两倍甚至更多,解析性能也差一些,尤其在移动端,这种差距会被放大到肉眼可见的程度。
二进制最快、最省空间,但你得自己处理字节序、版本兼容、字段增删,Unity原生的BinaryFormatter在跨平台、跨版本时经常抽风,而且在一些平台上因为安全限制根本不能用。
Json站在了这几者的中间点:可读性好、体积适中、解析速度快、生态成熟。人类能看懂,程序也好处理,配合各种序列化库,一条Java对象或C#类几行代码就能完整映射到文本。对Unity这种需要频繁调试、快速迭代的开发环境来说,Json就是性价比最高的选择。
1.2 Unity里序列化方案的选型逻辑
提到C#的Json库,最有名的三个是:JsonUtility(Unity自带)、LitJson(轻量开源)、Newtonsoft.Json(.NET界的国民库)。很多新手上来就纠结用哪个,其实不用纠结,先理解它们的底层差异,再根据你的场景选。
JsonUtility是Unity官方封装的,接口最简单:JsonUtility.ToJson()和JsonUtility.FromJson()两个方法走天下。但它的限制极其明显——只能序列化[Serializable]标记的类、结构体、List、数组,不支持字典Dictionary,不支持多态,不支持属性(Property),对enum的支持也很弱。你做个背包系统,想存个Dictionary<int, Item>,用JsonUtility直接给你白屏报错。
LitJson是早年从Unity社区火起来的,支持字典、支持属性、支持自定义转换器,性能也不错。但问题是它已经很多年不怎么更新了,对C#新特性、Nullable类型、System.Text.Json风格的属性命名支持都比较老,用起来总有点“上一个时代”的别扭感。
Newtonsoft.Json(通常也叫Json.NET)是功能最全面的,几乎能序列化任何东西:字典、多态、匿名对象、DateTime、枚举字符串化、自定义ContractResolver,没有它搞不定的。Unity官方早在2017版本之后就在UWP、IL2CPP后端里内置了它的一部分能力,而且通过官方包com.unity.nuget.newtonsoft-json可以直接引入完整版。唯一的所谓“缺点”是它比较重、反射用得比较多,在某些对AOT裁剪特别极端的平台需要配置,但这在绝大多数项目里都不构成问题。
我的结论很直接:新项目一律用Newtonsoft.Json,没有例外。如果你的项目真的因为包体紧张、环境受限只能用JsonUtility,那我会在后面给你一套绕过它限制的封装思路。
1.3 数据可持续化整体架构:不只是一个文件读写那么简单
很多教程教你的做法是:把Player数据类,Save()的时候File.WriteAllText(path, JsonConvert.SerializeObject(player)),Load()的时候反过来。这种写法不是不能用,但它把所有逻辑都堆在业务层里,一旦数据复杂度上来,你会陷入三个泥潭:
- 耦合:游戏逻辑到处直接读写文件,IO调度混乱,异步和同步混着用,卡帧卡成PPT。
- 不可维护:字段一多,Json结构就乱,每个模块都想往存档里塞自己的数据,结果就是存档文件越来越大,互相覆盖。
- 不健壮:如果Json文件因为意外被写坏了一半,或者版本升级后格式变了,要么读不出来,要么读出来全是默认值,玩家的进度说丢就丢。
正确的思路是:把持久化抽象成一层独立的“存档服务”。业务模块只管把自己的数据对象传给服务层,服务层负责序列化、加密、文件IO、版本迁移、容错恢复。这也是我下面要重点演示的内容,你跟着做一遍,以后任何项目都能直接套用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 Unity中访问文件系统的正确姿势:路径选择
这一步很多人栽过跟头。你在Unity编辑器里写File.WriteAllText("save.json", data),看到文件出现在工程目录下,觉得挺正常。但你打到手机上试试,要么直接IOException,要么就是文件存在但你找不到它在哪。
原因在于:PC端你可以任意写文件,但移动端、WebGL端出于安全沙箱策略,只有特定目录可以写。Unity给你封装了两个关键的路径常量:
Application.dataPath:指向游戏安装目录。在PC上可以写,但在移动端它是只读的,你往这写必报错。Application.persistentDataPath:指向平台分配的可持久化存储目录,这是跨平台存档的标准选择。iOS、Android、Windows、Mac各不一样,但Unity都帮你处理好了。
我在项目里一般这样封装路径:
csharp复制public static class PathHelper
{
public static string GetSavePath(string fileName)
{
var dir = Path.Combine(Application.persistentDataPath, "SaveData");
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
return Path.Combine(dir, fileName);
}
}
顺手提一句:别把临时缓存和正式存档混在同一个目录,主存档建议放在persistentDataPath根目录的独立文件夹里,临时缓存丢Application.temporaryCachePath,这样玩家清理缓存时不至于把你的档也清了。
2.2 Newtonsoft.Json在Unity中的安装与兼容配置
用老办法的话,你得去官网下个DLL拖进Plugins文件夹,再加上一堆平台宏控制,麻烦不说,还容易踩IL2CPP的AOT裁剪坑。现在官方出了UPM包,一行搞定:在Packages/manifest.json里加一句依赖:
json复制{
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
}
}
装好之后,你在代码里using Newtonsoft.Json;就能用了。
这里有个很关键的配置提示:如果你的项目开启了IL2CPP,建议在Player Settings的Scripting Define Symbols里加上NEWTONSOFT_JSON,然后去Package的link.xml或者你自己的link.xml里保留Newtonsoft的反射元数据,不然发布后某些类会被裁剪掉,运行时反序列化静默失败——这个问题我第4节会细讲排查方法。
2.3 序列化对象的建模规范:从“所有字段public”开始避免踩坑
Json序列化的本质是把一个内存对象的字段状态,映射成文本。所以你的数据类长什么样,直接决定存档好不好读、乱不乱、能不能扩展。
我见过太多新人直接这么写:
csharp复制public class PlayerData
{
public int hp;
public string name;
public int level;
}
不是说不能这么写,但这样写有几个隐患:字段全是public、类内部没有任何逻辑、后续改字段名时所有地方都要跟着改。更好的建模方式是这样:
csharp复制[Serializable]
public class PlayerData
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("hp")]
public int Hp { get; set; }
[Jso
