Json

1.Json概述

1.什么是数据持久化

就是玩游戏时将数据存储到硬盘上,等下次再玩游戏时从硬盘读取数据

image-20260311142403377

image-20260311142435847

2.什么是Json

一种人为定义的文件与规则,不是一种语言,是一种纯文本的数据格式

image-20260311142553835

image-20260311142615999

image-20260311142642400

image-20260311142656622

image-20260311142831582

2.Json配置规则

image-20260311143659605

image-20260311143958323

Json文件的键一定要用双引号包裹

image-20260311145154068

image-20260311145219559

3.Json配置规则练习

image-20260311153010174

4.Excel转Json

原因:

image-20260311162814382

栗子:

image-20260311213718584

将上述数据填到excel表中

image-20260311162945353

搜索一个excel转json的网站,就可以得到json文件

image-20260311163111884

如果觉得每次这样复制粘贴到网站再出json有点麻烦,在编辑器相关知识里会讲到自己写转换的代码

5.JsonUtility序列化

1.JsonUtility是什么

image-20260311171031388

2.必备知识点:File存读字符串

image-20260311171145932

存字符串的时候会帮你创建一个文件,但此文件的路径(父文件夹)必须是已经存在的

1
File.WriteAllText(Application.persistentDataPath + "/Test.json","唐老狮存储的json文件");

读文件会返回字符串类型

1
string content = File.ReadAllText(Application.persistentDataPath + "/Test.json");

3.使用JsonUtility进行序列化

下列注意点很重要,请仔细阅读

image-20260311171523251

在此之前已经定义了MrTang和Student的类,创建一个MrTang新对象,对成员变量分别赋值后,使用如下来序列化

1
string jsonStr = JsonUtility.ToJson(t);

image-20260311171638778

自定义类需要加上序列化特性,只作用于内部成员Student这样的类,在定义上方加,对于MrTang外部类不用

4.流程

  • 创建并填充对象
  • 使用JsonUtility.ToJson序列化
  • 通过File.WriteAllText存储到文件
  • 后续可通过File.ReadAllText读取

6.JsonUtility反序列化

image-20260311175717432

FromJson提供了两种重载,一般我们用泛化的:

1
2
string jsonStr=File.ReadAllText(Application.persistentDataPath+"/MrTang.json");
MrTang t3=JsonUtility.FromJson<MrTang>(jsonStr);

注意事项:

image-20260311180029687

无法读取数据集合([包含多个对象数据]),即无法让包含多个对象的json反序列化至一个List<>上,然而我们可以做一层包裹,创建一个新类并把List<>当做成员变量(别忘了给List<某类>里的某类加上序列化特性),然后在json文件里加上List<>的成员变量名作为键,读到外层的新对象即可

7.LitJson序列化

是一个第三方插件,只需要将src文件夹下的cs文件导入工程就可使用(年久失修,现在可能不是首选了,但仍有很多优点)

以下注意都很重要:

image-20260311203941820

1
string jsonStr=JsonMapper.ToJson(t);

8.LitJson反序列化

仍然建议使用通过泛型转换这种重载

image-20260311212342430

1
MrTang2 t2 =JsonMapper.ToObject<MrTang2>(jsonStr);

两个大坑:

image-20260311212604947

特殊之处:

可以反序列化数据集合,无需包裹类

image-20260311212715432

9.JsonUtility和LitJson对比

image-20260311214732643

10.总结

image-20260312153941119

image-20260312153957783

image-20260312154014388

11.实践(重要)

1.存储和读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System.IO;
using UnityEngine;
using LitJson;

public class JsonMgr
{
public enum JsonType
{
jsonUtility,
litJson,
}
//单例模式
private static JsonMgr instance;
public static JsonMgr Instance=>instance;
private JsonMgr(){}
//序列化
public void SaveData(object data,string fileName,JsonType type=JsonType.litJson)
{
string path = Application.persistentDataPath + "/" + fileName;
string jsonStr = "";
switch (type)
{
case JsonType.jsonUtility:
jsonStr = JsonUtility.ToJson(data);
break;
case JsonType.litJson:
jsonStr = JsonMapper.ToJson(data);
break;
}
File.WriteAllText(path, jsonStr);
}
//反序列化
public T LoadData<T>(string fileName,JsonType type=JsonType.litJson) where T: new()
{
string path = Application.streamingAssetsPath + "/" + fileName;
//判断文件是否存在,如果不存在则从持久化路径中读取
if (!File.Exists(path))
{
path = Application.persistentDataPath + "/" + fileName;
if(!File.Exists(path))
return new T();
}
T data = default;
string jsonStr = File.ReadAllText(path);
switch (type)
{
case JsonType.jsonUtility:
data=JsonUtility.FromJson<T>(jsonStr);
break;
case JsonType.litJson:
data=JsonMapper.ToObject<T>(jsonStr);
break;
}
return data;
}
}

where T: new() 有什么用?

这是一个泛型约束(Generic Constraint)

请看你代码里的第 10 行:

1
return new T();

这行代码的业务逻辑非常好:如果在所有路径下都没找到这个 JSON 文件(说明是玩家第一次玩这个游戏,或者存档丢了),为了防止游戏报错空指针,我们直接在内存里 new 一个全新的、空的数据对象返回给他。

但是!编译器在这个时候会“抬杠”:

编译器:“你凭什么保证这个未知的 T 可以被 new 出来?万一别人传进来一个不能被 new 的类(比如抽象类),或者它的构造函数必须传参数(比如 public PlayerData(int id)),那 new T() 不就崩溃了吗?”

为了让编译器闭嘴并放行,你必须给泛型 加上一个担保书,这就是 where T: new() 的作用:
它在告诉编译器:“我保证,凡是想用我这个 LoadData 方法的数据类型,它的内部必须有一个【公共的、无参数的默认构造函数】!”

加上这句约束后:

  1. 你在代码里写 new T() 就绝对安全合法了。
  2. 如果你在外部试图读取一个没有无参构造函数的类,代码在编译阶段就会直接标红报错,把隐患扼杀在摇篮里。

2.代码打包

选择需要的代码,导出包即可,以后在别的工程可以直接导入

image-20260312182203610

12.关于PersistentDataPath与StreamingAssetsDataPath

一、 Application.persistentDataPath 到底是哪里?

这个路径的名字直译过来叫**“持久化数据路径”。你可以把它通俗地理解为:“专门用来放玩家存档和配置的文件夹”**。

1. 它在电脑/手机上的具体位置:
它不在你 Unity 工程的 Assets 目录下,而是由操作系统(Windows/Mac/Android/iOS)分配给你的一个隐藏沙盒目录

  • Windows 电脑C:\Users\你的用户名\AppData\LocalLow\你的公司名\你的游戏名
  • Mac 电脑~/Library/Application Support/你的公司名/你的游戏名
  • Android 手机手机内部存储/Android/data/你的包名/files
  • iOS 手机沙盒目录/Documents
    (你可以自己在 Unity 里写一句 Debug.Log(Application.persistentDataPath); 运行一下,控制台会把你电脑上的具体路径打印出来,你可以顺着路径找过去看看。)

2. 为什么新 JSON(比如玩家存档)必须写在这个文件夹下?
因为只有这个文件夹是【绝对可读、可写】且【更新不丢失】的!

  • 原因 A(权限问题):当你的游戏打包成 .exe.apk 后,游戏安装目录里的文件会被系统锁定为只读状态。如果你试图把玩家的存档写进安装目录(比如你新建的那个 StreamingAssets 里),操作系统会直接给你报 UnauthorizedAccessException(无权限访问)错误,游戏直接崩溃。
  • 原因 B(更新不覆盖):假如你的游戏出了 2.0 版本,玩家更新游戏时,游戏的安装目录会被整个清空替换。但操作系统会保留 persistentDataPath 里的文件!所以你把存档写在这里,玩家更新游戏后存档才不会丢。

二、 新建的 StreamingAssets 文件夹

没错!只要你拼写完全正确(注意大小写,必须是 StreamingAssets),它就是代码里 Application.streamingAssetsPath 指向的那个文件夹!

这个文件夹有极其特殊的魔法:
在 Unity 中,所有的资源(图片、模型、普通 JSON)在打包时,都会被 Unity 疯狂压缩、加密、打乱,变成机器才认识的二进制乱码(比如 .assets 文件)。
但是,放在 StreamingAssets 文件夹里的任何文件,Unity 在打包时都会“原封不动”地拷贝到包里! 不压缩、不加密、保留原文件名和后缀!


三、这俩文件夹该怎么打配合?

现在你再回头看我上一条回答里帮你改的那个 LoadData 逻辑,就能完全看懂它的精妙之处了:

  • StreamingAssets(初始配置区)
    • 特点:天生自带,打包后只读不可写
    • 放什么:放游戏初始的默认 JSON 文件。比如系统默认的怪物数值表、初始装备表、一个完全没玩过的 DefaultPlayer.json
  • persistentDataPath(玩家存档区)
    • 特点:一开始是空的,游戏运行后动态生成,完全可读可写
    • 放什么:放玩家玩过之后发生变化的数据。比如 PlayerSave.json

你代码里的完美闭环流程是这样的:

  1. 玩家刚下载游戏,第一次打开。
  2. 代码去 persistentDataPath(存档区)找 Player.json。发现找不到(因为是新玩家)。
  3. 代码退而求其次,去 StreamingAssets(初始区)找 Player.json,把里面 1 级、0 金币的数据读出来,作为初始数据。
  4. 玩家玩了一会儿,升到了 10 级。
  5. 当你调用 SaveData 方法时,必须把新的 JSON 写入到 persistentDataPath 里!(因为 StreamingAssets 在手机上是只读的,写不进去)。
  6. 第二天玩家再打开游戏,代码再去 persistentDataPath 找,这次找到了!直接读出 10 级的存档,完美!

四、⚠️ 一个价值千金的避坑警告(针对 Android 打包)

如果你打算把游戏打包到 Android 手机上,请一定注意这件极其变态的事:

在 Android 手机上,StreamingAssets 文件夹会被整个打包进 .apk 压缩包的肚子里!
所以,你在 LoadData 里的这句代码:
File.ReadAllText(Application.streamingAssetsPath + "/" + fileName);
在 Windows 和 Mac 编辑器下跑得完美无缺,但在安卓手机上会直接报错找不到路径!(因为 C# 底层的 File 类没法直接读压缩包内部的文件)。

如果你要发安卓版,读 StreamingAssets 里的文件必须用 Unity 特有的 UnityWebRequest 方式读取。(如果你目前只在电脑上做 PC 游戏,就当没看见这条,放心地用 File.ReadAllText 吧!)

这是一个非常有前瞻性的问题!你能想到**“玩家硬盘空间”“系统盘(C盘)占用”**,说明你已经开始具备成熟的商业游戏开发者思维了!

直接给你吃颗定心丸:只要你存的是纯数据(比如 JSON 存档),它不仅不会过度占用空间,甚至小到可以忽略不计!

但是,如果你在这个文件夹里放了**“其他东西”**,那确实有可能把玩家的 C 盘塞爆。我们来把这笔“空间账”算清楚:


五、 纯 JSON 存档到底有多大?(小到离谱)

你的 JsonMgr 是用来把 C# 对象转换成文本序列化的。你要知道,纯文本文件占用的空间是极其微小的

  • 算一笔账:1 个英文字母占 1 个字节(Byte),1 个汉字占 2~3 个字节。1 MB(兆)= 1024 KB = 1,048,576 字节。
  • 打个比方:假设你的玩家存档非常复杂,包含了 100 个满级英雄的属性、几千个背包道具、上万条任务进度。转换成 JSON 文本后,哪怕有十万个字符,这个 JSON 文件的大小也就仅仅只有 100 KB 左右
  • 结论:就算玩家在你的游戏里建了 10 个存档槽,总共也就占 1 MB。对于现在动辄 1TB、2TB 的电脑硬盘来说,连一根汗毛都算不上。

所以,单靠“游戏存档”是绝对不可能把 C 盘塞满的。


六、 为什么非要放在 C 盘?(微软的硬性规矩)

你可能会觉得:“玩家把游戏安装在 D 盘,为什么存档非要跑到 C 盘去?”

其实,这是 Windows 操作系统(微软)定下的死规矩:
那个隐藏的 AppData(Application Data)文件夹,本来就是 Windows 专门设计给全天下所有软件**存放“用户个人配置”**的地方。

  • 权限最安全:C 盘的 AppData 是跟着“当前登录的 Windows 账号”走的。你在网吧或者公共电脑玩,张三登录他的 Windows 账号,看到的是张三的存档;李四登录,看到的是李四的存档。互相隔离,极其安全。
  • 不怕被误删:玩家如果在 D 盘直接把游戏安装目录 Shift + Delete 暴力删除了,只要他没清空系统盘,他下次重新下载游戏,存档依然还在!
  • Steam 云存档最爱:Steam、Epic 等平台的云存档功能,绝大多数默认抓取的就是 C 盘 AppDataDocuments(我的文档)下的文件。

(其实你现在打开你电脑的 C盘 -> 用户 -> AppData -> LocalLow 看一眼,你会发现市面上绝大部分用 Unity 做的单机游戏,存档全都在这里!)


七、 什么时候会把 C 盘塞爆?(热更新的“锅”)

虽然 JSON 存档很小,但有一个情况会让 persistentDataPath 变得极其庞大——热更新(Hot Update)

在**手机游戏(安卓/iOS)开发中:
因为手机 App 上架后,安装包(APK/IPA)是不能随便改的,所以当游戏出新活动、新角色时,开发者会把几十上百 MB 的
新模型、新图片、新音频(AssetBundles / Addressables)**下载到手机里。
下载到哪呢?没错,只能下载到手机的 persistentDataPath 里!像《原神》这种游戏,手机上的这个文件夹可能会高达十几甚至几十个 GB。

但是在 PC 电脑端
通常大型游戏更新是通过 Steam / Wegame 客户端直接覆盖 D 盘的安装目录的,PC 游戏一般不使用 persistentDataPath 来存美术资源

13.问题

image-20260312183206091

现在流程需要策划人员在excel写数据,然后在网页转换成json,程序员要根据这些数据创建对应类,相当繁琐

所以我们可以创建一个自动化工具来帮助处理这些事项,需要unity编辑器相关知识

image-20260312184021951