Unity durchsuchen

Unitys neue "Best Practices"

Zuletzt aktualisiert: Dezember 2018

Inhalt dieses Vortrags: aktualisierte Tipps zur Scripting-Leistung und Optimierung von Ian Dundore, die die Weiterentwicklung der Unity-Architektur zur Unterstützung eines datenorientierten Designs widerspiegeln

Unity entwickelt sich weiter. Daher stellen die alten Tricks unter Umständen nicht mehr die beste Möglichkeit dar, die beste Leistung aus der Engine herauszuholen. In diesem Artikel erhalten Sie einen Überblick über einige Änderungen in Unity (von Unity 5.x bis Unity 2018.x) und erfahren, wie Sie sich diese zunutze machen können. Alle diese Verfahren stammen aus dem Vortrag von Ian Dundore. Sie können sich also auch direkt an der Quelle informieren, wenn Sie dies bevorzugen.

Scripting-Performance

Eine der schwierigsten Aufgaben bei der Optimierung ist die Entscheidung, wie der Code optimiert werden soll, wenn ein Problembereich festgestellt wurde. Es spielen viele verschiedene Faktoren eine Rolle: plattformspezifische Betriebssystemdetails, CPU und GPU, Threading, Speicherzugriff und etliche andere. Daher ist es schwierig, im Voraus zu wissen, welche Optimierung schlussendlich den größten Nutzen bringt.

In der Regel ist es am besten, Optimierungen im Rahmen eines kleinen Testprojekts auf die Probe zu stellen, da so bedeutend schnellere Iterationen möglich sind. Das Isolieren des Codes in einem Testprojekt stellt aber wiederum eine Herausforderung dar: Das einfache Isolieren eines Teils des Codes verändert die Umgebung, in der er ausgeführt wird. Die Thread-Timings können sich unterscheiden und der verwaltete Heap kann kleiner oder weniger fragmentiert sein. Daher müssen Sie beim Designen Ihrer Tests mit Bedacht vorgehen.

Betrachten Sie zunächst die Inputs für Ihren Code und wie der Code reagiert, wenn Sie diese Inputs ändern.

  • Wie reagiert er auf hochkohärente Daten, die seriell gespeichert sind?
  • Wie handhabt er Cache-inkohärente Daten?
  • Wie viel Code haben Sie aus dem Loop entfernt, in dem Ihr Code ausgeführt wird? Haben Sie die Verwendung des Prozessor-Befehlscache geändert?
  • Auf welcher Hardware läuft er? Wie gut implementiert diese Hardware die Sprungvorhersage? Wie gut führt sie Out-of-Order-Mikrobefehle aus? Bietet sie SIMD-Unterstützung?
  • Bei einem Multithread-System: Wie reagiert das System, wenn Sie ein Mehrkernsystem bzw. ein System mit weniger Kernen verwenden?
  • Welche Skalierungsparameter gelten für Ihren Code? Skaliert er linear, wenn die Eingabemenge wächst, oder größer als linear?

Im Grunde müssen Sie genau darüber nachdenken, was Sie mit Ihrem Test-Harnisch messen.

Betrachten Sie als Beispiel den folgenden Test einer einfachen Operation: den Vergleich zweier Strings.

Wenn die C#-APIs zwei Strings vergleichen, führen sie gebietsspezifische Konvertierungen durch, um sicherzustellen, dass verschiedene Zeichen unterschiedlichen Zeichen entsprechen können, wenn sie aus verschiedenen Kulturen stammen. Sie werden feststellen, dass dies ziemlich langsam vonstatten geht.

Die meisten String-APIs in C# sind kultursensitiv, aber eine ist es nicht: String.Equals.

Wenn Sie String.CS über GitHub öffnen und sich String.Equals anschauen, sehen Sie Folgendes: Eine sehr einfache Funktion, die einige Prüfungen durchführt, bevor sie die Kontrolle an eine Funktion namens EqualsHelper übergibt – eine private Funktion, die Sie ohne Reflexion nicht direkt abrufen können.

lp MonoString CS1

EqualsHelper ist eine einfache Methode. Sie werden in 4-Byte-Schritten durch die Strings geführt, wobei die Roh-Byte der Eingabestrings verglichen werden. Bei Feststellung einer Diskrepanz, wird der Vorgang angehalten und „false” ausgegeben.

Es gibt aber noch andere Methoden zur Überprüfung der Gleichheit von Strings. Die scheinbar harmloseste ist ein Overload von String.Equals, der zwei Parameter akzeptiert: der String, mit dem verglichen werden soll, und eine Aufzählung namens StringComparison.

Wir haben bereits festgestellt, dass der Einzelparameter-Overload von String.Equals nur wenig Arbeit verrichtet, bevor er die Kontrolle an EqualsHelper übergibt. Was macht nun also der zweiparametrige Overload?

Wenn Sie sich den Code des zweiparametrigen Overloads anschauen, sehen Sie, dass dieser einige zusätzliche Prüfungen ausführt, bevor ein umfangreiches Switch-Statement abgegeben wird. Dieses Statement testet den Wert der StringComparison-Aufzählung. Da wir Parität mit dem Einzelparameter-Overload anstreben, möchten wir einen ordinalen Vergleich – einen Byte-für-Byte-Vergleich. In diesem Fall verläuft die Kontrolle über vier Prüfungen, bevor sie beim StringComparison.Ordinal-Fall ankommt, wo der Code dem des Einzelparameter-String.Equals-Overload sehr ähnelt. Das bedeutet, dass, wenn Sie den zweiparametrigen Overload von String.Equals satt des Einzelparameter-Overloads verwenden, der Prozessor einige zusätzliche Vergleichsoperationen ausführt. Man könnte annehmen, dass das länger dauert, aber es ist einen Versuch wert.

Wenn Sie an allen Möglichkeiten interessiert sind, Strings hinsichtlich ihrer Gleichheit zu vergleichen, werden Sie nicht nur String.Equals testen wollen. Es gibt einen Overload von String.Compare, der Ordinal-Vergleiche durchführen kann, sowie eine Methode namens String.CompareOrdinal, die selbst über zwei verschiedene Overloads verfügt.

Als Referenz-Implementierung finden Sie hier ein einfaches handcodiertes Beispiel. Es ist nur eine kleine Funktion mit einer Längenprüfung, die über jedes Zeichen in den beiden Eingabe-Strings iteriert und sie überprüft.

Nach der Überprüfung des Codes gibt es vier verschiedene Testfälle, die sofort nützlich erscheinen:

  • Zwei identische Strings, um die Worst-Case-Performance zu testen.
  • Zwei Strings mit zufälligen Zeichen, aber gleicher Länge, um Längenprüfungen zu umgehen.
  • Zwei Strings mit zufälligen Zeichen und gleicher Länge mit identischem ersten Zeichen, um eine interessante Optimierung zu umgehen, die nur in String.CompareOrdinal vorkommt.
  • Zwei Strings mit zufälligen Zeichen und von unterschiedlicher Länge, um die Best-Case-Performance zu testen.

Nach einigen Tests ist String.Equals der klare Gewinner. Dies gilt unabhängig von der Plattform, der Script-Laufzeitversion und der Verwendung von Mono oder IL2CPP.

Es ist beachtenswert, dass String.Equals die Methode ist, die vom String-Gleichheitsoperator == benutzt wird. Also ändern Sie nicht vorschnell in Ihrem gesamten Code auf a == b to a.Equals(b)!

Wenn man die Ergebnisse vergleicht, erscheint es merkwürdig, wie viel schlechter die handcodierte Implementierung abschneidet. Bei der Überprüfung des IL2CPP-Codes können wir sehen, dass Unity verschiedene Array-Bounds-Prüfungen und Null-Prüfungen einfügt, wenn unser Code querübersetzt ist.

Diese Prüfungen können deaktiviert werden. Suchen Sie in Ihrem Unity-Installationsordner den IL2CPP-Unterordner. In diesem Unterordner finden Sie die Datei IL2CPPSetOptionAttributes.cs. Wenn Sie diese in Ihr Projekt ziehen, erhalten Sie Zugriff auf Il2CppSetOptionAttribute.

Mit diesem Attribut können Sie Typen und Methoden ausgestalten. Sie können es so konfigurieren, dass Null-Prüfungen, automatische Array-Bounds-Prüfungen oder beide aktiviert werden. Dies kann den Code beschleunigen – teilweise deutlich. In unserem Testfall lief die handcodierte String-Vergleichsmethode 20 % schneller ab!

lp Null-Check-Tipp

Transforms

Wenn man sich lediglich die Hierarchie im Unity Editor ansieht, ist nicht auf den ersten Blick erkennbar, wie sehr sich die Transform-Komponente auf dem Weg von Unity 5 zu Unity 2018 verändert hat. Diese Änderungen bieten auch interessante neue Möglichkeiten für Performance-Verbesserungen.

In Unity 4 und Unity 5.0 wurde ein Objekt bei der Erstellung eines Transforms quasi beliebig in Unitys nativem Speicher-Heap zugewiesen. Das Objekt konnte sich überall im nativen Speicher-Heap befinden – es gab keine Garantie dafür, dass zwei sequenziell zugewiesene Transforms auch nah beieinander zugewiesen wurden oder ein Child-Transform in der Nähe seines Parent.

Das hatte zur Folge, dass bei einer linearen Iteration durch eine Transform-Hierarchie nicht linear durch einen fortlaufenden Speicherbereich iteriert wurde. Der Prozessor kam regelmäßig zum Stillstand, da er auf Transform-Daten warten musste, die aus dem L2-Cache oder dem Hauptspeicher abgerufen wurden.

Im Unity-Backend sendete ein Transform bei jeder Änderung seiner Position, Rotation oder Skalierung eine OnTransformChanged-Nachricht. Diese Nachricht musste von allen untergeordneten Transforms empfangen werden, damit diese ihre eigenen Daten aktualisieren und alle anderen Komponenten, die von diesen Änderungen betroffen waren, benachrichtigen konnten. Ein Child-Transform mit verbundenem Collider muss in einem solchen Fall beispielsweise jedes Mal das Physik-System aktualisieren, wenn sich das Child-Transform oder das Parent-Transform ändert.

Diese unvermeidbare Nachricht führte zu zahlreichen Performance-Problemen, zumal es keine integrierte Möglichkeit gab, störende Nachrichten zu vermeiden. Wenn man ein Transform ändern wollte, wusste man, dass man dadurch auch seine untergeordneten Transforms ändert und hatte keine Möglichkeit, Unity daran zu hindern, nach jeder Änderung eine OnTransformChanged-Nachricht zu senden. Dieser Vorgang verschwendete sehr viel CPU-Zeit.

Wegen dieses Details besteht einer der häufigsten Ratschläge für ältere Unity-Versionen darin, Transforms-Änderungen als Batch durchzuführen. Das bedeutet, die Position und Rotation eines Transforms einmal, am Anfang eines Frames, zu erfassen und diese gespeicherten Werte im gesamten Verlauf des Frames zu verwenden und zu aktualisieren. Angewendet werden die Änderungen an Position und Rotation nur einmal, und zwar am Ende des Frames. Dies ist bis Unity 2017.2 ein guter Ratschlag.

Glücklicherweise ist OnTransformChanged seit Unity 2017.4 und 2018.1 Geschichte. Es wurde durch ein neues System, TransformChangeDispatch, ersetzt.

TransformChangeDispatch wurde erstmals in Unity 5.4 eingeführt. In dieser Version waren Transforms nicht mehr einzelne Objekte, die beliebig in Unitys nativem Speicher-Heap platziert werden konnten. Stattdessen wurde jetzt jedes Root-Transform in einer Szene von einem fortlaufenden Datenpuffer repräsentiert. Dieser Puffer, der als TransformHierarchy bezeichnet wird, enthält sämtliche Daten aller Transforms unter einem Root-Transform.

Zusätzlich speichert eine TransformHierarchy auch Metadaten über jedes in ihr enthaltene Transform. Diese Metadaten beinhalten eine Bitmaske, die angibt, ob ein Transform "dirty" ist – das heißt, ob sich seine Position, Rotation oder Skalierung seit dem letzten Mal, als das Transform als "clean" erkannt wurde, geändert hat. Weiterhin ist eine Bitmaske enthalten, die verfolgt, welche anderen Unity-Systeme von den Änderungen an einem bestimmten Transform betroffen sein könnten.

Mit diesen Daten kann Unity jetzt eine Liste von "dirty" Transforms für jedes andere interne System erstellen. Das Physik-System kann beispielsweise das TransformChangeDispatch abfragen, um eine Liste der Transforms zu erhalten, deren Daten sich seit dem letzten FixedUpdate des Physik-Systems geändert haben.

Allerdings sollte das TransformChangeDispatch-System nicht über alle Transforms in einer Szene iterieren, um diese Liste der geänderten Transforms zusammenzustellen. Dies wäre ein recht langwieriger Prozess, wenn eine Szene viele Transforms enthält – wobei sich in den meisten Fällen nur wenige dieser Transforms geändert haben werden.

Um dieses Problem zu umgehen, verfolgt das TransformChangeDispatch eine Liste von "dirty" TransformHierarchy-Strukturen. Immer, wenn ein Transform geändert wird, markiert es sich selbst und seine untergeordneten Transforms als "dirty", woraufhin die TransformHierarchy, in der es gespeichert ist, beim TransformChangeDispatch-System registriert wird. Wenn ein anderes System in Unity eine Liste der geänderten Transforms anfordert, iteriert das TransformChangeDispatch über jedes Transform, das in den jeweiligen "dirty" TransformHierarchy-Strukturen gespeichert ist. Transforms mit den entsprechenden geänderten Bits werden einer Liste hinzugefügt, die dann an das System ausgegeben wird, das die Anfrage gestellt hat.

Durch diese Architektur wird Unitys Fähigkeit, Änderungen auf granularer Ebene zu verfolgen, umso besser, je weiter die Hierarchie unterteilt ist. Je mehr Transforms an der Root einer Szene existieren, desto weniger Transforms müssen überprüft werden, wenn nach Änderungen gesucht wird.

Es gibt jedoch eine weitere Auswirkung. TransformChangeDispatch nutzt Unitys internes Multithreading-System, um die zu verrichtenden Arbeitsprozesse zur Überprüfung der TransformHierarchy-Strukturen aufzuteilen. Diese Aufteilung und die Zusammenführung der Ergebnisse führt jedes Mal, wenn ein System eine Änderungsliste von TransformChangeDispatch abfragen muss, zu einem leichten Overhead.

Die meisten von Unitys internen Systemen fordern einmal pro Frame ein Update an, und zwar, bevor sie ausgeführt werden. Das Animationssystem fordert beispielsweise Updates, bevor es alle aktiven Animatoren in einer Szene bewertet. Ebenso fordert das Rendering-System Updates für alle aktiven Renderer in einer Szene, bevor es mit dem Culling der Liste von sichtbaren Objekten beginnt.

Ein System verhält sich etwas anders: das Physik-System.

In Unity 2017.1 (und älteren Versionen) verliefen Physik-Updates synchron. Wenn ein Transform mit verbundenem Collider verschoben oder rotiert wurde, wurde die Physik-Szene sofort aktualisiert. So wurde sichergestellt, dass die geänderte Position oder Rotation des Colliders in der Physik-Welt dargestellt wurde und Raycasts und andere Physik-Abfragen präzise durchführbar waren.

Als wir in Unity 2017.2 das Physik-System migriert haben, um TransformChangeDispatch zu verwenden, war dies eine notwendige Änderung, die aber auch Probleme verursachen konnte. Jedes Mal, wenn ein Raycast durchgeführt wurde, musste TransformChangeDispatch nach einer Liste geänderter Transforms abgefragt werden, die dann auf die Physik-Welt angewendet werden mussten. Dies konnte je nach Größe der Transform-Hierarchien und der Art und Wiese, wie der Code Physik-APIs aufruft, recht zeitaufwändig sein.

Dieses Verhalten wird jetzt von einer neuen Einstellung reguliert, Physics.autoSyncTransforms. Von Unity 2017.2 bis Unity 2018.2 ist diese Einstellung standardmäßig "true" und Unity synchronisiert jedes Mal, wenn eine Abfrage wie Raycast oder Spherecast durchgeführt wird, die Physik-Welt automatisch mit Transform-Updates.

Diese Einstellung kann entweder in den Physics-Einstellungen im Unity Editor oder zur Laufzeit durch Festlegung der Physics.autoSyncTransforms-Eigenschaft festgelegt werden. Wenn Sie diese Eigenschaft auf "false" setzen und die automatische Physik-Synchronisation deaktivieren, fragt das Physik-System das TransformChangeDispatch-System nur zu einem bestimmten Zeitpunkt nach Änderungen ab: direkt, bevor FixedUpdate ausgeführt wird.

Wenn es beim Aufrufen von Physik-Abfrage-APIs zu Performance-Problemen kommen sollte, gibt es zwei weitere Möglichkeiten, damit umzugehen.

Zuerst können Sie Physics.autoSyncTransforms auf "false" einstellen. Dies eliminiert Spikes aufgrund von TransformChangeDispatch und Physik-Szenenupdates durch Physik-Abfragen.

Wenn Sie das tun, werden Änderungen an Collidern jedoch bis zum nächsten FixedUpdate nicht in die Physik-Szene synchronisiert. Das heißt, wenn Sie autoSyncTransforms deaktivieren, einen Collider verschieben und dann Raycast mit einem Ray aufrufen, der auf die neue Position des Colliders gerichtet ist, könnte Raycast den Collider nicht treffen, da der Raycast mit der zuletzt aktualisierten Version der Physik-Szene arbeitet und die Szene noch nicht mit der neuen Position des Colliders aktualisiert wurde.

Dies kann zu seltsamen Bugs führen, und Sie sollten Ihr Spiel sorgfältig testen, um sicherzustellen, dass die Deaktivierung der automatischen Transform-Synchronisierung keine Probleme verursacht. Wenn Sie ein Physik-Update mit Transform-Änderungen erzwingen müssen, rufen Sie Physics.SyncTransforms auf. Diese API ist langsam, es empfiehlt sich also, sie nicht mehrmals pro Frame aufzurufen!

Beachten Sie, dass ab Unity 2018.3 Physics.autoSyncTransforms standardmäßig auf "false" gesetzt ist.

Die zweite Methode, den Zeitaufwand für die Abfrage von TransformChangeDispatch zu optimieren, ist eine Neuanordnung der Reihenfolge, in der Sie die Physik-Szene abfragen und aktualisieren, um dem neuen System die Arbeit zu erleichtern.

Denken Sie daran: Wenn Physics.autoSyncTransforms auf "true" gesetzt ist, prüft jede Physik-Abfrage mit TransformChangeDispatch auf Änderungen. Wenn TransformChangeDispatch jedoch keine geänderten ("dirty") TransformHierarchy-Strukturen zur Prüfung vorliegen und das Physik-System keine aktualisierten Transforms hat, die es auf die Physik-Szene anwenden kann, entsteht bei der Physik-Abfrage beinahe kein Overhead.

Sie könnten also sämtliche Physik-Abfragen in einem Batch durchführen und dann alle Transform-Änderungen in einem Batch anwenden. Kombinieren Sie Änderungen an Transforms nicht mit dem Aufrufen von Physik-Abfrage-APIs.

Diese Beispiel verdeutlicht den Unterschied:

lp Beispieltransformationen

Der Performance-Unterschied zwischen diesen beiden Beispielen ist auffallend und wird noch deutlicher, wenn eine Szene nur kleine Transform-Hierarchien enthält.

lp Ergebnisse von Beispieltransformationen

Das Audio-System

Intern verwendet Unity ein System namens FMOD zur Wiedergabe von AudioClips. FMOD läuft auf eigenen Threads ab, die für das Dekodieren und Mischen von Audio verantwortlich sind. Audio-Playback ist jedoch nicht komplett unabhängig: Für jede aktive Audioquelle in einer Szene wird im Haupt-Thread Arbeit verrichtet. Und auf Plattformen mit weniger Kernen (z. B. ältere Mobiltelefone) kann es sein, dass die Audio-Threads von FMOD mit Unitys Haupt- und Rendering-Threads um Prozessorkerne konkurrieren.

In jedem Frame durchläuft Unity alle aktiven Audioquellen. Für jede Audioquelle berechnet Unity den Abstand zwischen der Audioquelle und dem aktiven Audio-Empfänger sowie einige weitere Parameter. Diese Daten werden zur Berechnung der Lautstärkeabschwächung, der Doppler-Verschiebung und anderer Effekte verwendet, die sich auf einzelne Audioquellen auswirken können.

Ein häufiges Problem ergibt sich aus dem Kontrollkästchen "Mute" in einer Audioquelle. Man könnte meinen, dass die Einstellung von "Mute" auf "true" alle Berechnungen im Zusammenhang mit der stummgeschalteten Audioquelle eliminiert – dem ist aber nicht so!

lp Audio-Kontrollkästchen

Stattdessen wird bei der Einstellung "Mute" lediglich der Parameter "Lautstärke" auf Null gesetzt, nachdem alle anderen Lautstärke-bezogenen Berechnungen durchgeführt wurden, einschließlich der Abstandsprüfung. Unity sendet auch die stummgeschaltete Audioquelle an FMOD, die diese dann ignoriert. Die Berechnung von Audioquelle-Parametern und die Übermittlung von Audioquellen an FMOD werden im Unity-Profiler als Audiosystem.Update angezeigt.

Wenn Sie feststellen, dass diesem Profiler-Marker viel Zeit zugewiesen ist, überprüfen Sie, ob viele stummgeschaltete aktive Audioquellen vorliegen. Ist dies der Fall, sollten Sie in Erwägung ziehen, die stummgeschalteten Audioquellen-Komponenten stattdessen zu deaktivieren oder auch das GameObject zu deaktivieren. Sie können auch AudioSource.Stop aufrufen, wodurch die Wiedergabe gestoppt wird.

Eine weitere Möglichkeit ist, die Stimmenzahl in Unitys Audio-Einstellungen zu beschränken. Rufen Sie dazu AudioSettings.GetConfiguration auf, wodurch Sie eine Struktur erhalten, die zwei interessante Werte enthält: die Anzahl der virtuellen Stimmen und die tatsächliche Anzahl der Stimmen.

Wenn Sie die Anzahl der virtuellen Stimmen reduzieren, reduziert sich die Anzahl der Audioquellen, die FMOD untersucht, um zu bestimmen, welche Audioquelle tatsächlich wiedergegeben wird. Reduzieren Sie die Anzahl der tatsächlichen Stimmen, wird die Anzahl der Audioquellen reduziert, die FMOD tatsächlich für die Audio-Produktion in Ihrem Spiel zusammenfügt.

Um die Anzahl der virtuellen oder tatsächlichen Stimmen zu reduzieren, die FMOD verwendet, sollten Sie die entsprechenden Werte in der AudioConfiguration-Struktur ändern, die von AudioSettings.GetConfiguration ausgegeben werden, und dann das Audio-System mit der neuen Konfiguration zurücksetzen, indem Sie die AudioConfiguration-Struktur als Parameter an AudioSettings.Reset übergeben. Beachten Sie, dass dies die Audiowiedergabe unterbricht – Sie sollten diese Änderung also vorzugsweise vornehmen, wenn der Player gerade nicht arbeitet, beispielsweise während eines Ladebildschirms oder des Start-ups.

Animationen

Es gibt zwei unterschiedliche Systeme, mit denen Animationen in Unity wiedergegeben werden können: das Animator-System und das Animationssystem.

"Animator-System" bezieht sich auf das System, das die Animator-Komponente beinhaltet, die mit GameObjects verbunden ist, um diese zu animieren, und das AnimatorController-Asset, auf das ein oder mehrere Animatoren verweisen. Dieses System wurde Mecanim genannt und ist sehr funktionsreich.

In einem Animator-Controller werden Zustände ("States") definiert. Diese Zustände können entweder ein Animationsclip oder ein Blend Tree sein. Zustände können in Schichten organisiert werden. Bei jedem Frame wird der aktive Status auf jedem Layer ausgewertet, und die Ergebnisse der einzelnen Layer werden miteinander kombiniert und auf das animierte Modell angewendet. Beim einem Übergang zwischen zwei Zuständen werden beide Zustände ausgewertet.

Das andere System ist das "Animationssystem". Es wird von der Animationskomponente repräsentiert und ist sehr einfach. Bei jedem Frame iteriert jede aktive Animationskomponente linear durch alle Kurven des jeweils verbundenen Animationsclips, wertet diese Kurven aus und wendet die Ergebnisse an.

Der Unterschied zwischen den beiden Systemen liegt nicht nur in den Funktionen, sondern auch in den zugrunde liegenden Implementierungsdetails.

Das Animator-System ist ein Multithread-System. Seine Performance kann für verschiedene CPUs mit einer unterschiedlichen Anzahl von Kernen sehr unterschiedlich sein. Im Allgemeinen skaliert es weniger als linear, wenn die Anzahl der Kurven in den Animationsclips zunimmt. Daher funktioniert es sehr gut, wenn komplexe Animationen mit einer großen Anzahl von Kurven ausgewertet werden. Dabei entsteht jedoch ein ziemlich hoher Overhead.

Das Animationssystem ist einfach und es entsteht fast kein Overhead. Seine Performance skaliert linear mit der Anzahl der Kurven in den wiedergegebenen Animationsclips.

Der Unterschied wird am deutlichsten, wenn man die beiden Systeme bei der Wiedergabe identischer Animationsclips vergleicht.

lp Test von Animationsclips

Versuchen Sie, bei der Wiedergabe von Animationsclips das System auszuwählen, das der Komplexität Ihrer Inhalte und der Hardware, auf der Ihr Spiel laufen soll, am besten entspricht.

Ein weiteres Problem ist die übermäßige Verwendung von Layern in Animator-Controllern. Wenn ein Animator ausgeführt wird, wertet er jeden Frame alle Layer in seinem Animator-Controller aus. Dies beinhaltet Layer, deren Layer-Weight auf Null eingestellt ist, was bedeutet, dass dieser Layer keinen sichtbaren Beitrag zu den letztendlichen Animationsergebnissen leistet.

Jeder zusätzliche Layer fügt jedem Animator-Cotroller für jeden Frame zusätzliche Berechnungen hinzu. Versuchen Sie also allgemein, Layer sparsam einzusetzen. Wenn Sie Demo- oder Cinematic-Layer in einem Animator-Controller debuggen müssen, versuchen Sie, diese zu refaktorisieren. Fügen Sie sie in bestehende Layer ein oder entfernen Sie sie, bevor Sie Ihr Spiel ausliefern.

Generic vs. Humanoid Rig

Standardmäßig importiert Unity animierte Modelle mit Generic Ric, aber oft wechseln Entwickler zum Humanoid Rig, wenn sie einen Charakter animieren. Dies ist allerdings nicht völlig problemlos.

Das Humanoid Rig fügt dem Animator-System zwei zusätzliche Funktionen hinzu: inversive Kinematik und Animations-Retargeting. Animations-Retargeting ist äußerst nützlich und ermöglicht Ihnen die Wiederverwendung von Animationen für verschiedene Avatare.

Aber auch, wenn Sie inversive Kinematik oder Anitmations-Retargeting nicht verwenden, berechnet der Animator eines Humanoid-geriggten Charakters jeden Frame diese beiden Funktionen. Dies verbraucht etwa 30 bis 50 % mehr CPU-Zeit als das Generic Rig, das diese Berechnungen nicht durchführt.

Wenn Sie die Funktionen des Humanoid Rigs nicht verwenden möchten, sollten Sie daher Generic Rig benutzen.

Animator-Pooling

Object-Pooling ist eine Schlüsselstrategie zur Vermeidung von Leistungsspitzen während des Gameplays. Es war jedoch immer schwierig, Animatoren zusammen mit Object-Pooling zu verwenden. Wann immer das GameObject eines Animators aktiviert ist, muss es Datenzwischenspeicher für die Auswertung des Animator-Controllers dieses Animators wiederherstellen. Dies wird als Animator-Rebind bezeichnet und im Unity-Profiler als Animator.Rebind angezeigt.

Vor Unity 2018 war die einzige Möglichkeit, dieses Problem zu umgehen, die Animatorkomponente statt des GameObjects zu deaktivieren. Dies hatte einige Nebeneffekte: Wenn ein Charakter MonoBehaviours, Mesh Colliders oder Mesh Renderer beinhaltete, wollte man diese ebenfalls deaktivieren, um die gesamte CPU-Zeit zu sparen, die auf diesen Charakter verwendet wurde. Doch das erhöht die Komplexität des Codes und führt zu Fehleranfälligkeit.

Mit Unity 2018.1 wurde die Animator.KeepControllerStateOnEnable-API eingeführt. Diese Eigenschaft ist standardmäßig auf "false" eingestellt, was bedeutet, dass der Animator sich verhält wie gewohnt – er gibt die Datenzwischenspeicher frei, wenn er deaktiviert ist, und weist sie erneut zu, wenn er wieder aktiviert wird.

Wenn Sie die Eigenschaft jedoch auf "true" setzen, behalten Animatoren ihre Zwischenspeicher bei, wenn sie deaktiviert werden. Das bedeutet, dass Animator.Rebind nicht stattfindet, wenn dieser Animator erneut aktiviert wird. Animatoren können endlich zusammengefasst werden!

Weitere Ressourcen

Wir wollen es wissen! Haben Ihnen diese Inhalte gefallen?

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