为什么我的字段总是莫名其妙地丢失?深度解析 Unity 序列化的"黑盒"规则
适合人群:Unity 初中级开发者 | 预计阅读时间:18分钟
我能帮你什么:彻底理解 Unity 序列化机制、掌握字段保存/丢失的规则、避免常见的序列化陷阱
🎯 一、开篇:这些灵异现象你遇到过吗?
- 明明在 Inspector 里设置了值,运行后就丢了?
- Dictionary 在 Inspector 里不显示,怎么保存数据?
- 为什么
[SerializeField] private List<int>能保存,但List<MyGenericClass<T>>不行? - 场景中的引用在打包后变成
null? - 为什么
public字段会被序列化,但public const不会?
如果你被这些问题困扰过,这篇文章将带你深入 Unity 序列化系统的底层,揭开"黑盒"的神秘面纱。
📖 二、原理研究:Unity 序列化系统的底层机制
2.1 生活类比:序列化就像"拍照保存"
想象你在玩乐高积木:
- 序列化(Serialization):拍一张照片,记录下每块积木的位置、颜色、连接关系
- 反序列化(Deserialization):根据照片,重新搭建出一模一样的积木
但是,Unity 的"相机"有特殊规则:
- 只拍摄特定类型的积木:
int、float、string、数组、List<T>等 - 不拍摄某些类型:
Dictionary、复杂泛型、循环引用等 - 需要"标记"才拍摄:私有字段需要
[SerializeField] - 自动忽略某些标记:
[NonSerialized]、static、const等
2.2 Unity 序列化系统的三层架构
┌─────────────────────────────────────────┐
│ Unity Editor (Inspector UI) │ ← 显示层
│ - SerializedObject 包装 │
│ - SerializedProperty 遍历 │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Unity Scripting Backend (Mono/IL2CPP) │ ← 托管层
│ - C# 类型定义 │
│ - Reflection API │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Unity Serialization Engine (C++) │ ← 原生层
│ - YAML Writer/Reader │
│ - Binary Serializer │
│ - Type Cache & Field Scanner │
└─────────────────────────────────────────┘
源码路径:
Unity/Runtime/Serialize/SerializeUtility.cpp- 核心序列化引擎Unity/Runtime/Serialize/TransferFunctions.cpp- 数据传输Unity/Editor/Mono/SerializedProperty.bindings.cs- 编辑器接口
2.3 序列化的核心规则(官方文档+源码验证)
规则1:可序列化的类型
✅ 支持序列化的类型:
// 1. 基本类型
public int intValue;
public float floatValue;
public bool boolValue;
public string stringValue;
public Vector3 vector3Value;
public Color colorValue;
// 2. UnityEngine.Object 引用
public GameObject gameObjectRef;
public Transform transformRef;
public Material materialRef;
public Texture textureRef;
public ScriptableObject scriptableObjectRef;
// 3. 自定义可序列化类(需要 [Serializable] 标记)
[System.Serializable]
public class MyData
{
public int id;
public string name;
}
public MyData data;
// 4. 数组和 List<T>
public int[] intArray;
public List<string> stringList;
public MyData[] dataArray;
// 5. 枚举
public enum State { Idle, Running, Dead }
public State currentState;
❌ 不支持序列化的类型:
// 1. Dictionary(Unity 不支持)
public Dictionary<int, string> dict; // ❌ 不会保存
// 2. 多维数组
public int[,] matrix; // ❌ 不支持
// 3. Jagged 数组(间接支持,见后文)
public int[][] jaggedArray; // ❌ 直接不支持
// 4. 复杂泛型
public List<List<int>> nestedList; // ❌ 不支持
public MyGenericClass<T> genericField; // ❌ 泛型类不支持
// 5. 委托和事件
public Action onComplete; // ❌ 不支持
public event System.EventHandler OnEvent; // ❌ 不支持
// 6. 接口
public IMyInterface interfaceRef; // ❌ 不支持
// 7. 循环引用
[System.Serializable]
public class Node
{
public Node next; // ⚠️ 会导致无限递归,Unity 会报错
}
规则2:字段修饰符的影响
public class SerializeTest : MonoBehaviour
{
// ✅ public 字段:自动序列化
public int publicField = 100;
// ✅ private + [SerializeField]:强制序列化
[SerializeField] private int privateField = 200;
// ❌ private 无标记:不序列化
private int normalPrivate = 300; // 不会保存
// ❌ public + [NonSerialized]:强制不序列化
[System.NonSerialized] public int nonSerializedField = 400;
// ❌ public static:不序列化
public static int staticField = 500;
// ❌ public const:不序列化
public const int CONST_VALUE = 600;
// ❌ public readonly:不序列化(运行时只读)
public readonly int readonlyField = 700;
// ✅ Property with backing field
[SerializeField] private int _health;
public int Health
{
get => _health;
set => _health = value;
}
}
底层原因:Unity 的字段扫描器(Field Scanner)只扫描:
// 简化的 C++ 伪代码
bool ShouldSerializeField(FieldInfo field)
{
if (field.IsStatic) return false;
if (field.IsConst) return false;
if (field.HasAttribute("NonSerialized")) return false;
if (field.IsPublic) return true;
if (field.HasAttribute("SerializeField")) return true;
return false;
}
规则3:深度限制(防止无限递归)
Unity 序列化有深度限制:
[System.Serializable]
public class Level0 { public Level1 child; }
[System.Serializable]
public class Level1 { public Level2 child; }
// ... 继续嵌套
[System.Serializable]
public class Level7 { public Level8 child; }
[System.Serializable]
public class Level8 { public int value; } // ⚠️ 深度限制约为 7-10 层
源码常量:
// Unity C++ 代码中的限制
const int kMaxSerializationDepth = 10;
测试:超过深度限制后,Unity 会在 Console 输出警告:
Serialization depth limit exceeded at 'ClassName.fieldName'. There may be a circular reference.
规则4:Unity 对象引用的特殊处理
public class ReferenceTest : MonoBehaviour
{
// ✅ 场景中的对象引用
public Transform sceneObject; // 保存为 FileID(场景内 ID)
// ✅ 资产引用
public Material assetMaterial; // 保存为 GUID + FileID
// ❌ 运行时创建的对象
public GameObject runtimeObject; // 无法序列化(没有 GUID)
}
YAML 格式示例:
MonoBehaviour:
m_Script: {fileID: 11500000, guid: abcd1234..., type: 3}
sceneObject: {fileID: 123456789} # 场景内引用
assetMaterial: {fileID: 2100000, guid: xyz789..., type: 2} # 资产引用
runtimeObject: {fileID: 0} # null(运行时对象无法保存)
2.4 序列化的执行时机
[编辑器模式]
1. 修改 Inspector 值
↓
2. SerializedObject.ApplyModifiedProperties()
↓
3. 序列化到 .asset/.prefab/.scene 文件(YAML)
↓
4. 保存到磁盘
[进入播放模式]
5. 序列化当前编辑器状态(拍照)
↓
6. 启动 Play Mode
↓
7. 反序列化恢复编辑器状态
[退出播放模式]
8. 反序列化 Step 5 的快照
↓
9. 恢复编辑器状态(这就是为什么运行时修改不保存)
[构建(Build)]
10. 收集所有场景和资产
↓
11. 序列化为二进制格式(Binary)
↓
12. 打包到 .data 文件
关键洞察:编辑器使用 YAML(可读),构建后使用 Binary(高效)
🧪 三、实验验证:通过实验理解规则
实验1:测试哪些字段会被序列化
using UnityEngine;
using System.Collections.Generic;
public class SerializationExperiment : MonoBehaviour
{
[Header("=== 基本类型 ===")]
public int publicInt = 1;
[SerializeField] private int privateInt = 2;
private int normalPrivate = 3;
[System.NonSerialized] public int nonSerialized = 4;
public static int staticInt = 5;
public const int CONST_INT = 6;
[Header("=== 集合类型 ===")]
public List<int> listInt = new List<int> { 1, 2, 3 };
public int[] arrayInt = { 4, 5, 6 };
public Dictionary<int, string> dict = new Dictionary<int, string> { { 1, "one" } };
[Header("=== 自定义类型 ===")]
[System.Serializable]
public class SerializableClass
{
public int value = 100;
}
public class NonSerializableClass
{
public int value = 200;
}
public SerializableClass serializableObj = new SerializableClass();
public NonSerializableClass nonSerializableObj = new NonSerializableClass();
[Header("=== Unity 引用 ===")]
public GameObject gameObjectRef;
public Material materialRef;
void Start()
{
Debug.Log("===== 序列化测试 =====");
Debug.Log($"publicInt = {publicInt}");
Debug.Log($"privateInt = {privateInt}");
Debug.Log($"normalPrivate = {normalPrivate}");
Debug.Log($"nonSerialized = {nonSerialized}");
Debug.Log($"staticInt = {staticInt}");
Debug.Log($"listInt.Count = {listInt.Count}");
Debug.Log($"dict.Count = {dict.Count}");
Debug.Log($"serializableObj.value = {serializableObj?.value}");
Debug.Log($"nonSerializableObj.value = {nonSerializableObj?.value}");
}
}
实验步骤:
- 创建空对象,添加上述脚本
- 在 Inspector 中修改所有可见字段的值
- 保存场景
- 关闭 Unity,用文本编辑器打开场景文件
- 搜索
SerializationExperiment,查看实际保存的字段
实验结果(Scene 文件片段):
--- !u!114 &1234567890
MonoBehaviour:
m_GameObject: {fileID: ...}
m_Enabled: 1
m_Script: {fileID: 11500000, guid: ...}
m_Name:
publicInt: 100 # ✅ 保存了
privateInt: 200 # ✅ 保存了(有 [SerializeField])
# normalPrivate: 不存在 # ❌ 没保存
# nonSerialized: 不存在 # ❌ 没保存
# staticInt: 不存在 # ❌ 没保存
listInt: # ✅ 保存了
- 10
- 20
- 30
# dict: 不存在 # ❌ Dictionary 不支持
serializableObj: # ✅ 保存了
value: 500
nonSerializableObj: # ❌ 保存了,但是 null
value: 0
gameObjectRef: {fileID: ...} # ✅ 保存了引用
实验2:Dictionary 的替代方案
using UnityEngine;
using System.Collections.Generic;
public class DictionaryWorkaround : MonoBehaviour
{
// ❌ 原生 Dictionary 不会序列化
public Dictionary<int, string> dict = new Dictionary<int, string>();
// ✅ 方案1:使用两个 List 模拟
[System.Serializable]
public class SerializableDictionary
{
public List<int> keys = new List<int>();
public List<string> values = new List<string>();
public Dictionary<int, string> ToDictionary()
{
var dict = new Dictionary<int, string>();
for (int i = 0; i < keys.Count; i++)
{
dict[keys[i]] = values[i];
}
return dict;
}
public void FromDictionary(Dictionary<int, string> dict)
{
keys.Clear();
values.Clear();
foreach (var kvp in dict)
{
keys.Add(kvp.Key);
values.Add(kvp.Value);
}
}
}
public SerializableDictionary serializableDict = new SerializableDictionary();
// ✅ 方案2:使用 List<KeyValuePair>
[System.Serializable]
public class KVPair
{
public int key;
public string value;
}
public List<KVPair> kvPairs = new List<KVPair>();
// ✅ 方案3:使用开源库(推荐)
// https://github.com/azixMcAze/Unity-SerializableDictionary
void OnEnable()
{
// 反序列化后,转换为 Dictionary
dict = serializableDict.ToDictionary();
}
void OnDisable()
{
// 序列化前,转换回 List
serializableDict.FromDictionary(dict);
}
}
实验3:深度嵌套测试
using UnityEngine;
public class NestingTest : MonoBehaviour
{
[System.Serializable]
public class NestedData
{
public int level;
public NestedData child;
}
public NestedData root = new NestedData();
[ContextMenu("Create Deep Nesting")]
void CreateDeepNesting()
{
var current = root;
for (int i = 1; i <= 15; i++)
{
current.level = i;
current.child = new NestedData();
current = current.child;
}
Debug.Log("创建了 15 层嵌套,保存场景后查看 Console 是否有警告");
}
[ContextMenu("Print Depth")]
void PrintDepth()
{
int depth = 0;
var current = root;
while (current != null && current.child != null)
{
depth++;
current = current.child;
}
Debug.Log($"实际序列化深度: {depth}");
}
}
实验结果:
- 创建 15 层嵌套
- 保存场景
- 重新加载场景
- 运行
Print Depth - 输出:
实际序列化深度: 7(说明超过深度限制的部分被截断了)
实验4:运行时对象引用测试
using UnityEngine;
public class RuntimeReferenceTest : MonoBehaviour
{
[Header("编辑器设置的引用")]
public Transform editorReference;
[Header("运行时创建的引用")]
public Transform runtimeReference;
void Start()
{
// 运行时创建一个对象
var go = new GameObject("RuntimeObject");
runtimeReference = go.transform;
Debug.Log($"editorReference = {editorReference}");
Debug.Log($"runtimeReference = {runtimeReference}");
}
[ContextMenu("Test Serialization")]
void TestSerialization()
{
// 在编辑器模式下,修改值后进入播放模式再退出
// 观察 runtimeReference 是否被保存
Debug.Log("=== 测试序列化 ===");
Debug.Log($"editorReference: {(editorReference != null ? "存在" : "null")}");
Debug.Log($"runtimeReference: {(runtimeReference != null ? "存在" : "null")}");
}
}
实验结果:
- 编辑器模式:拖拽一个对象到
editorReference - 进入播放模式:
runtimeReference被赋值 - 退出播放模式:
runtimeReference变回null(运行时引用不会保存)
🏗️ 四、常见陷阱与解决方案
陷阱1:忘记添加 [Serializable] 标记
// ❌ 错误做法
public class PlayerData
{
public int level;
public string name;
}
public class GameManager : MonoBehaviour
{
public PlayerData player = new PlayerData { level = 1, name = "Hero" };
}
// Inspector 显示:None (Player Data)
// 数据不会保存
// ✅ 正确做法
[System.Serializable]
public class PlayerData
{
public int level;
public string name;
}
陷阱2:Property 不会被序列化
// ❌ 错误做法
public class HealthSystem : MonoBehaviour
{
public int Health { get; set; } = 100; // 不会保存!
}
// ✅ 正确做法
public class HealthSystem : MonoBehaviour
{
[SerializeField] private int _health = 100;
public int Health
{
get => _health;
set => _health = Mathf.Clamp(value, 0, 100);
}
}
陷阱3:接口引用丢失
// ❌ Unity 不支持接口序列化
public interface IWeapon
{
void Attack();
}
public class GameCharacter : MonoBehaviour
{
public IWeapon weapon; // ❌ 不会保存
}
// ✅ 解决方案1:使用抽象基类
public abstract class WeaponBase : MonoBehaviour
{
public abstract void Attack();
}
public class GameCharacter : MonoBehaviour
{
public WeaponBase weapon; // ✅ 可以保存 MonoBehaviour 引用
}
// ✅ 解决方案2:使用 SerializeReference(Unity 2019.3+)
public class GameCharacter : MonoBehaviour
{
[SerializeReference] public IWeapon weapon; // ✅ 支持接口和抽象类
}
陷阱4:泛型类不会被序列化
// ❌ 泛型类不支持
[System.Serializable]
public class Container<T>
{
public T value;
}
public class Test : MonoBehaviour
{
public Container<int> intContainer; // ❌ Inspector 不显示
}
// ✅ 解决方案:创建具体类型
[System.Serializable]
public class IntContainer
{
public int value;
}
public class Test : MonoBehaviour
{
public IntContainer intContainer; // ✅ 正常工作
}
陷阱5:循环引用导致崩溃
// ❌ 危险:循环引用
[System.Serializable]
public class TreeNode
{
public TreeNode parent; // ⚠️ 父节点
public TreeNode child; // ⚠️ 子节点
}
// 如果 A.parent = B 且 B.child = A → 无限循环
// ✅ 解决方案:使用 [NonSerialized] 打破循环
[System.Serializable]
public class TreeNode
{
[System.NonSerialized] public TreeNode parent; // 不序列化父节点
public TreeNode child;
public void SetParent(TreeNode p)
{
parent = p;
}
}
📊 五、技术图表
图表1:序列化决策树
字段是否会被序列化?
┌─ 字段类型
│
├─ 是 static?
│ └─ Yes → ❌ 不序列化
│
├─ 是 const?
│ └─ Yes → ❌ 不序列化
│
├─ 有 [NonSerialized] 标记?
│ └─ Yes → ❌ 不序列化
│
├─ 类型是否支持?
│ ├─ Dictionary / 多维数组 / 复杂泛型
│ │ └─ Yes → ❌ 不序列化
│ │
│ └─ 基本类型 / List<T> / 数组 / 自定义 [Serializable] 类
│ └─ Yes → 继续检查
│
├─ 自定义类有 [Serializable] 标记?
│ ├─ No → ❌ 不序列化
│ └─ Yes → 继续检查
│
└─ 访问修饰符
├─ public → ✅ 序列化
├─ private + [SerializeField] → ✅ 序列化
└─ private(无标记)→ ❌ 不序列化
图表2:序列化流程图
[编辑器修改 Inspector]
↓
检查字段是否可序列化
↓
┌─────────────────┐
│ 字段扫描器 │
│ - 遍历所有字段 │ ← C++ 层
│ - 应用规则过滤 │
│ - 构建类型信息 │
└────────┬────────┘
↓
┌─────────────────┐
│ 序列化引擎 │
│ - 递归遍历对象 │
│ - 解析引用 │
│ - 生成 YAML/Binary │
└────────┬────────┘
↓
┌─────────────────┐
│ 写入文件系统 │
│ - .scene │
│ - .prefab │
│ - .asset │
└─────────────────┘
[加载资源]
↓
┌─────────────────┐
│ 读取文件 │
│ - YAML Parser │
│ - Binary Reader │
└────────┬────────┘
↓
┌─────────────────┐
│ 反序列化引擎 │
│ - 创建对象实例 │
│ - 恢复字段值 │
│ - 解析引用链 │
└────────┬────────┘
↓
┌─────────────────┐
│ 内存中的对象 │
└─────────────────┘
图表3:YAML vs Binary 序列化对比
编辑器模式(YAML):
┌─────────────────────────────────────┐
│ MainPanel.prefab │
├─────────────────────────────────────┤
│ %YAML 1.1 │
│ --- !u!1 &1234567890 │
│ GameObject: │
│ m_Name: MainPanel │
│ m_TagString: Untagged │
│ m_Layer: 5 │
│ m_Component: │
│ - component: {fileID: 111111} │ ← 可读
│ - component: {fileID: 222222} │
│ --- !u!114 &111111 │
│ MonoBehaviour: │
│ m_Script: {guid: abc123...} │
│ health: 100 │ ← 可编辑
│ speed: 5.5 │
└─────────────────────────────────────┘
优点:可读、可手动编辑、便于版本控制
缺点:文件大、解析慢
构建后(Binary):
┌─────────────────────────────────────┐
│ level0.data │
├─────────────────────────────────────┤
│ 01 00 00 00 D2 04 96 49 ... │ ← 二进制数据
│ 64 00 00 00 00 00 B0 40 ... │
│ AB CD EF 12 34 56 78 90 ... │
└─────────────────────────────────────┘
优点:体积小、加载快
缺点:不可读、不可编辑
🎓 六、总结与延伸
核心要点回顾
-
序列化本质:
- Unity 通过字段扫描器和序列化引擎将对象状态保存到文件
- 编辑器使用 YAML(可读),构建后使用 Binary(高效)
-
可序列化类型:
- 基本类型、UnityEngine.Object 引用、数组、List
- 自定义类需要
[Serializable]标记 - 不支持:Dictionary、多维数组、复杂泛型、接口
-
字段修饰符规则:
public→ 自动序列化private+[SerializeField]→ 强制序列化static、const、[NonSerialized]→ 不序列化
-
常见陷阱:
- 忘记
[Serializable]标记 - 使用 Property 而不是字段
- 接口/泛型/Dictionary 不支持
- 循环引用导致崩溃
- 运行时对象引用丢失
- 忘记
最佳实践
// ✅ 推荐的数据类设计模式
[System.Serializable]
public class GameData
{
// 1. 使用 [SerializeField] + Property 模式
[SerializeField] private int _score;
public int Score
{
get => _score;
set => _score = Mathf.Max(0, value); // 可以加验证逻辑
}
// 2. 复杂类型使用 List 替代 Dictionary
[SerializeField] private List<int> _keys = new List<int>();
[SerializeField] private List<string> _values = new List<string>();
[System.NonSerialized] private Dictionary<int, string> _dictCache;
public Dictionary<int, string> DataDict
{
get
{
if (_dictCache == null)
{
_dictCache = new Dictionary<int, string>();
for (int i = 0; i < _keys.Count; i++)
_dictCache[_keys[i]] = _values[i];
}
return _dictCache;
}
}
// 3. 使用 [SerializeReference] 支持多态(Unity 2019.3+)
[SerializeReference] public IItem item;
// 4. 使用 OnBeforeSerialize/OnAfterDeserialize 回调
public void OnBeforeSerialize()
{
// 序列化前的准备工作
_keys.Clear();
_values.Clear();
if (_dictCache != null)
{
foreach (var kvp in _dictCache)
{
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}
}
public void OnAfterDeserialize()
{
// 反序列化后的恢复工作
_dictCache = null; // 清除缓存,下次访问时重建
}
}
调试技巧
// 使用 EditorUtility 检查字段是否被序列化
#if UNITY_EDITOR
using UnityEditor;
public static class SerializationDebugger
{
[MenuItem("Tools/Check Serialization")]
static void CheckSerialization()
{
var target = Selection.activeGameObject?.GetComponent<YourComponent>();
if (target == null) return;
var so = new SerializedObject(target);
var iterator = so.GetIterator();
Debug.Log("===== 可序列化字段 =====");
while (iterator.NextVisible(true))
{
Debug.Log($"{iterator.propertyPath} ({iterator.propertyType})");
}
}
}
#endif
延伸阅读清单
-
官方文档
-
深度文章
-
开源工具
-
本系列后续文章
- 《Unity 内存模型详解:Managed vs Native》
- 《PlayerLoop 与自定义更新机制》
💡 互动思考题
- 为什么 Unity 不支持 Dictionary 序列化?如果你来设计,如何实现?
[SerializeReference]和普通序列化有什么本质区别?适用场景是什么?- 如何设计一个通用的"可序列化 Dictionary"工具类?
欢迎在评论区分享你的序列化踩坑经历和解决方案!
下一篇预告:《Unity 内存模型详解:为什么 GC 总在关键帧掉链子?》
作者:[胡利光] | Unity 技术博主
专注于 Unity 底层原理与架构设计
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!

384

被折叠的 条评论
为什么被折叠?



