İçeriğe geç
Çılgın Yazılım

Mobil ve Oyun Geliştirme kod örnekli

Nesne Havuzu (Object Pool) ile Oyunda Kare Düşmesini Önlemek

Her mermi için yeni nesne üretmek çöp toplayıcıyı tetikler ve oyun takılır. Nesne havuzu bu maliyeti nasıl sıfırlar?

Çılgın Yazılım 3 dk okuma

Nesne Havuzu (Object Pool) ile Oyunda Kare Düşmesini Önlemek
İçindekiler
  1. Problem: çöp toplayıcı duraklaması
  2. Havuzun iskeleti
  3. En büyük risk: geri vermeyi unutmak
  4. Sabit zaman adımıyla birlikte düşünün
  5. Ne zaman gerek yok?
  6. Ölçmeden karar vermeyin
  7. Sonuç
  8. İlgili Yazılar

Nesne havuzu, sık üretilip sık yok edilen nesneleri her seferinde yeniden yaratmak yerine, önceden hazırlanmış bir koleksiyondan ödünç alıp iş bitince geri vermektir. Oyun geliştirmede bu desen bir stil tercihi değil, çoğu zaman akıcı kare hızının önkoşuludur.

Nesne havuzunun tahsis grafiğine etkisi
Testere dişi şeklindeki bellek grafiği, havuz ihtiyacının en net işaretidir.

Problem: çöp toplayıcı duraklaması

Bir nişancı oyununda saniyede 60 kare çiziliyorsa ve her karede on mermi doğuyorsa, dakikada 36.000 kısa ömürlü nesne üretiyorsunuz demektir. Bu nesneler hemen ölür, ama ölmeleri bedavaya gelmez: çöp toplayıcı belirli aralıklarla devreye girip belleği tarar ve bu sırada oyun donar. Duraklama 16 milisaniyeyi aştığı anda oyuncu bunu takılma olarak hisseder.

Ortalama kare süresi iyi görünse bile takılma yaşanabilir. Çöp toplayıcı duraklamaları ortalamayı değil, en kötü yüzdelik dilimi (p99 kare süresi) bozar. Ölçerken ortalamaya değil, en kötü kareye bakın.

Havuzun iskeleti

Havuz üç işten sorumludur: hazırla, ödünç ver, geri al. Aşağıdaki uygulama JavaScript ile yazıldı ama mantık her dilde aynıdır.

JavaScript
class NesneHavuzu {
  /**
   * @param {Function} uret  Yeni nesne üreten fabrika
   * @param {Function} sifirla  Nesneyi ilk hâline döndüren fonksiyon
   * @param {number}   onIsitma  Başlangıçta kaç nesne hazırlansın
   */
  constructor(uret, sifirla, onIsitma = 64) {
    this.uret = uret;
    this.sifirla = sifirla;
    this.bosta = [];
    this.kullanimda = 0;

    // Ön ısıtma: oyun başlarken bir kez tahsis, oyun sırasında hiç
    for (let i = 0; i < onIsitma; i++) this.bosta.push(uret());
  }

  al() {
    this.kullanimda++;
    const nesne = this.bosta.pop();
    return nesne !== undefined ? nesne : this.uret();   // havuz boşsa büyüt
  }

  birak(nesne) {
    this.sifirla(nesne);
    this.bosta.push(nesne);
    this.kullanimda--;
  }

  get istatistik() {
    return { bosta: this.bosta.length, kullanimda: this.kullanimda };
  }
}

Kullanımı, mermi mantığını neredeyse hiç değiştirmez:

JavaScript
const mermiHavuzu = new NesneHavuzu(
  () => ({ x: 0, y: 0, hiz: 0, aktif: false }),
  (m) => { m.x = 0; m.y = 0; m.hiz = 0; m.aktif = false; },
  256
);

function atesEt(x, y, hiz) {
  const m = mermiHavuzu.al();
  m.x = x; m.y = y; m.hiz = hiz; m.aktif = true;
  sahne.push(m);
}

function guncelle(dt) {
  for (let i = sahne.length - 1; i >= 0; i--) {
    const m = sahne[i];
    m.y -= m.hiz * dt;

    if (m.y < 0) {
      sahne.splice(i, 1);
      mermiHavuzu.birak(m);      // ← unutulursa havuz büyür, sızıntı olur
    }
  }
}

En büyük risk: geri vermeyi unutmak

Havuzun getirdiği tek yeni hata sınıfı budur. Nesne ödünç alınır ama iade edilmezse havuz sürekli büyür ve elle yönetilen bir bellek sızıntısı elde edersiniz. İki savunma işe yarar: her karede kullanimda sayacını izlemek ve nesneyi iade eden kodu, onu alan kodun hemen yanında tutmak. Bu, farklı bellek modellerinde sahiplik kavramının neden bu kadar merkezî olduğunu da açıklar — konuyu bellek yönetimi modelleri yazımızda ele almıştık.

Sabit zaman adımıyla birlikte düşünün

Havuz, tahsis kaynaklı takılmayı çözer; ancak fizik hesabı kare süresine bağlıysa yavaş karelerde nesneler yine sıçrar. İki teknik birlikte kullanılmalıdır: havuz belleği sabitler, sabit zaman adımı ise hareketi sabitler.

Ne zaman gerek yok?

Havuz da bir tür önbellektir; hangi katmanda ne kadar tutulacağını toplu yükleme deseni yazımızdaki gibi ölçerek belirlemek gerekir. Havuz her nesne için gerekmez. Ölçüt basittir: nesne sık üretiliyor mu ve kısa mı yaşıyor? İkisi birden doğruysa aday, biri yanlışsa gereksiz karmaşıklıktır. Menü ekranındaki düğmeler için havuz kurmak, kazanç sağlamadan kod okunurluğunu düşürür. Uzun ömürlü ama tahsisi pahalı kaynaklar (ağ bağlantısı, doku belleği) ise farklı bir desenle — tembel yükleme ve önbellek — yönetilmelidir.

Ölçmeden karar vermeyin

JavaScript
// En kötü kare süresini izlemek, ortalamadan çok daha bilgilendiricidir
let kareler = [];
function olc(dt) {
  kareler.push(dt);
  if (kareler.length === 600) {
    kareler.sort((a, b) => a - b);
    console.log('medyan', kareler[300].toFixed(2), 'p99', kareler[594].toFixed(2));
    kareler = [];
  }
}

Tarayıcı tarafında bellek davranışını görmek için MDN bellek yönetimi belgeleri ve tarayıcının performans profil aracı birlikte kullanılmalıdır.

Sonuç

Nesne havuzu, oyunlarda takılmanın en yaygın sebeplerinden birini — sık tahsis ve ardından gelen çöp toplayıcı duraklamasını — ortadan kaldırır. Uygulaması kırk satırdır, ama beraberinde tek bir disiplin gerektirir: aldığınız her nesneyi geri verin. Önce ölçün, testere dişi bir bellek grafiği görüyorsanız havuzu kurun, sonra en kötü kare süresini yeniden ölçüp kazancı kanıtlayın. Bir sonraki oyun döngünüzde saniyede kaç nesne doğduğunu bir kez sayın; sayı sizi şaşırtırsa doğru deseni bulmuşsunuz demektir.

İlgili Yazılar

Beğeniniz benzer içerikleri öne çıkarmamıza yardımcı olur.

Sık sorulan sorular

Nesne havuzu her oyun için gerekli mi?

Hayır. Basit bir bulmaca oyununda kare başına birkaç nesne üretiliyorsa havuz kazanç getirmez, yalnızca karmaşıklık ekler. Havuz, saniyede yüzlerce kısa ömürlü nesne üreten sistemlerde (mermi, parçacık efekti, düşman dalgası) anlamlıdır.

Havuz boyutunu nasıl seçmeliyim?

Oyunun en yoğun anındaki eşzamanlı nesne sayısını ölçün ve üzerine pay ekleyin. Havuz boşaldığında yeni nesne üretmesine izin veren esnek bir tasarım, yanlış tahmini ölümcül olmaktan çıkarır; sadece o anda bir tahsis maliyeti oluşur.

Havuzdan alınan nesneyi sıfırlamayı unutursam ne olur?

Önceki kullanımdan kalan değerler yeni nesneye sızar ve son derece zor bulunan hatalar üretir: örneğin ölmüş bir düşmanın canı yeni düşmana geçer. Bu yüzden sıfırlama fonksiyonunu havuzun kendi sorumluluğunda tutmak, çağıran koda bırakmaktan güvenlidir.

Çöp toplayıcısı olmayan dillerde havuz gerekli mi?

C++ veya Rust gibi dillerde çöp toplayıcı duraklaması yoktur, ama tahsis işleminin kendisi ve bellek parçalanması hâlâ maliyetlidir. Bu dillerde havuz, öngörülebilir bellek düzeni sağladığı için önbellek isabet oranını da artırır — gerekçe değişir, fayda kalır.

Yazan

Çılgın Yazılım

Ben Evren Çılgın; yazılım geliştirme, web teknolojileri ve sistem tasarımı alanlarında uzmanlaşmış bir geliştiriciyim. Uzun yıllardır hem frontend hem de backend tarafında üretken, ölçeklenebilir ve kullanıcı dostu çözümler üretiyorum.

Hakkımızda Bize ulaşın

Bu konuda bir projeniz mi var?

İhtiyacınızı birkaç cümleyle anlatın; uygun yaklaşımı birlikte belirleyelim.

Projenizi Anlatın

İlgili yazılar

Yorumlar

Henüz yorum yok. Sorunuzu ya da deneyiminizi ilk siz yazın.

Yorum yazın

Yorumunuz onaylandıktan sonra yayınlanır. Ekibimiz gerekirse konuyla ilgili bir yanıt da paylaşır.

Yayınlanmaz; yalnızca gerektiğinde size ulaşmak için.

En az 10 karakter.

Tüm yazılar