Unity durchsuchen

Optimal arbeiten mit C#-Datenstrukturen und Unity-APIs

Last updated: December 2018

What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.

A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.

All of these tips are from the Unite talk, "Squeezing Unity: Tips for raising performance". For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Neue "Best Practices".

Unity APIs

Problem: Rekursive Iteration mit Partikelsystem-APIs:

When you call one of the Particle System’s main APIs, such as Start, Stop, IsAlive(), it iterates recursively, by default, through all the children in a particle system’s hierarchy:

  • Die API findet alle Children im Transform eines Partikelsystems, ruft auf jedem Transform GetComponent ab und ruft die passende interne Methode auf, wenn ein Partikelsystem vorhanden ist.
  • Wenn diese Child-Transforms über eigene Children verfügen, wird bis ans untere Ende der Transform-Hierarchie eine Rekursion durchgeführt – jedes Child, Grandchild und so weiter wird bearbeitet. Dies kann bei einer tiefen Transform-Hierarchie zum Problem werden.

Lösung:

Diese APIs haben einen withChildren-Parameter, der standardmäßig "true" ist. Setzen Sie ihn auf "false", um das rekursive Verhalten zu beenden. Wenn withChildren auf "false" gesetzt ist, wird nur das Partikelsystem geändert, das Sie direkt abrufen.

Problem: Mehrere Partikelsysteme, die gleichzeitig starten und stoppen

Es ist üblich, dass Grafiker visuelle Effekte erstellen, deren Partikelsysteme sich über mehrere Child Transforms erstrecken, die Sie gegebenenfalls alle gleichzeitig starten und stoppen möchten.

Lösung:

Erstellen Sie ein MonoBehaviour, das die Liste der Partikelsysteme beim Initialisierungszeitpunkt durch Abruf der Liste GetComponent zwischenspeichert. Rufen Sie dann, wenn die Partikelsysteme geändert werden müssen, für jedes nacheinander Start, Stop etc. auf und stellen Sie sicher, dass "false" als withChildren-Parameter eingestellt ist.

Problem: Regelmäßige Speicherzuweisung

Wenn eine C#-Closure eine lokale Variable einschließt, muss die C#-Runtime dem Heap eine Referenz zuweisen, um diese Variable nachzuverfolgen. In den Unity-Versionen 5.4 bis 2017.1 gibt es einige Partikelsystem-APIs, die bei Aufruf intern Closures verwenden. In diesen Unity-Versionen weisen alle Aufrufe zu Stop and Simulate Speicherplatz zu, auch wenn das Partikelsystem bereits gestoppt wurde.

Lösung:

Alle Partikelsystem-APIs sind, wie die meisten von Unitys öffentlichen APIs, lediglich C#-Wrapper-Funktionen um interne Funktionen herum, die die tatsächliche Arbeit der API erledigen. Einige dieser internen Funktionen sind reines C#, aber die meisten dringen bis in den C++-Kern der Unity-Engine vor. Im Fall der Partikelsystem-APIs sind alle internen APIs übersichtlich mit "Internal_" benannt, gefolgt von der Bezeichnung der öffentlichen Funktion, zum Beispiel:

work-optimally-with-Unity-APIs-table-graph

... und so weiter.

Statt Unitys öffentliche APIs ihre internen Helfer über Closures abrufen zu lassen, können Sie eine Erweiterungsmethode schreiben oder die interne Methode per Reflection adressieren und die Referenz zur späteren Verwendung zwischenspeichern. Dies sind die relevanten Funktionssignaturen:

work-optimally-with-Unity-APIs-functions-signatures

Alle Argumente haben dieselbe Bedeutung wie die öffentliche API, bis auf das erste, das eine Referenz auf das Partikelsystem ist, das Sie stoppen oder simulieren möchten.

Problem: APIs, die Arrays ausgeben

Jedes Mal, wenn Sie auf eine Unity-API zugreifen, die ein Array ausgibt, weist sie eine neue Kopie dieses Arrays zu. Dies tritt sowohl bei der Verwendung von Funktionen als auch bei der Verwendung von Eigenschaften auf, die jeweils Arrays ausgeben. Typische Beispiele dafür sind Mesh.vertices: beim Zugriff darauf erhalten Sie eine neue Kopie der Vertices des Meshs. Und Input.touches erzeugt eine Kopie aller Touches, die der Benutzer im aktuellen Frame durchführt.

Lösung:

Viele Unity-APIs enthalten jetzt nicht-zuweisende Versionen, die Sie stattdessen verwenden sollten. Verwenden Sie beispielsweise statt Input.touches Input.GetTouch und Input.touchCount. Ab Version 5.3 wurden nicht-zuweisende Versionen für alle Physics-Query-APIs eingeführt. Für 2D-Anwendungen gibt es ebenfalls nicht-zuweisende Versionen aller Physics2D-Query-APIs. Weitere Informationen über die nicht-zuweisenden Versionen von Unitys Physics-APIs finden Sie hier.

Und schließlich gibt es bei der Verwendung von GetComponents oder GetComponentsInChildren jetzt Versionen, die eine generische, exemplarische Liste akzeptieren. Diese APIs füllen die Liste mit den Ergebnissen des GetComponents-Abrufs. Dieser Prozess ist nicht ausschließlich nicht-zuweisend: Wenn die Ergebnisse des GetComponents-Abrufs die Kapazität der Liste überschreiten, wird die Größe der Liste neu angepasst. Wenn Sie aber die Liste erneut verwenden oder zusammenfassen, verringert sich zumindest die Häufigkeit der Zuweisungen im Laufe der Lebenszeit Ihrer Anwendung.

Optimale Verwendung von Datenstrukturen

Problem: Die Auswahl unzulässiger Datenstrukturen

Vermeiden Sie, Datenstrukturen nur deshalb auszuwählen, weil sie einfach zu nutzen sind; entscheiden Sie sich stattdessen für diejenige, deren Performance-Merkmale dem Algorithmus oder dem Spielsystem, den oder das Sie schreiben, am besten entsprechen.

Lösung:

Die Indexierung in ein Array oder eine Liste ist einfach: es sind nur einige Ergänzungen erforderlich und der Prozess tritt zeitlich konstant auf. Deshalb erfolgt die zufällige Indexierung in oder die Iteration durch ein Array oder eine Liste mit einem sehr geringem Overhead. Wenn Sie also jeden Frame über eine Aufgabenliste iterieren, sollten Sie Arrays oder Listen bevorzugen.

Wenn Sie zeitlich konstante Ergänzungen oder Entfernungen vornehmen möchten, ziehen Sie vermutlich die Verwendung eines Dictionarys oder Hashsets in Erwägung (mehr dazu unten).

Wenn Sie Daten per Key-Value-System zuordnen, wobei ein Wert einem anderen eindeutig zugeordnet wird, verwenden Sie ein Dictionary.

Mehr über Hashsets & Dictionarys: Beide Datenstrukturen werden von einer Hashtabelle unterstützt. Denken Sie daran, dass eine Hashtabelle zahlreiche Buckets enthalten kann; jeder Bucket ist im Prinzip eine Liste, die alle Werte enthält, die mit einem bestimmten Hashcode versehen sind. In C# wird dieser Hashcode mit der GetHashCode-Methode bestimmt. Da die Anzahl der Werte in einem Bucket normalerweise viel kleiner ist als die Gesamtgröße des Hashsets oder Dictionarys, ist das Hinzufügen oder Entfernen von Objekten aus einem Bucket näher an einem konstanten Zeitaufwand als das zufällige Hinzufügen oder Entfernen von Objekten aus einer Liste oder einem Array. Der genaue Unterschied hängt von der Kapazität Ihrer Hashtabelle und der Zahl der darin gespeicherten Objekte ab.

Die Suche nach einem vorhandenen (oder nicht vorhandenen) Wert in einem Hashset oder Dictionary ist aus dem gleichen Grund sehr einfach: Die Überprüfung der (relativ wenigen) Werte im Bucket, der den Hashcode repräsentiert, geht schnell.

Problem: Zu hoher Overhead durch falsche Verwendung von Dictionarys

Wenn für jeden Frame über Datenobjekt-Paare iteriert werden soll, wird oft ein Dictionary benutzt, da dies zweckmäßig erscheint. Das Problem dabei ist, dass über eine Hashtabelle iteriert wird. Das bedeutet, dass über jeden einzelnen Bucket in dieser Hashtabelle iteriert werden muss, unabhängig davon, ob er Werte enthält oder nicht. Dies führt zu einem beträchtlichen Overhead, insbesondere, wenn über Hashtabellen iteriert wird, die nur wenige Werte enthalten.

Lösung:

Erstellen Sie stattdessen eine Struktur oder ein Tupel und speichern Sie dann eine Liste oder ein Array dieser Strukturen oder Tupels, die/das Ihre Datenzuordnungen enthält. Iterieren Sie über diese Liste/dieses Array, statt über das Dictionary zu iterieren.

Ungefähr bei Minute 14:25 von Ians Vortrag gibt er einen Tipp, wie und wann man Dictionarys mit InstanceID-Key verwenden sollte, um den Overhead zu verringern.

Problem: Was tun bei mehreren Concerns?

In der Realität präsentieren die meisten Probleme natürlich zahlreiche Overlapping-Erfordernisse und es gibt nicht "die eine" Datenstruktur, die alle Ihre Bedürfnisse erfüllt.

Ein verbreitetes Beispiel dafür ist ein Update-Manager. Dies ist ein Architektur-Entwurfsmuster, wobei ein Objekt (üblicherweise ein MonoBehaviour) Update-Callbacks zu verschiedenen Systemen in Ihrem Spiel durchführt. Wenn Systeme Updates erhalten möchten, beziehen sie diese über das Update-Manager-Objekt. Für ein solches System sind eine Iteration mit geringem Overhead, zeitlich konstante Einfügung und zeitlich konstante Duplikatsprüfungen erforderlich.

Lösung: Verwenden Sie zwei Datenstrukturen

  • Führen Sie eine Liste oder ein Array für Iterationen.
  • Verwenden Sie ein Hashset (oder ein anderes indexierendes Set), bevor Sie die Liste ändern, um sicherzustellen, dass das Objekt, das Sie hinzufügen oder entfernen, tatsächlich vorhanden ist.
  • Wenn die Entfernung ein Concern ist, könnte die Implementierung einer verketteten oder intrusiv-verketteten Liste eine Möglichkeit sein (beachten Sie dabei, dass dies einen erhöhten Speicherbedarf und einen etwas höheren Interations-Overhead zur Folge hat).
Weitere Ressourcen

Wir wollen es wissen! Haben Ihnen diese Inhalte gefallen?

Ja, weiter so. Na ja. Könnte besser sein.
Alles klar

Wir verwenden Cookies, damit wir Ihnen die beste Nutzererfahrung auf unserer Website bieten können. Weitere Informationen erhalten Sie in unserer Cookie-Richtlinie.