Giriş: Esnek ve Modüler Kodun Temel Taşları
Unity ve C# ile oyun geliştirirken, kodunuzun farklı bölümlerinin birbirleriyle iletişim kurması genellikle karmaşık bir hal alabilir. Bu iletişimi düzenli, esnek ve az bağımlı hale getirmek için C# Olaylar ve Delegates yapıları kritik öneme sahiptir. Bu makalede, bu iki güçlü C# özelliğini temelden ileri seviyeye kadar detaylı bir şekilde inceleyecek, Unity projelerinizde nasıl etkin bir şekilde kullanabileceğinizi göstereceğiz.
Delegates, metotları referans olarak tutmamızı sağlayan tip güvenli işaretçiler gibidir. Olaylar (Events) ise bu Delegates üzerine inşa edilmiş, bir sınıfın belirli bir durum değişikliğini veya eylemi diğer sınıflara bildirmesi için kullanılan özel bir yapıdır. Bu sayede, kodunuz daha modüler hale gelir, bakım maliyeti azalır ve genişletilebilirliği artar.
Delegates Nedir? Metotları Referans Olarak Tutmak
Bir Delegate, belirli bir imza (parametre listesi ve dönüş tipi) ile eşleşen metotlara referans olabilen bir tiptir. Onları, bir metodu doğrudan çağırmak yerine, o metodu temsil eden bir aracı gibi düşünebilirsiniz. C++’daki fonksiyon işaretçilerine benzerler, ancak C# ortamında tip güvenliği ve nesne yönelimli programlama prensipleriyle entegredirler.
Delegate Tanımlama ve Kullanımı
Bir Delegate tanımlamak için `delegate` anahtar kelimesini kullanırız:
public delegate void OlayIsleyici();
public delegate void KarakterCanKaybetti(int kalanCan, string hasarTuru);
Yukarıdaki örnekte, `OlayIsleyici` parametre almayan ve bir şey döndürmeyen metotlara, `KarakterCanKaybetti` ise bir `int` ve bir `string` parametresi alan ve bir şey döndürmeyen metotlara referans olabilir. Bir Delegate’i kullanmak için, önce onu bir metoda atar, sonra Delegate üzerinden metodu çağırırız:
public class OyunYöneticisi
{
public delegate void OyunDurumuDegisti(string yeniDurum);
public OyunDurumuDegisti durumDegisti;
public void Başlat()
{
// Delegate'e bir metot atama
durumDegisti += OyunDurumuMesajiGoster;
durumDegisti?.Invoke("Oyun Başladı"); // Delegate'i çağırma
}
private void OyunDurumuMesajiGoster(string durum)
{
UnityEngine.Debug.Log($"Oyun Durumu: {durum}");
}
}
Delegates, aslında çoklu yayın (multicast) özelliğine sahiptir. Yani, bir Delegate örneğine birden fazla metot atayabilirsiniz. `+=` operatörü ile metot eklenir, `-=` operatörü ile metot çıkarılır. Bu, bir olaya birden fazla dinleyicinin abone olmasını sağlar.
Events Nedir? Bildirim Mekanizması
Delegates tek başına güçlü olsa da, dışarıdan doğrudan erişilebilmeleri bazı güvenlik ve tasarım sorunlarına yol açabilir. Örneğin, birisi yanlışlıkla veya kötü niyetle bir Delegate’in tüm atamalarını silip olayı tetiklemeyi engelleyebilir. İşte bu noktada `event` anahtar kelimesi devreye girer.
C# Olaylar ve Delegates arasındaki ilişki şudur: Events, Delegates’in kapsüllenmiş halidir. Bir `event` tanımladığınızda, aslında bir Delegate örneği oluşturur ve ona erişimi kısıtlarsınız. Dışarıdan sadece `+=` ve `-=` operatörleri ile abone olunup çıkılabilir, ancak doğrudan `Invoke` metodu çağrılamaz veya atamalar silinemez. Bu, olayı yalnızca onu tanımlayan sınıfın tetikleyebileceği anlamına gelir.
Event Tanımlama ve Kullanımı
Bir `event` tanımlamak için `event` anahtar kelimesini Delegate tipiyle birlikte kullanırız:
public class Karakter
{
public event Action OnCanKaybetti;
public event EventHandler<CanKaybettiArgs> OnCanKaybettiDetayli;
private int can = 100;
public void HasarAl(int miktar)
{
can -= miktar;
if (can <= 0)
{
can = 0;
// Olayı tetikleme
OnCanKaybetti?.Invoke();
OnCanKaybettiDetayli?.Invoke(this, new CanKaybettiArgs { KalanCan = can, HasarMiktari = miktar });
}
}
}
public class CanKaybettiArgs : EventArgs
{
public int KalanCan { get; set; }
public int HasarMiktari { get; set; }
}
Yukarıdaki örnekte, `Karakter` sınıfı `OnCanKaybetti` olayını tanımlıyor. Bu olay, `Karakter` can kaybettiğinde tetiklenir. `Action` ve `EventHandler` .NET’in hazır Delegate tipleridir. `Action` parametresiz, `Action
Event’e Abone Olma ve Abonelikten Çıkma
Başka bir sınıf, bu olaya abone olarak can kaybı durumunu dinleyebilir:
public class UIYöneticisi
{
public Karakter oyuncuKarakteri;
public void Start()
{
if (oyuncuKarakteri != null)
{
oyuncuKarakteri.OnCanKaybetti += CanKaybıMesajıGoster;
oyuncuKarakteri.OnCanKaybettiDetayli += CanKaybıDetaylıMesajıGoster;
}
}
private void CanKaybıMesajıGoster()
{
UnityEngine.Debug.Log("Oyuncu can kaybetti!");
}
private void CanKaybıDetaylıMesajıGoster(object sender, CanKaybettiArgs e)
{
UnityEngine.Debug.Log($"Oyuncu can kaybetti! Kalan Can: {e.KalanCan}, Alınan Hasar: {e.HasarMiktari}");
}
public void OnDestroy()
{
// Bellek sızıntılarını önlemek için abonelikten çıkmak önemlidir!
if (oyuncuKarakteri != null)
{
oyuncuKarakteri.OnCanKaybetti -= CanKaybıMesajıGoster;
oyuncuKarakteri.OnCanKaybettiDetayli -= CanKaybıDetaylıMesajıGoster;
}
}
}
Pratik İpuçları ve En İyi Uygulamalar
1. Bellek Sızıntılarını Önlemek İçin Abonelikten Çıkın
Bir objenin ömrü sona erdiğinde (özellikle Unity’de bir `GameObject` yok edildiğinde), eğer hala bir olaya abone olmuşsa, olay kaynağı o objeye referans tutmaya devam eder. Bu, objenin çöp toplayıcı tarafından temizlenmesini engeller ve bellek sızıntılarına yol açar. Bu nedenle, abone olduğunuz her olaydan, ilgili objenin ömrü sona erdiğinde (`OnDestroy` veya `OnDisable` metotlarında) abonelikten çıkmayı unutmayın. Bu, C# Olaylar ve Delegates kullanımında en kritik adımlardan biridir.
2. Olay Çağırmadan Önce Null Kontrolü Yapın
Bir olaya hiçbir dinleyici abone olmamışsa, olayı tetiklemeye çalıştığınızda bir `NullReferenceException` alırsınız. Bu durumu önlemek için, olayı tetiklemeden önce Delegate’in `null` olup olmadığını kontrol etmek önemlidir. C# 6.0 ve sonraki sürümlerde `?.Invoke()` operatörü bu işi kolaylaştırır:
MyEvent?.Invoke();
3. `EventHandler` Desenini Kullanın
Standart .NET olay desenini takip etmek, kodunuzu daha okunabilir ve anlaşılır hale getirir. Bu desen, olayı tetikleyen objeyi (`sender`) ve olayla ilgili verileri (`EventArgs`’tan türetilmiş bir nesne) taşır. Bu, özellikle karmaşık olay sistemlerinde faydalıdır ve C# Olaylar ve Delegates kullanımında standart bir yaklaşımdır.
4. UnityEvent Kullanımını Değerlendirin
Unity’nin kendi `UnityEvent` sistemi, C# olaylarına bir alternatiftir ve özellikle editörde görsel olarak olaylara metot atamak istediğinizde çok kullanışlıdır. `UnityEvent`’ler, Inspector üzerinden metotları sürükle-bırak ile atamanıza olanak tanır. Ancak, performans açısından native C# Delegates’e göre biraz daha yavaş olabilirler (reflection kullandıkları için) ve daha dinamik, kod tabanlı olay yönetimi için C# Events genellikle daha esnektir.
Yaygın Hatalar ve Çözümleri
Hata 1: Abonelikten Çıkmayı Unutmak
Sorun: Bir objenin bir olaya abone olup, objenin yok olmasına rağmen abonelikten çıkmaması, bellek sızıntısına neden olur. Olay kaynağı, artık var olmayan objeye bir referans tutar.
Çözüm: Abone olduğunuz her olayı, ilgili objenin yaşam döngüsü sona erdiğinde (`OnDisable`, `OnDestroy` gibi Unity metotlarında) `-=` operatörü ile kaldırdığınızdan emin olun.
Hata 2: Olaya Doğrudan Atama Yapmak (`=`)
Sorun: Bir `event`’e `+=` yerine `=` kullanarak metot atamaya çalışmak derleyici hatası verir. Bu, `event` anahtar kelimesinin sağladığı kapsüllemenin bir sonucudur.
Çözüm: Her zaman `+=` operatörünü kullanarak olaylara abone olun. Bu, bir olaya birden fazla dinleyicinin eklenmesini sağlar ve mevcut dinleyicileri silmez.
Hata 3: Null Kontrolü Yapmadan Olayı Tetiklemek
Sorun: Hiçbir abonesi olmayan bir olayı doğrudan `Invoke()` ile çağırmaya çalışmak `NullReferenceException`’a yol açar.
Çözüm: Olayı tetiklemeden önce `?.Invoke()` operatörünü kullanın veya manuel olarak `if (MyEvent != null) { MyEvent.Invoke(); }` şeklinde kontrol yapın.
Performans ve Optimizasyon Notları
C# Olaylar ve Delegates genellikle performansa önemli bir yük getirmezler. Bir Delegate çağrısı, doğrudan metot çağrısına çok yakındır. Ancak, dikkat edilmesi gereken bazı noktalar vardır:
- Bellek Ayırma: Her Delegate ve Event aboneliği için küçük miktarda bellek ayrılır. Çok sayıda kısa ömürlü olay abonesi oluşturup yok etmek (özellikle `Update` döngüsü içinde), çöp toplama (Garbage Collection – GC) üzerinde hafif bir etki yaratabilir. Bu tür durumlardan kaçınmak en iyisidir.
- Abonelikten Çıkma: Bellek sızıntılarını önlemenin yanı sıra, abonelikten çıkmak, GC’nin kullanılmayan objeleri temizlemesine olanak tanır ve böylece oyununuzun performansını olumlu etkiler.
- UnityEvent vs. C# Events: `UnityEvent`’ler, `C# Events`’e göre genellikle biraz daha yavaştır çünkü Inspector’daki atamaları runtime’da reflection kullanarak çözerler. Çoğu durumda bu fark önemsizdir, ancak çok yüksek frekanslı veya performans kritik olaylar için native C# Events tercih edilebilir.
Sonuç
C# Olaylar ve Delegates, Unity projelerinizde modüler, esnek ve bakımı kolay kod yazmak için vazgeçilmez araçlardır. Delegates metotları referans olarak tutarken, Events bu Delegates’leri güvenli ve kontrollü bir bildirim mekanizması olarak kapsüller. Bu yapıları doğru bir şekilde kullanarak, oyunlarınızın farklı bileşenleri arasında güçlü ama gevşek bağlı bir iletişim ağı kurabilirsiniz. Bellek sızıntılarını önlemek için abonelikten çıkmayı, null kontrolü yapmayı ve standart olay desenlerini takip etmeyi unutmayın. Bu bilgilerle Unity projelerinizde daha temiz ve daha ölçeklenebilir bir kod tabanı oluşturmaya hazırsınız!



