Merhabalar,

Unity’de play mode’a her girişimizde (play tuşuna her bastığımızda) bir süre bekliyoruz. Projenin büyüklüğüne göre bu birkaç saniye de sürebilir onlarca saniye de. Configurable Enter Play Mode ayarlarıyla oynayarak bu süreyi devasa oranda düşürüp, play mode’a neredeyse anında girebileceğinizi biliyor muydunuz? O halde bu optimizasyon neymiş yakından bakalım!

Unity play mode’a her girişimizde scriptlerimizdeki değişkenleri ve sahnemizdeki objeleri resetliyor ve biz de saniyelerce bu operasyonu bekliyoruz. Configurable Enter Play Mode sayesinde Unity’e scriptlerimizi resetlememesini (Domain Reload) veya sahnedeki objeleri resetlememesini (Scene Reload) söyleyebiliyoruz. Bunu Edit-Project Settings-Editor-Enter Play Mode Settings‘ten yapıyoruz. Her ayarın kendisine göre dezavantajları var ki o sebepten ötürü bu optimizasyon varsayılan olarak kapalı. Ancak bu dezavantajların genelinin o ya da bu şekilde bir çözümü mevcut.

Domain Reload

Benim gözlemlediğim kadarıyla play mode’a girerken neredeyse tamamen burayı bekliyoruz. Eğer Domain Reload aktifse, Unity play mode’a her girişinizde scriptlerinizdeki değişkenleri resetliyor. Bu ayarı şu şekilde kapatabilirsiniz:

Bu ayarı kapattığınızda, artık static değişken ve event’ler play moda’a girerken veya çıkarken resetlenmez. Örneğin:

public class SkorManager
{
	public static int Skor;
	public static event Action<int> SkorDegisti;

	public static void SkorEkle(int deger)
	{
		Skor += deger;
		SkorDegisti?.Invoke(Skor);
	}
}

public class SkorUI : MonoBehaviour
{
	public Text SkorText;

	void Start()
	{
		SkorManager.SkorDegisti += SkoruGuncelle;
	}

	private void SkoruGuncelle(int skor)
	{
		SkorText.text = skor.ToString();
	}
}

Burada SkorManager sınıfında Skor isminde statik bir değişken ve SkorDegisti adında static bir callback/delegate/event var (aslında event olup olmaması Configurable Enter Play Mode’da bir fark oluşturmuyor; public static Action<int> SkorDegisti de olabilirdi). Normalde bu değişkenlerin değeri play mode’a her girişinizde resetlenir o yüzden bu kod sorunsuz şekilde çalışır. Ancak Domain Reload’u kapattığınızda artık resetlenmeyeceğinden, play mode’dan çıkıp geri girdiğinizde şu iki problem karşınıza çıkar:

  • Skor en son kaç kaldıysa oradan devam eder, otomatik sıfırlanmaz.
  • SkorDegisti event’i otomatik sıfırlanmaz, haliyle bu event’e SkorUI 2 kez kaydolmuş olur: önceki play mode’da ve şimdiki play mode’da. Hatta önceki play mode’daki objeler artık var olmadığı için, önceki play mode’un SkorUI.SkorText objesinin text değerini değiştirmeye çalışınca MissingReferenceException alırız.

Tam olarak bu problemin çözümü için, Unity RuntimeInitializeLoadType.SubsystemRegistration attribute’ünü geliştirmiş. Static bir fonksiyona bu attribute’ü verdiğinizde, oyuna başlarken bu fonksiyon otomatik olarak çağrılır. Fonksiyonun içerisinde de bu static değişken ve event’leri elle resetleyebilirsiniz:

public class SkorManager
{
    public static int Skor;
    public static event Action<int> SkorDegisti;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void StaticleriResetle()
    {
        Skor = 0;
        SkorDegisti = null;
    }

    public static void SkorEkle(int deger)
    {
        Skor += deger;
        SkorDegisti?.Invoke(Skor);
    }
}

Bu kod tamamen geçerli olsa da, ben event’lerde bu yöntemi tercih etmediğimi belirtmek istiyorum çünkü bir obje bir event’e kaydoluyorsa, o event’ten çıkmayı da kendisi yapmalı. Bu prensibi uygulamazsanız, Configurable Enter Play Mode haricinde yerlerde de problem yaşayabilirsiniz. Örneğin oyun esnasında başka bir sahneye geçtiğinizi hayal edin. SkorUI objesi DontDestroyOnLoad değilse, sahne geçişi esnasında yok olmuş olacak. Ancak hâlâ SkorDegisti event’ine kaydolmuş vaziyette. Böyle olunca da SkorDegisti event’i çağrıldığında, artık sahnede var olmayan SkorUI objesinin SkoruGuncelle fonksiyonu tetiklenecek ve bu durum envai çeşit hataya sebebiyet verebilir. Burda uygulayabileceğiniz çözüm çok basit: bir event’e Awake veya Start’ta kaydoluyorsanız, OnDestroy’da event’ten çıkın. OnEnable’da kaydoluyorsanız, OnDisable’da event’ten çıkın:

public class SkorUI : MonoBehaviour
{
    public Text SkorText;

    void Start()
    {
        SkorManager.SkorDegisti += SkoruGuncelle;
    }

    void OnDestroy()
    {
        SkorManager.SkorDegisti -= SkoruGuncelle;
    }

    private void SkoruGuncelle(int skor)
    {
        SkorText.text = skor.ToString();
    }
}

Şimdi gelelim işin keyifli kısmına: Domain Reload’u kapatıp kodda gerekli düzenlemeleri yaptıktan sonra, bunun bize ne kadar zaman kazandırdığını görelim. Tam olarak ne kadar kazanç sağladığımızı görebilmek için şu script’i projeme ekliyorum: https://gist.github.com/yasirkula/2a7b5c3a678215a7e819144f63964806. Bu script, play mode’a giriş ve çıkışın kaç saniye tuttuğunu konsola basmaya yarıyor. Akabinde Domain Reload açık ve kapalı iken play mode’a giriş yapıyorum:

  • Domain Reload açıkken: 4.4 saniye
  • Domain Reload kapalıyken: 0.46 saniye

Bu süreler projenizin ve sahnenizin büyüklüğüne göre değişiklik gösterecektir. Gördüğünüz üzere, bir kere bu optimizasyonu yaptıktan sonra bir daha geri dönmek istemeyeceksiniz 🙂

Konuyu bitirmeden önce, static değişkenleri elle resetlerken işinizi kolaylaştırabilecek bir eklenti paylaşmak istiyorum: https://github.com/joshcamas/unity-domain-reload-helper. Bu eklentiyi kurduktan sonra, static değişkenlerin başına [ClearOnReload] attribute’ü koyduğunuzda o değişkenler play mode’a girerken otomatik olarak resetlenir. Eğer bir parametre girmezseniz değişken null olur, valueToAssign=blabla derseniz girdiğiniz değere resetlenir, assignNewTypeInstance=true derseniz de sanki degisken = new Blabla() demişsiniz gibi çalışır (örneğin static List<int> list = new List<int>() şeklinde tanımlanan bir List’in içini boşaltmak için bu kullanılabilir). Maalesef bu eklenti sadece field’larda çalışmakta, yani property ve event’lerde çalışmamakta. İlaveten generic class’larda da çalışmamakta. Eklentiyi kurmak için Package Manager’da “Add package from from git URL…” seçeneğini seçip gelen kutucuğa “https://github.com/joshcamas/unity-domain-reload-helper.git” yazmanız yeterli.

Bu dersi yazarken yeni karşılaştığım ama denemediğim bir eklentiyi daha paylaşmak istiyorum. Siz isterseniz öncülük yapıp sonuçları yorum kısmında paylaşabilirsiniz: https://github.com/stalomeow/QuickPlayMode/blob/main/README_EN.md. Dokümantasyonda yazdığına göre, bu eklentide tek yapmanız gereken class’ın başına [ReloadOnEnterPlayMode] attribute’ü koymak. Artık o class’taki tüm static değişkenler, play mode’a girerken otomatik olarak default değerine ayarlanacak. Bu önceki eklentiye göre çok daha basit ve güçlü duruyor ancak dediğim gibi daha yeni gördüm ve o yüzden gerçek bir kullanıcı deneyimi paylaşamıyorum.

Scene Reload

Unity’nin söylediğine göre, bu ayarı kapattığınızda sahnedeki objeler play mode’a girerken gereksiz yere yok edilip tekrar oluşturulmaz. Açıkçası ben bu ayarı kullanmıyorum çünkü Domain Reload’a göre getirisini çok daha az buluyorum ve yan etkilerini daha çok buluyorum. Bu ayarı şu şekilde kapatabilirsiniz:

Dilerseniz Domain Reload ve Scene Reload’u beraber de kapatabilirsiniz:

Gelelim Scene Reload’un yan etkilerine:

  • Default değeri null olan private veya internal array ve List’ler, kodunuzda bir değişiklik yapıp play mode’a girdiğiniz ilk seferde null olmazlar. Yani “private int[] array = null;” diye tanımladığınız bir değişken sanki “private int[] array = new int[0];” diye tanımlanmış gibi olur. “private List<int> list = null;” diye tanımladığınız bir değişken ise sanki “private List<int> list = new List<int>();” diye tanımlanmış gibi olur. Play moddan çıkıp geri girdiğinizde bu problem yok olur (ta ki kodu yeniden compile edene kadar) ama sadece ilk seferde gerçekleşmesi bile bazen testlerinizi etkileyebilir. Eğer Domain Reload açıksa, bu problem her seferinde gerçekleşir. Bu yan etkiye karşı önlem almak için aklıma gelen tek çözüm, Awake veya OnEnable‘da ilgili değişkenleri elle null’a çekmek olur.
  • [ExecuteInEditMode] veya [ExecuteAlways] attribute’üne sahip MonoBehaviour’ların play mode’a girerken OnDestroy ve Awake fonksiyonları tetiklenmez. OnEnable ve OnDisable ise tetiklenmeye devam eder. Gördüğüm kadarıyla play mode’dan çıkarken fonksiyonlar düzgün çağrılmaya devam eder, sadece play mode’a girerken farklılık bulunmakta. Bu probleme karşı önlem almak için, Awake’te yaptığınız işlemleri OnEnable’a taşıyabilirsiniz. Ancak objenin her kapanıp açıldığında OnEnable’ının tekrar çağrıldığını unutmayın. Eğer kodun sadece bir kere çalışmasını istiyorsanız, kodun çalışıp çalışmadığını bir bool değişkende tutup değerini kontrol edip akabinde güncelleyebilirsiniz.

Reload Scene açık ve kapalı iken yaptığım performans testi sonuçları:

  • Scene Reload ve Domain Reload açıkken: 4.4 saniye
  • Sadece Scene Reload kapalıyken: 3.8 saniye
  • Sadece Domain Reload kapalıyken: 0.46 saniye
  • Scene Reload ve Domain Reload kapalıyken: 0.36 saniye

Böylelikle bu dersin de sonuna geldik. Benim en aktif olarak kullandığım optimizasyon tekniklerinden birini sizinle paylaşmaya çalıştım. Umarım faydalı olmuştur. Başka derslerde görüşmek dileğiyle!