Unity Editor Adventures: Serialisierte Matroschka

Kurze Einführung


In der Regel empfiehlt uns die manuelle Therapie, die FindPropertyRelative () -Methode zu verwenden, in die der Variablenname geworfen wird, um zum Feld einer serialisierten Eigenschaft zu gelangen, die uns interessiert.

Aus bestimmten Gründen ist dieser Ansatz nicht immer zweckmäßig. Die Gründe können sehr unterschiedlich sein. Zum Beispiel kann sich der Name einer Variablen ändern, wir benötigen Nasenbluten-Zugriff auf eine nicht serialisierte Eigenschaft, wir müssen Zugriff auf Setter-Getter oder sogar Methoden eines serialisierten Objekts haben. Wir werden nicht die Fragen stellen: "Warum brauchen Sie das überhaupt?" Und "Warum könnten Sie nicht ohne die traditionellen Methoden auskommen?". Angenommen, wir brauchen - und das ist es.

Lassen Sie uns also herausfinden, wie Sie das Objekt, mit dem wir arbeiten, sowie alle übergeordneten Objekte aus der serialisierten Eigenschaft abrufen und nicht auf der Serialisierungsstraße voller Fallstricke hängen bleiben.

Beachtung. Dieser Artikel impliziert, dass Sie bereits wissen, wie man mit UnityEditor arbeitet, mindestens einmal benutzerdefinierte PropertyDrawers geschrieben haben und zumindest allgemein verstehen, wie sich eine serialisierte Eigenschaft von einem serialisierten Objekt unterscheidet.


Serialisierungspfad




Setzen Sie zunächst alle Punkte über O. Im primitivsten Fall haben wir eine bestimmte Erbenklasse von MonoBehaviour, und es gibt ein bestimmtes Feld, das zu einer serialisierten Klasse gehört, offensichtlich kein Erbe der heiligen einheitlichen Kuh von A.K.A. UnityEngine.Object.

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

Im obigen Code ist SandboxField eine Klasse mit dem Attribut Serializable.

Der Zugriff auf das MonoBehaviour des Besitzers ist kein Problem:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

Wenn Sie möchten, können Sie es als durchführen, aber jetzt brauchen wir es nicht. Wir interessieren uns für das serialisierte Feld selbst, um es mit allen Blackjack wie in der folgenden Abbildung zu zeichnen.


Wir können den Serialisierungspfad wie folgt verfolgen:

string serializationPath = property.propertyPath;

In unserem Fall besteht der Serialisierungspfad aus einem unserer Felder und gibt "sandboxField" zurück, von dem wir weder kalt noch heiß sind, da wir für die erste Verschachtelungsebene nur den Namen der Variablen kennen müssen (die übrigens an uns zurückgegeben wurde).

Bitte beachten Sie, dass kein Elternteil MonoBehaviour unterwegs ist. Jetzt spielt es keine Rolle, aber es wird wichtig, wenn wir anfangen, eine russische Puppe zu zerlegen, die ungefähr so ​​aussieht:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
Um später nicht erwischt zu werden, wenn die Eigenschaften verschachtelt sind, werden wir im Voraus Folgendes tun:

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

Jetzt haben wir alle Knoten des Serialisierungspfads. Im primitivsten Fall benötigen wir jedoch nur den Nullknoten. Nimm es:

string pathNode = path[0];

Schalten Sie ein kleines Spiegelbild ein und holen Sie sich das Feld von hier:

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

Wir lassen die Frage nach der Geschwindigkeit dieses Unternehmens hinter den Kulissen. Für eine kleine Anzahl solcher Felder und eine kleine Verschachtelung sind die Reflexionskosten erheblich geringer als für alles, was während des Renderns unter der Haube von UnityEditor geschieht. Wenn Sie Beweise wollen - auf einem Github haben Unity-Entwickler eine so interessante Sache, UnityCsReference. Sehen Sie sich Ihre Freizeit an, wie beispielsweise das ObjectField-Rendering implementiert wird.

Eigentlich ist das nicht (ha ha) alles. Wir haben das Feld, wir sind glücklich, wir können damit machen, was wir wollen, und sogar versuchen, unser eigenes UnityEvent mit allen Schaltflächen und wichtigen Aktionen zu schreiben, die nur unser Feld betreffen, egal an welchem ​​Objekt es hängt.

Zumindest, während es an der Wurzel dieses Objekts hängt, wird alles in Ordnung sein, aber dann ist es nicht mehr so ​​viel. Auf dem Weg der Serialisierung warten wir auf Arrays und alle Arten von Listen, deren Hauptwunsch es ist, uns in Pantoffeln zu stecken und die Anzahl der Elemente rechtzeitig nach oben zu ändern. Aber zum Teufel damit würden wir zuerst unter dem Array selbst graben.

Unser Ziel ist der Widerstand gegen solche Puppen

Wenn wir keine Arrays im Serialisierungspfad hätten, wäre die Aufgabe trivial: Wir würden die Serialisierungsknoten durchlaufen, bis wir das Ende der Kette erreichen. So etwas wie der folgende Code:

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;

Hier warten wir auf zwei unangenehme Neuigkeiten gleichzeitig. Ich werde mit dem zweiten beginnen.

Der Schritt von nextObject kann plötzlich null anstelle des erwarteten Objekts zurückgeben. Normalerweise geschieht dies, wenn wir das übergeordnete Objekt zum ersten Mal im Inspektor erstellen und der Serialisierungspfad bereits vorhanden ist, das entsprechende Feld jedoch nicht (dies bringt uns noch mehr Annehmlichkeiten).

In diesem Fall wäre es schön, den Exit der Methode sofort mit der Rückgabe null hinzuzufügen:

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

"Warte eine Minute! - du sagst. "Und was tun in OnGUI, wenn Null an uns zurückgegeben wurde?"

Antwort: nichts. Nichts buchstäblich. Machen Sie einfach eine Rückkehr und überspringen Sie diesen Zeichenschritt, während Sie darauf warten, dass das Feld erstellt wird. Daraus wird nichts Schreckliches passieren.

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

Hier ist GetTarget () die entsprechende Funktion, die das serialisierte Objekt aus der Eigenschaft entnimmt.

Übrigens rate ich Ihnen, das Interessensgebiet nicht hier, sondern in GetPropertyHeight zu nutzen. Dies ist erforderlich, wenn wir zusammenklappbare, erweiterbare Felder mit unterschiedlichen Größen je nach Inhalt schreiben. GetPropertyHeight () wird vor OnGUI () aufgerufen. Wenn wir also das Feld dort nehmen und in das Feld unseres PropertyDrawer schreiben, müssen wir es nicht erneut in OnGUI übernehmen.

Beachten Sie, dass eine Instanz des benutzerdefinierten PropertyDrawer alleine erstellt wird, um alle derzeit sichtbaren serialisierten Eigenschaften zu zeichnen, und dass neue Eigenschaften der Reihe nach von oben nach unten darauf geworfen werden. Dies sollte berücksichtigt werden, um die Berechnung der Höhe der nächsten Eigenschaft nicht zu beeinträchtigen. Andernfalls kann es zu einer unangenehmen Situation kommen, wenn Sie auf Ausklappen klicken und das erwartete Feld erweitert wird.

Außerdem sollten Sie alle Lametta, die für die Anzeige des Felds im Editor verantwortlich sind und die Sie serialisieren möchten, auf der Seite der serialisierbaren Klasse und nicht von PropertyDrawer serialisieren und aus Gründen der Genauigkeit bedingte Kompilierungsklammern einschließen, damit all diese spanische Schande nicht versucht, zum Build zu gelangen ::

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

Eine weitere Gefahr, die uns hier erwartet: Alle vom Editor erstellten Felder wollten auf den Klassenkonstruktor und die in der Klasse angegebenen Standardwerte spucken. Wenn Sie dies beispielsweise so tun (Beispiel aus meinem Projekt, bei dem es sich um den Wert der Knoten der Wasseroberfläche handelte):

[SerializeField]private int m_nodesPerUnit = 5;

Dieser Wert wird ignoriert, sobald Sie der Liste ein neues Element hinzufügen. Das Aufrufen des Konstruktors um Hilfe ist nicht weniger nutzlos: Alles, was Sie dort geschrieben haben, wird ignoriert. Das neue Objekt ist ein Ziel wie ein Falke. Alle seine Werte sind wirklich die Standardwerte, aber nicht die, die Sie dort sehen möchten, sondern alle möglichen Arten von Null, Falsch, 0, Color.clear und anderen obszönen Dingen.

Krücke in Fleisch
. 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 .

: .



Kommen wir zurück zu unseren Schafen oder besser zu den Arrays. Ein weiteres Problem, das zu einem noch früheren Zeitpunkt auftreten kann, liegt in dieser Zeile:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

Die rechte Seite kann uns Null zurückgeben, wenn sie auf ein Array im Serialisierungspfad stößt (und jedes "IList" -Objekt das "Array" -Array ist). Unangenehm.

Was zu tun ist? Versuchen Sie, nicht in solche Situationen zu geraten, indem Sie einen Handler schreiben:

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;

Ja, wir können hier sogar in eine unangenehme Situation geraten, wenn der Serialisierungspfad beispielsweise bereits das Element data [0] oder data [1] enthält und das Array es noch nicht implementiert hat. Zum Beispiel haben wir eine leere Liste erstellt. Wir fragen ihn N Elemente - und ohne diese schöne Linie:

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

... wir bekommen eine Reihe von Ausnahmen beim Schnurren. Alles, was benötigt wurde, war, den Rendering-Schritt zu überspringen, nachdem gewartet wurde, bis die für uns interessanten Felder erstellt wurden.

Ich bin noch nicht auf andere Fälle gestoßen, in denen objectFieldInfo == null ist, aber gleichzeitig wird der Serialisierungsknoten nicht als Array bezeichnet. Wenn Sie also eine schreckliche Ausnahme in eine solche hypothetische Ausnahmesituation werfen, müssen Sie sie anschließend auflösen.

Im Allgemeinen haben wir eine mehr oder weniger funktionierende Funktion, mit der wir ein Feld anhand seiner serialisierten Eigenschaft extrahieren können. In Zukunft kann diese Funktion geändert werden, indem erzwungen wird, alle Objekte im Serialisierungspfad zu extrahieren und nach dem nächsten „übergeordneten Element“ zu suchen, einschließlich oder ohne Arrays entlang des Pfads.

Life Hack, um verschachtelte Eigenschaften zu zeichnen
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

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

Vielen Dank für Ihre Aufmerksamkeit.

All Articles