Hero background image
Fortgeschrittene Programmierung und Code-Architektur
Untersuchen Sie Ihre Codearchitektur, um das Rendering Ihrer Grafiken weiter zu optimieren. Dies ist der vierte Teil einer Reihe von Artikeln, die Optimierungstipps für Ihre Unity-Projekte erläutern. Verwenden Sie sie als Leitfaden, um mit weniger Ressourcen bei höheren Bildraten zu arbeiten. Sobald Sie diese bewährten Praktiken ausprobiert haben, sollten Sie sich auch die anderen Seiten der Reihe ansehen: Konfigurieren Sie Ihr Unity-Projekt für eine bessere Leistung Leistungsoptimierung für hochwertige Grafiken Verwaltung der GPU-Nutzung für PC- und Konsolenspiele Verbesserte Physikleistung für flüssiges Gameplay
Diese Seite wurde maschinell übersetzt. Um die Originalversion zu sehen, damit Sie die Genauigkeit anhand der Quelle prüfen können,
Verstehen Sie den Unity PlayerLoop

Der Unity PlayerLoop enthält Funktionen zur Interaktion mit dem Kern der Spiel-Engine. Diese Struktur umfasst eine Reihe von Systemen, die die Initialisierung und die Aktualisierungen pro Frame verwalten. Alle Ihre Skripte werden auf diesen PlayerLoop angewiesen sein, um das Gameplay zu erstellen. Beim Profiling sehen Sie den Benutzercode Ihres Projekts unter dem PlayerLoop – mit Editor Komponenten unter dem EditorLoop.

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

Einige Beispiele sind die Verwendung von FixedUpdate anstelle von Update, wenn man mit einem Rigidbody umgeht, 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. Die Awake-Funktion wird nur einmal während der Lebensdauer der Skriptinstanz aufgerufen und immer vor den Startfunktionen. Das bedeutet, dass Sie Start verwenden sollten, um mit Objekten umzugehen, von denen Sie wissen, dass sie mit anderen Objekten sprechen können, oder um sie abzufragen, da sie initialisiert wurden.

Siehe das Script-Lebenszyklus-Diagramm für die spezifische Reihenfolge der Ausführung der Ereignisfunktionen.

Diagramm Benutzerdefinierter Update-Manager
Einen benutzerdefinierten Update-Manager erstellen

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

Ein häufiges Nutzungsmuster für Update oder LateUpdate besteht darin, Logik nur auszuführen, wenn eine bestimmte Bedingung erfüllt ist. Dies kann zu einer Reihe von pro-Frame-Callbacks führen, die effektiv keinen Code ausführen, außer diese Bedingung zu überprüfen.

Immer wenn Unity eine Nachrichtenmethode wie Update oder LateUpdate aufruft, erfolgt ein Interop-Aufruf – das bedeutet, ein Aufruf von der C/C++-Seite zur verwalteten C#-Seite. Für eine kleine Anzahl von Objekten ist dies kein Problem. Wenn Sie Tausende von Objekten haben, wird dieser Overhead erheblich.

Abonnieren Sie aktive Objekte bei diesem Update-Manager, wenn sie Rückrufe benötigen, und kündigen Sie das Abonnement, wenn sie es nicht tun. Dieses Muster kann viele der Interop-Aufrufe zu Ihren Monobehaviour Objekten reduzieren.

Bitte beziehen Sie sich auf die spiel enginespezifischen Optimierungstechniken für Beispiele zur Implementierung.

Minimieren Sie den Code, der in jedem Frame ausgeführt wird.

Überlegen Sie, ob der Code in 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 in jedem Frame aktualisiert werden muss, aber Sie können jede Logik extrahieren, die nicht mit dieser Frequenz aktualisiert werden muss.

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

Wenn Sie Update verwenden müssen, sollten Sie den Code alle n Frames 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 führen wir die ExampleExpensiveFunction einmal alle drei Frames aus.

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

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

Speichern Sie die Ergebnisse teurer Funktionen im Cache.

In Unity-Versionen vor 2020.2 können GameObject.Find, GameObject.GetComponent und Camera.main teuer sein, daher ist es am besten, sie in Update-Methoden zu vermeiden.

Zusätzlich sollten teure Methoden in OnEnable und OnDisable vermieden werden, wenn sie häufig aufgerufen werden. Häufiges Aufrufen dieser Methoden kann zu CPU-Spitzen beitragen.

Wo immer möglich, führen Sie teure Funktionen aus, wie MonoBehaviour.Awake und MonoBehaviour.Start während der Initialisierungsphase. Speichern Sie die benötigten Referenzen und verwenden Sie sie später wieder. Schau dir unseren vorherigen Abschnitt über den Unity PlayerLoop für die Ausführungsreihenfolge des Skripts im Detail an.

Hier ist ein Beispiel, das die ineffiziente Nutzung eines wiederholten GetComponent Aufrufs demonstriert:

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

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

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

Vermeiden Sie leere Unity-Ereignisse und Debug-Protokollanweisungen

Protokollanweisungen (insbesondere in Update, LateUpdate oder FixedUpdate) können die Leistung beeinträchtigen, daher sollten Sie Ihre Protokollanweisungen vor dem Erstellen eines Builds deaktivieren. Um dies schnell zu tun, ziehen Sie in Betracht, ein Bedingtes Attribut zusammen mit einer Vorverarbeitungsanweisung zu erstellen.

Zum Beispiel möchten Sie möglicherweise eine benutzerdefinierte Klasse erstellen, wie unten gezeigt.

Generieren Sie Ihre Protokollnachricht mit Ihrer benutzerdefinierten Klasse. Wenn Sie den ENABLE_LOG Präprozessor in den Player Settings > Scripting Define Symbols deaktivieren, verschwinden alle Ihre Log-Anweisungen auf einen Schlag.

Die Handhabung von Zeichenfolgen und Text ist eine häufige Quelle für Leistungsprobleme in Unity-Projekten. Deshalb kann das Entfernen von Protokollanweisungen und deren teurer String-Formatierung potenziell einen großen Leistungsgewinn bringen.

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

#if Unity
void Update()
{
}
#endif

Hier können Sie das Update im Editor verwenden, um Tests durchzuführen, ohne dass unnötiger Overhead in Ihren Build rutscht.

Dieser Blogbeitrag zu 10.000 Update-Anrufen erklärt, wie Unity die Monobehaviour.Update ausführt.

Stack-Trace-Protokollierung deaktivieren

Verwenden Sie die Stack Trace Optionen in den Player Settings, um zu steuern, welche Art von Protokollnachrichten angezeigt werden. Wenn Ihre Anwendung Fehler oder Warnmeldungen in Ihrer Release-Version protokolliert (z. B. um Absturzberichte in der freien Wildbahn zu erstellen), deaktivieren Sie Stack-Traces, um die Leistung zu verbessern.

Erfahren Sie mehr über Stack Trace-Protokollierung.

Verwenden Sie Hash-Werte anstelle von String-Parametern

Unity verwendet keine Zeichenfolgennamen, um Animator, Material oder Shader Eigenschaften intern anzusprechen. Um die Geschwindigkeit zu erhöhen, werden alle Eigenschaftsnamen in Property IDs gehasht, und diese IDs werden verwendet, um auf die Eigenschaften zuzugreifen.

Wenn Sie eine Set oder Get Methode auf einem Animator, Material oder Shader verwenden, nutzen Sie stattdessen die ganzzahlige Methode anstelle der stringbasierten Methoden. Die stringwertigen Methoden führen eine String-Hashing durch und leiten dann die gehashte ID an die ganzzahlwertigen Methoden weiter.

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

Verwandt ist die Wahl der Datenstruktur, die die Leistung beeinflusst, während Sie tausende Male pro Frame iterieren. Befolgen Sie den MSDN-Leitfaden zu Datenstrukturen in C# als allgemeine Anleitung zur Auswahl der richtigen Struktur.

Objektpool-Skriptschnittstelle
Poolen Sie Ihre Objekte

Instanziieren und Zerstören können Garbage Collection (GC) Spitzen erzeugen. Dies ist im Allgemeinen ein langsamer Prozess. Anstatt regelmäßig GameObjects (z. B. Kugeln aus einer Waffe zu schießen) zu instanziieren und zu zerstören, verwenden Sie Pools von vorab zugewiesenen Objekten, die wiederverwendet und recycelt werden können.

Erstellen Sie die wiederverwendbaren Instanzen an einem Punkt im Spiel, wie während eines Menüs oder eines Ladebildschirms, wenn ein CPU-Spike weniger auffällig ist. Verfolgen Sie dieses "Pool" von Objekten mit einer Sammlung. Während des Spiels aktivieren Sie einfach die nächste verfügbare Instanz, wenn nötig, und deaktivieren Sie Objekte, anstatt sie zu zerstören, bevor Sie sie zurück in den Pool geben. Dies reduziert die Anzahl der verwalteten Zuweisungen in Ihrem Projekt und kann GC-Probleme verhindern.

Vermeiden Sie ebenfalls, Komponenten zur Laufzeit hinzuzufügen; Das Hinzufügen von Komponenten aufrufen hat einige Kosten. Unity muss beim Hinzufügen von Komponenten zur Laufzeit auf Duplikate oder andere erforderliche Komponenten überprüfen. Ein Prefab mit den gewünschten Komponenten, die bereits eingerichtet sind, zu instanziieren, ist leistungsfähiger, also verwenden Sie dies in Kombination mit Ihrem Objektpool.

Im Zusammenhang damit, wenn Sie Transforms verschieben, verwenden Sie Transform.SetPositionAndRotation, um sowohl die Position als auch die Rotation gleichzeitig zu aktualisieren. Dies vermeidet den Aufwand, eine Transformation zweimal zu ändern.

Wenn Sie ein GameObject zur Laufzeit instanziieren, es für die Optimierung anordnen und neu positionieren müssen, siehe unten.

Für mehr Informationen zu Object.Instantiate, siehe die Scripting API.

Lernen Sie, wie Sie ein einfaches Object Pooling-System in Unity hier erstellen.

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

Speichern Sie unveränderliche Werte oder Einstellungen in einem ScriptableObject anstelle eines MonoBehaviour. Das ScriptableObject ist ein Asset, das im Projekt lebt. Es 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 verweisen Sie dann im MonoBehaviour auf das ScriptableObject. Die Verwendung von Feldern aus dem ScriptableObject kann die unnötige Duplizierung von Daten verhindern, jedes Mal, wenn Sie ein Objekt mit diesem MonoBehaviour instanziieren.

Schau dir dieses Einführung in ScriptableObjects Tutorial an und finde die relevante Dokumentation hier.

Unity key art 21 11
Holen Sie sich das kostenlose E-Book

Einer unserer umfassendsten Leitfäden aller Zeiten sammelt über 80 umsetzbare Tipps, wie Sie Ihre Spiele für PC und Konsole optimieren können. Erstellt von unseren Experten für Success und Accelerate Solutions, werden diese umfassenden Tipps Ihnen helfen, das Beste aus Unity herauszuholen und die Leistung Ihres Spiels zu steigern.

Haben Ihnen diese Inhalte gefallen?