Wie wir Skripte in Unity optimiert haben

Es gibt viele großartige Performance-Artikel und Tutorials zu Unity. Wir versuchen nicht, sie durch diesen Artikel zu ersetzen oder zu verbessern. Dies ist nur eine kurze Zusammenfassung der Schritte, die wir nach dem Lesen dieser Artikel unternommen haben, sowie der Schritte, mit denen wir unsere Probleme lösen konnten. Ich empfehle Ihnen nachdrücklich, zumindest die Materialien unter https://learn.unity.com/ zu lesen .

Während der Entwicklung unseres Spiels stießen wir auf Probleme, die von Zeit zu Zeit zu einer Hemmung des Spielprozesses führten. Nachdem wir einige Zeit in Unity Profiler verbracht haben, haben wir zwei Arten von Problemen festgestellt:

  • Nicht optimierte Shader
  • Nicht optimierte Skripte in C #

Die meisten Probleme wurden von der zweiten Gruppe verursacht. Daher habe ich mich in diesem Artikel auf C # -Skripte konzentriert (wahrscheinlich auch, weil ich in meinem Leben keinen einzigen Shader geschrieben habe).

Suche nach Schwächen


In diesem Artikel wird kein Lernprogramm zur Verwendung eines Profilers verfasst. Ich wollte nur darüber sprechen, woran wir während des Profilierungsprozesses hauptsächlich interessiert waren.

Unity Profiler ist immer der beste Weg, um die Ursachen für Verzögerungen in Skripten zu finden. Ich empfehle dringend, das Spiel direkt im Gerät und nicht im Editor zu profilieren . Da unser Spiel für iOS erstellt wurde, musste ich das Gerät verbinden und die im Bild gezeigten Build-Einstellungen verwenden. Danach stellte der Profiler automatisch eine Verbindung her.


Einstellungen für die Profilerstellung erstellen

Wenn Sie versuchen, "Random Lag in Unity" oder eine andere ähnliche Anfrage zu googeln, werden Sie feststellen, dass die meisten Leute empfehlen, sich auf die Garbage Collection zu konzentrieren , weshalb ich es getan habe. Jedes Mal, wenn Sie aufhören, ein Objekt zu verwenden (Klasseninstanz), wird Müll erzeugt. Danach bereinigt der Unity-Garbage Collector von Zeit zu Zeit das Chaos und gibt Speicher frei, was unglaublich viel Zeit in Anspruch nimmt und zu einer Verringerung der Framerate führt.

Wie finde ich Junk-Skripte im Profiler?


Wählen Sie einfach CPU-Auslastung -> Hierarchieansicht auswählen -> Nach GC-Zuordnung sortieren


Profiler-Optionen für die Garbage Collection

Ihre Aufgabe ist es, in der GC-Zuweisungsspalte für die Gameplay-Szene einige Nullen zu erreichen.

Eine andere gute Möglichkeit besteht darin, die Einträge nach Zeit ms (Laufzeit) zu sortieren und die Skripte so zu optimieren, dass sie so wenig Zeit wie möglich in Anspruch nehmen. Dieser Schritt hatte große Auswirkungen auf uns, da eine unserer Komponenten eine große for-Schleife enthielt, die ewig dauerte (ja, wir haben noch keinen Weg gefunden, die Schleife zu beseitigen). Daher war es für uns unbedingt erforderlich, die Ausführungszeit aller Skripten zu optimieren. weil wir bei dieser teuren for-Schleife Laufzeit sparen und gleichzeitig eine stabile Frequenz von 60 fps beibehalten mussten.

Basierend auf den Profildaten habe ich die Optimierung in zwei Teile geteilt:

  • Müll entsorgen
  • Reduzierte Vorlaufzeit

Teil 1: Kampf gegen Müll


In diesem Teil erzähle ich Ihnen, was wir getan haben, um den Müll loszuwerden. Dies ist das grundlegendste Wissen, das jeder Entwickler verstehen sollte. Sie sind ein wichtiger Bestandteil unserer täglichen Analyse bei jeder Pull / Merge-Anfrage.

Erste Regel: Keine neuen Objekte in Update-Methoden


Idealerweise sollten die Update-, FixedUpdate- und LateUpdate-Methoden nicht die "neuen" Schlüsselwörter enthalten . Sie sollten immer das verwenden, was Sie bereits haben.

Manchmal ist das Erstellen eines neuen Objekts in einigen internen Unity-Methoden verborgen , sodass dies nicht so offensichtlich ist. Wir werden später darüber sprechen.

Zweite Regel: einmal erstellen und wiederverwenden!


Im Wesentlichen bedeutet dies, dass Sie für alles, was Sie in den Methoden Start und Awake können, Speicher zuweisen sollten. Diese Regel ist der ersten sehr ähnlich. Dies ist eigentlich nur eine andere Möglichkeit, die "neuen" Schlüsselwörter aus den Update-Methoden zu entfernen.

Code, dass:

  • erstellt neue Instanzen
  • Suche nach irgendwelchen Spielobjekten

Sie sollten immer versuchen, von den Update-Methoden zu Start oder Awake zu wechseln.

Hier sind Beispiele für unsere Änderungen:

Zuweisung von Speicher für Listen in der Start-Methode, deren Löschen (Clear) und ggf. Wiederverwendung.

//Bad code private List<GameObject> objectsList; void Update() { objectsList = new List<GameObject>(); objectsList.Add(......) } //Better Code private List<GameObject> objectsList; void Start() { objectsList = new List<GameObject>(); } void Update() { objectsList.Clear(); objectsList.Add(......) } 

Links speichern und wie folgt wiederverwenden:

 //Bad code void Update() { var levelObstacles = FindObjectsOfType<Obstacle>(); foreach(var obstacle in levelObstacles) { ....... } } //Better code private Object[] levelObstacles; void Start() { levelObstacles = FindObjectsOfType<Obstacle>(); } void Update() { foreach(var obstacle in levelObstacles) { ....... } } 

Gleiches gilt für die FindGameObjectsWithTag-Methode oder eine andere Methode, die ein neues Array zurückgibt.

Die dritte Regel: Achten Sie auf Zeichenfolgen und vermeiden Sie deren Verkettung


Wenn es darum geht, Müll zu schaffen, sind die Linien schrecklich. Selbst die einfachsten Zeichenfolgenoperationen können viel Müll erzeugen. Warum? Strings sind nur Arrays und diese Arrays sind unveränderlich. Dies bedeutet, dass jedes Mal, wenn Sie zwei Zeilen verketten, ein neues Array erstellt wird und das alte in Müll verwandelt wird. Glücklicherweise kann StringBuilder verwendet werden, um eine solche Speicherbereinigung zu vermeiden oder zu minimieren.

Hier ist ein Beispiel, wie Sie die Situation verbessern können:

 //Bad code void Start() { text = GetComponent<Text>(); } void Update() { text.text = "Player " + name + " has score " + score.toString(); } //Better code void Start() { text = GetComponent<Text>(); builder = new StringBuilder(50); } void Update() { //StringBuilder has overloaded Append method for all types builder.Length = 0; builder.Append("Player "); builder.Append(name); builder.Append(" has score "); builder.Append(score); text.text = builder.ToString(); } 

Mit dem oben gezeigten Beispiel ist alles in Ordnung, aber es gibt noch viele Möglichkeiten, den Code zu verbessern. Wie Sie sehen, kann fast die gesamte Zeichenfolge als statisch betrachtet werden. Wir teilen die Zeichenfolge für zwei UI.Text-Objekte in zwei Teile auf. Erstens enthält einer nur den statischen Text "Spieler" + Name + "hat Punktzahl" , der in der Startmethode zugewiesen werden kann, und der zweite enthält den Punktzahlwert, der in jedem Frame aktualisiert wird. Machen Sie statische Linien immer statisch und generieren Sie sie mit der Methode Start oder Awake . Nach dieser Verbesserung ist fast alles in Ordnung, aber beim Aufrufen von Int.ToString (), Float.ToString () usw. wird immer noch ein wenig Müll erzeugt.

Wir haben dieses Problem gelöst, indem wir Speicher für alle möglichen Zeilen generiert und vorbelegt haben. Es mag wie eine blöde Verschwendung von Speicher wirken, aber eine solche Lösung passt perfekt zu unseren Bedürfnissen und löst das Problem vollständig. Am Ende haben wir also ein statisches Array, auf das über Indizes direkt zugegriffen werden kann, um die gewünschte Zeichenfolge für eine Zahl zu erhalten:

 public static readonly string[] NUMBERS_THREE_DECIMAL = { "000", "001", "002", "003", "004", "005", "006",.......... 

Vierte Regel: Von Zugriffsmethoden zurückgegebene Cache-Werte


Dies kann sehr schwierig sein, da selbst eine einfache Zugriffsmethode wie die unten gezeigte Müll erzeugt:

 //Bad Code void Update() { gameObject.tag; //or gameObject.name; } 

Versuchen Sie, die Verwendung von Zugriffsmethoden in der Update-Methode zu vermeiden. Rufen Sie die Zugriffsmethode nur einmal in der Start-Methode auf und zwischenspeichern Sie den Rückgabewert.

Im Allgemeinen empfehle ich, KEINE String-Zugriffsmethoden oder Array-Zugriffsmethoden in der Update-Methode aufzurufen . In den meisten Fällen reicht es aus , den Link einmal in der Start-Methode abzurufen .

Hier sind zwei weitere Beispiele für einen anderen nicht optimierten Zugriffsmethodencode:

 //Bad Code void Update() { //Allocates new array containing all touches Input.touches[0]; } //Better Code void Update() { Input.GetTouch(0); } //Bad Code void Update() { //Returns new string(garbage) and compare the two strings gameObject.Tag == "MyTag"; } //Better Code void Update() { gameObject.CompareTag("MyTag"); } 

Fünfte Regel: Verwenden Sie Funktionen, die keinen Speicher zuweisen


Für einige Unity-Funktionen können Nicht-Speicher-Alternativen gefunden werden. In unserem Fall beziehen sich alle diese Funktionen auf die Physik. Unsere Kollisionserkennung basiert auf

 Physics2D. CircleCast(); 

In diesem speziellen Fall finden Sie eine Nicht-Speicherfunktion namens

 Physics2D. CircleCastNonAlloc(); 

Viele andere Funktionen haben ähnliche Alternativen. Überprüfen Sie daher immer die Dokumentation für NonAlloc-Funktionen .

Sechste Regel: Verwenden Sie nicht LINQ


Tu es einfach nicht. Ich meine, Sie müssen es in keinem Code verwenden, der häufig ausgeführt wird. Ich weiß, dass der Code bei Verwendung von LINQ leichter zu lesen ist, aber in vielen Fällen sind die Leistung und die Speicherzuweisung eines solchen Codes schrecklich. Natürlich kann es manchmal verwendet werden, aber um ehrlich zu sein, verwenden wir in unserem Spiel überhaupt kein LINQ.

Siebte Regel: Einmal erstellen und wiederverwenden, Teil 2


Dieses Mal geht es um das Zusammenfassen von Objekten. Ich werde nicht auf die Details des Poolings eingehen, da dies schon oft gesagt wurde. Studieren Sie beispielsweise dieses Tutorial: https://learn.unity.com/tutorial/object-pooling

In unserem Fall wird das folgende Objektpooling-Skript verwendet. Wir haben ein generiertes Level mit Hindernissen gefüllt, die für einen bestimmten Zeitraum existieren, bis der Spieler diesen Teil des Levels passiert. Beispiele für solche Hindernisse werden aus Fertigteilen erstellt, wenn bestimmte Bedingungen erfüllt sind. Der Code befindet sich in der Update-Methode. Dieser Code ist in Bezug auf Speicher und Laufzeit völlig ineffizient. Wir haben das Problem gelöst, indem wir einen Pool mit 40 Hindernissen erstellt haben: Falls erforderlich, holen wir Hindernisse aus dem Pool und bringen das Objekt zurück in den Pool, wenn es nicht mehr benötigt wird.

Die achte Regel: aufmerksamer mit Verpackungstransformation (Boxen)!


Boxen erzeugt Müll! Aber was ist Boxen? Am häufigsten tritt ein Boxing auf, wenn Sie einen Werttyp (int, float, bool usw.) an eine Funktion übergeben, die ein Objekt vom Typ Object erwartet.

Hier ist ein Beispiel für das Boxen, das wir in unserem Projekt beheben müssen:

Wir haben im Projekt ein eigenes Messaging-System implementiert. Jede Nachricht kann eine unbegrenzte Datenmenge enthalten. Die Daten werden in einem Wörterbuch gespeichert, das wie folgt definiert ist:

 Dictionary<string, object> data; 

Wir haben auch einen Setter, der Werte in diesem Wörterbuch setzt:

 public Action SetAttribute(string attribute, object value) { data[attribute] = value; } 

Boxen ist hier ziemlich offensichtlich. Sie können die Funktion folgendermaßen aufrufen:

 SetAttribute("my_int_value", 12); 

Dann wird der Wert "12" dem Boxen unterworfen und dies erzeugt Müll.

Wir haben das Problem gelöst, indem wir separate Datencontainer für jeden primitiven Typ erstellt haben. Der vorherige Objektcontainer wird nur für Referenztypen verwendet.

 Dictionary<string, object> data; Dictionary<string, bool> dataBool; Dictionary<string, int> dataInt; ....... 

Wir haben auch separate Setter für jeden Datentyp:

 SetBoolAttribute(string attribute, bool value) SetIntAttribute(string attribute, int value) 

Und all diese Setter sind so implementiert, dass sie dieselbe verallgemeinerte Funktion aufrufen:

 SetAttribute<T>(ref Dictionary<string, T> dict, string attribute, T value) 

Das Boxproblem wurde behoben!

Weitere Informationen hierzu finden Sie im Artikel https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/types/boxing-and-unboxing .

Die neunte Regel: Zyklen stehen immer unter Verdacht


Diese Regel ist der ersten und zweiten sehr ähnlich. Versuchen Sie einfach, den gesamten optionalen Code aus Leistungs- und Speichergründen aus den Schleifen zu entfernen.

Im Allgemeinen versuchen wir, Schleifen in Update-Methoden zu beseitigen. Wenn wir jedoch nicht auf sie verzichten können, vermeiden wir zumindest die Speicherzuweisung in solchen Schleifen. Befolgen Sie daher die Regeln 1 bis 8 und wenden Sie sie im Allgemeinen auf Schleifen an, nicht nur auf Aktualisierungsmethoden.

Regel 10: Kein Müll in externen Bibliotheken


Wenn sich herausstellt, dass ein Teil des Mülls durch aus dem Asset Store heruntergeladenen Code generiert wird, hat dieses Problem viele Lösungen. Bevor Sie jedoch Reverse Engineering und Debugging ausführen, kehren Sie zum Asset Store zurück und aktualisieren Sie die Bibliothek. In unserem Fall wurden alle verwendeten Assets weiterhin von Autoren unterstützt, die weiterhin leistungsverbessernde Updates veröffentlichten. Dies löste also alle unsere Probleme. Abhängigkeiten müssen relevant sein! Ich möchte lieber die Bibliothek loswerden, als nicht unterstützt zu werden.

Teil 2: Maximierung der Laufzeit


Einige der oben genannten Regeln machen einen subtilen Unterschied, wenn der Code selten aufgerufen wird. In unserem Code gibt es eine große Schleife, die in jedem Frame ausgeführt wird, sodass selbst diese kleinen Änderungen eine enorme Auswirkung hatten.

Einige dieser Änderungen können bei unsachgemäßer Verwendung oder in der falschen Situation zu einer noch schlechteren Laufzeit führen. Überprüfen Sie den Profiler immer, nachdem Sie jede Optimierung in den Code eingegeben haben, um sicherzustellen, dass Sie sich in die richtige Richtung bewegen .

Ehrlich gesagt führen einige dieser Regeln zu viel schlechter lesbarem Code und verstoßen manchmal sogar gegen Empfehlungen , z. B. das Einbetten von Code, das in einer der folgenden Regeln erwähnt wird.

Viele dieser Regeln überschneiden sich mit denen im ersten Teil des Artikels. In der Regel ist die Leistung von müllgenerierendem Code im Vergleich zu Code ohne müllgenerierenden Code geringer.

Die erste Regel: die richtige Ausführungsreihenfolge


Verschieben Sie den Code von den Methoden FixedUpdate, Update, LateUpdate in die Methoden Start und Awake . Ich weiß, das hört sich verrückt an, aber glauben Sie mir, wenn Sie sich mit Ihrem Code befassen, werden Sie Hunderte von Codezeilen finden, die in Methoden verschoben werden können, die nur einmal ausgeführt werden.

In unserem Fall ist dieser Code normalerweise mit verknüpft

  • Ruft GetComponent <> auf
  • Berechnungen, die in jedem Frame dasselbe Ergebnis liefern
  • Mehrere Instanzen desselben Objekts, normalerweise Listen
  • Suchen Sie nach GameObjects
  • Erhalten von Links zu Transform und Verwenden anderer Zugriffsmethoden

Hier ist eine Liste von Beispielcode, den wir von Update-Methoden zu Start-Methoden verschoben haben:

 //There must be a good reason to keep GetComponent in Update gameObject.GetComponent<LineRenderer>(); gameObject.GetComponent<CircleCollider2D>(); //Examples of calculations returning same result every frame Mathf.FloorToInt(Screen.width / 2); var width = 2f * mainCamera.orthographicSize * mainCamera.aspect; var castRadius = circleCollider.radius * transform.lossyScale.x; var halfSize = GetComponent<SpriteRenderer>().bounds.size.x / 2f; //Finding objects var levelObstacles = FindObjectsOfType<Obstacle>(); var levelCollectibles = FindGameObjectsWithTag("COLLECTIBLE"); //References objectTransform = gameObject.transform; mainCamera = Camera.main; 

Zweite Regel: Code nur bei Bedarf ausführen


In unserem Fall betraf dies hauptsächlich UI-Aktualisierungsskripte. Hier ist ein Beispiel, wie wir die Implementierung des Codes geändert haben, der den aktuellen Status der gesammelten Elemente auf der Ebene anzeigt.

 //Bad code Text text; GameState gameState; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); } void Update() { text.text = gameState.CollectedCollectibles.ToString(); } 

Da auf jeder Ebene nur wenige Elemente gesammelt werden müssen, ist es nicht sinnvoll, den UI-Text in jedem Frame zu ändern. Daher ändern wir den Text nur, wenn sich die Nummer ändert.

 //Better code Text text; GameState gameState; int collectiblesCount; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); collectiblesCount = gameState.CollectedCollectibles; } void Update() { if(collectiblesCount != gameState.CollectedCollectibles) { //This code is ran only about 5 times each level collectiblesCount = gameState.CollectedCollectibles; text.text = collectiblesCount.ToString(); } } 

Dieser Code ist viel besser, insbesondere wenn die Aktionen viel komplizierter sind als nur das Ändern der Benutzeroberfläche.

Wenn Sie nach einer umfassenderen Lösung suchen, empfehle ich die Implementierung der Observer-Vorlage mithilfe von C # -Ereignissen ( https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/ ).

Jedenfalls hat uns das noch nicht gereicht, und wir wollten eine vollständig verallgemeinerte Lösung implementieren. Deshalb haben wir eine Bibliothek erstellt, die Flux in Unity implementiert. Dies führte zu einer sehr einfachen Lösung, bei der der gesamte Status des Spiels im "Store" -Objekt gespeichert ist und alle UI-Elemente und andere Komponenten benachrichtigt werden, wenn sich der Status ändert, und ohne Code in der Update-Methode auf diese Änderung reagieren.

Die dritte Regel: Zyklen stehen immer unter Verdacht


Dies ist genau die Regel, die ich im ersten Teil des Artikels erwähnt habe. Wenn der Code eine Art Schleife hat, die eine große Anzahl von Elementen iterativ umgeht, verwenden Sie beide Regeln aus beiden Teilen des Artikels, um die Leistung der Schleife zu verbessern.

Vierte Regel: Besser als Foreach


Die Foreach-Schleife ist sehr einfach zu schreiben, aber "sehr schwierig" auszuführen. In der Foreach-Schleife wird Enumerator verwendet, um das Dataset iterativ zu verarbeiten und den Wert zurückzugeben. Dies ist komplizierter als das Durchlaufen von Indizes in einer einfachen For-Schleife.

Daher haben wir in unserem Projekt Foreach-Schleifen nach Möglichkeit durch For ersetzt:

 //Bad code foreach (GameObject obstacle in obstacles) //Better code var count = obstacles.Count; for (int i = 0; i < count; i++) { obstacles[i]; } 

In unserem Fall mit einer großen for-Schleife ist diese Änderung sehr bedeutend. Eine einfache for-Schleife beschleunigt den Code zweimal .

Fünfte Regel: Arrays sind besser als Listen


In unserem Code haben wir festgestellt, dass die meisten Listen eine konstante Länge haben, oder wir können die maximale Anzahl von Elementen berechnen. Aus diesem Grund haben wir sie basierend auf Arrays erneut implementiert. In einigen Fällen führte dies zu einer zweifachen Beschleunigung der Dateniterationen.

In einigen Fällen können Listen oder andere komplexe Datenstrukturen nicht vermieden werden. Es kommt vor, dass Sie häufig Elemente hinzufügen oder entfernen müssen. In diesem Fall ist es besser, Listen zu verwenden. Generell sollten Arrays jedoch immer für Listen mit fester Länge verwendet werden .

Sechste Regel: Float-Operationen sind besser als Vektoroperationen


Dieser Unterschied ist kaum spürbar, wenn Sie nicht wie in unserem Fall Tausende solcher Vorgänge ausführen, sodass sich für uns eine erhebliche Produktivitätssteigerung herausgestellt hat.

Wir haben ähnliche Änderungen vorgenommen:

 Vector3 pos1 = new Vector3(1,2,3); Vector3 pos2 = new Vector3(4,5,6); //Bad code var pos3 = pos1 + pos2; //Better code var pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, ......); Vector3 pos1 = new Vector3(1,2,3); //Bad code var pos2 = pos1 * 2f; //Better code var pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, ......); 

Siebte Regel: Objekte richtig suchen


Überlegen Sie immer, ob Sie die GameObject.Find () -Methode wirklich verwenden müssen. Diese Methode ist schwerfällig und dauert wahnsinnig lange. Sie sollten diese Methode niemals in Update-Methoden verwenden. Wir haben festgestellt, dass die meisten unserer Find-Aufrufe durch direkte Links im Editor ersetzt werden können , was natürlich viel besser ist.

 //Bad Code GameObject player; void Start() { player = GameObject.Find("PLAYER"); } //Better Code //Assign the reference to the player object in editor [SerializeField] GameObject player; void Start() { } 

Wenn dies nicht möglich ist, sollten Sie zumindest die Verwendung von Tags (Tag) in Betracht ziehen und mithilfe von GameObject.FindWithTag nach einem Objekt anhand seiner Bezeichnung suchen .

Im allgemeinen Fall also: Direkter Link> GameObject.FindWithTag ()> GameObject.Find ()

Achte Regel: Nur mit relevanten Objekten arbeiten


In unserem Fall war dies wichtig, um Kollisionen mit RayCast-s (CircleCast usw.) zu erkennen. Anstatt Kollisionen zu erkennen und zu entscheiden, welche davon im Code wichtig sind, haben wir die Spielobjekte auf die entsprechenden Ebenen verschoben, sodass wir Kollisionen nur für die erforderlichen Objekte berechnen können.

Hier ist ein Beispiel

 //Bad Code void DetectCollision() { var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance); for (int i = 0; i < count; i++) { var obj = results[i].collider.transform.gameObject; if(obj.CompareTag("FOO")) { ProcessCollision(results[i]); } } } //Better Code //We added all objects with tag FOO into the same layer void DetectCollision() { //8 is number of the desired layer var mask = 1 << 8; var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance, mask); for (int i = 0; i < count; i++) { ProcessCollision(results[i]); } } 

Die neunte Regel: Verwenden Sie Etiketten richtig


Es besteht kein Zweifel, dass Beschriftungen sehr nützlich sind und die Codeleistung verbessern können. Denken Sie jedoch daran, dass es nur eine richtige Methode zum Vergleichen von Objektbeschriftungen gibt !

 //Bad Code gameObject.Tag == "MyTag"; //Better Code gameObject.CompareTag("MyTag"); 

Die zehnte Regel: Vorsicht vor Tricks mit der Kamera!


Es ist so einfach, Camera.main zu verwenden, aber die Leistung dieser Aktion ist sehr schlecht. Der Grund dafür ist, dass die Unity-Engine hinter den Kulissen jedes Aufrufs von Camera.main tatsächlich das FindGameObjectsWithTag () -Ergebnis ausführt, sodass wir bereits verstehen, dass Sie es nicht oft aufrufen müssen. Es ist am besten, dieses Problem durch Zwischenspeichern des Links in der Start-Methode zu lösen oder wach.

 //Bad code void Update() { Camera.main.orthographicSize //Some operation with camera } //Better Code private Camera cam; void Start() { cam = Camera.main; } void Update() { cam.orthographicSize //Some operation with camera } 

Elfte Regel: LocalPosition ist besser als Position


Verwenden Sie nach Möglichkeit Transform.LocalPosition für Getter und Setter anstelle von Transform.Position . Innerhalb jedes Transform.Position-Aufrufs werden viel mehr Operationen ausgeführt, beispielsweise das Berechnen der globalen Position im Fall eines Getter-Aufrufs oder das Berechnen der lokalen Position aus der globalen Position im Fall eines Setter-Aufrufs. In unserem Projekt stellte sich heraus, dass Sie LocalPositions in 99% der Fälle mit Transform.Position verwenden können und keine weiteren Änderungen im Code vornehmen müssen.

Zwölfte Regel: Verwenden Sie LINQ nicht


Dies wurde bereits im ersten Teil besprochen. Benutz es einfach nicht, das ist alles.

Dreizehnte Regel: Hab (manchmal) keine Angst, die Regeln zu brechen


Manchmal kann sogar das Aufrufen einer einfachen Funktion zu kostspielig sein. In diesem Fall sollten Sie immer überlegen, Code einzubetten (Code Inlining). Was bedeutet das? Tatsächlich nehmen wir einfach den Code aus der Funktion und kopieren ihn direkt an die Stelle, an der wir die Funktion verwenden möchten, um den Aufruf zusätzlicher Methoden zu vermeiden.

In den meisten Fällen hat dies keine Auswirkung, da das Einbetten des Codes bereits in der Kompilierungsphase automatisch erfolgt. Der Compiler entscheidet jedoch nach bestimmten Regeln, ob der Code eingebettet werden soll (virtuelle Methoden werden beispielsweise nie eingebettet. Weitere Informationen finden Sie unter https: //docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html ). Öffnen Sie einfach den Profiler, starten Sie das Spiel auf dem Zielgerät und prüfen Sie, ob etwas verbessert werden kann.

In unserem Fall gab es mehrere Funktionen, die wir integriert haben, um die Leistung zu verbessern, insbesondere in der großen for-Schleife.

Fazit


Unter Anwendung der im Artikel aufgeführten Regeln haben wir im Spiel für iOS problemlos stabile 60 fps erreicht, auch auf dem iPhone 5S. Vielleicht sind einige der Regeln nur für unser Projekt spezifisch, aber ich denke, dass die meisten davon beachtet werden sollten, wenn Code geschrieben oder überprüft wird, um künftige Probleme zu vermeiden. Es ist immer besser, ständig auf der Leistung basierenden Code zu schreiben, als später große Codeteile neu zu faktorisieren.

Source: https://habr.com/ru/post/de481980/


All Articles