[Unity编辑器扩展基础总结] 第5章 序列化对象 SerializedObject
第5章 序列化对象 SerializedObject
Unity可以将资源序列化成特殊的格式来使用,这是在Unity中使用对象的基础。在Unity官方手册中有“SerializedObject”的详细信息。http://docs.unity3d.com/Manual/script-Serialization.html
5.1 SerializedObject简介
SerializedObject是将数据结构或对象转换为Unity便于存储和处理的格式。Unity的很多内置功能都在使用序列化,比如保存和加载数据,监视器窗口,预设。
SerializedObject与在Unity上处理的所有对象都有关系。如果没有SerializedObject,就无法创建通常要处理的资源(材质,纹理,动画剪辑等)。
UnityEngine.Object和SerializedObject之间的关系
在Unity编辑器中,所有对象(UnityEngine.Object)都会被转换为SerializedObject并进行处理。当我们在Inspector中编辑组件的值时,不是在编辑Component组件的实例,而是在编辑SerializedObject的实例。
在Unity编辑器中或者在编辑器扩展中,所有对象的操作应该尽可能用SerializedObject。这是因为SerializedObject不仅能处理序列化的数据,而且还能处理Undo和Selection的操作。
- 撤消处理Undo
- 当使用SerializedObject编辑值时,撤消过程可以自动注册。如果直接编辑UnityEngine.Object实例,则必须自己实现撤消过程。有关撤消的更多信息,请参见以后的第12章。
- 选择处理Selection
- 当我们在项目窗口中选择资源时,它会立即对其进行反序列化以获取UnityEngine.Object的实例,并在Inspector中显示它的值。这样处理的主要用处是可以允许我们在选择多个对象的时候同时进行编辑。
资源和SerializedObject之间的关系
将UnityEngine.Object保存为资源时,会将其另存为二进制或YAML文本数据,而SerializedObject负责这些序列化。
要将UnityEngine.Object保存为资源,必须将其先转为SerializedObject。然后,转换后的SerializedObject尝试创建资源和.meta文件。
资源和.meta文件之间的关系
SerializedObject会创建两个文件,一个资源文件和一个.meta文件。资源文件是真实对象的序列化版本,而.meta文件保存导入的相关设置等。
参考以下代码:
using UnityEditor;
using UnityEngine;
public class EditorTest : ScriptableObject
{
[InitializeOnLoadMethod]
static void CheckPropertyPaths()
{
var so = new SerializedObject(Texture2D.whiteTexture);
var pop = so.GetIterator();
while (pop.NextVisible(true))
{
Debug.Log(pop.propertyPath);
}
}
}
备注:[InitializeOnLoadMethod] Editor文件夹下,添加了InitializeOnLoad特性后,其构造方法会自动执行,测试结果是,每次修改这个类的内容,就会重新执行一遍构造方法。可以在构造方法中执行一些操作,来控制Editor模式下的代码执行。
日志中显示的内容如下:
正如我们所看到的,当转换为SerializedObject时,Texture2D对象也带有很多导入相关的设置。将Texture2D保存为资源时,是无法将这些设置写入纹理文件的(jpg或png),因此只能将其写入.meta文件,也就是元数据。
同样,导入资源时,会从资源和.meta文件(如果没有.meta文件,默认情况下会自动生成)生成SerializedObject,并将其转换为UnityEngine.Object。
可序列化的类变量
判断UnityEngine.Object的派生类(我们经常使用的MonoBehaviour,ScriptableObject,Editor,EditorWindow等)中字段是否可以序列化要看一下几点
- 必须是公共变量或具有SerializeField属性的字段
- 必须是Unity支持可序列化的类型(sbyte,short,int,long,byte,ushort,uint,ulong,float,double,bool,char,string,UnityEngine.Object,具有Serializable属性和结构的类等)
以下两点要重点说明一下
- 变量不能带static,const,readonly修饰符
- 不是抽象类
我们经常看到一些书中说把需要在监视器中展示的字段设成公共变量,这只是为了方便非程序员更容易理解,而使其成为公共变量只是序列化的条件之一,有时候给私有变量添加SerializeField属性是更加合理的解决方案。
using UnityEngine;
public class EditorTest : MonoBehaviour
{
[SerializeField]
private string m_str;
public string str
{
get
{
return m_str;
}
set
{
m_str = value;
}
}
}
从外部访问带有SerializeField属性的字段时,需要通过SerializedObject访问。
5.2 如何使用SerializedObject
从SerializedObject中获取参数
序列化后的数据字段可以通过SerializedProperty来检索,就像本章开头那样,使用迭代器来遍历操作所有属性。
using UnityEditor;
using UnityEngine;
public class EditorTest : ScriptableObject
{
[InitializeOnLoadMethod]
static void CheckPropertyPaths()
{
var so = new SerializedObject(Texture2D.whiteTexture);
var pop = so.GetIterator();
while (pop.NextVisible(true))
{
Debug.Log(pop.propertyPath);
}
}
}
我们还可以获取指定路径的SerializedProperty。
例如,要获取”Vector3类型变量的值时“
var seObj = 类实例
var serializedObject = new SerializedObject(seObj);
serializedObject.FindProperty ("position").vector3Value;
当我们想要获取A类中的变量类型为B的字符串变量值时
[System.Serializable]
public class B
{
[SerializeField]
string bar;
}
public class A : MonoBehaviour
{
[SerializeField] B m_b;
}
var m_a = /* 获取a的实例 */;
var serializedObject = new SerializedObject(m_a);
serializedObject.FindProperty ("m_b.bar").stringValue;
当我们想获取数组中对应索引的值时
serializedObject.FindProperty("数组变量").GetArrayElementAtIndex(1);
获取变量并更新最新数据
SerializedObject在内部进行缓存,如果在实例化时已被缓存,则将从缓存中取出SerializedObject。
例如,如果在编辑器窗口内和检视器内分别为一个对象生成一个SerializedObject,并且不同步两个SerializedObjects,那么其中一个可能会使用旧信息进行更新。
因此,如果一个对象有两个SerializedObjects,则两个SerializedObjects应该始终保持最新状态,以使其中一个不会因信息没及时更新而过期。
Unity提供了两个API来解决此问题。
Update
从内部缓存中获取最新数据。在访问SerializedObject之前先对其进行更新,以使其保持最新状态。
using UnityEditor;
using UnityEngine;
public class EditorTest : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("name"));
}
}
ApplyModifiedProperties
将更改应用到内部缓存,使其保持最新。通过ApplyModifiedProperties应用更改,这样将此视为一组。
除非有特定的条件可以应用更改,否则将Update放在方法的第一行上,并将ApplyModifiedProperties放在方法的最后一行。
using UnityEditor;
using UnityEngine;
public class EditorTest : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("name"));
//其他处理
serializedObject.ApplyModifiedProperties();
}
}
5.3 在一个SerializedObject中处理多个UnityEngine.Objects
我们可以简单地通过在SerializedObject构造函数中传递一个数组来同时处理多个UnityEngine.Objects的情况。但是,只能将相同的类型作为参数传递。如果将不同类型的对象作为参数传递,会导致键值映射不匹配,从而发生错误。
Rigidbody[] rigidbodies = /* 获取组件 */;
var serializedObject = new SerializedObject(rigidbodies);
serializedObject.FindProperty ("m_UseGravity").boolValue = true;
5.4 获取属性名称
要想访问SerializedProperty,我们首先需要知道属性的路径。如果要访问MonoBehaviour组件,则可以通过查看脚本文件轻松地找到对应属性路径。在Unity端实现的组件以及与UnityEngine.Object相关的属性名称中都包含m_,由于m_在检查器中会被省略并显示为属性名称,因此很难知道实际属性。另外,在检查器中显示的属性名称可能与实际的属性名称并不匹配。
有两种获取属性名称的主要方法:
SerializedObject.GetIterator
可以使用迭代器把所有属性的名称打印出来,文章开头有介绍。
在文本编辑器中查看资源
如果目标是组件,可以在设置中将预设体“Asset Serialization”设置为“Force Text“,然后在文本编辑器中打开预制件。
这样就可以看到YAML格式的数据,其中列出了属性名称。
我们也可以使用UnityEditorInternal命名空间中的InternalEditorUtility.SaveToSerializedFileAndForget将UnityEngine.Object另存为资源。
using UnityEngine;
using UnityEditorInternal;
using UnityEditor;
public class EditorTest : MonoBehaviour
{
void Start ()
{
var rigidbody = GetComponent<Rigidbody> ();
InternalEditorUtility.SaveToSerializedFileAndForget(new Object[]{ rigidbody }, "Rigidbody.yml", true);
}
}