三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity中Newtonsoft.Json集成指南:从NuGet安装到跨平台避坑

Unity中Newtonsoft.Json集成指南:从NuGet安装到跨平台避坑

1. 项目概述:为什么Unity开发者绕不开Newtonsoft.Json?

如果你在Unity里做过数据持久化、网络通信或者配置管理,大概率已经和Json打过交道了。Unity自带的JsonUtility好用吗?对于简单的MonoBehaviour序列化,它确实够用,但一旦你的数据结构复杂起来——比如有字典、有接口、有继承关系,或者你需要处理DateTimeEnum这些类型时,JsonUtility就会立刻显得力不从心,甚至直接罢工。这时候,社区和商业项目里几乎清一色的选择,就是Newtonsoft.Json(现在也叫Json.NET)。

这个库的名气太大了,大到几乎成了C#世界里Json处理的代名词。它功能强大、高度可配置、性能经过多年优化,社区支持也极其丰富。但在Unity这个特殊的环境里,直接把它“请”进来,可不像在普通的.NET项目里敲一句Install-Package Newtonsoft.Json那么简单。Unity的脚本运行时(Mono或IL2CPP)、程序集版本、跨平台编译目标(尤其是WebGL和iOS),每一个环节都可能藏着坑。我自己就经历过在编辑器里跑得好好的,一打包到WebGL就报TypeLoadException的噩梦。

所以,这篇指南的目的很明确:手把手带你从零开始,在Unity项目中安全、稳定地引入并使用Newtonsoft.Json,并重点分享那些只有踩过坑才知道的实战经验和避坑要点。无论你是刚接触Unity的新手,还是被JsonUtility折磨已久的老兵,这篇文章都能帮你把Json数据处理这件“基础活”干得又快又稳。

2. 核心思路与方案选型:为什么是NuGetForUnity?

当决定在Unity中使用Newtonsoft.Json时,你面前通常有三条路:

  1. 直接下载DLL:从官网或NuGet包中手动提取Newtonsoft.Json.dll,拖入Unity项目的Assets/Plugins文件夹。这是最原始的方法,问题在于你需要自己管理版本兼容性,并且对于其他有依赖项的NuGet包(比如某些库依赖特定版本的Newtonsoft.Json),手动管理会非常头疼。
  2. 使用Unity的Package Manager (UPM) 和 Scoped Registries:理论上,你可以将NuGet源配置为UPM的作用域注册表,然后通过UPM窗口安装。这种方法更“Unity”,但配置过程相对繁琐,且对网络环境有一定要求,对于新手不够直观。
  3. 使用NuGetForUnity插件:这是一个专门为Unity设计的NuGet客户端。它直接在Unity编辑器内运行,让你可以像在Visual Studio里一样,搜索、安装、更新和卸载NuGet包,并自动处理包依赖和程序集引用。这是我们强烈推荐的首选方案。

为什么首选NuGetForUnity?核心优势在于“省心”“可管理”。它抽象掉了手动处理DLL、解决依赖冲突的复杂性。你只需要知道包名(Newtonsoft.Json)和大概需要的版本,剩下的工作(如下载依赖、将程序集放入正确的Assets子目录、配置API兼容性级别)它会自动完成。此外,它还能方便地更新到新版本或回退到旧版本,这对于长期项目维护至关重要。它就像一个专为Unity定制的“包管家”。

注意:NuGetForUnity安装的包,其程序集通常会被放在Assets/Packages目录下,这与手动放置的Plugins文件夹有所区别,但Unity都能正常识别和编译。

3. 环境准备与NuGetForUnity安装

工欲善其事,必先利其器。首先我们需要把“包管家”请进门。

3.1 获取NuGetForUnity

最可靠的方式是从其GitHub仓库发布页面直接下载最新的.unitypackage文件。

  1. 打开浏览器,访问 NuGetForUnity 的 GitHub Releases 页面(你可以通过搜索引擎轻松找到)。
  2. 在最新的发布版本(Release)中,找到名为NuGetForUnity.x.x.x.unitypackage的文件(x.x.x是版本号),点击下载。
  3. 下载完成后,不要解压,直接备用。

3.2 在Unity项目中安装

  1. 打开你的Unity项目(建议使用2020 LTS或更新版本,以获得更好的.NET支持)。
  2. 在Unity编辑器中,依次点击菜单栏的Assets->Import Package->Custom Package...
  3. 在弹出的文件选择器中,找到并选中你刚刚下载的.unitypackage文件,点击“打开”。
  4. 随后会弹出一个导入对话框,通常默认全选所有文件,直接点击Import按钮即可。

安装完成后,你会在Unity编辑器顶部菜单栏看到一个新的菜单项:NuGet。这就表示安装成功了。同时,在Assets文件夹下,你会看到一个名为Packages的新目录,NuGetForUnity自身及其后续安装的包都会管理在这里。

3.3 首次使用与可能的问题

安装后第一次点击NuGet->Manage NuGet Packages时,插件需要初始化并在线获取包列表,这可能需要几秒钟到一分钟,取决于你的网络。如果长时间卡住或报错,可能是网络连接问题。

实操心得:有时因为网络环境,访问默认的NuGet源(nuget.org)可能较慢或不稳定。NuGetForUnity目前不支持图形化修改源,但如果遇到问题,可以尝试使用网络加速工具或检查本地网络设置。绝大多数情况下,直接访问是可行的。

4. 安装Newtonsoft.Json并理解关键配置

“管家”就位,现在可以请“主角”入场了。

4.1 通过NuGetForUnity安装

  1. 点击菜单栏NuGet->Manage NuGet Packages,打开包管理窗口。
  2. 在搜索框中输入Newtonsoft.Json。在结果列表中,你应该能看到它,作者是James Newton-King
  3. 点击右侧的Install按钮。NuGetForUnity会自动下载该包及其所有依赖(Newtonsoft.Json通常没有其他依赖),并将其安装到Assets/Packages目录下的一个特定子文件夹中,例如Assets/Packages/Newtonsoft.Json.13.0.3(版本号可能不同)。
  4. 安装完成后,关闭窗口即可。你不需要手动做任何引用操作,Unity在下次编译时会自动识别这些新的程序集。

4.2 安装后的项目结构检查

安装成功后,建议去Assets/Packages目录下看一眼。你会找到一个以Newtonsoft.Json开头的文件夹,里面至少包含:

  • lib文件夹:存放着针对不同.NET框架版本编译的程序集。Unity通常会使用netstandard2.0netstandard2.1下的DLL,这是NuGetForUnity和Unity的.NET兼容性设置共同决定的。
  • Newtonsoft.Json.dll:主程序集文件。
  • Newtonsoft.Json.xml:XML文档注释文件,如果你在IDE(如Rider、VS)中编写代码,它能提供API的智能提示和注释。

这个过程完全自动化,避免了手动下载、选择正确框架版本、处理依赖的麻烦。

4.3 至关重要的Unity项目设置检查

安装完库只是第一步,让它在Unity的所有平台上都能正常工作,还需要检查几个关键设置。这是避坑的核心环节

1. Api Compatibility Level(API兼容性级别)这个设置告诉Unity使用哪个版本的.NET基础类库。

  • 路径File->Build Settings->Player Settings->Player->Other Settings->Configuration
  • 推荐设置:选择.NET Standard 2.1.NET Framework(如果项目需要)。绝对不要使用.NET 4.x的旧子集(如.NET 4.x Subset。Newtonsoft.Json等现代NuGet包大多以.NET Standard 2.0/2.1为目标,使用旧的子集可能导致找不到所需程序集而编译失败。
  • 原理.NET Standard是一个API规范,.NET Standard 2.1包含了非常广泛的API,能确保大多数现代NuGet包(包括Newtonsoft.Json)的兼容性。Unity对新版.NET的支持越来越好,使用.NET Standard 2.1是平衡兼容性和功能性的最佳选择。

2. Scripting Backend(脚本后端)这决定了你的C#代码如何被编译和执行。

  • 路径:同上,在Configuration下方。
  • 对于PC、Mac、Linux、Android平台:可以选择MonoIL2CPP。Mono编译快,IL2CPP能带来更好的性能和安全性(代码被编译成C++)。Newtonsoft.Json两者都支持。
  • 对于iOS和WebGL平台强制使用IL2CPP。这是苹果和浏览器安全沙箱的要求。幸运的是,Newtonsoft.Json与IL2CPP兼容良好。
  • 注意:如果你选择IL2CPP,在第一次为某个平台构建时,编译(代码剥离和转换)会花费更长时间。

3. Managed Stripping Level(代码剥离级别)为了减小发布包体积,Unity会尝试移除未使用的代码。但过度剥离可能会误删通过反射调用的代码,而Newtonsoft.Json大量使用反射来序列化/反序列化对象。

  • 路径Player Settings->Player->Other Settings->Optimization->Managed Stripping Level
  • 安全设置:对于使用了Newtonsoft.Json的项目,建议设置为LowMedium。如果设置为High,你可能会在打包后遇到运行时错误,提示找不到某个类型或方法,即使它在编辑器模式下工作正常。
  • 高级避坑:如果因为包体大小限制必须使用High剥离级别,你需要为Newtonsoft.Json(或其他使用反射的库)提供link.xml文件来告诉Unity链接器保留哪些代码。这是一个更高级的话题,通常可以将Newtonsoft.Json官方提供的link.xml文件(可在其GitHub仓库找到)放置于Assets根目录。内容大致如下:
    <?xml version="1.0" encoding="utf-8"?> <linker> <assembly fullname="Newtonsoft.Json" preserve="all"/> </linker>

完成以上检查和设置,你的Unity项目才算为Newtonsoft.Json搭建好了一个稳固的“运行环境”。

5. 从基础到进阶:Newtonsoft.Json核心实战

环境就绪,让我们开始写代码。Newtonsoft.Json的API设计非常直观,核心是JsonConvert这个静态类。

5.1 基础序列化与反序列化

假设我们有一个简单的玩家数据类:

[System.Serializable] // 这个特性对Newtonsoft.Json不是必须的,但保留它不影响Unity序列化 public class PlayerData { public string PlayerName { get; set; } public int Level { get; set; } public Vector3 LastPosition { get; set; } // Unity内置类型 public List<string> Inventory { get; set; } = new List<string>(); }

序列化(对象 -> JSON字符串)

using Newtonsoft.Json; // 引入命名空间 PlayerData player = new PlayerData { PlayerName = "开发者", Level = 99, LastPosition = new Vector3(10, 2, -5), Inventory = new List<string> { "Health Potion", "Magic Sword", "Key" } }; string jsonString = JsonConvert.SerializeObject(player, Formatting.Indented); Debug.Log(jsonString);

Formatting.Indented参数会让生成的JSON字符串带有缩进,便于阅读。输出如下:

{ "PlayerName": "开发者", "Level": 99, "LastPosition": { "x": 10.0, "y": 2.0, "z": -5.0 }, "Inventory": [ "Health Potion", "Magic Sword", "Key" ] }

注意,Vector3被自动序列化成了一个包含x, y, z的对象。这是因为Newtonsoft.Json有内置的转换器来处理一些常见类型,但对于更复杂的Unity类型,我们可能需要自定义。

反序列化(JSON字符串 -> 对象)

string receivedJson = @"{ 'PlayerName': '归来者', 'Level': 1, 'LastPosition': {'x': 0, 'y': 0, 'z': 0}, 'Inventory': ['Wooden Sword'] }"; // 注意:这里JSON字符串中用了单引号,Newtonsoft.Json允许这种宽松语法 PlayerData newPlayer = JsonConvert.DeserializeObject<PlayerData>(receivedJson); Debug.Log($"欢迎玩家 {newPlayer.PlayerName}, 等级 {newPlayer.Level}");

5.2 处理Unity特殊类型与自定义转换器

Unity引擎有很多特殊类型,如Vector3QuaternionColorSprite等。Newtonsoft.Json默认不认识它们。对于Vector3这类简单结构体,它可能能靠反射“蒙对”,但为了可靠性和自定义格式,我们通常需要编写JsonConverter

示例:为Color编写一个简单的转换器假设我们希望将Color序列化为一个十六进制颜色字符串(如“#FF5733FF”)。

using Newtonsoft.Json; using UnityEngine; public class ColorHexConverter : JsonConverter<Color> { public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) { // 将Color转换为包含RGBA的十六进制字符串 string hexColor = ColorUtility.ToHtmlStringRGBA(value); writer.WriteValue("#" + hexColor); } public override Color ReadJson(JsonReader reader, System.Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) { string hexString = reader.Value as string; if (ColorUtility.TryParseHtmlString(hexString, out Color color)) { return color; } return Color.white; // 解析失败返回默认值 } }

使用转换器: 有两种方式:

  1. 特性标注(适用于固定类型):
    public class UITheme { [JsonConverter(typeof(ColorHexConverter))] public Color PrimaryColor { get; set; } public Color SecondaryColor { get; set; } }
  2. 全局或序列化设置(适用于整个项目或某次序列化):
    JsonSerializerSettings settings = new JsonSerializerSettings(); settings.Converters.Add(new ColorHexConverter()); string json = JsonConvert.SerializeObject(uiTheme, Formatting.Indented, settings); UITheme theme = JsonConvert.DeserializeObject<UITheme>(json, settings);

实操心得:对于Vector3Quaternion这类常用类型,社区已经有成熟的开源转换器库(例如Newtonsoft.Json.UnityConverters),你可以通过NuGetForUnity搜索并安装,避免重复造轮子。自己写转换器时,务必处理好空值和异常情况,保证反序列化的鲁棒性。

5.3 高级特性应用:灵活控制序列化过程

Newtonsoft.Json提供了丰富的特性(Attributes)来控制序列化行为,这是它比JsonUtility强大的关键。

  • [JsonProperty]:自定义JSON属性名、顺序、是否必须等。

    public class PlayerData { [JsonProperty("name")] // 在JSON中字段名为"name" public string PlayerName { get; set; } [JsonProperty(Order = -1)] // 让Level在序列化时排在前面 public int Level { get; set; } [JsonProperty(Required = Required.Always)] // 反序列化时该字段必须存在 public string UserId { get; set; } }
  • [JsonIgnore]:完全忽略该属性,不参与序列化和反序列化。常用于存储临时计算值或敏感信息。

    [JsonIgnore] public float CurrentHealthPercentage => CurrentHealth / MaxHealth; // 只读属性,动态计算,不需要保存
  • [JsonConverter]:如前所述,为特定属性指定自定义转换器。

  • NullValueHandlingDefaultValueHandling:通过JsonSerializerSettings控制空值和默认值的处理。

    JsonSerializerSettings settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, // 忽略所有值为null的属性 DefaultValueHandling = DefaultValueHandling.Ignore // 忽略所有等于默认值(如int的0)的属性 }; // 这可以显著减少不必要的数据传输,尤其在网络通信中。

5.4 性能优化与最佳实践

  1. 重用JsonSerializerSettings:创建JsonSerializerSettings实例有一定开销。如果你的应用使用固定的序列化/反序列化配置(比如相同的转换器、命名策略、空值处理),请创建一个静态的、共享的JsonSerializerSettings实例并重复使用。

    public static class JsonSettings { public static readonly JsonSerializerSettings Default = new JsonSerializerSettings { Formatting = Formatting.None, // 生产环境去掉缩进节省空间 NullValueHandling = NullValueHandling.Ignore, Converters = new List<JsonConverter> { new Vector3Converter(), new ColorHexConverter() } }; } // 使用时 string json = JsonConvert.SerializeObject(obj, JsonSettings.Default);
  2. 使用流式API处理大文件:如果你需要处理非常大的JSON文件(如几十MB的配置表),使用JsonConvert.SerializeObject一次性加载到内存可能会导致卡顿甚至内存溢出。此时应使用JsonTextReaderJsonTextWriter进行流式读写。

    using (StreamReader file = File.OpenText(@"largefile.json")) using (JsonTextReader reader = new JsonTextReader(file)) { while (reader.Read()) { if (reader.TokenType == JsonToken.StartObject) { // 逐对象处理 JObject obj = JObject.Load(reader); // ... 处理单个对象 } } }
  3. 注意循环引用:如果两个对象互相引用(例如,Player引用其所属的Team,而Team又有一个Players列表包含该Player),默认序列化会进入死循环。你需要通过设置ReferenceLoopHandling = ReferenceLoopHandling.Ignore来忽略循环引用,或者在数据模型设计上避免这种情况(例如,使用ID代替直接对象引用)。

6. 跨平台与打包实战避坑指南

这是Unity开发特有的挑战,也是问题高发区。很多Bug在编辑器模式下不会出现,只在特定平台的打包版本中显现。

6.1 WebGL平台的特殊处理

WebGL平台运行在浏览器的安全沙箱中,且代码通过IL2CPP编译为WebAssembly,限制最多。

  • AOT编译与代码剥离:如前所述,Managed Stripping Level务必设为Low,并考虑使用link.xml。WebGL对代码大小极其敏感,但过度剥离是Newtonsoft.Json在WebGL上失效的首要原因。
  • 线程问题:WebGL不支持多线程。Newtonsoft.Json内部某些操作默认可能使用线程池。虽然大部分情况下它已处理了单线程环境,但在极端复杂的序列化场景下,如果遇到与线程相关的错误,可以尝试在序列化设置中指定MaxDepth等限制性参数,避免过于深度的递归操作。
  • 文件系统访问:如果你想在WebGL中读取本地JSON文件,不能使用System.IO.File。必须使用UnityWebRequest或通过Application.streamingAssetsPath路径,并使用UnityWebRequest进行异步加载。

6.2 iOS/Android移动端注意事项

  • IL2CPP与代码剥离:同样适用。确保剥离级别为LowMedium,并使用link.xml
  • 尺寸优化:移动端包体大小至关重要。除了设置Formatting.None生成紧凑JSON外,可以考虑使用更激进的代码裁剪(Code Stripping)配合完整的link.xml描述,而不是简单地设置Low剥离。这需要更精细地分析哪些Newtonsoft.Json的功能被真正用到。
  • 性能考量:在移动设备上频繁进行复杂的JSON序列化/反序列化(例如每帧处理大量网络消息)可能成为性能瓶颈。考虑:
    • 对不变的数据使用缓存(反序列化后的对象)。
    • 使用更简单的、扁平化的数据格式。
    • 在非关键帧或分帧进行JSON处理。

6.3 版本管理与依赖冲突

这是使用NuGet包时另一个常见陷阱。

  • 问题场景:你的项目安装了Newtonsoft.Json 13.0.1。然后你又通过NuGetForUnity安装了另一个库AwesomeNetworkingLib,而这个库内部依赖Newtonsoft.Json (>=12.0.0 && <13.0.0)。此时就发生了依赖冲突。
  • NuGetForUnity的处理:NuGetForUnity会尝试解决依赖,但可能无法自动解决这种版本范围不兼容的情况。它可能会安装两个版本,导致项目中出现多个不同版本的Newtonsoft.Json.dll,引发TypeLoadException(类型加载异常)。
  • 解决方案
    1. 统一版本:尽可能让所有包依赖同一个主版本。在NuGetForUnity中,你可以尝试手动将Newtonsoft.Json升级或降级到一个能满足所有依赖的版本(例如,如果所有库都支持12.x,就降到12.0.3)。
    2. 使用Assembly Versioning(高级):如果无法统一,可以考虑使用Assembly-CSharp项目文件(.csproj)中的绑定重定向(binding redirect),但这在Unity中管理起来比较复杂,不推荐新手尝试。
    3. 寻找替代库:如果冲突无法解决,考虑寻找不依赖Newtonsoft.Json的替代通信库,或者使用Unity自带的JsonUtility处理与AwesomeNetworkingLib交互的特定数据部分(如果该库允许传递字符串而非对象)。

避坑技巧:在引入一个新的NuGet包之前,先查看其文档或通过NuGetForUnity的“Dependencies”信息,了解其依赖的Newtonsoft.Json版本范围。提前规划可以避免后期的依赖地狱。

7. 常见问题排查与解决方案实录

这里记录了一些我亲自踩过或从社区常见问题中总结的坑。

问题1:编辑器运行正常,打包后(尤其是WebGL/iOS)运行时抛出JsonSerializationExceptionTypeLoadException,提示找不到某个类型或方法。

  • 原因99%是代码剥离(Code Stripping)。IL2CPP在打包时会移除它认为“未使用”的代码,而Newtonsoft.Json大量使用反射和泛型,链接器无法静态分析出所有需要的类型。
  • 解决方案
    1. Managed Stripping Level设置为Low
    2. 如果必须用MediumHigh,必须在Assets目录下创建(或添加)link.xml文件,并确保包含了Newtonsoft.Json程序集。一个更安全的link.xml示例如下:
      <?xml version="1.0" encoding="utf-8"?> <linker> <assembly fullname="Newtonsoft.Json" preserve="all"/> <!-- 如果你使用了其他通过反射调用的库,也一并加上 --> <assembly fullname="MyGame.Core" preserve="all"/> </linker>
    3. 如果使用了自定义转换器(JsonConverter),请确保转换器类本身没有被剥离。可以尝试在转换器类上添加[Preserve]特性(需要引用UnityEngine.Scripting命名空间)。

问题2:序列化包含Dictionary<enum, T>Dictionary<UnityEngine.Object, T>类型的对象时,行为异常或报错。

  • 原因:Newtonsoft.Json默认的字典键序列化器可能无法正确处理非字符串键(如枚举、对象)。对于Unity的Object(如Sprite,GameObject)作为键,这通常不是一种合理的设计,因为对象的实例ID在运行时是不稳定的。
  • 解决方案
    • 对于Dictionary<enum, T>,可以使用JsonConvert设置中的Converters集合,添加StringEnumConverter来将枚举转换为字符串键。
      settings.Converters.Add(new StringEnumConverter());
    • 对于复杂对象作为键,强烈建议重新设计数据结构,例如使用对象的唯一ID(intstring)作为字典键。

问题3:反序列化后,Unity特有类型(如Vector3)的字段值全部为0。

  • 原因:Newtonsoft.Json没有为该类型注册合适的转换器。它可能通过反射创建了对象,但无法正确解析JSON中的子字段(x,y,z)。
  • 解决方案:为该Unity类型编写并注册一个自定义的JsonConverter(如前面ColorHexConverter的例子),或者安装社区提供的转换器包(如Newtonsoft.Json.UnityConverters),并在序列化设置中全局添加。

问题4:在Unity协程(Coroutine)或异步回调中反序列化JSON,导致意外错误或数据错乱。

  • 原因:Newtonsoft.Json的默认序列化是同步的,如果在多线程环境下使用(虽然Unity主线程不是真多线程,但某些异步操作可能在后台线程完成回调),并且反序列化设置或转换器不是线程安全的,就可能出问题。
  • 解决方案:确保在Unity的主线程中进行最终的序列化/反序列化操作。如果数据来自网络请求,在UnityWebRequest的完成回调或async/await的上下文中,使用JsonConvert是安全的,因为这些回调默认是在主线程执行的。但如果使用了真正的.NET多线程(如Task.Run),则需要将结果调度回主线程再处理。更简单的做法是,始终在MonoBehaviour的生命周期方法(如Update)或协程中调用JsonConvert

问题5:JSON字符串中有额外的字段,反序列化时想忽略它们,而不是抛出异常。

  • 原因:默认情况下,Newtonsoft.Json会严格检查JSON属性与对象属性的匹配。
  • 解决方案:在反序列化设置中,将MissingMemberHandling设置为MissingMemberHandling.Ignore
    JsonSerializerSettings settings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; var obj = JsonConvert.DeserializeObject<MyClass>(jsonString, settings);
    这样,JSON中多出来的字段就会被安静地忽略掉,非常适合处理版本不一致的API数据。

通过以上从安装、配置、编码到打包、排查的完整流程,你应该能在Unity项目中游刃有余地使用Newtonsoft.Json这个强大的工具了。记住,关键不在于记住所有API,而在于理解其核心机制(如转换器、序列化设置)和适应Unity特殊生态(如跨平台、代码剥离)的应对策略。剩下的,就是根据你的具体业务需求,灵活运用这些知识了。

← 返回列表