Unity optimiert Ihr Spiel mit Profiler
Leistung ist ein Schlüsselaspekt jedes Spiels und es ist keine Überraschung, dass das Spiel, egal wie gut es ist, nicht so angenehm ist, wenn es auf dem Computer des Benutzers schlecht läuft.
Da nicht jeder über einen High-End-PC oder ein High-End-Gerät verfügt (wenn Sie auf Mobilgeräte abzielen), ist es wichtig, die Leistung während des gesamten Entwicklungsverlaufs im Auge zu behalten.
Es gibt mehrere Gründe, warum das Spiel langsam laufen könnte:
- Rendering (Zu viele High-Poly-Netze, komplexe Shader oder Bildeffekte)
- Audio (hauptsächlich verursacht durch falsche Audio-Importeinstellungen)
- Nicht optimierter Code (Skripte, die leistungsintensive Funktionen an den falschen Stellen enthalten)
In diesem Tutorial zeige ich, wie Sie Ihren Code mit Hilfe von Unity Profiler optimieren.
Profiler
Historisch gesehen war das Debuggen der Leistung in Unity eine mühsame Aufgabe, aber seitdem wurde eine neue Funktion namens Profiler hinzugefügt.
Profiler ist ein Tool in Unity, mit dem Sie Engpässe in Ihrem Spiel schnell lokalisieren können, indem Sie den Speicherverbrauch überwachen, was den Optimierungsprozess erheblich vereinfacht.
Schlechte Darbietung
Eine schlechte Leistung kann jederzeit auftreten: Nehmen wir an, Sie arbeiten an der gegnerischen Instanz und wenn Sie sie in der Szene platzieren, funktioniert sie ohne Probleme, aber wenn Sie mehr Feinde erzeugen, bemerken Sie möglicherweise fps (Bilder pro Sekunde). ) beginnen zu fallen.
Sehen Sie sich das folgende Beispiel an:
In der Szene habe ich einen Würfel mit einem daran angehängten Skript, das den Würfel von einer Seite zur anderen bewegt und den Objektnamen anzeigt:
SC_ShowName.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
}
Wenn wir uns die Statistiken ansehen, können wir sehen, dass das Spiel mit gut 800+ fps läuft, sodass es kaum Auswirkungen auf die Leistung hat.
Aber schauen wir mal, was passiert, wenn wir den Würfel 100 Mal duplizieren:
Die FPS sind um mehr als 700 Punkte gesunken!
HINWEIS: Alle Tests wurden mit deaktiviertem Vsync durchgeführt
Im Allgemeinen ist es eine gute Idee, mit der Optimierung zu beginnen, wenn das Spiel ruckelt, einfriert oder die FPS unter 120 fällt.
Wie verwende ich Profiler?
Um Profiler verwenden zu können, benötigen Sie:
- Starten Sie Ihr Spiel, indem Sie auf „Play“ drücken
- Öffnen Sie Profiler, indem Sie zu Fenster -> Analyse -> Profiler gehen (oder Strg + 7 drücken).
- Es erscheint ein neues Fenster, das etwa so aussieht:
- Es mag auf den ersten Blick einschüchternd wirken (besonders angesichts all dieser Diagramme usw.), aber es ist nicht der Teil, den wir uns ansehen werden.
- Klicken Sie auf die Registerkarte „Zeitleiste“ und ändern Sie sie in „Hierarchie“:
- Sie werden drei Abschnitte bemerken (EditorLoop, PlayerLoop und Profiler.CollectEditorStats):
- Erweitern Sie den PlayerLoop, um alle Teile anzuzeigen, in denen die Rechenleistung verbraucht wird (HINWEIS: Wenn die PlayerLoop-Werte nicht aktualisiert werden, klicken Sie oben im Profiler-Fenster auf die Schaltfläche "Clear").
Um die besten Ergebnisse zu erzielen, leiten Sie Ihren Spielcharakter zu der Situation (oder dem Ort), an der das Spiel am meisten zurückbleibt, und warten Sie einige Sekunden.
- Nachdem Sie eine Weile gewartet haben, stoppen Sie das Spiel und beobachten Sie die PlayerLoop-Liste
Sie müssen sich den Wert GC Alloc ansehen, der für Garbage Collection Allocation steht. Dies ist ein Speichertyp, der von der -Komponente zugewiesen wurde, aber nicht mehr benötigt wird und darauf wartet, von der Garbage Collection freigegeben zu werden. Im Idealfall sollte der Code keinen Müll erzeugen (oder möglichst nahe bei 0 liegen).
Zeit ms ist ebenfalls ein wichtiger Wert. Sie zeigt an, wie lange die Ausführung des Codes in Millisekunden gedauert hat. Idealerweise sollten Sie also versuchen, auch diesen Wert zu reduzieren (durch Zwischenspeichern von Werten, Vermeiden des Aufrufs leistungsintensiver Funktionen bei jedem Update usw.)..).
Um die problematischen Teile schneller zu finden, klicken Sie auf die Spalte „GC-Zuordnung“, um die Werte von höher nach niedriger zu sortieren.)
- Klicken Sie im CPU-Auslastungsdiagramm auf eine beliebige Stelle, um zu diesem Frame zu springen. Konkret müssen wir uns Spitzenwerte ansehen, bei denen die fps am niedrigsten waren:
Das hat der Profiler verraten:
GUI.Repaint weist 45,4 KB zu, was ziemlich viel ist. Beim Erweitern wurden weitere Informationen angezeigt:
- Es zeigt, dass die meisten Zuweisungen von den Methoden GUIUtility.BeginGUI() und OnGUI() im SC_ShowName-Skript stammen, da wir wissen, dass wir mit der Optimierung beginnen können.
GUIUtility.BeginGUI() stellt eine leere OnGUI()-Methode dar (Ja, selbst die leere OnGUI()-Methode reserviert ziemlich viel Speicher).
Verwenden Sie Google (oder eine andere Suchmaschine), um Namen zu finden, die Sie nicht kennen.
Hier ist der OnGUI()-Teil, der optimiert werden muss:
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
Optimierung
Beginnen wir mit der Optimierung.
Jedes SC_ShowName-Skript ruft seine eigene OnGUI()-Methode auf, was angesichts der 100 Instanzen nicht gut ist. Was kann man also dagegen tun? Die Antwort lautet: Ein einzelnes Skript mit der OnGUI()-Methode, das die GUI-Methode für jeden Cube aufruft.
- Zuerst habe ich die Standardeinstellung OnGUI() im SC_ShowName-Skript durch public void GUIMethod() ersetzt, die von einem anderen Skript aufgerufen wird:
public void GUIMethod()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
- Dann habe ich ein neues Skript erstellt und es SC_GUIMethod genannt:
SC_GUIMethod.cs
using UnityEngine;
public class SC_GUIMethod : MonoBehaviour
{
SC_ShowName[] instances; //All instances where GUI method will be called
void Start()
{
//Find all instances
instances = FindObjectsOfType<SC_ShowName>();
}
void OnGUI()
{
for(int i = 0; i < instances.Length; i++)
{
instances[i].GUIMethod();
}
}
}
SC_GUIMethod wird an ein zufälliges Objekt in der Szene angehängt und ruft alle GUI-Methoden auf.
- Wir sind von 100 einzelnen OnGUI()-Methoden auf nur eine übergegangen. Drücken wir „Play“ und sehen wir uns das Ergebnis an:
- GUIUtility.BeginGUI() weist jetzt nur noch 368 B statt 36,7 KB zu, eine große Reduzierung!
Allerdings reserviert die OnGUI()-Methode immer noch Speicher, aber da wir wissen, dass sie nur GUIMethod() aus dem SC_ShowName-Skript aufruft, gehen wir direkt zum Debuggen dieser Methode über.
Der Profiler zeigt jedoch nur globale Informationen an. Wie können wir sehen, was genau innerhalb der Methode passiert?
Zum Debuggen innerhalb der Methode verfügt Unity über eine praktische API namens Profiler.BeginSample
Mit Profiler.BeginSample können Sie einen bestimmten Abschnitt des Skripts erfassen und anzeigen, wie lange die Fertigstellung gedauert hat und wie viel Speicher zugewiesen wurde.
- Bevor wir die Profiler-Klasse im Code verwenden, müssen wir den UnityEngine.Profiling-Namespace am Anfang des Skripts importieren:
using UnityEngine.Profiling;
- Das Profiler-Beispiel wird erfasst, indem am Anfang der Erfassung Profiler.BeginSample("SOME_NAME"); und am Ende der Erfassung Profiler.EndSample(); hinzugefügt wird Das:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
Da ich nicht weiß, welcher Teil von GUIMethod() Speicherzuweisungen verursacht, habe ich jede Zeile in Profiler.BeginSample und Profiler.EndSample eingeschlossen. (Aber wenn Ihre Methode viele Zeilen hat, müssen Sie sie definitiv nicht einschließen Teilen Sie jede Zeile einfach in gleichmäßige Abschnitte auf und arbeiten Sie dann von dort aus.
Hier ist eine endgültige Methode mit implementierten Profiler-Beispielen:
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Profiler.EndSample();
}
- Jetzt drücke ich Play und schaue, was im Profiler angezeigt wird:
- Der Einfachheit halber habe ich im Profiler nach "sc_show_" gesucht, da alle Samples mit diesem Namen beginnen.
- Interessant... In sc_show_names Teil 3 wird viel Speicher zugewiesen, was diesem Teil des Codes entspricht:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Nach einigem Googeln stellte ich fest, dass das Erhalten des Objektnamens ziemlich viel Speicher belegt. Die Lösung besteht darin, den Namen eines Objekts einer String-Variablen in void Start() zuzuweisen, sodass es nur einmal aufgerufen wird.
Hier ist der optimierte Code:
SC_ShowName.cs
using UnityEngine;
using UnityEngine.Profiling;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
string objectName = "";
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
objectName = gameObject.name; //Store Object name to a variable
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
Profiler.EndSample();
}
}
- Mal sehen, was der Profiler anzeigt:
Alle Samples weisen 0B zu, sodass kein weiterer Speicher zugewiesen wird.