Hero background image
Objekt-Pooling zur Leistungssteigerung von C#-Skripten in Unity verwenden

Durch die Implementierung gängiger Entwurfsmuster für die Spieleprogrammierung in Ihrem Unity-Projekt können Sie effizient eine saubere, organisierte und lesbare Codebasis erstellen und pflegen. Design Patterns reduzieren nicht nur das Refactoring und den Zeitaufwand für das Testen, sie beschleunigen auch die Einführungs- und Entwicklungsprozesse und tragen so zu einer soliden Grundlage für das Wachstum Ihres Spiels, Entwicklungsteams und Unternehmens bei.

Betrachten Sie Entwurfsmuster nicht als fertige Lösungen, die Sie kopieren und in Ihren Code einfügen können, sondern als zusätzliche Werkzeuge, die Ihnen bei richtiger Anwendung helfen können, größere, skalierbare Anwendungen zu erstellen.

Auf dieser Seite wird das Pooling von Objekten erklärt und wie es dazu beitragen kann, die Leistung Ihres Spiels zu verbessern. Es enthält ein Beispiel dafür, wie Sie das in Unity eingebaute Objekt-Pooling-System in Ihren Projekten implementieren können.

Der Inhalt hier basiert auf dem kostenlosen E-Book, Level up your code with game programming patternsdas bekannte Entwurfsmuster erklärt und praktische Beispiele für deren Verwendung in Ihrem Unity-Projekt enthält.

Weitere Artikel aus der Reihe "Unity Game Programming Patterns" finden Sie im Unity Best Practices Hub, oder klicken Sie auf die folgenden Links:

4-2 Hierarchie
Objektpooling in Unity verstehen

Das Pooling von Objekten ist ein Entwurfsmuster, mit dem die Leistung optimiert werden kann, indem die von der CPU für die Ausführung wiederholter Aufrufe zum Erstellen und Zerstören von Objekten benötigte Verarbeitungsleistung reduziert wird. Stattdessen können mit dem Objekt-Pooling vorhandene GameObjects immer wieder verwendet werden.

Die Hauptfunktion des Objekt-Poolings besteht darin, Objekte im Voraus zu erstellen und in einem Pool zu speichern, anstatt sie bei Bedarf zu erstellen und zu zerstören. Wenn ein Objekt benötigt wird, wird es aus dem Pool entnommen und verwendet, und wenn es nicht mehr benötigt wird, wird es in den Pool zurückgegeben, anstatt zerstört zu werden.

Das obige Bild zeigt einen häufigen Anwendungsfall für Objektpooling, nämlich das Abfeuern von Geschossen aus einem Geschützturm. Lassen Sie uns dieses Beispiel Schritt für Schritt auspacken.

Anstatt ein Objekt zu erstellen und dann zu zerstören, verwendet das Objektpool-Muster eine Reihe von initialisierten Objekten, die in einem deaktivierten Pool bereitgehalten werden. Das Muster stellt dann alle Objekte bereit, die zu einem bestimmten Zeitpunkt vor dem Spiel benötigt werden. Der Pool sollte zu einem günstigen Zeitpunkt aktiviert werden, wenn der Spieler das Stottern nicht bemerkt, z. B. während eines Ladebildschirms.

Sobald die GameObjects aus dem Pool verwendet wurden, werden sie deaktiviert und stehen zur Verfügung, wenn das Spiel sie wieder benötigt. Wenn ein Objekt benötigt wird, muss Ihre Anwendung es nicht erst instanziieren. Stattdessen kann er sie aus dem Pool anfordern, aktivieren und deaktivieren und dann in den Pool zurückgeben, anstatt sie zu zerstören.

Dieses Muster kann die Kosten für die schwere Arbeit reduzieren, die von der Speicherverwaltung benötigt wird, um die Garbage Collection durchzuführen, wie im nächsten Abschnitt erläutert.

Unity Profiling Ebook
Speicherzuweisung

Bevor wir uns mit Beispielen für die Nutzung von Objekt-Pooling befassen, wollen wir kurz auf das Grundproblem eingehen, das damit gelöst werden kann.

Die Pooling-Technik ist nicht nur nützlich, um CPU-Zyklen zu reduzieren, die für Instanziierungs- und Zerstörungsoperationen aufgewendet werden. Es optimiert auch die Speicherverwaltung, indem es den Overhead der Objekterzeugung und -zerstörung reduziert, der die Zuweisung und Freigabe von Speicher sowie den Aufruf von Konstruktoren und Destruktoren erfordert.

Verwalteter Speicher in Unity

Die C#-Skripting-Umgebung von Unity bietet ein verwaltetes Speichersystem. Es hilft bei der Verwaltung der Freigabe von Speicher, so dass Sie dies nicht manuell über Ihren Code anfordern müssen. Das Speicherverwaltungssystem hilft auch bei der Überwachung des Speicherzugriffs, indem es sicherstellt, dass nicht mehr benötigter Speicher freigegeben wird und der Zugriff auf Speicher verhindert wird, der für Ihren Code nicht gültig ist.

Unity verwendet einen Garbage Collector, um Speicher von Objekten zurückzugewinnen, die von Ihrer Anwendung und Unity nicht mehr verwendet werden. Dies wirkt sich jedoch auch auf die Laufzeitleistung aus, da die Zuweisung von verwaltetem Speicher für die CPU zeitaufwändig ist und die Garbage Collection (GC) die CPU möglicherweise von anderen Arbeiten abhält, bis sie ihre Aufgabe abgeschlossen hat.

Jedes Mal, wenn Sie in Unity ein neues Objekt erstellen oder ein bestehendes Objekt zerstören, wird Speicher zugewiesen und freigegeben. Hier kommt das Objekt-Pooling ins Spiel: Dadurch wird das Stottern reduziert, das durch Spitzen bei der Garbage Collection entstehen kann. GC-Spikes gehen oft mit der Erstellung oder Zerstörung einer großen Anzahl von Objekten einher, die auf die Zuweisung von Speicher zurückzuführen sind. Neben vorzeitigen Garbage Collections kann der Prozess auch zu einer Speicherfragmentierung führen, die es schwieriger macht, freie zusammenhängende Speicherbereiche zu finden.

Durch die Wiederverwendung derselben vorhandenen Objekte durch Deaktivieren und Aktivieren können Sie einen Effekt erzeugen, z. B. das Abfeuern von Hunderten von Kugeln aus dem Bildschirm, während Sie sie in Wirklichkeit nur deaktivieren und wiederverwenden.

Erfahren Sie mehr über Speicherverwaltung in unserem Leitfaden für fortgeschrittene Profilerstellung.

Objektpool-Beispielprojekt
Using UnityEngine.Pool

Obwohl Sie Ihr eigenes System zur Implementierung von Objekt-Pooling erstellen können, gibt es eine eingebaute ObjectPool-Klasse in Unity, die Sie verwenden können, um dieses Muster effizient in Ihrem Projekt zu implementieren (verfügbar in Unity 2021 LTS und höher).

Schauen wir uns an, wie man das eingebaute Objekt-Pooling-System mit Hilfe der UnityEngine.Pool API mit diesem Beispielprojekt, das auf Github verfügbar ist, nutzen kann. Auf der Github-Seite finden Sie die Dateien unter Assets>7 Object Pool >Scripts > ExampleUsage2021.

Anmerkung: Sie können sich dieses Tutorial von Unity Learn ansehen, um ein Beispiel für Objekt-Pooling aus einer früheren Version von Unity zu sehen.

Dieses Beispiel besteht aus einem Geschützturm, der schnell Geschosse abfeuert (standardmäßig 10 Geschosse pro Sekunde), wenn die Maustaste gedrückt wird. Jedes Projektil fliegt über den Bildschirm und muss zerstört werden, wenn es den Bildschirm verlässt. Ohne Objekt-Pooling kann dies zu einer erheblichen Belastung der CPU und der Speicherverwaltung führen, wie im vorherigen Abschnitt erläutert.

Durch die Verwendung von Objekt-Pooling sieht es so aus, als würden Hunderte von Kugeln aus dem Bildschirm abgefeuert, während sie in Wirklichkeit nur deaktiviert und immer wieder verwendet werden.

Der Code im Beispielskript stellt sicher, dass die Größe des Pools groß genug ist, um die gleichzeitig aktiven Objekte anzuzeigen und so die Tatsache zu verschleiern, dass dieselben Objekte ständig wiederverwendet werden.

Wenn Sie das Partikelsystem von Unity verwendet haben, dann haben Sie aus erster Hand Erfahrung mit einem Objektpool. Die Komponente Partikelsystem enthält eine Einstellung für die maximale Anzahl von Partikeln. Dadurch werden verfügbare Partikel recycelt und verhindert, dass der Effekt eine maximale Anzahl überschreitet. Der Objektpool funktioniert ähnlich, allerdings mit einem beliebigen GameObject Ihrer Wahl.

RevisedGun.cs auspacken

Werfen wir einen Blick auf den Code in RevisedGun.cs der sich in der Github-Demo unter Assets>7 Object Pool >Scripts > ExampleUsage2021 befindet .

Das erste, was auffällt, ist die Einbeziehung des Pool-Namensraums:

using UnityEngine.Pool;

Durch die Verwendung der UnityEngine.Pool API erhalten Sie einen stapelbasierten ObjectPool Klasse, um Objekte mit dem Objektpool-Muster zu verfolgen. Je nach Bedarf können Sie auch eine CollectionPool-Klasse (List, HashSet, Dictionary, etc.) verwenden.

Dann wendet man spezifische Einstellungen für die Eigenschaften der Waffe an, einschließlich der zu erzeugenden Prefab (projectilePrefab vom Typ RevisedProjectile).

Die ObjectPool-Schnittstelle wird von RevisedProjectile.cs referenziert (was im nächsten Abschnitt erläutert wird) und in der Funktion Awake initialisiert.

private void Awake()

{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,

OnGetFromPool, OnReleaseToPool,

OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}

Wenn Sie den ObjectPool<T0>-Konstruktor untersuchen, werden Sie sehen, dass er die hilfreiche Fähigkeit enthält, eine Logik einzurichten, wenn:

Zuerst einen Sammelposten erstellen, um den Pool zu füllen

Entnahme eines Gegenstandes aus dem Pool

Rückgabe eines Gegenstandes in den Pool

Zerstörung eines gepoolten Objekts (z.B. wenn Sie eine Höchstgrenze erreichen)

Beachten Sie, dass die eingebaute ObjectPool-Klasse auch Optionen für eine Standard- und eine maximale Poolgröße enthält, wobei letztere die maximale Anzahl der im Pool gespeicherten Elemente angibt. Sie wird ausgelöst, wenn Sie Release aufrufen, und wenn der Pool voll ist, wird er stattdessen zerstört.

Schauen wir uns an, wie das Code-Beispiel mehrere Aktionen ausführt, die festlegen, wie Unity das Objekt-Pooling entsprechend Ihrem spezifischen Anwendungsfall effizient handhaben soll.

Zuerst wird die createFunc übergeben, die verwendet wird, um eine neue Instanz zu erstellen, wenn der Pool leer ist. In diesem Fall ist es die CreateProjectile(), die ein neues Profil Prefab instanziiert.

private RevisedProjectile CreateProjectile()

{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;

return projectileInstance;
}

OnGetFromPool wird aufgerufen, wenn Sie nach einer Instanz des GameObjects fragen, so dass Sie das GameObject, das Sie aus dem Pool erhalten, standardmäßig aktivieren.

private void OnGetFromPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(true);
}

OnReleaseToPool wird verwendet, wenn das GameObject nicht mehr benötigt wird und wieder in den Pool zurückgegeben wird - in diesem Beispiel geht es einfach darum, es wieder zu deaktivieren.

private void OnReleaseToPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(false);
}

OnDestroyPooledObject wird aufgerufen, wenn Sie die maximal zulässige Anzahl von gepoolten Objekten überschreiten. Wenn der Pool bereits voll ist, wird das Objekt zerstört.

private void OnDestroyPooledObject(RevisedProjectile pooledObject)

{
Destroy(pooledObject.gameObject);
}

Die collectionChecks werden verwendet, um den IObjectPool zu initialisieren und lösen eine Exception aus, wenn man versucht, ein GameObject freizugeben, das bereits an den Poolmanager zurückgegeben wurde, aber diese Prüfung wird nur im Editor durchgeführt. Wenn Sie diese Funktion deaktivieren, können Sie einige CPU-Zyklen einsparen, allerdings mit dem Risiko, ein Objekt zurückzubekommen, das bereits reaktiviert wurde.

Wie der Name schon sagt, ist die defaultCapacity die Standardgröße des Stacks/der Liste, die Ihre Elemente enthalten wird, und damit auch, wie viel Speicherplatz Sie im Voraus zuweisen wollen. Die maxPoolSize ist die maximale Größe des Stacks, und die erzeugten gepoolten GameObjects sollten diese Größe nie überschreiten. Das heißt, wenn Sie einen Gegenstand in einen vollen Pool zurückgeben, wird er stattdessen zerstört.

Dann erhalten Sie in FixedUpdate() ein gepooltes Objekt, anstatt jedes Mal ein neues Projektil zu instanzieren, wenn Sie die Logik zum Abfeuern eines Geschosses ausführen.

RevisedProjectile bulletObject = objectPool.Get();

So einfach ist das.

Entpacken von RevisedProjectile.cs

Werfen wir nun einen Blick auf das Skript RevisedProjectile.cs.

Neben der Einrichtung eines Verweises auf den ObjectPool, der die Freigabe des Objekts an den Pool erleichtert, gibt es einige interessante Details.

Die Zeitspanne timeoutDelay wird verwendet, um festzustellen, wann das Projektil "verbraucht" wurde und wieder in den Spielpool zurückgegeben werden kann - dies geschieht standardmäßig nach drei Sekunden.

Die Funktion Deactivate() aktiviert eine Coroutine namens DeactivateRoutine(float delay), die nicht nur das Projektil mit objectPool.Release(this) in den Pool zurückgibt, sondern auch die Geschwindigkeitsparameter des sich bewegenden Rigidbodys zurücksetzt.

Dieses Verfahren befasst sich mit dem Problem der "schmutzigen Gegenstände": Gegenstände, die in der Vergangenheit benutzt wurden und aufgrund ihres unerwünschten Zustands zurückgesetzt werden müssen.

Wie Sie in diesem Beispiel sehen können, macht die UnityEngine.Pool-API die Einrichtung von Objektpools effizient, da Sie das Muster nicht von Grund auf neu erstellen müssen, es sei denn, Sie haben einen speziellen Anwendungsfall dafür.

Sie sind nicht nur auf GameObjects beschränkt. Pooling ist eine Technik zur Leistungsoptimierung für die Wiederverwendung jeder Art von C#-Entität: ein GameObject, eine instanzierte Prefab, ein C#-Dictionary und so weiter. Unity bietet einige alternative Pooling-Klassen für andere Entitäten, wie z.B. DictionaryPool<T0,T1>, das Dictionaries unterstützt, und HashSetPool<T0> für HashSets. Weitere Informationen dazu finden Sie in der Dokumentation.

Der LinkedPool verwendet eine verknüpfte Liste, um eine Sammlung von Objektinstanzen für die Wiederverwendung zu speichern, was zu einer besseren Speicherverwaltung führen kann (abhängig von Ihrem Fall), da Sie nur Speicher für die Elemente verwenden, die tatsächlich im Pool gespeichert sind.

Vergleichen Sie dies mit dem ObjectPool, der einfach einen C#-Stack und ein darunter liegendes C#-Array verwendet und als solcher einen großen zusammenhängenden Speicherbereich enthält. Der Nachteil ist, dass Sie mehr Speicher pro Element und mehr CPU-Zyklen benötigen, um diese Datenstruktur im LinkedPool zu verwalten, als im ObjectPool, wo Sie die defaultSize und maxSize verwenden können, um Ihre Bedürfnisse zu konfigurieren.

Blog-Titel
Andere Möglichkeiten zur Implementierung von Objektpooling

Wie Sie Objektpools verwenden, hängt von der jeweiligen Anwendung ab, aber das Muster tritt häufig auf, wenn eine Waffe mehrere Projektile abfeuern muss, wie im Beispiel zuvor dargestellt.

Eine gute Faustregel ist es, jedes Mal, wenn Sie eine große Anzahl von Objekten instanziieren, ein Profil Ihres Codes zu erstellen, da Sie sonst Gefahr laufen, einen Garbage Collection Spike zu verursachen. Wenn Sie signifikante Spitzen feststellen, die Ihr Spiel zum Stottern bringen, sollten Sie einen Objektpool verwenden. Denken Sie daran, dass das Pooling von Objekten die Komplexität Ihrer Codebasis erhöhen kann, da Sie die verschiedenen Lebenszyklen der Pools verwalten müssen. Außerdem kann es passieren, dass Sie durch die Erstellung zu vieler vorzeitiger Pools Speicher reservieren, den Ihr Spiel nicht unbedingt benötigt.

Wie bereits erwähnt, gibt es mehrere andere Möglichkeiten, das Objekt-Pooling außerhalb des in diesem Artikel enthaltenen Beispiels zu implementieren. Eine Möglichkeit besteht darin, eine eigene Implementierung zu erstellen, die Sie an Ihre Bedürfnisse anpassen können. Sie müssen jedoch die Komplikationen der Typ- und Threadsicherheit sowie die Definition der benutzerdefinierten Objektzuweisung/-freigabe berücksichtigen.

Glücklicherweise bietet der Unity Asset Store einige großartige Alternativen, mit denen Sie Zeit sparen können.

Weitere fortgeschrittene Ressourcen für die Programmierung in Unity

Das E-Book, Level up your code with game programming patternsenthält ein ausführlicheres Beispiel für ein einfaches benutzerdefiniertes Objektpoolsystem. Unity Learn bietet auch eine Einführung in das Objekt-Pooling, die Sie hier finden können, sowie ein vollständiges Tutorial zur Verwendung des neuen eingebauten Objekt-Pooling-Systems in 2021 LTS.

Alle fortgeschrittenen technischen E-Books und Artikel sind auf der Website Bewährte Praktiken von Unity Hub verfügbar. Die E-Books sind auch auf der Website verfügbar. Erweiterte bewährte Praktiken in der Dokumentation verfügbar.

Haben Ihnen diese Inhalte gefallen?