Notifications
Article
Object Pooling [PL]
Published 3 months ago
93
0
Object Pooling [PL]
Gdy tworzymy gry bardzo ważnym czynnikiem jest ich wydajność. Chcemy aby nasze gry działały jak najbardziej optymalnie i były odświeżane z jak największą częstotliwością (mówiąc kolokwialnie - aby gra miała jak najwięcej FPSów).
Jedną z technik optymalizacji gier jest Object Pooling.

Przykłady z tego artykułu

Poniżej możecie znaleźć link do przykładowego projektu, który omawiam w tym artykule. https://github.com/uvivagabond/Patterns---Object-Pooling Po otworzeniu projektu otwórzcie scenę o nazwie Object Pooling.

Czym jest Object Pooling?

Object Pooling polega na tworzeniu pewnej puli obiektów a potem ciągłym wykorzystywaniu obiektów z tej puli (zamiast ciągłego tworzenia i niszczenia obiektów). Często w grach mamy do czynienia z sytuacją, że tworzymy wiele identycznych obiektów, które istnieją tylko przez krótki okres czasu np. pociski wystrzeliwane z armaty. Tymi obiektami możemy zarządzać na dwa sposoby.
  • Możemy tworzyć kule armatnie przy każdym wystrzale a potem je niszczyć po trafieniu w cel. Takie rozwiązanie nie będzie wydajne bo proces tworzenia i niszczenia obiektów jest dość kosztowny.
  • Możemy także na początku rozgrywki utworzyć jakąś pulę kul armatnich. Podczas wystrzału pobieramy kule z puli, a gdy kula trafi cel zamiast niszczyć kulę, umieszczamy ją ponownie w puli. Takie podejście może poprawić wydajność naszej aplikacji bo nie wykonujemy operacji tworzenia i niszczenia obiektów przy każdym wystrzale. Takie rozwiązanie nosi nazwę Object Pooling.


Jak zaimplementować Object Pooling w naszej grze?

W internecie możecie znaleźć wiele gotowych przykładów i rozwiązań dla Object Poolingu. Zaprezentuję wam teraz działanie mojego rozwiązania. Moje rozwiązanie było inspirowane w dużej mierze oficjalnym tutorialem od Unity:

I rozwiązaniem z tego artykułu: https://www.raywenderlich.com/847-object-pooling-in-unity

Przykład implementacji puli obiektów

Poniżej znajdziecie gotowy skrypt, który pomoże wam tworzyć pule obiektów. Możecie go skopiować bezpośrednio do waszego projektu.
using System; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class PoolingManager { PoolInfo poolInfo; Stack<GameObject> pool = new Stack<GameObject>(); Action<GameObject> initializationFunction; public PoolingManager(PoolInfo poolInfo) { this.poolInfo = poolInfo; InitializePoolItems(); } public PoolingManager(GameObject poolItem, GameObject container, int poolSize = 1, bool isPoolExpandable = true) { this.poolInfo.poolItem = poolItem; this.poolInfo.isPoolExpandable = isPoolExpandable; this.poolInfo.basePoolSize = poolSize; this.poolInfo.container = container; this.pool = new Stack<GameObject>(); InitializePoolItems(); } public void SetInitializationFunction(Action<GameObject> initializationFunction) { this.initializationFunction = initializationFunction; } void ResetPoolItem(GameObject poolItem) { initializationFunction?.Invoke(poolItem); } void InitializePoolItems() { for (int i = 0; i < poolInfo.basePoolSize; i++) { GameObject newItem = InstantiateAndResetPoolItem(); PutToPool(newItem); } } public GameObject GetFromPool() { if (this.pool.Count > 0) { GameObject itemFromPool = pool.Pop(); itemFromPool.SetActive(true); ResetPoolItem(itemFromPool); return itemFromPool; } else if (poolInfo.isPoolExpandable) { return InstantiateAndResetPoolItem(); } else { return null; } } public void PutToPool(GameObject poolItem) { this.pool.Push(poolItem); poolItem.transform.parent = poolInfo.container.transform; poolItem.SetActive(false); } private GameObject InstantiateAndResetPoolItem() { GameObject newItem = UnityEngine.Object.Instantiate<GameObject>(this.poolInfo.poolItem, Vector3.zero, Quaternion.identity); ResetPoolItem(newItem); return newItem; } } [System.Serializable] public class PoolInfo { public GameObject poolItem; public GameObject container; public int basePoolSize = 1; public bool isPoolExpandable = true; }

Jak utworzyć pulę obiektów?

Pierw utwórzmy skrypt CannoBallPool w którym będziemy przechowywać wszystkie pule obiektów.
using UnityEngine; public class CannoBallPool : MonoBehaviour { }
Aby w łatwy sposób móc odwoływać się do tego skryptu skorzystamy z wzorca singleton. Poniżej znajduję się skrypt, który implementuję wzorzec singletonu.
using UnityEngine; public abstract class Singleton<T> : MonoBehaviour where T : Component { private static T m_Instance; public static T SharedInstance { get { if (m_Instance == null) { m_Instance = FindObjectOfType<T>(); if (m_Instance == null) { GameObject gO = new GameObject(); gO.name = typeof(T).Name; m_Instance = gO.AddComponent<T>(); } } return m_Instance; } } public virtual void Awake() { if (m_Instance == null) { m_Instance = this as T; } else { Destroy(gameObject); } } }
Modyfikujemy skrypt CannoBallPool aby dziedziczył z klasy Singleton.
public class CannoBallPool : Singleton<CannoBallPool> { }
Teraz jeśli będziemy chcieli uzyskać referencje do CannoBallPool w jakimś innym skrypcie to wystarczy, że skorzystamy z tego zapisu.
// taki zapis pozwala nam uzyskać referencje do skryptu CannoBallPool // a tym samym do jego zawartości CannoBallPool cannoBallPool = CannoBallPool.SharedInstance;
Utwórzmy teraz pulę obiektów w skrypcie CannoBallPool z pomocą klasy PoolingManager.
using System; using UnityEngine; // ta klasa będzie przechowywać wszystkie pule obiektów // w tym przykładzie mamy tylko jedną pule public class CannoBallPool : Singleton<CannoBallPool> { // z pomocą tej instancji definiujemy zachowanie naszej puli obiektów // ten parametr zostanie wykorzystany w konstruktorze klasy PoolingManager public PoolInfo poolInfo; // ta instancja reprezentuję jedną pule objektów PoolingManager cannonBalls; // korzystamy z property i leniwe incjalizowanie aby mieć pewność, że konstruktor zostanie wywołany // przed pobraniem elementu z puli public PoolingManager CannonBalls { get { if (cannonBalls == null) { cannonBalls = new PoolingManager(poolInfo); } return cannonBalls; } } }
Gdy dodamy skrypt CannoBallPool do jakiegoś gameObjectu to wyświetlą się takie informacje.


Jak definiujemy pule?

Każdą pule obiektów będą opisywać 4 wielkości przechowywane w polu PoolInfo.
  • Pool Item - jest to gameObject na bazie, którego będziemy tworzyć obiekty w puli. W naszym przypadku będzie to prefab kuli armatniej.
  • Container - w tym gameObjectie będą zagnieżdzone objekty puli gdy nie są używane. Ten gameObject przechowuję dostępną pule obiektów.
  • Base Pool size - ta wielkość określa bazową wielkość puli czyli ile bazowo będzie znajdować się w niej obiektów, które możemy potem wykorzystać. Te obiekty powstaną podczas inicjalizacji puli (czyli gdy zostanie wywołany konstruktor)
  • Is Pool Expandable - ten parametr określa czy możemy zwiększać ilość elementów w puli. Czasami może zdarzyć się sytuacja, że chcemy w danym momencie zwiększyć ilość elementów w puli. Gdy ta opcja jest ustawiona na true pula automatycznie zostanie powiększona gdy będzie brakować elementów puli. Jeśli ta opcja jest ustawiona na false wielkość puli pozostanie stała. Jak skończą się obiekty w puli to będziemy musieli czekać na powrót ich do puli aby móc z nich znowu skorzystać.

Kiedy ustawić Is Pool Expandable na false a kiedy na true?

Na początek przykład statku kosmicznego walczącego z niezliczoną ilością statków kosmicznych. Nasz statek ma nieskończoną ilość amunicji. W takim przypadku nie jesteśmy w stanie określić ile kul będzie nam potrzebnych w danej chwili. Możemy potrzebować 10 kul a może 30, albo jeszcze więcej. W takim przypadku warto ustawić IsPoolExpandable na true aby pula rozszerzała się w zależności od naszych potrzeb. Drugi przykład. Mam ninje, który ma przy sobie zestaw dziesięciu gwiazdek. Nasz ninja rzuca wszystkie gwiazdki w swego wroga. Dopóki ich nie pozbiera, nie będzie miał czym rzucać w przeciwników. W takim przypadku warto ustawić IsPoolExpandable na false aby ninja mógł rzucać gwiazdkami tylko jeśli posiada je w ekwipunku.

Jak pobierać obiekty z puli?

Poniżej przygotowałem skrypt, który pozwala armacie strzelać (dodałem go do gameObjectu Cannon). Przy każdym wystrzale pobierana jest kula z puli.
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cannon : MonoBehaviour { // przechowujemy referencje do puli kul armatnich PoolingManager cannonBalls; private void Start() { // pobieramy referencje do puli kul armatnich, która przechowywana jest w skrypcie CannoBallPool cannonBalls = CannoBallPool.SharedInstance.CannonBalls; // przypisujemy puli kul armatnich metodę resetującą // robimy to po to aby zawsze wystrzeliwane kule miały właściwe ustawienia gdy wyciągamy je z puli // nie musimy przypisywać metody resetującej w metodzie SetInitializationFunction() // możemy także resetować objekty zaraz po wyjęciu ich z puli, ale będzie to mnie eleganckie rozwiązanie cannonBalls.SetInitializationFunction(ResetPoolItem); } // metoda resetująca // ja w tym przykładzie resetuję kule poprzez podpięcie ich do gameObjectu Cannon, zresetowanie ich pozycji // i ustawienie im odpowiedniej prędkości z pomocą komponentu Rigidbody private void ResetPoolItem(GameObject cannonBall) { cannonBall.transform.parent = transform; cannonBall.transform.localPosition = Vector3.zero; cannonBall.GetComponent<Rigidbody2D>().velocity = new Vector3(10, 4, 0); } void Update() { // gdy wciśniemy spacje bądź LPM to zostanie wystrzelona kula if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0)) { // uzyskujemy kule armatnią z puli, która jest już zresetowana i gotowa to użycia GameObject cannonBall = cannonBalls.GetFromPool(); // oczywiście możecie zmodyfikować zachowanie kuli także dopiero po wydobyciu jej z puli } } }

Resetowanie kul armatnich

Zwróćcie szczególną uwagę w jaki sposób resetuję kulę armatnie aby były zdatne do użycia. Metodę resetującą kulę armatnie czyli ResetPoolItem() przypisuję w taki sposób:
cannonBalls.SetInitializationFunction(ResetPoolItem);
Dzięki czemu możemy pobierać od razu zresetowane kulę z puli gotowe do użycia:
GameObject cannonBall = cannonBalls.GetFromPool();
Metoda resetująca ma następującą sygnaturę:
private void NazwaMetodyResetującej(GameObject cannonBall) { // tu definiujemy jak resetujemy kule armatnią modyfikując właściwości gameObjectu cannonBall }
Nie musimy przypisywać metody resetującej w metodzie SetInitializationFunction(). Ale w takim przypadku będziecie musieli zresetować kulę armatnią po wyciągnięciu jej z puli czyli w taki sposób:
GameObject cannonBall = cannonBalls.GetFromPool(); // wysyłujemy metodę resetującą i jako argument podajemy uzyskany z puli gameObject ResetPoolItem(cannonBall);
Na powyższym filmie widać, że kule są pobierane z puli, niestety póki co nie wracają do puli.

Jak zwracać obiekty do puli?

Teraz pokaże wam jak wkładać nieużywane obiekty z powrotem do puli.
Poniżej przygotowałem skrypt o nazwie Wall, którego zadaniem jest zwrócenie kuli armatniej z powrotem do puli, gdy kula trafi w ścianę. Skrypt umieściłem w gameObjectie Wall.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Wall : MonoBehaviour { // przechowujemy referencje do puli kul armatnich PoolingManager cannonBallsPool; private void Start() { // pobieramy referencje do puli kul armatnich, która przechowywana jest w skrypcie CannoBallPool cannonBallsPool = CannoBallPool.SharedInstance.CannonBalls; } // ta funkcja zostanie wywołana gdy kula armatnia zderzy się ze ścianą // parametr cannonBall będzie przechowywać informacje o zaistniałem kolizji ze ścianą // a tym samy informacje o obiekcie, który w ścianę uderzył void OnCollisionEnter2D(Collision2D cannonBall) { // gdy kula trafi w ścianę zostaję umieszczona z powrotem w puli obiektów cannonBallsPool.PutToPool(cannonBall.gameObject); } }
Zobaczmy co się dzieje z kulami, które trafiają w ścianę. Jak widać z poniższego filmu kule po trafieniu w ścianę wracają z powrotem do puli obiektów.
Kule można na powrót umieszczać w puli dzięki metodzie PutToPool(). Metodę wywołujemy na puli, w której chcemy umieścić obiekt czyli na obiekcie cannonBallsPool. Jako argument podajemy gameObject, który chcemy umieścić w puli czyli w tym przypadku kulę armatnią.
cannonBalls.PutToPool(cannonBall.gameObject);

Podsumowanie

Object pooling jest bardzo przydatną techniką, która może bardzo wpłynąć na wydajność waszych projektów.
Powyżej mieliście okazje zobaczyć jak możecie tworzyć pule obiektów z pomocą przygotowanego przeze mnie skryptu. Mam nadzieję, że powyższe skrypty będą dla was przydatne.
Rafał Tadajewski
I haven't duck. I took dino. - Programmer
10
Comments