Unity3D中LitJson-0.16.0集合序列化实战:从原理到库存系统实现

📅 2026/8/4 7:50:24
Unity3D中LitJson-0.16.0集合序列化实战:从原理到库存系统实现
1. 项目概述为什么Unity3D开发者需要LitJson-0.16.0来处理集合序列化在Unity3D项目开发中数据交换是家常便饭。无论是从服务器拉取配置表、保存本地游戏存档还是不同模块间的数据传递JSON格式几乎成了首选。Unity自带的JsonUtility虽然方便但它在处理复杂数据结构尤其是集合如ListT、DictionaryK, V和自定义类时常常表现得力不从心。这时一个轻量、高效且与Unity兼容性好的第三方JSON库就显得至关重要。LitJson-0.16.0正是这样一个在Unity社区里经久不衰的“老兵”。我最初接触LitJson就是因为被JsonUtility在序列化一个包含ListItem的玩家背包类时给“坑”了。它要么直接忽略掉集合要么在反序列化时得到一个空列表调试起来非常头疼。而LitJson-0.16.0版本作为其发展历程中一个稳定且功能相对完善的节点对C#的集合类型有着原生且可靠的支持。它不仅仅是一个“能用”的工具更是一个能让你在数据层设计上更加自由、减少样板代码的利器。对于需要处理复杂游戏数据如技能树、对话系统、装备合成表的开发者来说掌握LitJson对集合的序列化与反序列化是提升开发效率和代码健壮性的基本功。简单来说这个“项目”的核心就是在Unity3D中集成并使用LitJson-0.16.0库以正确、高效地完成对各类C#集合对象的JSON序列化与反序列化操作规避Unity原生方案的局限性。无论你是独立开发者还是团队协作这都是一项能让你在数据持久化和网络通信中游刃有余的必备技能。2. LitJson-0.16.0核心特性与集成方案解析在深入集合序列化之前我们有必要先搞清楚LitJson-0.16.0这个工具本身。为什么是0.16.0这个版本它和更新版本或者Unity的JsonUtility、Newtonsoft.Json现为Json.NET相比有什么不同理解了这些你才能做出最合适的技术选型。2.1 版本选择与特性定位LitJson是一个用C#编写的轻量级JSON库其设计目标就是快速和易于使用。0.16.0版本是一个在功能和稳定性上取得很好平衡的经典版本。相较于更早的版本它修复了许多bug并增强了对泛型集合的支持而相较于一些更新的版本虽然LitJson本身更新并不频繁0.16.0足够稳定网上资料和解决方案也最丰富避免了使用最新版可能遇到的未知兼容性问题。它的核心优势在于轻量级单个LitJson.dll文件体积小巧不会明显增加项目构建大小。零依赖除了.NET或Unity的运行时库它不依赖任何其他第三方库集成简单。良好的集合支持这是相对于JsonUtility最大的优势。它能正确处理ListT,DictionaryK, V, 数组等。与Unity的亲和性在Unity的脚本执行顺序和AOT编译环境下工作良好。与Newtonsoft.Json对比LitJson功能上要简单得多。Newtonsoft.Json功能极其强大定制化程度极高但相应地也更重在移动平台可能会对IL2CPP代码裁剪和性能产生一些影响。对于绝大多数Unity游戏的数据序列化需求LitJson-0.16.0的功能已经绰绰有余属于“刚好够用”的甜点区。2.2 多种集成方式与实操要点将LitJson-0.16.0集成到Unity项目中有几种常见方式每种都有其适用场景。方式一直接导入DLL最推荐这是最干净、最直接的方式。你可以从其GitHub仓库的Release中下载编译好的LitJson.dll或者从其他包含此版本的项目中获取。在Unity项目的Assets文件夹下创建一个名为Plugins的文件夹如果不存在。这是Unity识别托管插件DLL的标准位置。将LitJson.dll文件拖入Plugins文件夹。在任意C#脚本中添加using LitJson;命名空间即可开始使用。注意确保你获取的DLL是适用于.NET Standard 2.0或.NET Framework对应版本的以兼容Unity的运行时。通常为LitJson编译的DLL都能很好地工作。方式二导入源码便于调试和微调你也可以将LitJson的整个C#源代码文件夹通常包含JsonData.cs,JsonMapper.cs等复制到你的Assets/Scripts目录下的某个文件夹中。这样做的好处是你可以在Unity中直接断点调试LitJson的内部逻辑或者在极端情况下对源码进行微调。缺点是可能会稍微增加项目编译时间并且需要自行管理源码版本。方式三通过Unity Package Manager或Asset Store不常见一些资源包或框架可能会将LitJson作为依赖打包。但直接获取纯LitJson库前两种方式更可控。我个人强烈推荐方式一。它分离了依赖和业务逻辑项目结构清晰也符合第三方库的管理惯例。集成后你可以在代码中通过JsonMapper.ToJson()和JsonMapper.ToObject()这两个核心方法进行序列化和反序列化接下来我们就聚焦于集合操作。3. 核心集合类型的序列化与反序列化实战集合是数据结构的骨架。在游戏中角色技能列表、背包物品数组、关卡配置字典无一不是集合。LitJson处理这些类型的基本原理是通过反射分析对象的类型信息将公有字段和属性具有getter和setter转换为JSON的键值对。对于集合它会递归地处理其中的每个元素。3.1 列表与数组的序列化列表(ListT)和数组(T[])在JSON中都被表示为数组方括号[]包围的结构。LitJson对它们的支持非常直接。using LitJson; using System.Collections.Generic; [System.Serializable] // 这个特性对于LitJson不是必须的但保留它是一个好习惯。 public class PlayerData { public string PlayerName; public int Level; public Liststring CompletedMissions; // 字符串列表 public ListEquipment Equipments; // 自定义对象列表 } [System.Serializable] public class Equipment { public int Id; public string Name; } // 序列化示例 PlayerData player new PlayerData(); player.PlayerName Hero; player.Level 10; player.CompletedMissions new Liststring { Mission1, Mission3, BossRush }; player.Equipments new ListEquipment { new Equipment { Id 101, Name Iron Sword }, new Equipment { Id 205, Name Wooden Shield } }; string json JsonMapper.ToJson(player); Debug.Log(json);上述代码输出的JSON大致如下{ PlayerName: Hero, Level: 10, CompletedMissions: [Mission1, Mission3, BossRush], Equipments: [ {Id: 101, Name: Iron Sword}, {Id: 205, Name: Wooden Shield} ] }反序列化同样简单string jsonString { PlayerName: Hero, Level: 10, CompletedMissions: [Mission1, Mission3, BossRush], Equipments: [{Id: 101, Name: Iron Sword}, {Id: 205, Name: Wooden Shield}] }; // 注意JSON字符串中可以使用单引号LitJson能识别。 PlayerData loadedPlayer JsonMapper.ToObjectPlayerData(jsonString); Debug.Log(loadedPlayer.Equipments[0].Name); // 输出Iron Sword实操心得对于数组操作方式与ListT完全一致。LitJson在反序列化时会自动创建适当大小的数组。确保集合中的元素类型T本身也是可以被LitJson序列化的。基本类型int,float,string,bool和包含基本类型或可序列化对象的自定义类都没问题。3.2 字典的序列化与关键陷阱字典(DictionaryK, V)的序列化是重点也是容易踩坑的地方。在JSON中字典被自然地表示为对象花括号{}包围的键值对集合。public class GameConfig { public Dictionarystring, int LevelExpRequirement; // 键为关卡名值为所需经验 public Dictionaryint, string ItemNameMap; // 键为物品ID值为物品名 } GameConfig config new GameConfig(); config.LevelExpRequirement new Dictionarystring, int { {Level1, 100}, {Level2, 300}, {Level5, 1200} }; config.ItemNameMap new Dictionaryint, string { {101, Health Potion}, {205, Magic Crystal} }; string configJson JsonMapper.ToJson(config); Debug.Log(configJson);输出{ LevelExpRequirement: { Level1: 100, Level2: 300, Level5: 1200 }, ItemNameMap: { 101: Health Potion, // 注意键被转换为字符串“101” 205: Magic Crystal } }这里有一个至关重要的陷阱JSON规范要求对象的键必须是字符串。因此当你的字典键类型是int、enum或其他非字符串类型时LitJson在序列化时会调用它们的ToString()方法将其转换为字符串。在反序列化时它需要将这些字符串键再转换回原始类型。反序列化字典string dictJson {101: Health Potion, 205: Magic Crystal}; Dictionaryint, string loadedDict JsonMapper.ToObjectDictionaryint, string(dictJson); // 成功loadedDict 包含键 101 和 205。重要警告这个转换过程依赖于LitJson内部的类型转换器。对于int、float等基本类型转换通常很稳定。但是对于自定义类型作为字典键情况就复杂了。如果自定义类没有正确重写ToString()和提供相应的从字符串解析的方法反序列化很可能会失败或者导致字典行为异常。因此在项目实践中强烈建议字典的键使用string类型这样可以避免绝大多数麻烦。如果必须用其他类型请务必进行充分的测试。3.3 嵌套集合与复杂数据结构的处理游戏数据往往是树状或图状结构嵌套集合非常常见。例如一个公会信息包含成员列表每个成员又有自己的成就列表。public class Achievement { public string Id; public string Name; } public class GuildMember { public string Name; public ListAchievement Achievements; } public class Guild { public string GuildName; public ListGuildMember Members; } Guild myGuild new Guild { GuildName Dragon Slayers, Members new ListGuildMember { new GuildMember { Name Arthur, Achievements new ListAchievement { new Achievement { Id ACH_01, Name First Kill }, new Achievement { Id ACH_05, Name Dragon Hunter } } }, new GuildMember { Name Lancelot, Achievements new ListAchievement { new Achievement { Id ACH_03, Name PvP Champion } } } } }; string guildJson JsonMapper.ToJson(myGuild); // LitJson 会递归地处理所有嵌套对象和集合生成结构完整的JSON。LitJson处理这类嵌套结构的能力很强只要每一层的类型都是可序列化的它就能生成层次分明的JSON数据。反序列化时它也能正确地重建整个对象树。注意事项循环引用如果对象之间存在循环引用例如Player对象有一个Party引用而Party对象又包含一个ListPlayerLitJson在序列化时会陷入无限递归导致堆栈溢出。这是大多数简单JSON库的通病。在设计数据结构时应避免循环引用或者使用ID引用代替直接对象引用。性能考虑深度嵌套的复杂对象进行频繁的序列化/反序列化可能成为性能瓶颈尤其是在移动设备上。对于不变的核心配置数据可以序列化一次并缓存结果。对于频繁变动的数据要考虑数据量的大小。4. 高级定制与疑难问题排查指南掌握了基本操作后你可能会遇到一些特殊需求或棘手问题。LitJson-0.16.0提供了一些机制来进行定制同时也存在一些需要绕行的“坑”。4.1 自定义序列化行为与属性排除默认情况下LitJson会序列化所有公有字段和具有getter/setter的属性。但有时我们想排除某些敏感或临时字段如缓存数据、运行时状态或者对序列化过程进行自定义。方法一使用[JsonIgnore]特性这是最简洁的方式。为字段或属性标记[JsonIgnore]LitJson就会在序列化和反序列化时忽略它。using LitJson; public class SaveData { public string UserId; public string Token; [JsonIgnore] // 这个字段不会被序列化到JSON中 public DateTime LastLoginTimeCache; public Listint UnlockedLevels; }方法二实现IJsonWrapper接口高级如果你需要对一个复杂类型的序列化过程进行完全控制可以实现IJsonWrapper接口。这让你可以手动决定如何将对象转换为JsonDataLitJson的内部表示以及如何从JsonData重建对象。这种方法较为复杂通常只在处理特殊第三方库类型或需要非常规映射时才使用。方法三使用JsonMapper的注册类型转换器你可以通过JsonMapper.RegisterExporter和JsonMapper.RegisterImporter来为特定类型注册自定义的导出序列化和导入反序列化逻辑。例如Unity的Vector3类型默认无法被LitJson直接序列化你可以为其注册转换器// 注册Vector3的导出器 JsonMapper.RegisterExporterVector3((v, writer) { writer.WriteObjectStart(); writer.WritePropertyName(x); writer.Write(v.x); writer.WritePropertyName(y); writer.Write(v.y); writer.WritePropertyName(z); writer.Write(v.z); writer.WriteObjectEnd(); }); // 注册Vector3的导入器 JsonMapper.RegisterImporterdouble, float(input (float)input); // 可能需要先注册double到float的转换 // 注意为Vector3注册一个完整的导入器稍微复杂需要从JsonData对象解析这里是一个简化示例思路。通过这种方式当你序列化一个包含Vector3的对象时它会输出为{x:1.0, y:2.0, z:3.0}的格式。4.2 常见问题排查与解决方案实录在实际项目中你肯定会遇到一些报错或非预期行为。下面是我踩过的一些坑和解决方案。问题一反序列化后集合为null或空。可能原因1JSON字符串中的键名与C#类中的字段/属性名大小写不匹配。LitJson默认是大小写敏感的。解决方案确保JSON键名与C#字段名完全一致。或者在反序列化前使用JsonMapper.ToObject的重载版本传入一个JsonReader并设置其属性但0.16.0版本对大小写转换的支持较弱。更稳妥的做法是统一命名规范如都用驼峰式。可能原因2类没有无参构造函数。LitJson在反序列化时需要调用无参构造函数来创建对象实例。解决方案为你的数据类添加一个公共的无参构造函数。可能原因3集合字段本身在JSON中不存在或为null。解决方案检查你的JSON数据源是否完整。可以在类定义中为集合字段赋予一个空的初始值如public Liststring Items new Liststring();这样即使JSON中没有对应字段反序列化后也会得到一个空列表而非null。问题二序列化/反序列化时抛出类型转换异常。可能原因JSON中的数据类型与C#字段类型不兼容。例如JSON中某个值是字符串100但C#字段是int类型。解决方案LitJson会尝试进行基本类型间的转换如字符串转数字。如果失败需要检查数据源的正确性。对于自定义类型确保其字段类型匹配。问题三字典反序列化失败尤其是键为枚举类型时。可能原因如前所述字典键在JSON中必须是字符串。如果键是枚举LitJson会使用枚举值的名称字符串形式作为键。反序列化时它需要将字符串转换回枚举值。解决方案为枚举类型注册一个导入器或者更简单的方法在代码中使用字符串作为字典键在逻辑层再将字符串转换为枚举。这是最保险的做法。// 不推荐易出错 // public DictionaryWeaponType, int WeaponCount; // 推荐 public Dictionarystring, int WeaponCount; // 键存为 Sword, Axe问题四性能问题序列化大量数据时卡顿。可能原因对非常大的对象树进行频繁的完整序列化。解决方案增量更新只序列化发生变化的部分数据而不是整个对象。缓存结果对于不常变化的数据如配置表序列化一次后将JSON字符串缓存起来。评估需求是否真的需要将整个复杂对象序列化有时只传递一个ID或最小数据集更高效。考虑替代方案如果数据量极大且性能要求苛刻可以评估MemoryPack、MessagePack等二进制序列化方案它们速度更快体积更小但可读性差。4.3 与Unity工作流的结合ScriptableObject与预制体在Unity中ScriptableObject是存储游戏数据如物品、技能、关卡配置的绝佳工具。我们可以结合LitJson实现ScriptableObject数据的导入导出便于策划配置和版本管理。从JSON文件加载数据到ScriptableObject// 在Editor脚本中 using UnityEditor; using LitJson; public class DataImporter : EditorWindow { [MenuItem(Tools/Load JSON to SO)] static void LoadJsonToScriptableObject() { string jsonPath EditorUtility.OpenFilePanel(Select JSON file, , json); if (!string.IsNullOrEmpty(jsonPath)) { string jsonContent File.ReadAllText(jsonPath); ItemListData itemData JsonMapper.ToObjectItemListData(jsonContent); // 假设ItemListData是一个ScriptableObject类包含ListItem var so ScriptableObject.CreateInstanceItemListData(); so.items itemData.items; // 赋值 string assetPath Assets/Resources/ItemData.asset; AssetDatabase.CreateAsset(so, assetPath); AssetDatabase.SaveAssets(); EditorUtility.DisplayDialog(Success, Data loaded and saved to SO!, OK); } } }将ScriptableObject数据导出为JSON反向操作即可读取SO中的数据用JsonMapper.ToJson()转换为字符串再保存为文本文件。这种做法分离了数据配置JSON文件可由策划在Excel导出后获得和运行时数据ScriptableObjectUnity可高效加载是中型以上项目常用的数据管理模式。5. 实战案例构建一个可持久化的游戏库存系统让我们通过一个综合案例将上述所有知识点串联起来。我们将构建一个简单的玩家库存系统支持物品的添加、移除、保存到本地和从本地加载。5.1 系统设计与数据模型定义首先定义核心数据类。我们将使用Dictionary来存储物品ID和数量的映射因为查找和更新效率高。// InventoryItem.cs - 基础物品定义可创建为ScriptableObject [CreateAssetMenu(fileName New Item, menuName Inventory/Item)] public class InventoryItem : ScriptableObject { public string itemId; // 唯一标识符用作字典键 public string displayName; public Sprite icon; // ... 其他属性如描述、类型、使用效果等 } // InventorySaveData.cs - 纯数据类用于序列化 [System.Serializable] public class InventorySaveData { // 键物品ID 值物品数量 public Dictionarystring, int itemQuantityMap new Dictionarystring, int(); public int currencyGold; public DateTime lastSaveTime; // 注意DateTime需要特殊处理 } // InventoryManager.cs - 单例管理器负责库存逻辑和持久化 public class InventoryManager : MonoBehaviour { public static InventoryManager Instance { get; private set; } private InventorySaveData currentSaveData; private string saveFilePath; void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); saveFilePath Path.Combine(Application.persistentDataPath, inventory_save.json); LoadInventory(); } else { Destroy(gameObject); } } }5.2 核心序列化与反序列化实现在InventoryManager中实现加载和保存方法。这里需要处理DateTime的序列化问题因为LitJson默认不支持。public void SaveInventory() { if (currentSaveData null) return; // 注册DateTime的转换器简易版转换为ISO8601字符串 if (!JsonMapper.IsTypeRegistered(typeof(DateTime))) { JsonMapper.RegisterExporterDateTime((dt, writer) writer.Write(dt.ToString(o))); // o 是ISO8601格式 JsonMapper.RegisterImporterstring, DateTime(input DateTime.Parse(input)); } currentSaveData.lastSaveTime DateTime.Now; string json JsonMapper.ToJson(currentSaveData); try { File.WriteAllText(saveFilePath, json); Debug.Log($Inventory saved to: {saveFilePath}); } catch (System.Exception e) { Debug.LogError($Failed to save inventory: {e.Message}); } } public void LoadInventory() { if (!File.Exists(saveFilePath)) { currentSaveData new InventorySaveData(); Debug.Log(No save file found, creating new inventory.); return; } try { string json File.ReadAllText(saveFilePath); // 同样需要确保DateTime转换器已注册 if (!JsonMapper.IsTypeRegistered(typeof(DateTime))) { JsonMapper.RegisterImporterstring, DateTime(input DateTime.Parse(input)); } currentSaveData JsonMapper.ToObjectInventorySaveData(json); if (currentSaveData.itemQuantityMap null) { currentSaveData.itemQuantityMap new Dictionarystring, int(); } Debug.Log($Inventory loaded. Gold: {currentSaveData.currencyGold}, Items: {currentSaveData.itemQuantityMap.Count}); } catch (System.Exception e) { Debug.LogError($Failed to load inventory: {e.Message}. Creating new one.); currentSaveData new InventorySaveData(); } }5.3 业务逻辑封装与测试添加操作库存的方法并确保任何修改后自动调用保存或提供手动保存按钮。public bool AddItem(string itemId, int quantity 1) { if (string.IsNullOrEmpty(itemId) || quantity 0) return false; if (currentSaveData.itemQuantityMap.ContainsKey(itemId)) { currentSaveData.itemQuantityMap[itemId] quantity; } else { currentSaveData.itemQuantityMap[itemId] quantity; } SaveInventory(); // 自动保存 return true; } public bool RemoveItem(string itemId, int quantity 1) { if (!currentSaveData.itemQuantityMap.ContainsKey(itemId)) return false; int currentQty currentSaveData.itemQuantityMap[itemId]; if (currentQty quantity) return false; currentQty - quantity; if (currentQty 0) { currentSaveData.itemQuantityMap.Remove(itemId); } else { currentSaveData.itemQuantityMap[itemId] currentQty; } SaveInventory(); // 自动保存 return true; } public int GetItemQuantity(string itemId) { if (currentSaveData.itemQuantityMap.TryGetValue(itemId, out int qty)) { return qty; } return 0; }测试与验证 在Unity编辑器中创建一个空场景挂载InventoryManager。然后可以写一个简单的测试脚本在Start中调用AddItem(potion_health, 5)和AddItem(sword_iron, 1)。运行游戏后去Application.persistentDataPath目录可以在Unity Editor中通过Debug.Log(Application.persistentDataPath)查看路径找到生成的inventory_save.json文件。用文本编辑器打开你应该能看到类似以下的结构{ itemQuantityMap: { potion_health: 5, sword_iron: 1 }, currencyGold: 0, lastSaveTime: 2023-10-27T10:30:00.1234567Z }停止运行再次启动游戏LoadInventory方法会读取这个文件并恢复你的物品数据。这就实现了一个完整的、基于LitJson和本地JSON文件的库存持久化系统。这个案例展示了从数据模型设计、LitJson集成、特殊类型处理到完整业务逻辑的闭环。你可以在此基础上扩展更多功能如按物品类型分类、背包容量限制、装备栏位等。关键在于所有复杂的数据结构变化最终都能通过LitJson可靠地转化为可存储、可传输的JSON字符串。