Unity编辑器历险记:序列化的Matryoshka

简短介绍


通常,为了进入我们感兴趣的序列化属性领域,手动疗法建议我们使用FindPropertyRelative()方法,在该方法中添加变量名。

由于某些原因,这种方法并不总是很方便。原因可能非常多样。例如,变量的名称可以更改,我们需要对非序列化的属性进行流鼻血的访问,我们需要对getter-setter或什至是序列化对象的方法的访问。我们不会问“为什么您根本需要这个”和“为什么没有传统方式就不能做”的问题。假设我们需要-就是这样。

因此,让我们弄清楚如何从序列化属性中获取正在使用的对象及其所有父对象,而不会陷入充满陷阱的序列化道路上。

注意。本文意味着您已经知道如何使用UnityEditor,至少一次编写了自定义PropertyDrawers,并且至少通俗地了解了序列化属性与序列化对象的区别。


序列化路径


首先,将所有点都放在O上。

在最原始的情况下,我们有MonoBehaviour的某个继承人类别,并且它具有某个属于序列化类别的字段,显然不是A.K.A.神圣unit牛的继承人。UnityEngine.Object。

public class ExamplePulsar : MonoBehaviour
{
...
        [Space][Header("Example Sandbox Inspector Field")]
        public SandboxField sandboxField;
...
}

在上面的代码中,SandboxField是具有Seri​​alizable属性的类。

要访问所有者的MonoBehaviour并不是问题:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

如果您愿意,可以按as进行操作,但是现在我们不需要它了。我们对序列化字段本身很感兴趣,以便使用所有二十一点来绘制它,如下图所示。


我们可以采用如下序列化路径:

string serializationPath = property.propertyPath;

在我们的例子中,序列化路径将包含我们的一个字段,并将返回“ sandboxField”,从中我们既不冷也不热,因为对于第一层嵌套,我们只需要知道变量的名称(顺便说一下,变量的名称已返回给我们)。

请注意,途中没有父MonoBehaviour。现在没关系,但是当我们开始拆卸看起来像这样的俄罗斯玩偶时,它将变得很重要:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
为了以后不会被捕获,在嵌套属性时,我们将提前执行​​以下操作:

string[] path = property.propertyPath.Split('.');

现在我们有了序列化路径的所有节点。但是在最原始的情况下,我们只需要零节点。拿着:

string pathNode = path[0];

打开一点反射,然后从这里获得视野:

Type objectType = serializationRoot.GetType();
FieldInfo objectFieldInfo = objectType.GetField(pathNode);
object field = objectFieldInfo.GetValue(serializationRoot);

我们把这个项目的速度问题抛之脑后。对于少量此类字段和少量嵌套,反射的成本将大大低于渲染期间UnityEditor进行的所有操作。如果您需要证明-在github上,Unity开发人员有一个有趣的东西,UnityCsReference,看看您的闲暇时间,例如ObjectField渲染的实现方式。

实际上,这还不是(哈哈)一切。我们有了这个领域,我们很高兴,我们可以用它做任何我们想做的事情,甚至尝试编写所有影响我们领域的所有按钮和重要动作的自定义UnityEvent,无论它挂在什么对象上。

至少,虽然它挂在该对象的根部,但一切都会好起来的,但现在已经不那么多了。在序列化的过程中,我们正在等待数组和各种列表,其主要目的是将我们放入拖鞋中,及时向上更改元素数量。但是,对此,我们首先要在数组本身下进行挖掘。

我们的目标是抵抗这种玩偶

如果序列化路径中没有数组,那么任务将变得微不足道:我们将遍历序列化节点,直到到达链的末端。类似于以下代码:

object currentObject = serializationRoot;
for (int i = 0; i < directSearchDepth; i++)
{
       string pathNode = path[i];
       Type objectType = currentObject.GetType();
       FieldInfo objectFieldInfo = objectType.GetField(pathNode);
       object nextObject = objectFieldInfo.GetValue(currentObject);
       currentObject = nextObject;
}
return currentObject;

在这里,我们正在等待两个令人不快的消息。我将从第二个开始。

采取nextObject的步骤可能突然返回null而不是预期的对象。通常,这是在我们首先在检查器中创建父对象时发生的,并且序列化路径已经存在,但是相应的字段不存在(这将为我们带来更多便利)。

在这种情况下,最好立即使用return null从方法中添加出口:

object nextObject = objectFieldInfo.GetValue(currentObject);
if (nextObject == null) return null;
currentObject = nextObject;

“等一下!- 你说。“如果返回零,那么在OnGUI中该怎么办?”

答:没有。没什么。只需返回即可跳过此绘制步骤,等待创建字段。一切都不会发生。

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
       SandboxField sandboxField = GetTarget<T>(property);
       if (sandboxField == null) return;
}

这里的GetTarget()是相应的函数,它从属性中获取序列化的对象。

顺便说一句,我建议您不要将我们感兴趣的领域带入GetPropertyHeight中。如果我们根据内容编写不同大小的可折叠可扩展字段,则将需要此字段。 GetPropertyHeight()在OnGUI()之前被调用,因此,如果我们在此处获取一个字段并将其写入PropertyDrawer的字段中,则无需再次将其带到OnGUI。

请注意,将单独创建自定义PropertyDrawer的实例以绘制所有当前可见的序列化属性,然后从上到下依次向其抛出新属性。应该考虑到这一点,以免影响下一个属性的高度的计算,否则,当您单击折叠并扩展期望的字段时,可能会遇到不愉快的情况。

另外,所有负责在编辑器中显示该字段并要进行序列化的金属丝,都应该在可序列化类(而不是PropertyDrawer)的一侧进行序列化,并且出于准确性的考虑,请附上条件编译括号,以免所有此类西班牙耻辱都不会尝试进行构建:

[Serializable]
public class SandboxField
{
#if UNITY_EDITOR
        public bool editorFoldout;
#endif
}

在这里等待着我们的另一个陷阱:通过编辑器创建的所有字段都想吐给类构造函数和类中指定的默认值。例如,如果您这样做(例如,在我的项目中,它是水面节点的值):

[SerializeField]private int m_nodesPerUnit = 5;

将新项目添加到列表后,该值将被忽略。调用构造函数来寻求帮助也无济于事:您在此处编写的所有内容都会被忽略。新对象是一个像猎鹰一样的目标,它的所有值实际上都是默认值,不是您想要在其中看到的值,而是各种各样的null,false,0,Color.clear和其他淫秽的东西。

肉拐杖
. NonUnitySerializableClass, . , DefaultEditorObject(), .

- :

public abstract class NonUnitySerializableClass
{
        protected virtual void DefaultEditorObject()
        {
            // virtually do nothing
        }

        [SerializeField]private bool validated = false;
        public void EditorCreated(bool force = false)
        {
            if (validated && !force) return;
            DefaultEditorObject();
            validated = true;
        }

        public NonUnitySerializableClass()
        {
            EditorCreated(true);
        }
}

DefaultEditorObject(), , EditorCreated .

: .



让我们回到绵羊,或者说数组。在更早的阶段可能会出现的另一个问题是:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

如果右侧遇到序列化路径中的数组,则右侧可以将我们返回零(并且任何“ IList”对象都将是“ Array”数组)。不愉快。

该怎么办?通过编写处理程序,尽量不要陷入这种情况

for (int i = 0; i < pathNode.Length; i++)
{
        string pathNode = path[i];
        Type objectType = currentObject.GetType();
        FieldInfo objectFieldInfo = objectType.GetField(pathNode);

        if (objectFieldInfo == null)
        {
                if (pathNode == "Array")
                {
                        i++;
                        string nextNode = path[i];
                        string idxstr = nextNode.Substring(nextNode.IndexOf("[") + 1);
                        idxstr = idxstr.Replace("]", "");
                        int arrayNumber = Convert.ToInt32(idxstr);
                        IList collection = currentObject as IList;
                        if (collection.Count <= arrayNumber) return null;
                        currentObject = collection[arrayNumber];
                }
                else
                {
                        throw new NotImplementedException("   ");
                }
        }
        else //  ,     
        {
                object nextObject = objectFieldInfo.GetValue(currentObject);
                if (nextObject == null) return null;
                currentObject = nextObject;
        }
}
return currentObject;

是的,当串行化路径已具有例如data [0]或data [1]元素,而数组尚未实现它时,我们甚至可以陷入一种不愉快的境地。例如,我们创建了一个空列表。我们问他N个元素-没有这个漂亮的线条:

if (collection.Count <= arrayNumber) return null;

...我们会收到很多异常的声音。而所有需要做的就是跳过渲染步骤,直到创建了我们感兴趣的字段。

我还没有遇到objectFieldInfo == null的其他情况,但是未将序列化节点指定为Array,因此在这种假设的特殊情况下抛出可怕的异常是随后对其进行破解。

通常,我们有一个或多或少的工作函数,使我们能够通过其序列化属性提取字段。将来,可以通过强制提取序列化路径中的所有对象以及搜索最接近的“父对象”(包括或排除沿该路径的数组)来修改此功能。

生活黑客绘制嵌套属性
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

, MonoScript ( -, ), static-, AssetDatabase, .

谢谢您的关注。

All Articles