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!
Hocam merhabalar
Oyunumda 10 farklı 2D objem var. Bunları tek bir havuzda tutup birer saniye aralıklarla random bi şekilde oluşup destroy edilmesini sağlayabilir miyim? Çünkü her obje için ayrı havuz oluşturunca random döngüye sokmak daha da zorlaşıyor gibi. Yardımcı olabilirseniz çok mutlu olurum. İyi çalışmalar..
Merhaba, Stack yerine List havuz kullanabilirsiniz. Böyle olunca rastgele bir index’e Random.Range(0, list.Count) şeklinde erişip onu RemoveAt ile listeden silebilirsiniz.
Hocam bu kadar hızlı dönüş sağlayacağınızı beklemiyordum :)) hemen deneyeceğim teşekkür ederim.
merhabalar insan ölünce hemen önüne bir silah bıraksın istiyorum. burada işimi görecek hangisi
İlk safhada, obje havuzu kullanmadan düz Instantiate ile bu işlemi yapabiliyor olmanız lazım. Yerdeki silaha ihtiyaç olmadığında da onu Destroy ediyor olmanız lazım. Bunların akabinde obje havuzu pattern’inden faydalanarak optimizasyon yapabilirsiniz. Instantiate’i ObjeHavuzu’nun HavuzdanObjeCek fonksiyonu ile, Destroy’u da HavuzaObjeEkle fonksiyonu ile değiştirebilirsiniz.
cevabınız için tşrkürler yanlız bir sorun var. Sahne’de düşman ölürken düşman nerede başlarsa parayı orada bırakıyor düşmanın olduğu yere atmıyor.
Instantiate veya HavuzdanObjeCek fonksiyonundan sonra, paranın transform.position’ını düşmanın transform.position’ına eşitleyebilirsiniz.
Hocam ben bunu generic type yapmaya çalıştım, prefab’den her seferinde GetComponent yaparak performans kaybı yaşamamak için. Ama bu defa da GameObject class’ı MonoBehaviour’dan inherit etmediğinden düz GameObject pool’u oluşturamıyorum, var mı öneriniz? Daha esnek bir yapı için soruyorum yeni class oluşturmaktansa
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool where T : MonoBehaviour
{
private T prefab;
private Stack objectPool = new Stack();
public ObjectPool(T prefab)
{
this.prefab = prefab;
}
public void LoadPool(int number)
{
for (int i = 0; i 0)
{
T gameObject = objectPool.Pop();
gameObject.gameObject.SetActive(true);
return gameObject;
}
return Object.Instantiate(prefab);
}
public void AddObject(T gameObject)
{
gameObject.gameObject.SetActive(false);
objectPool.Push(gameObject);
}
}
EDIT: tamamdır hocam github’ınızda generic pool buldum mantıklı geldi teşekkürler
AKLINDA CANLANMAYA ARKADAŞLAR İÇİN TÜM DOSYA
büyük İ ve objeHavuzu yazarken de küçük harfle yazmışım, dosyayı yazarken önermiyor sistem hepsini elle yazdım kusura bakmayın
Yasir bey
Ben oyun ekranına başlangıçta 10 adet obje instante ediyorum . 10 obje oyuncu tarafından yok edildikten sonra sadece son olarak bir obje oluşturmak istiyorum . (10 objeyi void Start fonksiyonona yazıp for(int=i;İ<10;i++ ) yazıyorm 10 obje çıkıyor .Bu 10 obje bitince if(İ<1)instante yapmaya çalışıyorum ama o 1 obje instante olmuyor . Kod hata vermiyor ama o 1 obje instante olmuyor .)Yardım ricasıyla .
for’da kullandığınız i değişkeni, objeler yok olunca otomatik olarak azalmaz. Bunun için dilerseniz public static int şeklinde bir değişken kullanabilirsiniz. Bu değişken başta 10 olur ve objelerinizin OnDestroy’unda değeri 1 azalır. Update’te de değeri 0 mı diye kontrol edip eğer 0 ise son objeyi Instantiate edersiniz.
Static’le ilgili biraz daha bilgi için: https://yasirkula.com/2019/07/27/unity-bir-scriptten-baska-bir-scriptteki-degiskene-ulasmak/
Yasir bey
Kod bu şekilde . Bir turlu yapamadım. i 1 ‘den kuçukse instante olmuyor .
Yardım ricası ile iyi akşamlar .
public int i;
public GameObject bir;
public GameObject iki;
public GameObject uc;
public GameObject dort;
public GameObject bes;
public GameObject alti;
public GameObject yedi;
public GameObject sekiz;
public GameObject dokuz;
public GameObject on;
// Use this for initialization
void Start () {
for(i=0;i<6;i++) {
Instantiate(bir,new Vector3(Random.Range(-173f,-7550f),690f,Random.Range(-188f,-12350f)),Quaternion.identity);
Instantiate(iki,new Vector3(Random.Range(-173f,-7550f),760f,Random.Range(-188f,-12350f)),Quaternion.identity);
Instantiate(uc,new Vector3(Random.Range(-173f,-7550f),740f,Random.Range(-188f,-12350f)),Quaternion.identity);
Instantiate(dort,new Vector3(Random.Range(-173f,-7550f),860f,Random.Range(-188f,-12350f)),Quaternion.identity);
Instantiate(bes,new Vector3(Random.Range(-173f,-7550f),780f,Random.Range(-188f,-12350f)),Quaternion.identity);
Instantiate(alti,new Vector3(Random.Range(-139f,-2993f),499.64f,Random.Range(-2022f,-12200f)),Quaternion.identity);
Instantiate(yedi,new Vector3(Random.Range(-139f,-2993f),499.64f,Random.Range(-2022f,-12200f)),Quaternion.identity);
Instantiate(sekiz,new Vector3(Random.Range(-4767f,-7684f),499.64f,Random.Range(-118f,-10589f)),Quaternion.identity);
Instantiate(dokuz,new Vector3(Random.Range(-4767f,-7684f),499.64f,Random.Range(-118f,-10589f)),Quaternion.identity);
if (i < 1) {
Instantiate(on,new Vector3(Random.Range(-173f,-7550f),690f,Random.Range(-188f,-12350f)),Quaternion.identity);
}
}
}
}
if(i<1)'in çalışmaması için bir sebep görmüyorum ama isterseniz o if'in içindeki Instantiate'i, for'un bir alt satırına taşımayı deneyebilirsiniz.
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 ?
Kaplar ama Update gibi fonksiyonları çalışmıyor olacağı için performansa olumsuz etkileri minimum düzeyde olur.
Ö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
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.