Hero background image
Fortgeschrittene Programmierung und Code-Architektur
Untersuchen Sie Ihre Code-Architektur, um das Rendering Ihrer Grafiken weiter zu optimieren. Dies ist der vierte Teil einer Reihe von Artikeln, die Tipps zur Optimierung Ihrer Unity-Projekte enthalten. Verwenden Sie sie als Leitfaden für die Ausführung mit höheren Bildraten und weniger Ressourcen. Wenn Sie diese bewährten Verfahren ausprobiert haben, sollten Sie sich die anderen Seiten dieser Reihe ansehen: Konfigurieren Ihres Unity-Projekts für mehr Leistung Leistungsoptimierung für High-End-Grafik Verwalten der GPU-Nutzung für PC- und Konsolenspiele Verbesserte Physikleistung für flüssiges Gameplay
Verstehen der Unity PlayerLoop

Die Unity PlayerLoop enthält Funktionen zur Interaktion mit dem Kern der Spiel-Engine. Diese Struktur umfasst eine Reihe von Systemen, die für die Initialisierung und die Aktualisierung pro Frame zuständig sind. Alle Ihre Skripte werden sich auf diese PlayerLoop stützen, um Gameplay zu erzeugen. Bei der Profilerstellung sehen Sie den Anwendercode Ihres Projekts unter der PlayerLoop - mit den Editor-Komponenten unter der EditorLoop.

Es ist wichtig, die Ausführungsreihenfolge von Unitys FrameLoop zu verstehen. Jedes Unity-Skript führt mehrere Ereignisfunktionen in einer vorgegebenen Reihenfolge aus. Lernen Sie den Unterschied zwischen "Awake", "Start", " Update" und anderen Funktionen, die den Lebenszyklus eines Skripts erstellen, um die Leistung zu verbessern.

Einige Beispiele hierfür sind die Verwendung von FixedUpdate anstelle von Update, wenn es sich um einen Rigidbody handelt, oder die Verwendung von Awake anstelle von Start, um Variablen oder den Spielstatus zu initialisieren, bevor das Spiel beginnt. Verwenden Sie diese, um den Code zu minimieren, der in jedem Frame ausgeführt wird. Awake wird nur einmal während der Lebensdauer der Skriptinstanz und immer vor den Startfunktionen aufgerufen. Das bedeutet, dass Sie Start für den Umgang mit Objekten verwenden sollten, von denen Sie wissen, dass sie mit anderen Objekten sprechen können, oder sie abfragen, sobald sie initialisiert wurden.

Die Ausführungsreihenfolge der Ereignisfunktionen entnehmen Sie bitte dem Flussdiagramm zum Skriptlebenszyklus.

Diagramm Benutzerdefinierter Update-Manager
Erstellen Sie einen benutzerdefinierten Update Manager

Wenn Ihr Projekt hohe Leistungsanforderungen stellt (z. B. ein Open-World-Spiel), sollten Sie einen benutzerdefinierten Update-Manager mit Update, LateUpdate oder FixedUpdate erstellen.

Ein gängiges Verwendungsmuster für Update oder LateUpdate besteht darin, eine Logik nur dann auszuführen, wenn eine Bedingung erfüllt ist. Dies kann zu einer Reihe von Callbacks pro Frame führen, die außer der Überprüfung dieser Bedingung keinen Code ausführen.

Wenn Unity eine Nachrichtenmethode wie Update oder LateUpdate aufruft, handelt es sich um einen Interop-Aufruf - also einen Aufruf von der C/C++-Seite zur verwalteten C#-Seite. Bei einer geringen Anzahl von Objekten ist dies kein Problem. Wenn Sie Tausende von Objekten haben, wird dieser Overhead erheblich.

Melden Sie aktive Objekte bei diesem Aktualisierungsmanager an, wenn sie Rückrufe benötigen, und melden Sie sie ab, wenn sie dies nicht tun. Dieses Muster kann viele der Interop-Aufrufe zu Ihren Monobehaviour-Objekten reduzieren.

Beispiele für die Umsetzung finden Sie in den Game-Engine-spezifischen Optimierungstechniken.

Minimierung von Code, der bei jedem Frame ausgeführt wird

Überlegen Sie, ob der Code bei jedem Frame ausgeführt werden muss. Sie können unnötige Logik aus Update, LateUpdate und FixedUpdate herausnehmen. Diese Unity-Ereignisfunktionen sind praktische Orte, um Code zu platzieren, der bei jedem Frame aktualisiert werden muss, aber Sie können jede Logik extrahieren, die nicht mit dieser Frequenz aktualisiert werden muss.

Führen Sie die Logik nur aus, wenn sich die Dinge ändern. Denken Sie daran, Techniken wie das Beobachtermuster in Form von Ereignissen zu nutzen, um eine bestimmte Funktionssignatur auszulösen.

Wenn Sie Update verwenden müssen, können Sie den Code alle n Bilder ausführen. Dies ist eine Möglichkeit, Time Slicing anzuwenden, eine gängige Technik zur Verteilung einer hohen Arbeitslast auf mehrere Frames.

In diesem Beispiel wird die ExampleExpensiveFunction einmal alle drei Frames ausgeführt.

Der Trick besteht darin, dies mit anderen Arbeiten, die auf den anderen Frames laufen, zu verbinden. In diesem Beispiel könnten Sie andere teure Funktionen "planen", wenn Time.frameCount % interval == 1 oder Time.frameCount % interval == 2.

Alternativ können Sie auch eine benutzerdefinierte Update-Manager-Klasse verwenden, um die abonnierten Objekte alle n Frames zu aktualisieren.

Zwischenspeichern der Ergebnisse von teuren Funktionen

In Unity-Versionen vor 2020.2 können GameObject.Find, GameObject.GetComponent und Camera.main sehr teuer sein, so dass es am besten ist, sie nicht in Update-Methoden aufzurufen.

Versuchen Sie außerdem, teure Methoden in OnEnable und OnDisable zu vermeiden, wenn sie häufig aufgerufen werden. Der häufige Aufruf dieser Methoden kann zu CPU-Spitzen beitragen.

Wo immer möglich, führen Sie teure Funktionen, wie z.B. MonoBehaviour.Awake und MonoBehaviour.Startwährend der Initialisierungsphase aus. Zwischenspeichern Sie die benötigten Referenzen und verwenden Sie sie später wieder. In unserem früheren Abschnitt über die Unity PlayerLoop wird die Ausführung der Skriptreihenfolge im Detail beschrieben.

Hier ist ein Beispiel, das die ineffiziente Verwendung eines wiederholten GetComponent-Aufrufs zeigt:

void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}

Rufen Sie GetComponent stattdessen nur einmal auf, da das Ergebnis der Funktion zwischengespeichert wird. Das zwischengespeicherte Ergebnis kann in Update ohne weitere Aufrufe von GetComponent wiederverwendet werden.

Lesen Sie mehr über die Reihenfolge der Ausführung von Ereignisfunktionen.

Vermeiden Sie leere Unity-Ereignisse und Debug-Log-Anweisungen

Log-Anweisungen (insbesondere in Update, LateUpdate oder FixedUpdate) können die Leistung beeinträchtigen, daher sollten Sie die Log-Anweisungen deaktivieren, bevor Sie einen Build erstellen. Um dies schnell zu tun, können Sie ein Bedingtes Attribut zusammen mit einer Vorverarbeitungsrichtlinie.

Sie könnten zum Beispiel eine benutzerdefinierte Klasse wie unten gezeigt erstellen wollen.

Erzeugen Sie Ihre Protokollnachricht mit Ihrer benutzerdefinierten Klasse. Wenn Sie den ENABLE_LOG-Präprozessor in den Player-Einstellungen > Skripting-Symbole definieren deaktivieren, verschwinden alle Ihre Logstatements auf einen Schlag.

Der Umgang mit Strings und Text ist eine häufige Quelle für Leistungsprobleme in Unity-Projekten. Aus diesem Grund kann das Entfernen von Log-Anweisungen und ihrer teuren String-Formatierung einen großen Leistungsgewinn bedeuten.

Ebenso benötigen leere MonoBehaviours Ressourcen, so dass Sie leere Update- oder LateUpdate-Methoden entfernen sollten. Verwenden Sie Präprozessoranweisungen, wenn Sie diese Methoden zum Testen verwenden:

#if UNITY_EDITOR
void Update()
{
}
#endif

Hier können Sie das Update in-Editor zum Testen verwenden, ohne dass unnötiger Overhead in Ihren Build einfließt.

Dieser Blog-Beitrag über 10.000 Update-Aufrufe erklärt, wie Unity das Monobehaviour.Update ausführt.

Stack-Trace-Protokollierung deaktivieren

Verwenden Sie die Stack Trace-Optionen in den Player-Einstellungen , um zu steuern, welche Art von Protokollmeldungen angezeigt werden. Wenn Ihre Anwendung Fehler- oder Warnmeldungen in Ihrem Release-Build protokolliert (z. B. um Absturzberichte in freier Wildbahn zu erstellen), deaktivieren Sie Stack Traces, um die Leistung zu verbessern.

Erfahren Sie mehr über die Stack Trace-Protokollierung.

Hash-Werte anstelle von String-Parametern verwenden

Unity verwendet keine Stringnamen, um Animator-, Material- oder Shader-Eigenschaften intern anzusprechen. Um die Geschwindigkeit zu erhöhen, werden alle Eigenschaftsnamen in Eigenschafts-IDsumgewandelt, und diese IDs werden zur Adressierung der Eigenschaften verwendet.

Wenn Sie eine Set- oder Get-Methode für einen Animator, ein Material oder einen Shader verwenden, sollten Sie die Integer-Methode anstelle der String-Methode verwenden. Die string-bewerteten Methoden führen ein String-Hashing durch und leiten dann die gehashte ID an die integer-bewerteten Methoden weiter.

Verwenden Sie Animator.StringToHash für Animator-Eigenschaftsnamen und Shader.PropertyToID für Material- und Shader-Eigenschaftsnamen.

Damit verbunden ist die Wahl der Datenstruktur, die sich auf die Leistung auswirkt, da Sie tausende Male pro Frame iterieren. Befolgen Sie den MSDN-Leitfaden zu Datenstrukturen in C# als allgemeinen Leitfaden für die Auswahl der richtigen Struktur.

Objektpool-Skriptschnittstelle
Poolen Sie Ihre Objekte

Instantiate und Destroy können Garbage Collection (GC) Spikes erzeugen. Dies ist in der Regel ein langsamer Prozess. Anstatt also regelmäßig GameObjects zu instanziieren und zu zerstören (z.B. Kugeln aus einer Pistole zu schießen), sollten Sie Pools von vorab zugewiesenen Objekten verwenden, die wiederverwendet und recycelt werden können.

Erstellen Sie die wiederverwendbaren Instanzen zu einem Zeitpunkt im Spiel, z. B. während eines Menübildschirms oder eines Ladebildschirms, wenn eine CPU-Spitze weniger auffällig ist. Verfolgen Sie diesen "Pool" von Objekten mit einer Sammlung. Während des Spiels aktivieren Sie bei Bedarf einfach die nächste verfügbare Instanz und deaktivieren Objekte, anstatt sie zu zerstören, bevor Sie sie in den Pool zurückgeben. Dies reduziert die Anzahl der verwalteten Zuweisungen in Ihrem Projekt und kann GC-Probleme verhindern.

Vermeiden Sie auch das Hinzufügen von Komponenten zur Laufzeit; der Aufruf von AddComponent ist mit einigen Kosten verbunden. Unity muss beim Hinzufügen von Komponenten zur Laufzeit auf Duplikate oder andere erforderliche Komponenten prüfen. Die Instanziierung eines Prefab mit den gewünschten Komponenten, die bereits eingerichtet sind, ist leistungsfähiger, daher sollten Sie dies in Kombination mit Ihrem Objektpool verwenden.

Zum Verschieben von Transforms verwenden Sie Transform.SetPositionAndRotation um sowohl die Position als auch die Drehung auf einmal zu aktualisieren. Dadurch wird der Aufwand vermieden, eine Transformation zweimal zu ändern.

Wenn Sie ein GameObject zur Laufzeit instanziieren müssen, parentieren und positionieren Sie es zur Optimierung neu, siehe unten.

Weitere Informationen zu Object.Instantiate finden Sie in der Scripting-API.

Hier erfahren Sie, wie Sie ein einfaches Object Pooling System in Unity erstellen können.

Pool für skriptfähige Objekte
Nutzen Sie die Leistungsfähigkeit von ScriptableObjects

Speichern Sie unveränderliche Werte oder Einstellungen in einem ScriptableObject statt in einem MonoBehaviour. Das ScriptableObject ist ein Asset, das sich innerhalb des Projekts befindet. Sie muss nur einmal eingerichtet werden und kann nicht direkt an ein GameObject angehängt werden.

Erstellen Sie Felder im ScriptableObject, um Ihre Werte oder Einstellungen zu speichern, und referenzieren Sie dann das ScriptableObject in Ihren MonoBehaviours. Die Verwendung von Feldern aus dem ScriptableObject kann die unnötige Duplizierung von Daten bei jeder Instanziierung eines Objekts mit diesem MonoBehaviour verhindern.

Sehen Sie sich das Tutorial Einführung in ScriptableObjects an und finden Sie die entsprechende Dokumentation hier.

einheitsschlüssel art 21 11
Holen Sie sich das kostenlose E-Book

Einer unserer umfassendsten Leitfäden aller Zeiten enthält über 80 umsetzbare Tipps zur Optimierung Ihrer Spiele für PC und Konsole. Diese ausführlichen Tipps wurden von unseren Experten von Success und Accelerate Solutions erstellt und helfen Ihnen, das Beste aus Unity herauszuholen und die Leistung Ihres Spiels zu steigern.

Haben Ihnen diese Inhalte gefallen?