Unity Editor Adventures: Matriochka sérialisé

Brève introduction


En règle générale, pour accéder au domaine d'une propriété sérialisée qui nous intéresse, la thérapie manuelle nous conseille d'utiliser la méthode FindPropertyRelative (), dans laquelle le nom de la variable est jeté.

Pour certaines raisons, cette approche n'est pas toujours pratique. Les raisons peuvent être très diverses. Par exemple, le nom d'une variable peut changer, nous avons besoin d'un accès par le nez à une propriété non sérialisée, nous devons avoir accès à des accesseurs de définition ou même à des méthodes d'un objet sérialisé. Nous ne poserons pas les questions «pourquoi en avez-vous besoin du tout» et «pourquoi vous ne pourriez pas vous passer des méthodes traditionnelles». Supposons que nous ayons besoin - et c'est tout.

Alors, découvrons comment obtenir l'objet avec lequel nous travaillons, ainsi que tous ses objets parents à partir de la propriété sérialisée, et ne pas se laisser prendre sur la route de sérialisation pleine d'embûches.

Attention. Cet article suppose que vous savez déjà comment travailler avec UnityEditor, que vous avez écrit au moins une fois des PropertyDrawers personnalisés et que vous comprenez en général comment la propriété sérialisée diffère d'un objet sérialisé.


Chemin de sérialisation


Pour commencer, mettez tous les points sur O.Dans

le cas le plus primitif, nous avons une certaine classe héritière de MonoBehaviour, et elle a un certain champ appartenant à une classe sérialisée, évidemment pas un héritier de la vache unitaire sacrée de A.K.A. UnityEngine.Object.

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

Dans le code ci-dessus, SandboxField est une classe avec l'attribut Serializable.

Pour accéder au MonoBehaviour du propriétaire n'est pas un problème:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

Si vous le souhaitez, vous pouvez le faire au fur et à mesure, mais maintenant nous n'en avons plus besoin. Nous nous intéressons au champ sérialisé lui-même afin de le dessiner avec tout le blackjack comme dans la figure ci-dessous.


Nous pouvons prendre le chemin de sérialisation comme suit:

string serializationPath = property.propertyPath;

Dans notre cas, le chemin de sérialisation se composera de l'un de nos champs et renverra «sandboxField», dont nous ne sommes ni froid ni chaud, car pour le premier niveau d'imbrication nous n'avons besoin de connaître que le nom de la variable (qui, soit dit en passant, nous a été retournée).

Veuillez noter qu'il n'y a pas de comportement Mono parent sur le chemin. Maintenant, cela n'a plus d'importance, mais cela deviendra important lorsque nous commencerons à démonter une poupée russe qui ressemble à ceci:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
Afin de ne pas se faire prendre plus tard, lorsque les propriétés sont imbriquées, nous procéderons à l'avance comme suit:

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

Nous avons maintenant tous les nœuds du chemin de sérialisation. Mais dans le cas le plus primitif, nous n'avons besoin que du nœud zéro. Prends-le:

string pathNode = path[0];

Allumez un peu de réflexion et obtenez le champ d'ici:

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

Nous laissons derrière les coulisses la question de la rapidité de cette aventure. Pour un petit nombre de ces champs et une petite imbrication, les coûts de réflexion seront nettement inférieurs à ceux de tout ce qui se passe sous le capot d'UnityEditor lors du rendu. Si vous voulez des preuves - sur un github, les développeurs Unity ont une chose si intéressante, UnityCsReference, regardez à votre guise, comment le rendu ObjectField est implémenté, par exemple.

En fait, ce n'est pas (ha ha) tout. Nous avons le champ, nous sommes heureux, nous pouvons faire tout ce que nous voulons avec lui et même essayer d'écrire notre propre UnityEvent avec tous les boutons et actions importantes qui n'affectent que notre champ, quel que soit l'objet sur lequel il s'accroche.

Au moins, alors qu'il se bloque à la racine de cet objet, tout ira bien, mais alors ce n'est plus tellement. Sur le chemin de la sérialisation, nous attendons des tableaux et toutes sortes de listes, dont le principal désir est de nous mettre dans des pantoufles, en changeant en temps opportun le nombre d'éléments. Mais au diable avec cela, nous creuserions d'abord sous le tableau lui-même.

Notre objectif est la résistance à de telles poupées

Si nous n'avions pas de tableaux dans le chemin de sérialisation, la tâche serait triviale: nous ferions une boucle à travers les nœuds de sérialisation jusqu'à atteindre la fin de la chaîne. Quelque chose comme le code suivant:

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;

Ici, nous attendons deux nouvelles désagréables à la fois. Je vais commencer par le second.

L'étape de prise de nextObject peut soudainement retourner null au lieu de l'objet attendu. Habituellement, cela se produit lorsque nous créons pour la première fois l'objet parent dans l'inspecteur et que le chemin de sérialisation existe déjà, mais le champ correspondant n'existe pas (cela nous apportera encore plus de commodités).

Dans ce cas, il serait bien d'ajouter immédiatement la sortie de la méthode avec le retour null:

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

"Attends une minute! - vous dites. "Et que faire alors dans OnGUI si zéro nous était retourné?"

Réponse: rien. Rien littéralement. Faites simplement un retour et sautez ainsi cette étape de dessin, en attendant que le champ soit créé. Il ne se passera rien de terrible de cela.

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

ici GetTarget () est la fonction correspondante, prenant l'objet sérialisé de la propriété.

Soit dit en passant, je vous conseillerai de prendre le champ d'intérêt pour nous non pas ici, mais dans GetPropertyHeight. Cela sera nécessaire dans le cas où nous écrivons des champs extensibles pliables avec différentes tailles en fonction du contenu. GetPropertyHeight () est appelée avant OnGUI (), donc si nous y prenons un champ et l'écrivons dans le champ de notre PropertyDrawer, nous n'aurons pas à le reprendre à OnGUI.

Veuillez noter qu'une instance du PropertyDrawer personnalisé est créée seule pour dessiner toutes les propriétés sérialisées actuellement visibles, et de nouvelles propriétés sont lancées à son tour de haut en bas. Cela doit être pris en compte afin de ne pas perturber le calcul de la hauteur de la propriété suivante, sinon vous risquez d'obtenir une situation désagréable lorsque vous cliquez sur le dépliant et que le champ que vous attendez est développé.

En outre, tous les guirlandes responsables de l'affichage du champ dans l'éditeur et que vous souhaitez sérialiser, vous devez sérialiser du côté de la classe sérialisable, et non par PropertyDrawer, et pour des raisons de précision - joignez des crochets de compilation conditionnels afin que toute cette honte espagnole n'essaye pas d'aller à la génération :

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

Autre écueil qui nous attend ici: tous les champs créés via l'éditeur ont voulu cracher sur le constructeur de classe et sur les valeurs par défaut spécifiées dans la classe. Si vous faites, par exemple, comme ça (exemple de mon projet, où c'était la valeur des nœuds de la surface de l'eau):

[SerializeField]private int m_nodesPerUnit = 5;

Cette valeur sera ignorée de la sérialisation à blanc dès que vous ajouterez un nouvel élément à la liste. Appeler le constructeur à l'aide n'est pas moins inutile: tout ce que vous y avez écrit sera ignoré. Le nouvel objet est un objectif comme un faucon, toutes ses valeurs sont vraiment les valeurs par défaut, ce n'est tout simplement pas celles que vous voulez voir là-bas, mais toutes sortes de null, false, 0, Color.clear et autres choses obscènes.

Béquille à la viande
. 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 .

: .



Revenons à nos moutons, ou plutôt aux tableaux. Un autre problème qui peut survenir à un stade encore plus précoce réside dans cette ligne:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

Le côté droit peut nous renvoyer zéro s'il se heurte à un tableau dans le chemin de sérialisation (et tout objet "IList" sera le tableau "Array"). Désagréable.

Que faire? Essayez de ne pas tomber dans de telles situations en écrivant un gestionnaire:

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;

Oui, nous pouvons même entrer dans une situation désagréable ici, lorsque le chemin de sérialisation a déjà, par exemple, l'élément data [0] ou data [1] et que le tableau ne l'a pas encore implémenté. Par exemple, nous avons créé une liste vide. On lui demande N éléments - et sans cette belle ligne:

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

... nous obtenons un tas d'exceptions dans les ronronnements. Et tout ce qui était nécessaire était de sauter l'étape de rendu, après avoir attendu la création des champs qui nous intéressaient.

Je n'ai pas encore rencontré d'autres cas où objectFieldInfo == null, mais le nœud de sérialisation n'est pas désigné comme Array, donc lancer une terrible exception dans une situation aussi hypothétique exceptionnelle est de la casser par la suite.

En général, nous avons une fonction plus ou moins fonctionnelle qui nous permet d'extraire un champ par sa propriété sérialisée. À l'avenir, cette fonction peut être modifiée en forçant à extraire tous les objets dans le chemin de sérialisation, ainsi qu'en recherchant le «parent» le plus proche, en incluant ou en excluant les tableaux le long du chemin.

Life hack pour dessiner des propriétés imbriquées
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

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

Merci pour l'attention.

All Articles