Serialisierung in Unity

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
Jun 24, 2014|11 Min.
Serialisierung in Unity
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Um mehr über die Technik hinter den Kulissen und die Gründe zu erfahren, warum manche Dinge so sind, wie sie sind, finden Sie in diesem Beitrag einen Überblick über das Serialisierungssystem von Unity. Wenn Sie dieses System sehr gut verstehen, kann das einen großen Einfluss auf die Effektivität Ihrer Entwicklung und die Leistung der Dinge haben, die Sie herstellen. Jetzt geht's los.

Die Serialisierung von "Dingen" ist der Kern von Unity. Viele unserer Funktionen bauen auf dem Serialisierungssystem auf:

  • Speichern von Daten, die in Ihren Skripten gespeichert sind. Dieses Thema ist den meisten Menschen wahrscheinlich einigermaßen bekannt.
  • Inspektor-Fenster. Das Inspektorfenster kommuniziert nicht mit der C# API, um die Werte der Eigenschaften des zu inspizierenden Objekts herauszufinden. Es fordert das Objekt auf, sich zu serialisieren, und zeigt dann die serialisierten Daten an.
  • Fertighäuser. Intern ist eine Vorabversion der serialisierte Datenstrom eines (oder mehrerer) Spielobjekte und Komponenten. Eine Prefab-Instanz ist eine Liste von Änderungen, die an den serialisierten Daten für diese Instanz vorgenommen werden sollen. Das Konzept der Vorabversion gibt es eigentlich nur im Editor. Die Änderungen an der Vorabversion werden in einen normalen Serialisierungsstrom integriert, wenn Unity einen Build erstellt. Wenn dieser instanziiert wird, haben die instanziierten GameObjects keine Ahnung, dass sie eine Vorabversion waren, als sie noch im Editor lebten.
  • Instanziierung. Wenn Sie Instantiate() für ein Prefab, ein GameObject in der Szene oder für irgendetwas anderes aufrufen (alles, was sich von UnityEngine.Object ableitet, kann serialisiert werden), serialisieren wir das Objekt, erstellen ein neues Objekt und "deserialisieren" dann die Daten in das neue Objekt. (Anschließend führen wir denselben Serialisierungscode noch einmal in einer anderen Variante aus, in der wir ihn verwenden, um zu melden, welche anderen UnityEngine.Objects referenziert werden. Wir überprüfen dann alle referenzierten UnityEngine.Objects, ob sie Teil der Daten sind, die instantiiert() werden. Wenn der Verweis auf etwas "Externes" (z.B. eine Textur) verweist, behalten wir diesen Verweis bei, wenn er auf etwas "Internes" (z.B. ein untergeordnetes Spielobjekt) verweist, patchen wir den Verweis auf die entsprechende Kopie).
  • Sparen. Wenn Sie eine .unity-Szenendatei mit einem Texteditor öffnen und unity auf "Text-Serialisierung erzwingen" eingestellt haben, führen wir den Serialisierer mit einem yaml-Backend aus.
  • Laden. Es mag nicht überraschen, aber auch das rückwärtskompatible Laden ist ein System, das auf der Serialisierung aufbaut. Das Laden von yaml im Editor verwendet das Serialisierungssystem, ebenso wie das Laden von Szenen und Assets zur Laufzeit. Assetbundles nutzen auch das Serialisierungssystem.
  • Schnelles Nachladen des Editor-Codes. Wenn Sie ein Editor-Skript ändern, serialisieren wir alle Editor-Fenster (sie leiten sich von UnityEngine.Object ab!), zerstören dann alle Fenster, entladen den alten C#-Code, laden den neuen C#-Code, erstellen die Fenster neu und deserialisieren schließlich die Datenströme der Fenster zurück auf die neuen Fenster.
  • Resource.GarbageCollectSharedAssets(). Dies ist unser nativer Garbage Collector und unterscheidet sich vom C# Garbage Collector. Das ist die Funktion, die wir ausführen, nachdem Sie eine Szene geladen haben, um herauszufinden, welche Dinge aus der vorherigen Szene nicht mehr referenziert sind, damit wir sie entladen können. Der native Garbage Collector führt den Serializer in einem Modus aus, in dem wir ihn dazu verwenden, dass Objekte alle Verweise auf externe UnityEngine.Objects melden. Dadurch werden Texturen, die von Szene1 verwendet wurden, entladen, wenn Sie Szene2 laden.

Das Serialisierungssystem ist in C++ geschrieben, wir verwenden es für alle unsere internen Objekttypen (Texturen, AnimationClip, Kamera, usw.). Die Serialisierung erfolgt auf der Ebene von UnityEngine.Object, jedes UnityEngine.Object wird immer als Ganzes serialisiert. Sie können Referenzen auf andere UnityEngine.Objects enthalten und diese Referenzen werden ordnungsgemäß serialisiert.

Vielleicht sagen Sie jetzt, dass Sie das alles nicht sonderlich beunruhigt, sondern einfach nur froh sind, dass es funktioniert, und dass Sie sich an die Erstellung von Inhalten machen wollen. Dies ist jedoch für Sie von Bedeutung, da wir denselben Serializer zur Serialisierung von MonoBehaviour-Komponenten verwenden, die von Ihren Skripten unterstützt werden. Aufgrund der sehr hohen Leistungsanforderungen, die der Serializer stellt, verhält er sich nicht in allen Fällen genau so, wie es ein C#-Entwickler von einem Serializer erwarten würde. Hier beschreiben wir, wie der Serializer funktioniert und wie Sie ihn am besten nutzen können.

Wie muss ein Feld in meinem Skript beschaffen sein, damit es serialisiert werden kann?

  • Öffentlich sein oder das Attribut [SerializeField] haben
  • Nicht statisch sein
  • Nicht konstant sein
  • Nicht schreibgeschützt sein
  • Der Feldtyp muss von einem Typ sein, den wir serialisieren können.

Welche Feldtypen können wir serialisieren?

  • Benutzerdefinierte nicht abstrakte Klassen mit dem Attribut [Serializable].
  • Benutzerdefinierte Strukturen mit dem Attribut [Serializable]. (neu in Unity4.5)
  • Verweise auf Objekte, die von UntiyEngine.Object abgeleitet sind
  • Primitive Datentypen (int,float,double,bool,string,etc)
  • Array eines Feldtyps, den wir serialisieren können
  • List<T> eines Feldtyps, den wir serialisieren können

So weit, so gut. Was sind also die Situationen, in denen sich der Serializer anders verhält als erwartet?

Benutzerdefinierte Klassen verhalten sich wie Structs

[Serializable]
Klasse Tier
{
public string name;
}

class MyScript : MonoBehaviour
{
public Animal[] animals;
}

Wenn Sie das Array animals mit drei Referenzen auf ein einzelnes Animal-Objekt füllen, finden Sie im Serialisierungsstrom 3 Objekte. Wenn es deserialisiert wird, gibt es jetzt drei verschiedene Objekte. Wenn Sie einen komplexen Objektgraphen mit Referenzen serialisieren müssen, können Sie sich nicht darauf verlassen, dass der Serializer von Unity dies automatisch für Sie erledigt, sondern müssen selbst etwas tun, um den Objektgraphen zu serialisieren. Im folgenden Beispiel sehen Sie, wie Sie Dinge serialisieren können, die Unity nicht von selbst serialisiert.

Beachten Sie, dass dies nur für benutzerdefinierte Klassen gilt, da diese "inline" serialisiert werden, da ihre Daten Teil der vollständigen Serialisierungsdaten für das MonoBehaviour werden, in dem sie verwendet werden. Wenn Sie Felder haben, die auf etwas verweisen, das eine von UnityEngine.Object abgeleitete Klasse ist, wie z.B. "public Camera myCamera", werden die Daten dieser Kamera nicht inline serialisiert, sondern ein tatsächlicher Verweis auf die Kamera UnityEngine.Object wird serialisiert.

Keine Unterstützung für null für benutzerdefinierte Klassen

Quizfrage. Wie viele Zuweisungen bei der Deserialisierung eines MonoBehaviours, der dieses Skript verwendet, vorgenommen werden:

Klasse Test : MonoBehaviour
{
öffentliches Ärgernis t;
}

[Serializable]
Klasse Trouble
{
public Trouble t1;
public Trouble t2;
public Trouble t3;
}

Es wäre nicht verwunderlich, 1 Zuweisung zu erwarten, nämlich die des Testobjekts. Es wäre auch nicht ungewöhnlich, 2 Zuweisungen zu erwarten, eine für das Test-Objekt und eine für ein Trouble-Objekt. Die richtige Antwort lautet 729. Der Serialisierer unterstützt null nicht. Wenn ein Objekt serialisiert wird und ein Feld null ist, instanziieren wir einfach ein neues Objekt dieses Typs und serialisieren es. Das könnte natürlich zu unendlichen Zyklen führen, also haben wir eine relativ magische Tiefenbegrenzung von 7 Ebenen. An diesem Punkt hören wir einfach auf, Felder zu serialisieren, die Typen von benutzerdefinierten Klassen/Strukturen, Listen und Arrays haben. [1]

Da so viele unserer Subsysteme auf dem Serialisierungssystem aufbauen, wird dieser unerwartet große Serialisierungsstrom für das MonoBehaviour Test dazu führen, dass alle diese Subsysteme langsamer als nötig arbeiten. Wenn wir Performance-Probleme in Kundenprojekten untersuchen, finden wir fast immer dieses Problem und wir haben in Unity 4.5 eine Warnung für diese Situation hinzugefügt. Wir haben die Implementierung der Warnungen so vermasselt, dass Sie so viele Warnungen erhalten, dass Sie keine andere Wahl haben, als sie sofort zu korrigieren. Die Warnung wird nicht verschwinden, aber Sie werden nur noch eine Warnung pro "Betreten des Spielmodus" erhalten, so dass Sie nicht mehr so viel Spamming bekommen. Sie würden Ihren Code trotzdem korrigieren wollen, aber Sie sollten es zu einem Zeitpunkt tun können, der Ihnen passt.

Keine Unterstützung für Polymorphismus

Wenn Sie eine

public Tier[] Tiere

und Sie geben eine Instanz eines Hundes, einer Katze und einer Giraffe ein, haben Sie nach der Serialisierung drei Instanzen von Animal.

Eine Möglichkeit, mit dieser Einschränkung umzugehen, ist die Erkenntnis, dass sie nur für "benutzerdefinierte Klassen" gilt, die inline serialisiert werden. Referenzen auf andere UnityEngine.Objects werden als tatsächliche Referenzen serialisiert, und für diese funktioniert die Polymorphie tatsächlich. Sie würden eine von ScriptableObject abgeleitete Klasse oder eine andere von MonoBehaviour abgeleitete Klasse erstellen und diese referenzieren. Der Nachteil dieser Vorgehensweise ist, dass Sie das MonoBehaviour oder das skriptfähige Objekt irgendwo speichern müssen und es nicht einfach inline serialisieren können.

Der Grund für diese Einschränkungen ist, dass eine der wichtigsten Grundlagen des Serialisierungssystems darin besteht, dass das Layout des Datenstroms für ein Objekt im Voraus bekannt ist und von den Typen der Felder der Klasse abhängt und nicht davon, was in den Feldern gespeichert ist.

Ich möchte etwas serialisieren, das der Serializer von Unity nicht unterstützt. Was muss ich tun?

In vielen Fällen ist der beste Ansatz die Verwendung von Serialisierungs-Callbacks. Sie ermöglichen es Ihnen, benachrichtigt zu werden, bevor der Serialisierer Daten aus Ihren Feldern liest und nachdem er sie in sie geschrieben hat. Sie können dies verwenden, um zur Laufzeit eine andere Darstellung Ihrer schwer zu serialisierenden Daten zu erhalten, als wenn Sie tatsächlich serialisieren. Sie verwenden diese, um Ihre Daten in etwas umzuwandeln, das Unity versteht, kurz bevor Unity sie serialisieren will. Sie verwenden sie auch, um die serialisierte Form wieder in die Form umzuwandeln, in der Sie Ihre Daten zur Laufzeit haben möchten, direkt nachdem Unity die Daten in Ihre Felder geschrieben hat.

Nehmen wir an, Sie möchten eine Baumdatenstruktur haben. Wenn Sie die Datenstruktur direkt von Unity serialisieren lassen, würde die Einschränkung "keine Unterstützung für null" dazu führen, dass Ihr Datenstrom sehr groß wird, was in vielen Systemen zu Leistungseinbußen führt:

using UnityEngine;
using System.Collections.Generic;
mit System;

public class VerySlowBehaviourDoNotDoThis : MonoBehaviour
{
[Serializable]
public class Node
{
public string interestingValue = "Wert";

//Das Feld darunter lässt die Serialisierungsdaten riesig werden, denn
//er führt einen 'Klassenzyklus' ein.
public List<Node> children = new List<Node>();
}

//das wird serialisiert
public Node root = new Node();

void OnGUI()
{
Anzeige (root);
}

void Display(Node node)
{
GUILayout.Label ("Wert: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));

GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();

Foreach (var kind in node.kinder)
Anzeige (Kind);

if (GUILayout.Button ("Kind hinzufügen"))
node.children.Add (new Node ());

GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}

Stattdessen weisen Sie Unity an, den Baum nicht direkt zu serialisieren, und erstellen ein separates Feld, um den Baum in einem serialisierten Format zu speichern, das für Unitys Serializer geeignet ist:

using UnityEngine;
using System.Collections.Generic;
mit System;

public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
//Knotenklasse, die zur Laufzeit verwendet wird
public class Node
{
public string interestingValue = "Wert";
public List<Node> children = new List<Node>();
}

//Knotenklasse, die wir für die Serialisierung verwenden werden
[Serializable]
public struct SerializableNode
{
public string interestingValue;
public int childCount;
public int indexOfFirstChild;
}

//die Wurzel dessen, was wir zur Laufzeit verwenden. nicht serialisiert.
Node root = new Node();

//das Feld, das wir Unity zur Serialisierung übergeben.
public List<SerializableNode> serializedNodes;

public void OnBeforeSerialize()
{
//Unity ist dabei, den Inhalt des serialisierten FeldesNodes zu lesen.
//schreiben wir die richtigen Daten in dieses Feld "just in time".
serializedNodes.Clear();
AddNodeToSerializedNodes(root);
}

void AddNodeToSerializedNodes(Node n)
{
var serializedNode = new SerializableNode () {
interestingValue = n.interestingValue,
childCount = n.children.Count,
indexOfFirstChild = serializedNodes.Count+1
};

serializedNodes.Add (serializedNode);
Foreach (var kind in n.kinder)
AddNodeToSerializedNodes (Kind);
}

public void OnAfterDeserialize()
{
//Unity hat gerade neue Daten in das Feld serializedNodes geschrieben.
//lassen Sie uns unsere aktuellen Laufzeitdaten mit diesen neuen Werten füllen.

if (serializedNodes.Count > 0)
root = ReadNodeFromSerializedNodes (0);
else
root = new Node ();
}

Knoten ReadNodeFromSerializedNodes(int index)
{
var serializedNode = serializedNodes [index];
var children = new List<Node> ();
for(int i=0; i!= serializedNode.childCount; i++)
children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));

return new Node() {
interestingValue = serializedNode.interestingValue,
Kinder = Kinder
};
}

void OnGUI()
{
Anzeige (root);
}

void Display(Node node)
{
GUILayout.Label ("Wert: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));

GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();

Foreach (var kind in node.kinder)
Anzeige (Kind);

if (GUILayout.Button ("Kind hinzufügen"))
node.children.Add (new Node ());

GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}

Beachten Sie, dass der Serializer, einschließlich der vom Serializer stammenden Callbacks, in der Regel nicht auf dem Haupt-Thread läuft, so dass Sie in Bezug auf den Aufruf der Unity API sehr eingeschränkt sind. (Die Serialisierung im Rahmen des Ladens einer Szene erfolgt in einem Lade-Thread. Die Serialisierung erfolgt als Teil des Aufrufs von Instantiate() aus dem Skript im Hauptthread). Sie können jedoch die notwendigen Datentransformationen durchführen, um Ihre Daten von einem nicht Unity-serialisierungsfreundlichen Format in ein Unity-serialisierungsfreundliches Format zu bringen.

Sie haben es bis zum Ende geschafft!

Vielen Dank, dass Sie bis hierher gelesen haben. Ich hoffe, Sie können einige dieser Informationen in Ihren Projekten gut gebrauchen.

Bye, Lucas. (@lucasmeijer)

PS: Wir werden alle diese Informationen auch in die Dokumentation aufnehmen.

[1] Ich habe gelogen, die richtige Antwort lautet nicht 729. Der Grund dafür ist, dass Unity in den alten Zeiten, als es noch keine 7 Ebenen gab, in einer Endlosschleife lief und Ihnen der Speicher ausging, wenn Sie ein Skript wie das Trouble-Skript erstellten, das ich gerade geschrieben habe. Unsere erste Lösung für dieses Problem bestand vor 5 Jahren darin, Feldtypen, die vom gleichen Typ wie die Klasse selbst waren, einfach nicht zu serialisieren. Offensichtlich war dies nicht die robusteste Lösung, da es einfach ist, einen Zyklus mit der Klasse Trouble1->Trouble2->Trouble1->Trouble2 zu erstellen. Deshalb haben wir kurz darauf die Tiefenbegrenzung auf 7 Ebenen eingeführt, um auch diese Fälle zu erfassen. Für den Punkt, auf den ich hinaus will, spielt das keine Rolle. Wichtig ist nur, dass Sie erkennen, dass Sie in Schwierigkeiten sind, wenn es einen Zyklus gibt.