Mayıs 2022'de Alexandre Mutel ve Kristyna Hougaard, "Unity ve .NET, sırada ne var?" gönderilerini duyurdular. Unity, eşzamansız beklemeyi kullanma kolaylığı da dahil olmak üzere .NET'in daha fazla özelliğini benimsemeyi planlıyor. Görünüşe göre Unity sözünü yerine getiriyor. Unity 2023.1 alfa sürümünde, eşzamansız kod yazmak için daha fazla fırsat sağlayan Awaitable sınıfı tanıtıldı.
Bu bölümde, bence resmi Unity belgelerinde yeterli açıklamaya sahip olan yöntemleri çok derinlemesine incelemeyeceğim. Hepsi asenkron beklemeyle ilgilidir.
Awaitable.WaitForSecondsAsync() belirli bir oyun süresi kadar beklemenize olanak tanır. Gerçek zamanlı olarak bekleme gerçekleştiren Task.Delay()'ın aksine. Farkı netleştirmeye yardımcı olmak için daha sonra kod bloğunda küçük bir örnek vereceğim.
private void Start() { Time.timeScale = 0; StartCoroutine(RunGameplay()); Task.WhenAll( WaitWithWaitForSecondsAsync(), WaitWithTaskDelay()); } private IEnumerator RunGameplay() { yield return new WaitForSecondsRealtime(5); Time.timeScale = 1; } private async Task WaitWithWaitForSecondsAsync() { await Awaitable.WaitForSecondsAsync(1); Debug.Log("Waiting WithWaitForSecondsAsync() ended."); } private async Task WaitWithTaskDelay() { await Task.Delay(1); Debug.Log("Waiting WaitWithTaskDelay() ended."); }
Bu örnekte Start() yönteminin başlangıcında oyun süresi Time.timeScale kullanılarak durdurulur. Deneyin amacına uygun olarak, RunGameplay() yönteminde 5 saniye sonra akışını sürdürmek için bir Coroutine kullanılacaktır. Daha sonra iki adet birer saniyelik bekleme yöntemini başlatıyoruz. Biri Awaitable.WaitForSecondsAsync()'yi, diğeri Task.Delay()'ı kullanıyor. Bir saniye sonra konsolda "Waiting WaitWithTaskDelay() sona erdi" mesajını alacağız. Ve 5 saniye sonra "Waiting WaitWithTaskDelay() sona erdi" mesajı görünecektir.
Unity'nin temel Oyuncu Döngüsünde size daha fazla esneklik sağlamak için başka kullanışlı yöntemler de eklendi. Amaçları adından da bellidir ve Coroutine'leri kullanırkenki benzetmelerine karşılık gelir:
Coroutine'lerle çalışmaya yeniyseniz, daha iyi bir anlayış elde etmek için bunları kendi başınıza denemenizi öneririm.
Eski API AsyncOperation ile geriye dönük uyumluluk sağlamak için Awaitable.FromAsyncOperation() adlı bir yöntem de eklenmiştir.
Coroutine'leri kullanmanın kolaylıklarından biri, bileşen kaldırıldığında veya devre dışı bırakıldığında otomatik olarak durmalarıdır. Unity 2022.2'de, MonoBehaviour'a destroyCancellationToken özelliği eklendi ve nesne silme sırasında eşzamansız yürütmeyi durdurmanıza olanak tanıdı. CancellationToken iptali yoluyla görevi durdurmanın OperationCanceledException'ı oluşturduğunu unutmamak önemlidir. Çağıran yöntem Görev veya Beklenebilir değerini döndürmezse bu istisnanın yakalanması gerekir.
private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged."); } private void Start() { Destroy(this); }
Bu örnekte, nesne Start()'ta hemen yok edilir, ancak bundan önce Awake(), DoAwaitAsync()'nin yürütülmesini başlatmayı başarır. Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) komutu 1 saniye bekler ve ardından "Bu mesaj günlüğe kaydedilmeyecek" mesajını vermelidir. Nesne hemen silindiğinden destroyCancellationToken, OperationCanceledException'ı atarak tüm zincirin yürütülmesini durdurur. Bu şekilde destroyCancellationToken bizi manuel olarak CancellationToken oluşturma zorunluluğundan kurtarır.
Ancak bunu, örneğin nesnenin devre dışı bırakılması sırasında yürütmeyi durdurmak için yine de yapabiliriz. Bir örnek vereceğim.
using System; using System.Threading; using UnityEngine; public class Example : MonoBehaviour { private CancellationTokenSource _tokenSource; private async void OnEnable() { _tokenSource = new CancellationTokenSource(); try { await DoAwaitAsync(_tokenSource.Token); } catch (OperationCanceledException) { } } private void OnDisable() { _tokenSource.Cancel(); _tokenSource.Dispose(); } private static async Awaitable DoAwaitAsync(CancellationToken token) { while (!token.IsCancellationRequested) { await Awaitable.WaitForSecondsAsync(1, token); Debug.Log("This message is logged every second."); } } }
Bu formda bu MonoBehaviour'un takıldığı nesne açık olduğu sürece "Bu mesaj her saniye kaydedilir" mesajı gönderilecektir. Nesne kapatılıp tekrar açılabilir.
Bu kod gereksiz görünebilir. Unity, benzer görevleri çok daha kolay gerçekleştirmenize olanak tanıyan Coroutines ve InvokeRepeating() gibi birçok kullanışlı aracı zaten içeriyor. Ancak bu sadece bir kullanım örneğidir. Burada sadece Awaitable ile uğraşıyoruz.
Unity'de, eşzamansız yöntem yürütme, düzenleyicide Oynatma Modundan çıktıktan sonra bile kendi kendine durmaz. Projeye benzer bir script ekleyelim.
using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { while (true) { Debug.Log("This message is logged every second."); await Task.Delay(1000); } } }
Bu örnekte, Oynatma Moduna geçtikten sonra konsola "Bu mesaj her saniye kaydediliyor" mesajı gönderilecektir. Oynat düğmesi bırakıldıktan sonra bile çıktı alınmaya devam eder. Bu örnekte Awaitable.WaitForSecondsAsync() yerine Task.Delay() kullanıldı, çünkü burada eylemi göstermek için oyun zamanında değil gerçek zamanlı bir gecikmeye ihtiyaç var.
destroyCancellationToken'a benzer şekilde, Oynatma Modundan çıkışta eşzamansız yöntemlerin yürütülmesini kesintiye uğratan Application.exitCancellationToken'ı kullanabiliriz. Senaryoyu düzeltelim.
using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { var cancellationToken = Application.exitCancellationToken; while (!cancellationToken.IsCancellationRequested) { Debug.Log("This message is logged every second."); await Task.Delay(1000, cancellationToken); } } }
Artık betik amaçlandığı gibi yürütülecektir.
Unity'de bazı olay işlevleri Coroutines olabilir; örneğin Start, OnCollisionEnter veya OnCollisionExit. Ancak Unity 2023.1'den başlayarak Update(), LateUpdate ve hatta OnDestroy() da dahil olmak üzere hepsi Beklenebilir olabilir.
Eşzamansız yürütmeleri beklenmediği için dikkatli kullanılmalıdırlar. Örneğin aşağıdaki kod için:
private async Awaitable Awake() { Debug.Log("Awake() started"); await Awaitable.NextFrameAsync(); Debug.Log("Awake() finished"); } private void OnEnable() { Debug.Log("OnEnable()"); } private void Start() { Debug.Log("Start()"); }
Konsolda aşağıdaki sonucu elde edeceğiz:
Awake() started OnEnable() Start() Awake() finished
Eşzamansız kod hala yürütülürken MonoBehaviour'un kendisinin ve hatta oyun nesnesinin varlığının sona erebileceğini de hatırlamakta fayda var. Böyle bir durumda:
private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }
Bir sonraki çerçevede MonoBehaviour'un silindiği kabul edilir. Konsolda aşağıdaki sonucu elde edeceğiz:
True Flase
Bu aynı zamanda OnDestroy() yöntemi için de geçerlidir. Yöntemi eşzamansız yaparsanız, wait deyiminden sonra MonoBehaviour'un zaten silinmiş sayılacağını dikkate almalısınız. Nesnenin kendisi silindiğinde üzerinde bulunan birçok MonoBehaviour'un çalışması bu noktada doğru çalışmayabilir.
Etkinlik işlevleriyle çalışırken yürütme sırasını bilmenin önemli olduğunu belirtmekte fayda var. Eşzamansız kod beklediğiniz sırayla yürütülmeyebilir ve komut dosyalarınızı tasarlarken bunu aklınızda tutmanız önemlidir.
Beklenebilen Olay İşlevlerinin beklenmedik olabilecek her türlü istisnayı yakaladığını belirtmekte fayda var. Yalnızca OperationCanceledExceptions'ı yakalamalarını bekliyordum ki bu daha mantıklı olurdu. Ancak her türlü istisnayı yakalamak, onları şu anda kullanıma uygun hale getirmiyor. Bunun yerine, önceki örnekte gösterildiği gibi eşzamansız yöntemleri çalıştırabilir ve gerekli mesajları manuel olarak yakalayabilirsiniz.
private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged"); } private void Start() { Destroy(this); }
Bileşen başlangıçta hemen silindiğinden DoAwaitAsync()'nin yürütülmesi kesintiye uğrayacaktır. Konsolda "Bu mesaj günlüğe kaydedilmeyecek" mesajı görünmeyecektir. Yalnızca OperationCanceledException() yakalanır, diğer tüm istisnalar atılabilir.
Bu yaklaşımın gelecekte düzeltileceğini umuyorum. Şu anda Beklenebilir Olay İşlevlerinin kullanımı güvenli değildir.
Bilindiği gibi oyun nesneleri ve MonoBehaviour'larla yapılan tüm işlemlere yalnızca ana iş parçacığında izin verilmektedir. Bazen oyunun donmasına yol açabilecek devasa hesaplamalar yapmak gerekebilir. Bunları ana iş parçacığının dışında gerçekleştirmek daha iyidir. Awaitable, ana iş parçacığından uzaklaşıp ona geri dönmeye olanak tanıyan, Arka PlanThreadAsync() ve MainThreadAsync() olmak üzere iki yöntem sunar. Bir örnek vereceğim.
private async Awaitable DoAwaitAsync(CancellationToken token) { await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(10000); await Awaitable.MainThreadAsync(); if (token.IsCancellationRequested) { return; } Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); gameObject.SetActive(false); await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); }
Burada yöntem başladığında ek bir iş parçacığına geçiş yapacaktır. Burada bu ek iş parçacığının kimliğini konsola aktarıyorum. 1 ana iş parçacığı olduğundan 1'e eşit olmayacaktır.
Daha sonra iş parçacığı 10 saniye boyunca dondurulur (Thread.Sleep(10000)) ve büyük hesaplamalar simüle edilir. Bunu ana başlıkta yaparsanız oyun yürütüldüğü süre boyunca donmuş gibi görünecektir. Ancak bu durumda her şey istikrarlı bir şekilde çalışmaya devam ediyor. Gereksiz bir işlemi durdurmak için bu hesaplamalarda CancellationToken'ı da kullanabilirsiniz.
Bundan sonra ana konuya geri dönüyoruz. Ve artık tüm Unity fonksiyonları yeniden kullanıma sunuldu. Örneğin, bu durumda olduğu gibi, ana iş parçacığı olmadan yapılması mümkün olmayan bir oyun nesnesini devre dışı bırakmak.
Sonuç olarak, Unity 2023.1'de sunulan yeni Awaitable sınıfı, geliştiricilere eşzamansız kod yazma konusunda daha fazla fırsat sunarak duyarlı ve performanslı oyunlar oluşturmayı kolaylaştırıyor. Awaitable sınıfı, WaitForSecondsAsync(), EndOfFrameAsync(), FixUpdateAsync() ve NextFrameAsync() gibi çeşitli bekleme yöntemlerini içerir ve bunlar Unity'nin temel Oynatıcı Döngüsünde daha fazla esneklik sağlar. destroyCancellationToken ve Application.exitCancellationToken özellikleri aynı zamanda nesne silme veya Oynatma Modundan çıkılma sırasında eşzamansız yürütmeyi durdurmanın uygun bir yolunu da sağlar.
Awaitable sınıfının Unity'de eşzamansız kod yazmanın yeni bir yolunu sağlamasına rağmen, en iyi sonuçları elde etmek için Coroutines ve InvokeRepeating gibi diğer Unity araçlarıyla birlikte kullanılması gerektiğini unutmamak önemlidir. Ek olarak, zaman uyumsuz beklemenin temellerini ve performansın ve yanıt verme hızının artırılması gibi oyun geliştirme sürecine getirebileceği faydaları anlamak da önemlidir.
Özetle Awaitable sınıfı, Unity geliştiricileri için güçlü bir araçtır ancak en iyi sonuçları elde etmek için diğer Unity araçları ve konseptleriyle birlikte dikkatli kullanılmalıdır. Yeteneklerini ve sınırlamalarını daha iyi anlamak için denemeler yapmak önemlidir.