This page needs JavaScript activated to work.
/blog

Object Pooling

23. Juli 2021

Object Pooling – Überblick
Bevor ich näher darauf eingehe, wie Object Pooling in Unity funktioniert, müssen wir erst einmal klären, was das eigentlich ist. Ein Object Pool funktioniert so, dass mehrere Objekte eines Types, z.B. eines roten Rechtecks, erzeugt werden und dann für den Spieler unsichtbar gemacht werden. Wenn jetzt eines dieser Objekte in der Scene benötigt wird, dann wird das nächste freie Objekt des Pools sichtbar gemacht und an die nötige Stelle gesetzt. Diese Methode spart unglaublich viele Resourcen, denn es muss nicht jedes Mal eine neue Instanz von einem Objekt angelegt werden.
Schritt 1 – Objekt Pool Klasse
Ok, starten wir ganz langsam und ziehen dann an. Als erstes erstellen wir eine Klasse „ObjectPool“, die unseren Object Pool verwaltet, Objekte instantieren und zerstören kann. Zusätzlich fügen wir eine Instanz des Object Pools in den Script ein, damit wir diesen von überall aus verwenden können (ObjectPool.Instance).
public class ObjectPool : MonoBehaviour {
    public static ObjectPool Instance;
    
    public ObjectPool() {
        Instance = this;
    }
}        
Schritt 2 – Objekt Instanz Klasse
Im nächsten Schritt erstellen wir eine Klasse für jede Objektinstanz unseres Pools. Die Klasse besitzt die Eigenschaften name [string], obj [GameObject] und maxAmount [int]. MaxAmount gibt an, wie viele Instanzen später von dem Objekt angelegt werden können. Zusätzlich legen wir eine Liste an die alle Instanzen des Pools beinhaltet. WICHTIG: damit Unity das Objekt im Inspektor erkennt muss es Serializable sein. Das bedeutet die Klasse bekommt das Attribut [System.Serializable].
public List objectInstances = new List();

[System.Serializable]
public class ObjectInstance {
    public string name = "";
    public GameObject obj;
    public int maxAmount = 10;
}        
Schritt 3 – Objekt Liste
Nachdem wir eine Klasse für die Objektinstanz angelegt haben, müssen wir noch eine Klasse für die Liste eines einzelnen Objekttypes erstellen. Es hat die Eigenschaften list und currentObj. List beeinhaltet ganz einfach alle Objekte eines Typs und currentObj ist der Index des nächsten freien Objekts. Im Standardkonsturktor wird die Liste als Argument „list“ übergeben. Das Schlüsselwort this verweißt auf die Liste der Klasse sleber, damit es zu keinen Zuweisungsfehlern kommt. Denn die Variablen list und list sind gleich benannt.
Zu guter letzt erstellen wir eine Funktion mit dem Rückgabewert eines GameObjects „GetNextObject“. Sie soll das nächste freie Objekt zurückgeben. Dafür rechnen wir einfach die Variable currentObj + 1 und geben das Objekt an der Stelle currentObj zurück. Wenn currentObj nun größer ist, als die Anzahl der Objekte in der Liste, dann soll currentObj wieder auf 0 gesetzt werden, sonst kommt es zu einer StackOverflow exception. Der Index wäre dann größer als die Größe der Liste.
private List objectLists = new List();

public class ObjectList {
    public List list;
    private int currentObj;

    public ObjectList(List list) { // Standardkonstruktor
        this.list = list;
        currentObj = list.Count;
    }

    public GameObject GetNextObject() {
        currentObj += 1;
        if (currentObj >= list.Count - 1) currentObj = 0;
        return list[currentObj];
    }
}        
Schritt 4 – Object Pool Startsequenz
Wenn das Spiel gestartet wird, dann sollen alle Instanzen der Objekte erstellt werden, sodass sie später nicht mehr geladen werden müssen. Dies findet in der ObjectPool Klasse statt. Wir iterieren für jedes verschiedene Objekt die maximale Anzahl an Instanzen durch und erhalten die unten aufgeführte For-Schleifen Konstellation. In der Schleife mit einer ObjectInstance Instantiaten wir das Objekt der ObjektInstanz an Stelle x. Danach fügen wir dieses einer Liste „objects“ hinzu und setzen die Sichtbarkeit auf „false“. Optional kann der Parent des Objekts auch noch zum Objekt des Scripts gestzt werden. Das dient nur einer besseren Übersicht im Inspektor. Am Ende wird die Liste als neue ObjectList den objectLists hinzugefügt.
private void Awake() { // in ObjectPool.cs
    for (int x = 0; x < objectInstances.Count; x++) {
        List objects = new List();

        for (int z = 0; z < objectInstances[x].maxAmount; z++) {
            GameObject obj = Instantiate(objectInstances[x].obj);
            objects.Add(obj);
            obj.SetActive(false);
            obj.transform.parent = this.transform; // optional
        }

        objectLists.Add(new ObjectList(objects));
    }
}        
Schritt 5 – Index des Objekttypes
Wir haben schon eine Liste aller Objekte erstellt nun müssen wir den Index des Objekts auswählen, welches zurückgegeben werden soll. Dafür erstellen wir die Funktion „GetObjectIndex“. Wir iterieren durch alle Objektinstanzen und geben den Index zurück der mit dem Argument objectName [string] übereinstimmt. Falls der Name in keinem der Instanzen übereinstimmt, dann geben wir -1 zurück und werten dies als Error. Das wird im folgenden Schritt noch wichtig.
private int GetObjectIndex(string objectName) { // in ObjectPool.cs
    for (int x = 0; x < objectInstances.Count; x++) {
        if (objectInstances[x].name == objectName) return x;
    }
    return -1;
}        
Schritt 6 – Instantiate Funktion
Die Instantiate-Funktion ermöglicht es uns später Objekte zu erstellen, die aus dem Object Pool geladen werden. Wir erstellen eine Variable objectIndex [int] und benutzen unsere Funktion „GetObjectIndex“, um diese zu initialisieren. Wenn der objectIndex -1 beträgt, dann wird eine Error ausgegeben und null zurückgegeben. Jetzt erstellen wir ein GameObject „nextObject“ ,welches mit dem nächsten Objekt der Objektinstanz aus objectLists an der Stelle von objectIndex zugewiesen wird. Danach wird das Objekt wieder sichtbar gemacht und die Poisition zurückgesetzt. Zum Schluss muss das Objekt noch zurückgegeben werden.
public GameObject Instantiate(string objectName) { // in ObjectPool.cs
    int objectIndex = GetObjectIndex(objectName);
    if (objectIndex == -1) {
        Debug.LogError("Object with name \"" + objectName + "\" not found.");
        return null;
    }

    GameObject nextObject = objectLists[objectIndex].GetNextObject();
    nextObject.SetActive(true);
    nextObject.transform.position = new Vector3(0, 0, 0); // Position zurücksetzen
    return nextObject;
}        
Schritt 7 – Erweiterte Instantiate Funktion
Um ein bisschen vielfältiger in der Verwendung zu sein, überladen wir die Instantiate-Funktion mit dieser, welche die Argumente objectName [string], position [Vector3] und rotation [Quaternion] beinhaltet. Wir erstelle ein GameObject und übernehemen alle Eigenschaften vom alten Instantiate und übergeben objectName als Namen. Danach muss nur noch die Position und die Roation gesetzt werden, dann kann das Objekt zurückgegeben werden.
public GameObject Instantiate(string objectName, Vector3 position, Quaternion rotation) {
    GameObject nextObject = Instantiate(objectName);
    nextObject.transform.position = position;
    nextObject.transform.rotation = rotation;
    return nextObject;
}        
Schritt 8 – Objekte zerstören
Damit Objekte auch wieder verschwinden, benötigen wir eine „Destroy“ Methode. Die Methode braucht obj [GameObject] als Argument, welches zerstört werden soll. Zuerst legen wir einen Boolean „containsObject“ an, der aussagt, ob das Objekt in der Liste der Objektinstanz vorkommt. Wenn das der Fall ist, dann wird containsObject auf true gesetzt. Sollte containsObject am Ende true sein, dann wird das Objekt unsichtbar gemacht andernfalls wird eine Warnung ausgegeben, dass das übergebene Objekt nicht aus dem Object Pool stammt.
public void Destroy(GameObject obj) {
    bool containsObject = false;
    for (int x = 0; x < objectLists.Count; x++) {
        if (objectLists[x].list.Contains(obj)) containsObject = true;
    }
    if (containsObject) obj.SetActive(false);
    else Debug.LogWarning("\"" + obj.name + "\" is not from ObjectPool.");
}        
Schritt 9 – Testen
Hier testen wir nochmal einige Funktionen. Zuerst legen wir ein Empty GameObject an und fügen den Script ObjectPool hinzu. Danach erstellen wir einige Prefabs und fügen sie als Objekte unserem Object Pool hinzu, wie unten gezeigt.
Object Pool Script Komponente
Die Funktionen können folgendermaßen getestet werden:
public class Test : MonoBehaviour {
    void Start() {
        ObjectPool.Instance.Instantiate("Blue");
        ObjectPool.Instance.Instantiate("Red", new Vector3(1, 1), Quaternion.identity);
    }
}