Unity Pooling (Obje Havuzu) Pattern’i

Yayınlandı: 30 Ekim 2019 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

Yine ve yeniden merhabalar,

Bu kısa derste, Unity’de pooling (obje havuzu) optimizasyonunu göreceğiz.

O halde vakit kaybetmeden derse başlayalım!

Pooling pattern nedir? Bir obje ile işiniz bittikten sonra o objeyi yok etmek yerine, objeyi havuz dediğimiz bir yerde tutup daha sonra aynı türde bir objeye ihtiyaç duyduğunuzda, yeni bir obje oluşturmak yerine havuzdaki objeyi tekrardan kullanmaktır. Bu şekilde Instantiate ve Destroy fonksiyonlarını minimuma indirebilirsiniz. Özellikle mobil oyunlarda, sürekli Instantiate/Destroy yapıyorsanız, bu pattern performansı ciddi anlamda iyileştirebilir. Ancak bu pattern’i aşırı abartmaya da gerek yok. Bir objeden oyun boyunca sadece 1-2 defa Instantiate/Destroy ediyorsanız, burada pooling pattern’i kullanmasanız da olur.

Kabaca mantığı anladığınızı düşünüyorum. Peki obje havuzunu nasıl oluşturabiliriz? Açıkçası bu size kalmış. Eğer havuzun kapasitesini biliyorsanız bir array kullanabileceğiniz gibi, dinamik havuzlar için List veya Stack de kullanabilirsiniz. Stack veri türü, List veri türüne çok benzer. Aralarındaki temel farklar şunlardır:

  • List’te listObjesi[0] şeklinde List’teki herhangi bir elemana erişebilirken bu Stack’te mümkün değildir.
  • List.Add fonksiyonunun karşılığı Stack.Push‘tur (List/Stack’in sonuna yeni bir obje eklemeye yarar).
  • Stack’te Pop fonksiyonu bulunmaktadır; bu fonksiyon Stack’in en sonundaki objeyi döndürür ve aynı zamanda bu objeyi Stack’ten çıkarır. Pop’un List’teki karşılığı şudur:
int sonIndex = listObjesi.Count - 1;
var obje = listObjesi[sonIndex];
listObjesi.RemoveAt( sonIndex );

return obje;

Görebileceğiniz üzere, Stack‘teki Push ve Pop fonksiyonları, havuza eleman ekleyip havuzdan eleman çıkarmak için çok idealdir. Bu yüzden biz de Stack kullanacağız.

İsterseniz basit bir örnekle başlayalım:

using System.Collections;
using UnityEngine;

public class HavuzTest : MonoBehaviour
{
	public GameObject prefab;

	void Start()
	{
		// Coroutine başlat
		// Coroutine'ler hakkında daha fazla bilgi için: https://yasirkula.com/2018/11/20/unity-3d-coroutineler/
		StartCoroutine( SurekliObjeOlusturVeYokEt() );
	}

	IEnumerator SurekliObjeOlusturVeYokEt()
	{
		// Bu coroutine sürekli çalıştırılır
		while( true )
		{
			// x, y ve z değerleri -3 ile 3 arasında olan rastgele bir Vector3 döndürür
			Vector3 konum = Random.insideUnitSphere * 3f;

			// Yeni bir obje oluştur
			GameObject obje = Instantiate( prefab, konum, Quaternion.identity );

			// 1 saniye bekle
			yield return new WaitForSeconds( 1f );

			// Oluşturduğumuz objeyi yok et
			Destroy( obje );
		}
	}
}

Bu kodda henüz havuz pattern’i kullanmadık. Start fonksiyonunda SurekliObjeOlusturVeYokEt isminde bir coroutine’i başlatıyoruz ve bu coroutine’de de her saniye yeni bir obje oluşturuyoruz. 1 saniye geçince de bu objeyi yok ediyoruz.

Şimdi bu kodu pooling pattern ile optimize etmeyi görelim:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HavuzTest : MonoBehaviour
{
	public GameObject prefab;

	private Stack<GameObject> objeHavuzu = new Stack<GameObject>();

	void Start()
	{
		StartCoroutine( SurekliObjeOlusturVeYokEt() );
	}

	IEnumerator SurekliObjeOlusturVeYokEt()
	{
		while( true )
		{
			Vector3 konum = Random.insideUnitSphere * 3f;

			// Havuzdan obje çekip konumunu değiştir
			GameObject obje = HavuzdanObjeCek();
			obje.transform.position = konum;

			// 1 saniye bekle
			yield return new WaitForSeconds( 1f );

			// Objeyi havuza geri yolla
			HavuzaObjeEkle( obje );
		}
	}

	GameObject HavuzdanObjeCek()
	{
		// Havuzda obje var mı kontrol et
		if( objeHavuzu.Count > 0 )
		{
			// Havuzdaki en son objeyi çek
			GameObject obje = objeHavuzu.Pop();

			// Objeyi aktif hale getir
			obje.gameObject.SetActive( true );

			// Objeyi döndür
			return obje;
		}

		// Havuz boş, mecburen yeni bir obje Instantiate et
		return Instantiate( prefab );
	}

	void HavuzaObjeEkle( GameObject obje )
	{
		// Objeyi inaktif hale getir (böylece obje artık ekrana çizilmeyecek ve objede
		// Update vs. fonksiyonlar varsa, bu fonksiyonlar obje havuzdayken çalıştırılmayacak)
		obje.gameObject.SetActive( false );

		// Objeyi havuza ekle
		objeHavuzu.Push( obje );
	}
}

Koda bir Stack objesi ekledik ve Stack’i kullanabilmek için de script’in başına “using System.Collections.Generic;” satırını ekledik. İlaveten koda iki yeni fonksiyon ekledik:

  • HavuzdanObjeCek: objeHavuzu‘ndan bir obje çekmeye yarar. Eğer havuzda obje yoksa yeni bir obje Instantiate eder.
  • HavuzaObjeEkle: objeHavuzu‘na bir obje eklemeye yarar. Önce objeyi inaktif hale getiriyoruz ki obje havuzdayken oyunu hiçbir şekilde etkileyemesin (benzer şekilde, HavuzdanObjeCek fonksiyonunda da havuzdan çektiğimiz objeyi aktif hale getirdiğimize dikkat edin).

Son olarak da, coroutine’imizdeki Instantiate’i HavuzdanObjeCek() ile, Destroy’u da HavuzaObjeEkle ile değiştirdik.

Şu anda ekranda sadece bir obje gösterdiğimiz için havuzun etkisini hissetmeyebilirsiniz. İsterseniz 1 değil 500 obje ile kodu test edelim:

void Start()
{
	// Coroutine'i 500 defa başlat
	for( int i = 0; i < 500; i++ )
	{
		StartCoroutine( SurekliObjeOlusturVeYokEt() );
	}
}

Şimdi Instantiate/Destroy ile pooling pattern arasındaki performans farkını daha iyi gözlemleyebilirsiniz.

Havuz kodumuzu biraz daha iyileştirmenin bir yolu, havuzun içini oyun başlar başlamaz doldurmaktır. Şu anda HavuzdanObjeCek() fonksiyonunu ilk çağırdığımız zamanlarda havuz boş olduğu için Instantiate kullanmak zorunda kalıyoruz ve bu esnada Instantiate’in oyunu kastırmasıyla karşı karşıya kalıyoruz. Eğer havuzu oyun başlar başlamaz yeteri miktarda obje ile doldurursak, HavuzdanObjeCek fonksiyonunu çağırdığımızda havuz genelde boş olmayacağı için Instantiate’in oyun esnasında çalışmasının olabildiğince önüne geçeceğiz:

void Start()
{
	// Havuzu oyunun başında 500 obje ile doldur
	for( int i = 0; i < 500; i++ )
	{
		// Instantiate'in sebep olacağı kastırmanın oyunun ortasında değil başında gerçekleşmesini sağlıyoruz
		GameObject obje = Instantiate( prefab );
		HavuzaObjeEkle( obje );
	}

	// Coroutine'i 500 defa başlat
	for( int i = 0; i < 500; i++ )
	{
		StartCoroutine( SurekliObjeOlusturVeYokEt() );
	}
}

Başka obje türleri için de havuz yazacak olsak bile, havuz kodumuzda şunların standart olduğunu fark etmişsinizdir:

  • Havuzdaki objeleri tutan bir Stack objemiz var
  • Havuza obje eklemeden önce objeyi inaktif hale getiriyoruz
  • Havuzdan obje çekerken, çektiğimiz objeyi aktif hale getiriyoruz
  • Havuzda obje yoksa Instantiate kullanıyoruz
  • Havuzu oyunun başında objelerle dolduruyoruz

O halde tüm bu standart kodu tek bir havuz class’ında birleştirerek kodumuzu daha düzenli hale sokabiliriz:

using System.Collections.Generic;
using UnityEngine;

public class ObjeHavuzu
{
	private GameObject prefab;
	private Stack<GameObject> objeHavuzu = new Stack<GameObject>();

	public ObjeHavuzu( GameObject prefab )
	{
		this.prefab = prefab;
	}

	public void HavuzuDoldur( int miktar )
	{
		for( int i = 0; i < miktar; i++ )
		{
			GameObject obje = Object.Instantiate( prefab );
			HavuzaObjeEkle( obje );
		}
	}

	public GameObject HavuzdanObjeCek()
	{
		if( objeHavuzu.Count > 0 )
		{
			GameObject obje = objeHavuzu.Pop();
			obje.gameObject.SetActive( true );

			return obje;
		}

		return Object.Instantiate( prefab );
	}

	public void HavuzaObjeEkle( GameObject obje )
	{
		obje.gameObject.SetActive( false );
		objeHavuzu.Push( obje );
	}
}

Test kodumuzu, bu havuz class’ını kullanacak şekilde güncelleyecek olursak:

using System.Collections;
using UnityEngine;

public class HavuzTest : MonoBehaviour
{
	public GameObject prefab;

	// Artık havuz class'ımızı kullanıyoruz
	private ObjeHavuzu havuz;

	void Start()
	{
		// Havuzu oluştur ve 500 obje ile doldur
		havuz = new ObjeHavuzu( prefab );
		havuz.HavuzuDoldur( 500 );

		for( int i = 0; i < 500; i++ )
		{
			StartCoroutine( SurekliObjeOlusturVeYokEt() );
		}
	}

	IEnumerator SurekliObjeOlusturVeYokEt()
	{
		while( true )
		{
			Vector3 konum = Random.insideUnitSphere * 3f;

			// Havuzdan obje çekip konumunu değiştir
			GameObject obje = havuz.HavuzdanObjeCek();
			obje.transform.position = konum;

			// 1 saniye bekle
			yield return new WaitForSeconds( 1f );

			// Objeyi havuza geri yolla
			havuz.HavuzaObjeEkle( obje );
		}
	}
}

ObjeHavuzu class’ı oluşturmamızın avantajı, birden fazla obje havuzu oluşturmak istediğimizde karşımıza çıkacak. Diyelim prefab2 diye ayrı bir prefab’ımız daha olsa ve onun için de havuz oluşturmak istesek:

using System.Collections;
using UnityEngine;

public class HavuzTest : MonoBehaviour
{
	public GameObject prefab;
	public GameObject prefab2;

	private ObjeHavuzu havuz;
	private ObjeHavuzu havuz2;

	void Start()
	{
		havuz = new ObjeHavuzu( prefab );
		havuz2 = new ObjeHavuzu( prefab2 );

		havuz.HavuzuDoldur( 250 );
		havuz2.HavuzuDoldur( 250 );

		for( int i = 0; i < 250; i++ )
		{
			StartCoroutine( SurekliObjeOlusturVeYokEt() );
		}
	}

	IEnumerator SurekliObjeOlusturVeYokEt()
	{
		while( true )
		{
			GameObject obje = havuz.HavuzdanObjeCek();
			obje.transform.position = Random.insideUnitSphere * 3f;

			GameObject obje2 = havuz2.HavuzdanObjeCek();
			obje2.transform.position = Random.insideUnitSphere * 3f;

			yield return new WaitForSeconds( 1f );

			havuz.HavuzaObjeEkle( obje );
			havuz2.HavuzaObjeEkle( obje2 );
		}
	}
}

Şu ana kadar hep GameObject tutan havuzlarla uğraştık ama bununla sınırlı kalmak zorunda değilsiniz. İsterseniz normal C# objeleri tutan havuzlar da oluşturabilirsiniz:

using System.Collections.Generic;
using UnityEngine;

public class TestClass
{
}

public class HavuzTest : MonoBehaviour
{
	private Stack<TestClass> objeHavuzu = new Stack<TestClass>();

	void Start()
	{
		// Havuzdan obje çek
		TestClass obje;
		if( objeHavuzu.Count > 0 )
			obje = objeHavuzu.Pop();
		else
			obje = new TestClass();

		// Burada obje'yi kullan
		// ...

		// obje'yi havuza geri yolla
		objeHavuzu.Push( obje );
	}
}

Bu şekilde bu dersin de sonuna geldik. Dilerseniz havuz kodunuza yeni özellikler de ekleyebilirsiniz; mesela havuzun kapasitesi belli bir miktarı aştıysa yeni objeleri havuza kabul etmemek veya havuz yeni bir obje Instantiate ederken tek bir prefab değil de birkaç prefab arasından rastgele seçim yapmak gibi. Bunları kolayca ObjeHavuzu koduna ekleyebileceğinize inanıyorum.

Öyleyse sonraki derslerde görüşmek üzere!

yorum
  1. Murat dedi ki:

    Blog yazınız için teşekkür ederim. Obje havuzu ile gevşek bağlılık güzel işlenmiş. Özellikle shooter oyunları için iyi bir örnek olmuş.
    Size birşey sormak istiyorum peki bu obje havuzunda inaktif ettiğimiz objeler memory’de yer kaplıyor mu ?

  2. Yunus Emre Kara dedi ki:

    Öncelikle kolay gelsin bi konuda yardıma ihtiyacım varda açık dünya bir oyunda yayaların rastgele spawn olmasını sağlıyorum fakat haritada yüzlerce spawn point oluşturup yayaları random biçimde oluşturmak performans ve görsel açıdan istediğim bir şey değil karakter objesinin childı olarak spawn noktaları oluşturup karakterle birlikte yer değiştircek şekilde bir sistem düşündüm fakat bu seferde haritadaki nesnelerin üzerinde doğması gibi sorunlar oluşacak nası bir sistem kullanmalıyım optimizasyon açısından ?

    Büyük oyunlarda nasıl bir sistem kullanılır bu konuda bir bilginiz varsa yardımcı olursanız sevinirim

    • yasirkula dedi ki:

      Büyük oyunlar nasıl yapıyor kesin bir bilgim olmamakla birlikte, ben olsam haritadaki spawn noktalarını 10×10’luk bir grid oluşturacak şekilde 100 eşit parçaya ayırır ve player hangi grid’in içinde ise, o grid ile onun bitişiğindeki grid’lerde spawn kodunu çalıştırırdım. Burada verdiğim rakamlar oyunun boyutuna göre artıp azalabilir.

Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Google fotoğrafı

Google hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.