文章目录
- [1 由根对象决定序列化](#1 由根对象决定序列化)
- [2 实现 Odin 序列化器](#2 实现 Odin 序列化器)
-
- [2.1 继承已有序列化类](#2.1 继承已有序列化类)
- [2.2 自定义序列化类](#2.2 自定义序列化类)
- [3 避免 Unity 无限深度警告](#3 避免 Unity 无限深度警告)
- [4 指定序列化秘钥](#4 指定序列化秘钥)
-
- [4.1 External String Reference Resolver](#4.1 External String Reference Resolver)
- [4.2 External GUID Reference Resolver](#4.2 External GUID Reference Resolver)
- [4.3 External Index Reference Resolver](#4.3 External Index Reference Resolver)
- [4 功能与限制](#4 功能与限制)
-
- [4.1 平台支持](#4.1 平台支持)
- [4.2 启用序列化](#4.2 启用序列化)
- [4.3 序列化内容](#4.3 序列化内容)
- [4.4 支持的数据格式](#4.4 支持的数据格式)
- [5 序列化重构](#5 序列化重构)
-
- [5.1 重命名字段和属性](#5.1 重命名字段和属性)
- [5.2 重命名类型](#5.2 重命名类型)
- [5.3 从 Unity 转移到 Odin](#5.3 从 Unity 转移到 Odin)
- [6 序列化 Dictionary](#6 序列化 Dictionary)
-
- [6.1 使用 Odin 序列化字典](#6.1 使用 Odin 序列化字典)
- [6.2 使用 Unity 序列化字典](#6.2 使用 Unity 序列化字典)
- [7 序列化协议](#7 序列化协议)
-
- [7.1 Odin 序列化协议](#7.1 Odin 序列化协议)
- [7.2 Unity 序列化协议](#7.2 Unity 序列化协议)
- [8 使用 Odin 保存数据](#8 使用 Odin 保存数据)
- [9 序列化调试器](#9 序列化调试器)
- [10 自定义序列化类](#10 自定义序列化类)
- [11 AOT 序列化](#11 AOT 序列化)
- [12 建议与总结](#12 建议与总结)
1 由根对象决定序列化
在使用 Odin 时,Odin 会尽量避免序列化 Unity 本身应该已经序列化的任何字段。这是在根序列化对象的级别确定的:
csharp
public class ExampleScript : SerializedScriptableObject
{
// Unity 不会序列化, Odin 会序列化。
public Dictionary<int, string> FirstDictionary;
// Unity 会序列化,因此 Odin 将不会序列化。
public MyClass MyReference;
}
[Serializable]
public class MyClass
{
// 尽管有 OdinSerialize 属性,但此字段未序列化。
[OdinSerialize]
public Dictionary<int, string> SecondDictionary;
}
-
FirstDictionary 字段将由 Odin 序列化,因为 Unity 不会对其进行序列化,并且 ExampleScript 是序列化的根对象(所有 SerializedMonoBehaviour 或 SerializedScriptableObject 始终是根序列化对象)。
-
ExampleScript 类中的 MyReference 字段可以由 Unity 序列化,因此 Odin 将跳过它。由于 Odin 跳过了 MyReference 字段,因此 Odin 甚至不会考虑 MyClass 类型的任何成员。因此,尽管有 OdinSerialize 属性,但 SecondDictionary 字段不会被序列化。
在这种情况下,解决方案很简单。可以强制 Odin 序列化它:
csharppublic class ExampleScript : SerializedScriptableObject { public Dictionary<int, string> FirstDictionary; [NonSerialized, OdinSerialize] public MyClass MyReference; }
NonSerialized 属性阻止 Unity 序列化字段,而 OdinSerialize 属性强制 Odin 序列化该字段。这样,MyClass 类中的 SecondDictionary 字段现在将仅由 Odin 序列化。如果只添加了 [OdinSerialize] 而没有添加 [NonSerialized],则 Unity 和 Odin 将序列化同一字段,这可能会导致细微的错误和不必要的数据重复出现。
2 实现 Odin 序列化器
2.1 继承已有序列化类
虽然 Odin 默认会处理所有没有自定义编辑器的类型,但它的序列化系统更为谨慎。你需要明确选择使用 Odin 的序列化系统。
使用时,只需从特殊序列化类之一继承,例如 SerializedMonoBehaviour 或 SerializedScriptableObject。这种方式支持所有常用继承的 Unity 类型。
csharp
public class YourMonoBehaviour : SerializedMonoBehaviour
{
[SerializeField]
private object SerializedPrivateField;
public object SerializedField;
[OdinSerialize]
public object SerializedProperty { get; set; }
}
2.2 自定义序列化类
如果已有需要继承的特定基类型,则通过实现 Unity 的 ISerializationCallbackReceiver 接口并使用 Odin 的 UnitySerializationUtility 类,可以在需要的地方自行实现序列化。
- 自定义 Odin-serialized
ScriptableObject
:
csharp
[ShowOdinSerializedPropertiesInInspector]
public class CustomSerializedScriptableObject : ScriptableObject, ISerializationCallbackReceiver
{
[SerializeField, HideInInspector]
private SerializationData serializationData;
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
UnitySerializationUtility.DeserializeUnityObject(this, ref this.serializationData);
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
UnitySerializationUtility.SerializeUnityObject(this, ref this.serializationData);
}
}
- 自定义 Odin-serialized
MonoBehaviour
:
csharp
[ShowOdinSerializedPropertiesInInspector]
public class CustomSerializedMonoBehaviour : MonoBehaviour, ISerializationCallbackReceiver, ISupportsPrefabSerialization
{
[SerializeField, HideInInspector]
private SerializationData serializationData;
SerializationData ISupportsPrefabSerialization.SerializationData { get { return this.serializationData; } set { this.serializationData = value; } }
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
UnitySerializationUtility.DeserializeUnityObject(this, ref this.serializationData);
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
UnitySerializationUtility.SerializeUnityObject(this, ref this.serializationData);
}
}
3 避免 Unity 无限深度警告
默认情况下,Unity 不支持序列化引用、多态性或空值,目的是保护自己免受无限深度序列化循环的影响。
当某个类型 NodeA 的成员 NodeB 包含其自身 NodeA 时,序列化将会无限递归,然后在控制台中向您发出警告。
相比之下,Odin 支持序列化引用、多态性和空值。但 Unity 仍会发出警告,因此一种解决方法是:将成员同时标记为 NonSerialized 和 OdinSerialize,则该成员将由 Odin 序列化。这意味着 Unity 会忽略该成员的序列化,但 Odin 仍然会找到它并序列化它。
4 指定序列化秘钥
使用 Odin 序列化可以自定义处理和恢复序列化数据的方式。
Odin 提供 3 种类型的外部参考解析器,允许使用 3 种不同类型的密钥。通过从接口继承来实现,并且必须在序列化和反序列化时在 SerializationContext 中指定。
IExternalStringReferenceResolver
IExternalGuidReferenceResolver
IExternalIndexReferenceResolver
4.1 External String Reference Resolver
允许您指定字符串 ID 以序列化和反序列化引用。例如,在 Unity 编辑器中,您可以使用它按照 Asset 的 GUID 序列化资产引用:
csharp
public class ScriptableObjectStringReferenceResolver : IExternalStringReferenceResolver
{
// 可以将多个字符串引用解析器串联在一起。
public IExternalStringReferenceResolver NextResolver { get; set; }
public bool CanReference(object value, out string id)
{
if (value is ScriptableObject)
{
id = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject));
return true;
}
id = null;
return false;
}
public bool TryResolveReference(string id, out object value)
{
value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id));
return value != null;
}
}
序列化和反序列化时,需要在序列化上下文中指定解析程序:
csharp
byte[] Serialize(object obj)
{
var context = new SerializationContext()
{
StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
};
return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}
object Deserialize(byte[] bytes)
{
var context = new DeserializationContext()
{
StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
};
return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}
4.2 External GUID Reference Resolver
外部 GUID 引用解析程序的工作方式与字符串解析程序大致相同,但它严格使用 GUID 键。
该实现也与字符串解析器几乎相同:
csharp
public class ScriptableObjectGuidReferenceResolver : IExternalGuidReferenceResolver
{
// 可以将多个字符串引用解析器串联在一起。
public IExternalGuidReferenceResolver NextResolver { get; set; }
public bool CanReference(Guid value, out string id)
{
if (value is ScriptableObject)
{
id = new Guid(AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject)));
return true;
}
id = default(Guid);
return false;
}
public bool TryResolveReference(Guid id, out object value)
{
value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id.ToString()));
return value != null;
}
}
当然,在序列化和反序列化时需要指定它。
csharp
byte[] Serialize(object obj)
{
var context = new SerializationContext()
{
GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
};
return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}
object Deserialize(byte[] bytes)
{
var context = new DeserializationContext()
{
GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
};
return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}
4.3 External Index Reference Resolver
最后,索引解析器允许使用索引 ID 进行引用。这在 Serialized- 类中使用,例如 SerializedScriptableObject,用于重建由 Odin 序列化的 Unity 引用。
如果希望以相同的方式重建引用,在序列化和反序列化之间保持列表相同非常重要。与字符串和 guid 解析器的一个显著区别是,不能将多个索引解析器链接在一起。
csharp
public class IndexResolver : IExternalIndexReferenceResolver
{
public List<UnityEngine.Object> ReferenceList;
public IndexResolver()
{
this.referenceList = new List<UnityEngine.Object>();
}
public IndexResolver(List<UnityEngine.Object> references)
{
this.referenceList = references;
}
public bool CanReference(object value, out int index)
{
if (value is UnityEngine.Object)
{
index = this.referenceList.Count;
this.referenceList.Add(value);
}
index = 0;
return false;
}
public bool TryResolveReference(int index, out object value)
{
value = this.referencedUnityObjects[index];
return true;
}
}
另一个显着的区别是,在序列化和反序列化过程中,为引用添加了列表本身。
csharp
byte[] Serialize(object obj, out List<UnityEngine.Object> references)
{
var resolver = new IndexResolver();
var context = new SerializationContext()
{
IndexReferenceResolver = resolver,
};
var bytes = SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
references = resolver.ReferenceList;
return bytes;
}
object Deserialize(byte[] bytes, List<UnityEngine.Object> references)
{
var context = new DeserializationContext()
{
IndexReferenceResolver = new IndexResolver(references),
};
return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}
4 功能与限制
4.1 平台支持
Odin 序列化目前在所有平台上都受支持:
- 在非 IL2CPP 版本中,保存通用 Windows 平台;
- 在 IL2CPP 或 Mono AOT 平台上使用 Odin 的序列化时,必须遵循特殊的 AOT 程序,才能使 Odin 的序列化在构建中正常工作。
4.2 启用序列化
因此,使用 Odin 序列化通常不需要考虑太多,它将在必要时填补空白。对于如下情况, Odin 都会自动采取序列化:
- 任何未被 Unity 序列化的公共字段;
- 任何未被 Unity 序列化并标有 SerializeField 的私有字段。
但如果确实需要确保特定成员始终由 Odin 序列化,则可以使用特定于 Odin 的 OdinSerialize 特性标记任何字段或自动属性,来确保这一点。
4.3 序列化内容
默认情况下,Odin 将针对 Unity 不会序列化的对象,进行拓展序列化,这包括:
- 字典;
- 委托;
- 属性。
- 等。
默认情况下,Odin 会尝试序列化你交给它的几乎所有东西。在大多数情况下,这将正常工作,在某些情况下则不会。对于不会的情况,可以手动扩展序列化。
4.4 支持的数据格式
-
Binary
性能最佳的格式,针对速度进行优化,并将垃圾分配保持在最低限度。默认情况下,它将在播放模式和构建中使用。
-
Json
默认情况下从不使用 json 格式,但支持对某些内容进行手动序列化并且希望序列化数据可读的情况。需要注意的是,json 格式没有最佳性能,并且在当前版本的 Odin 中分配了很多不必要的垃圾。目前,仅在必要时使用 json 格式。
-
Nodes
编辑器中的默认格式,但在播放模式下不是。它不会序列化为字节流,而是转成节点列表,由 Unity 再进行序列化,主要用于 Unity 编辑器的文本资产序列化。当与 Unity 的文本格式一起使用时,节点数据会被格式化,以便在版本控制中更容易合并,因此适合多人的协作项目。节点格式性能不错,但不如二进制格式快,默认只能在编辑器中使用。
5 序列化重构
5.1 重命名字段和属性
重命名字段或属性时,可以使用 Unity 的 FormerlySerializedAs 或 Odin 的 BeforeSerializedAs 特性,让 Odin 知道如何将旧序列化数据映射到新字段。
csharp
[FormerlySerializedAs("oldName")]
public int NewName;
建议使用 Unity 的 FormerlySerializedAs 来支持 Odin 的 BeforeSerializedAs,因为 Unity 只支持其中之一,但 Odin 同时支持两者。Odin 的 BeforeSerializedAs 变体主要用于支持罕见的(不推荐的)序列化情况。
注意:不能在 Odin 和 Unity 之间传输序列化数据。如果某个字段已被 Unity 序列化,则不能使用它将其反序列化为 Odin 字段。
5.2 重命名类型
使用 BindTypeNameToType 属性使用 Odin 完成重构类型。允许指定旧类型名称,以及要将类型反序列化的类型。
csharp
// 也可以使用该特性对命名空间进行重命名
[assembly: BindTypeNameToType("MyOldNamespace.MyNewTypeName", typeof(MyNewNamesSpace.MyNewTypeName))]
namespace MyNewNamesSpace
{
public class MyNewTypeName { ... }
}
此特性仅适用于 Odin 序列化数据。
5.3 从 Unity 转移到 Odin
有时可能需要在 Unity 和 Odin 序列化器之间传输数据,但没有现成的属性可以用来实现该功能,需要按几个顺序步骤进行,以确保不会丢失数据。
-
不推荐的方法:
仅仅重命名或从 Unity 可序列化的格式更改为 Odin 可序列化的格式,可能会导致数据丢失(建议使用版本控制项目,以便随时回退版本恢复错误)。
-
推荐方法:
更好的方法是简单地复制字段,把新字段改为 Odin 序列化,并在对象反序列化时手动传输和复制数据。
csharp
public class MyMono : SerializedMonoBehaviour
{
// 隐藏旧数据的显示
[HideInInspector]
public List<MyClass> OldList;
[NonSerialized, OdinSerialize]
public List<MyClass> NewList = null;
// OnAfterDeserialize 方法定义在 SerializedMonoBehaviour 类中,
// 但也可以在 Unity 中使用 ISerializationCallbackReceiver,
// Odin 还支持 System.Runtime.Serialization 命名空间中定义的序列化事件属性,
// 如 [OnDeserialized]。
protected override OnAfterDeserialize()
{
if (this.NewList != null)
{
// 这种情况下,我们只复制旧列表,但你可以根据具体情况做适当的处理。
this.NewList = this.OldList.ToList();
// 此时,最好将对象标记为 dirty,以便 Unity 重新序列化它。
}
}
}
[Serializable]
public class MyClass
{
...
}
这样,数据会从旧格式复制到新格式。
注意:Unity 并不会每次都重新序列化和保存对象,所以不能立即删除旧列表。要么保留旧列表一段时间,要么强制 Unity 重新序列化项目中的所有对象。从 Unity 2018.1 开始,可以使用 AssetDatabase.ForceReserializeAssets API 来实现,调用此 API 会在整个项目中迁移数据。
6 序列化 Dictionary
6.1 使用 Odin 序列化字典
只需继承序列化类 SerializedScriptableObject,就可以序列化字典了!
csharp
public MyScriptableObject : SerializedScriptableObject
{
// 该字典将被 Odin 序列化
public Dictionary<int, string> IntStringMap;
}
6.2 使用 Unity 序列化字典
通过创建继承 Dictionary 和 Unity 的 ISerializationCallbackReceiver 接口的新类,可以将 Dictionary 数据转换为 Unity 可以序列化的格式。
csharp
public abstract class UnitySerializedDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
[SerializeField, HideInInspector]
private List<TKey> keyData = new List<TKey>();
[SerializeField, HideInInspector]
private List<TValue> valueData = new List<TValue>();
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
this.Clear();
for (int i = 0; i < this.keyData.Count && i < this.valueData.Count; i++)
{
this[this.keyData[i]] = this.valueData[i];
}
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
this.keyData.Clear();
this.valueData.Clear();
foreach (var item in this)
{
this.keyData.Add(item.Key);
this.valueData.Add(item.Value);
}
}
}
由于 Unity 不序列化泛型类型,因此必须通过继承 UnitySerializedDictionary 来创建具体的 Dictionary 类型:
csharp
[Serializable]
public class KeyCodeGameObjectListDictionary : UnitySerializedDictionary<KeyCode, List<GameObject>> { }
[Serializable]
public class StringScriptableObjectDictionary : UnitySerializedDictionary<string, ScriptableObject> { }
这样一来,就有了可以在 Unity 中使用的字典。
csharp
public class MyScriptableObject : ScriptableObject
{
public KeyCodeGameObjectListDictionary KeyCodeMap;
public StringScriptableObjectDictionary StringMap;
}
这些 UnitySerializedDictionaries 可以像任何普通的字典一样在 Inspector 窗口中显示,甚至可以在 Odin Inspector 中显示为普通的字典。
注意:Unity 的预制件修改系统可能存在一些奇怪的交互,且预制件修改后的值可能无法在 Inspector 窗口中正确显示。
7 序列化协议
7.1 Odin 序列化协议
在 MonoBehaviours、ScriptableObjects 等中使用 Odin 进行序列化时,具有如下特点:
-
Odin 不会替换 Unity 序列化器,而是扩展它。
Odin 通过将非 Unity 序列化的字段和属性转换为 Unity 可以序列化的数据格式来实现这一点。
-
Odin 将序列化数据存储在 SerializationData 字段中。
Unity 的序列化器将启动并使用自己的序列化器序列化 Odin 序列化数据,并将其写入某个位置的文件。
这种实现具有很多优势:
- 利用了 Unity 的所有现有系统,因此不会改变通常的 Unity 工作流程。
- 允许 Unity 处理其自己对象引用的序列化。这允许在几乎任何地方引用 Unity 对象,并且这些引用在播放器中仍然有效。
但此种实现也带来了一个缺点:对于同一字段,可能序列化两次:一次是 Unity,一次是 Odin。对此,Odin 将尝试智能地过滤掉应该已经由 Unity 序列化的字段:
csharp
public class MyScriptableObject : SerializedScriptableObject
{
// Unity 可以序列化基本类型,因此 Odin 会跳过这个字段。
public float PublicFloat;
// 这个字段会被 Unity 和 Odin 都跳过。
private float privateFloat;
// OdinSerialize 强制 Odin 序列化这个字段。然而,这个字段也会同时被 Unity 序列化,导致数据重复。
[OdinSerialize]
public float OdinAndUnityFloat;
// 在 Odin 中,NonSerialized 属性会被 OdinSerialize 属性覆盖。Unity 不会序列化这个字段,但 Odin 会。
[NonSerialized, OdinSerialize]
public float OdinOnlyFloat;
}
序列化程序仅在根级别选择:类或结构的任何类成员都将由序列化程序序列化,该序列化最终包含该值的"根"字段:
csharp
public class MyScriptableObject : SerializedScriptableObject
{
// 由于 MySerializedObject 类定义了 Serializable 属性,所以 Unity 会序列化这个字段。
// 但是,Unity 不会序列化 Dictionary 字段,因此 Dictionary 字段不会被序列化。
public MySerializedObject UnitySerialized;
// 和上面的例子一样,Odin 被强制序列化这个字段。
// Dictionary 将会被保存。
[NonSerialized, OdinSerialize]
public MySerializedObject OdinSerialized;
}
// 另一种强制 Odin 序列化这个类的方法是移除 MySerializedObject 定义中的 Serializable 属性。
// 这样做有效,因为 Unity 只会序列化带有 Serializable 属性的类型。
[Serializable]
public class MySerializedObject
{
// 在这里使用类似 [NonSerialized, OdinSerialize] 的属性不会有区别,
// 因为这种类型的序列化取决于选择的顶层序列化器。
public Dictionary<int, string> MyDictionary;
}
一个好的经验法则:如果某个属性或字段未显示在 Inspector 窗口中(不使用 ShowInInspector 属性之类的内容),则不会序列化该成员。
注意:
Odin Serializer 速度非常快,但由于它扩展了 Unity 序列化器,因此它只能增加序列化和反序列化时间。
最重要的是,Unity 序列化器具有刻意的简单设计和原生实现,非常非常快------比 Odin 快得多。
因此,如果使用 Unity 序列化就能满足要求,那么这绝对是推荐的方法。
7.2 Unity 序列化协议
-
Unity 仅序列化定义 Serializable 属性的任何类或结构。
-
Unity 不会序列化任何泛型类型变体,例如 Dictionary。
但是,可以通过继承泛型类型来对泛型类进行具体定义,并且该类和所有泛型字段(当然其他规则仍然适用于这些)将由 Unity 序列化。List 是此规则的一个例外。
-
Unity 不会序列化任何多态引用,因此不会序列化任何抽象、接口或对象字段。
-
Unity 不会序列化 DateTime 字段或 mscorlib 中定义的任何其他结构和类。
-
根据版本的不同,Unity 不支持所有基础枚举类型。
Unity 5.6 版引入了对除 long 和 ulong 之外的所有基元类型的支持。在此版本之前,仅支持 byte 和 int 枚举类型。
8 使用 Odin 保存数据
- 创建一个数据类,其包含多种数据类型,使数据与 Unity 组件分开。因为在运行时,Odin 无法保存对实际 Unity 对象的引用或内容,即 Unity 对象的引用或依赖关系。因此,建议将其分离为一个干净的 C# 数据类。
csharp
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;
[Serializable]
public class Data
{
public int i;
public float f;
public string s;
public Vector3 v;
public List<GameObject> lg = new List<GameObject>();
[ShowInInspector]
public Dictionary<string, Vector2> dsv = new Dictionary<string, Vector2>();
}
- 需要添加 Serializable 特性,以实现该类的序列化。
- 使用 ShowInInspector 特性,可以在 Inspector 窗口中查看该 Dictionary。
-
创建 Test 脚本,其包含一个 Data 成员作为数据,并添加如下特性:
- SerializeField:使该数据被序列化(public 成员可默认不指定该特性)。
- InlineProperty:将 data 内容显示在标签旁,而不是以折叠方式呈现。
- LabelWidth:指定 data 标签长度。
同时编写存储(Svae)和读取(Load)数据的方法,使用 Button 特性将其作为按钮使用。
csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;
public class Test : MonoBehaviour
{
[SerializeField, InlineProperty, LabelWidth(45)]
public Data data;
[Button]
public void Save() {
byte[] bytes = SerializationUtility.SerializeValue(data, DataFormat.Binary);
File.WriteAllBytes(Application.streamingAssetsPath + "/data", bytes);
}
[Button]
public void Load() {
byte[] bytes = File.ReadAllBytes(Application.streamingAssetsPath + "/data");
data = SerializationUtility.DeserializeValue<Data>(bytes, DataFormat.Binary);
}
}
- 将 Test 脚本拖拽到任意一个场景物体上,并在 Inspector 窗口上随意更改 Test 中的 data 数据。点击 Save 按钮,则 data 数据被存储到 StreamingAssets 文件夹下的二进制文件 data 中(可右键点击 Fresh 刷新 Project 窗口查看)。
- 清除数据(remove 脚本再重新拖拽进来),点击 Load 按钮,可以看到数据被加载。这里 Lg 中的成员为 None,是因为 Odin 序列化不保存 Unity 对象的引用,这点需要注意。
9 序列化调试器
Odin 包含一个序列化调试实用程序,如果遇到值消失或未显示在检查器中的问题,它可以为提供帮助。
调试器将显示:
- 任何给定类型的哪些成员正在序列化;
- 是否由 Unity、Odin 还是两者共同序列化。
- 从序列化的角度,提供任何给定成员所发生情况的详细描述。
打开方式:
- 从
Tools > Odin > Serializer > Serialization Debugger
访问窗口,然后从下拉列表中选择脚本类型以开始调试。
- 单击组件齿轮下拉菜单中的"调试序列化"按钮直接开始调试组件。
10 自定义序列化类
使用 SerializationUtility 类,该类包装序列化系统并处理所涉及的所有样板代码。示例代码如下所示:
csharp
// 定义一个包含一些随机垃圾字符串数据、随机数字、Unity 对象和循环引用的类
public class MyData
{
public string str = new string(Enumerable.Range(0, 20).Select(i => (char)UnityEngine.Random.Range(50, 150)).ToArray());
public List<float> numbers = new List<float>(Enumerable.Range(0, 10).Select(i => UnityEngine.Random.Range(0f, 100f)));
public GameObject unityObjectReference = UnityEngine.Object.FindObjectOfType<UnityEngine.GameObject>();
public MyData reference;
}
// 某处的方法可能如下所示,用于将数据序列化为 JSON
private void SerializeData()
{
// 保存到 Assets 文件夹
string path = Application.dataPath + "/data.json";
// 初始化一些数据
var originalData = new MyData();
originalData.reference = new MyData();
originalData.reference.reference = originalData;
// Unity 应该能够处理其自己奇怪对象的序列化和反序列化。
// 如果你的数据图包含 UnityEngine.Object 类型,你需要为 Odin 提供一个 UnityEngine.Object 列表,
// Odin 将使用它作为外部引用解析器。
List<UnityEngine.Object> unityObjectReferences = new List<UnityEngine.Object>();
// 数据格式
DataFormat dataFormat = DataFormat.JSON;
// DataFormat dataFormat = DataFormat.Binary;
// DataFormat dataFormat = DataFormat.Nodes;
// 序列化
{
var bytes = SerializationUtility.SerializeValue(originalData, dataFormat, out unityObjectReferences);
File.WriteAllBytes(path, bytes);
// 如果你需要 JSON 字符串,使用 UTF8 编码
// var jsonString = System.Text.Encoding.UTF8.GetString(bytes);
}
// 反序列化
{
var bytes = File.ReadAllBytes(path);
// 如果你有要反序列化的字符串,使用 UTF8 编码获取字节
// var bytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
var data = SerializationUtility.DeserializeValue<MyData>(bytes, dataFormat, unityObjectReferences);
}
}
如果查看生成的 json 文件,会看到 Odin 的 json 格式在特殊的 $ 前缀条目中保留了一堆元数据。因此,其他 Json 序列化器可能无法正常解析 Odin 序列化的 json 文件。Odin 序列化的 json 文件规定了额外元数据的特殊格式,例如类型信息,因此生成的 json 文件可能并不是标准的序列化 json。
注意:
是否应该使用它取决于您的确切用例。如果只是想将一些非常简单的数据保存到 json 中,建议使用 Unity 自己的 JsonUtility 类 - 它比 Odin 的 json 序列化快得多,后者相对较慢。(
Odin 的二进制格式非常快且高度优化。相比之下,json 优化不是那么多。因为它优先级不高,且默认情况下,Json 从未在 Odin 中使用。
11 AOT 序列化
详情见 https://odininspector.com/tutorials/serialize-anything/aot-serialization#odin-inspector。
12 建议与总结
-
尽可能使用 Unity 自己的序列化器!
-
对于 MonoBehaviours/Components 和 ScriptableObjects,Odin 序列化程序不会替换,而是扩展现有的 Unity 序列化程序。
- Odin 首先序列化 Unity 未接触的所有数据,并将其转换为 Unity 可以理解的数据存储格式和 Unity 对象列表。
- 然后,Unity 将此数据与对象的其余部分一起序列化。
因此,总会有两个序列化传递。当然这也意味着使用 Odin 序列化器总是比仅使用 Unity 的序列化器慢。
因此,建议仅在 Unity 序列化器无法满足要求时才使用 Odin。