Unity Editor Adventures: Matryoshka serializada

Breve introducción


Como regla general, para llegar al campo de una propiedad serializada de interés para nosotros, la terapia manual nos aconseja utilizar el método FindPropertyRelative (), en el que se arroja el nombre de la variable.

Por ciertas razones, este enfoque no siempre es conveniente. Las razones pueden ser muy diversas. Por ejemplo, el nombre de una variable puede cambiar, necesitamos acceso de hemorragia nasal a una propiedad no serializada, necesitamos tener acceso a getter setter o incluso métodos de un objeto serializado. No le haremos las preguntas "por qué necesita esto en absoluto" y "por qué no podría prescindir de las formas tradicionales". Supongamos que necesitamos, y eso es todo.

Entonces, descubramos cómo obtener el objeto con el que estamos trabajando, así como todos sus objetos principales de la propiedad serializada, y no quedar atrapados en el camino de la serialización lleno de trampas.

Atención. Este artículo implica que ya sabe cómo trabajar con UnityEditor, al menos una vez escribió PropertyDrawers personalizados, y al menos en términos generales comprende cómo una propiedad serializada difiere de un objeto serializado.


Ruta de serialización


Para empezar, coloque todos los puntos sobre O.

En el caso más primitivo, tenemos una cierta clase de heredero de MonoBehaviour, y tiene un cierto campo que pertenece a una clase serializada, obviamente no un heredero de la vaca sagrada unitaria de A.K.A. UnityEngine.Object.

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

En el código anterior, SandboxField es una clase con el atributo Serializable.

Para obtener acceso al MonoBehaviour del propietario no es un problema:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

Si lo desea, puede hacerlo como, pero ahora no lo necesitamos. Estamos interesados ​​en el campo serializado en sí mismo para dibujarlo con todo el blackjack como en la figura a continuación.


Podemos tomar la ruta de serialización de la siguiente manera:

string serializationPath = property.propertyPath;

En nuestro caso, la ruta de serialización consistirá en uno de nuestros campos y devolverá “sandboxField”, del cual no estamos fríos ni calientes, ya que para el primer nivel de anidamiento solo necesitamos saber el nombre de la variable (que, por cierto, nos fue devuelta).

Tenga en cuenta que no hay MonoBehaviour padre en el camino. Ahora no importa, pero será importante cuando comencemos a desmontar una muñeca rusa que se parece a esto:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
Para no quedar atrapado más tarde cuando las propiedades están anidadas, con anticipación haremos lo siguiente:

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

Ahora tenemos todos los nodos de la ruta de serialización. Pero en el caso más primitivo, solo necesitamos el nodo cero. Tómalo:

string pathNode = path[0];

Encienda un pequeño reflejo y obtenga el campo desde aquí:

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

Dejamos detrás de escena la cuestión de la velocidad de esta empresa. Para una pequeña cantidad de tales campos y un pequeño anidamiento, el costo de reflexión será significativamente menor que todo lo que sucede bajo el capó de UnityEditor durante el renderizado. Si desea pruebas: en un github, los desarrolladores de Unity tienen algo tan interesante, UnityCsReference, mire su tiempo libre, cómo se implementa la representación de ObjectField, por ejemplo.

En realidad, esto no es (ja, ja) todo. Tenemos el campo, estamos contentos, podemos hacer lo que queramos con él e incluso tratar de escribir nuestro propio UnityEvent con todos los botones y acciones importantes que afectan solo a nuestro campo, sin importar en qué objeto cuelgue.

Al menos, mientras se cuelga en la raíz de este objeto, todo estará bien, pero ya no es tanto. En el camino de la serialización, estamos esperando matrices y todo tipo de listas, cuyo principal deseo es ponernos en zapatillas, cambiando oportunamente el número de elementos hacia arriba. Pero al diablo con esto, primero cavaríamos debajo de la matriz misma.

Nuestro objetivo es la resistencia a tales muñecas.

Si no tuviéramos matrices en la ruta de serialización, la tarea sería trivial: recorreríamos los nodos de serialización hasta llegar al final de la cadena. Algo así como el siguiente código:

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;

Aquí estamos esperando dos noticias desagradables a la vez. Comenzaré con el segundo.

El paso de tomar nextObject puede devolver repentinamente nulo en lugar del objeto esperado. Por lo general, esto sucede cuando creamos por primera vez el objeto padre en el inspector, y la ruta de serialización ya existe, pero el campo correspondiente no (esto nos brindará aún más servicios).

En este caso, sería bueno agregar inmediatamente la salida del método con el retorno nulo:

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

"¡Espera un minuto! - tu dices. "¿Y qué hacer en OnGUI si nos devuelven cero?"

Respuesta: nada. Nada literalmente. Simplemente realice una devolución y omita este paso de dibujo, esperando a que se cree el campo. Nada terrible sucederá de esto.

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

aquí GetTarget () es la función correspondiente, tomando el objeto serializado de la propiedad.

Por cierto, le aconsejaré que tome el campo de interés para nosotros no aquí, sino en GetPropertyHeight. Esto será necesario en caso de que escribamos campos plegables expandibles con diferentes tamaños según el contenido. Se llama a GetPropertyHeight () antes de OnGUI (), por lo que si tomamos el campo allí y lo escribimos en el campo de nuestro PropertyDrawer, no tendremos que llevarlo nuevamente a OnGUI.

Tenga en cuenta que una instancia del PropertyDrawer personalizado se crea solo para dibujar todas las propiedades serializadas actualmente visibles, y las nuevas propiedades se lanzan a su vez de arriba a abajo. Esto debe tenerse en cuenta para no interferir con el cálculo de la altura de la siguiente propiedad, de lo contrario, puede obtener una situación desagradable cuando hace clic en desplegable y el campo que espera se expande.

Además, todo el oropel que es responsable de mostrar el campo en el editor y que desea serializar, debe serializar al lado de la clase serializable, no PropertyDrawer, y por razones de precisión, incluya corchetes de compilación condicional para que toda esta vergüenza española no intente ir a la compilación :

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

Otro escollo que nos espera aquí: todos los campos creados a través del editor querían escupir en el constructor de la clase y en los valores predeterminados especificados en la clase. Si lo hace, por ejemplo, de esta manera (ejemplo de mi proyecto, donde fue el valor de los nodos de la superficie del agua):

[SerializeField]private int m_nodesPerUnit = 5;

Este valor se ignorará la serialización en blanco tan pronto como agregue un nuevo elemento a la lista. Llamar al constructor para pedir ayuda no es menos inútil: todo lo que escribiste allí será ignorado. El nuevo objeto es un objetivo como un halcón, todos sus valores son realmente los valores predeterminados, pero no los que desea ver allí, sino todo tipo de cosas nulas, falsas, 0, Color.clear y otras cosas obscenas.

Muleta en carne
. 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 .

: .



Volvamos a nuestras ovejas, o más bien, a las matrices. Otro problema que puede surgir incluso en una etapa anterior radica en esta línea:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

El lado derecho puede devolvernos cero si se encuentra con una matriz en la ruta de serialización (y cualquier objeto "IList" será la matriz "Array"). Desagradable.

¿Qué hacer? Intente no caer en tales situaciones escribiendo un controlador:

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;

Sí, incluso podemos entrar en una situación desagradable aquí, cuando la ruta de serialización ya tiene, por ejemplo, el elemento data [0] o data [1], y la matriz aún no lo ha implementado. Por ejemplo, creamos una Lista vacía. Le preguntamos N elementos - y sin esta hermosa línea:

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

... tenemos un montón de excepciones en ronroneos. Y todo lo que tenía que hacer era omitir el paso de representación, esperando hasta que se crearan los campos de interés para nosotros.

Todavía no me he encontrado con otros casos cuando objectFieldInfo == null, pero al mismo tiempo el nodo de serialización no está designado como Array, por lo que arrojar una terrible excepción en una situación tan hipotética excepcional es romperlo posteriormente.

En general, tenemos una función más o menos funcional que nos permite extraer un campo por su propiedad serializada. En el futuro, esta función puede modificarse obligando a extraer todos los objetos en la ruta de serialización, así como a buscar el "padre" más cercano, incluyendo o excluyendo matrices a lo largo de la ruta.

Hack de vida para dibujar propiedades anidadas
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

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

Gracias por la atención.

All Articles