paint-brush
Unity Geliştirme için Eksiksiz Eşzamansız Programlama Başlangıç Kılavuzuile@dmitrii
5,692 okumalar
5,692 okumalar

Unity Geliştirme için Eksiksiz Eşzamansız Programlama Başlangıç Kılavuzu

ile Dmitrii Ivashchenko31m2023/03/30
Read on Terminal Reader
Read this story w/o Javascript

Çok uzun; Okumak

Bu yazımızda bu tür sorunlardan kaçınmanın yollarını konuşacağız. Bu görevleri ayrı bir iş parçacığında gerçekleştirmek için eşzamansız programlama tekniklerini önereceğiz, böylece ana iş parçacığını diğer görevleri gerçekleştirmek için serbest bırakacağız. Bu, sorunsuz ve duyarlı bir oyun deneyiminin ve (umarız) oyuncuların memnun kalmasının sağlanmasına yardımcı olacaktır.
featured image - Unity Geliştirme için Eksiksiz Eşzamansız Programlama Başlangıç Kılavuzu
Dmitrii Ivashchenko HackerNoon profile picture
0-item

Oyun geliştirmedeki bazı görevler eşzamanlı değildir; eşzamansızdırlar. Bu, oyun kodu içerisinde doğrusal olarak yürütülmedikleri anlamına gelir. Bu asenkron görevlerden bazılarının tamamlanması oldukça uzun zaman alabilir, bazıları ise yoğun hesaplamalarla ilişkilidir.


En yaygın eşzamansız oyun görevlerinden bazıları şunlardır:

  • Ağ isteklerini gerçekleştirme

  • Sahneleri, kaynakları ve diğer varlıkları yükleme

  • Dosyaları okuma ve yazma

  • Karar vermede yapay zeka

  • Uzun animasyon dizileri

  • Büyük miktarda verinin işlenmesi

  • Yol bulma


Şimdi, en önemlisi, tüm Unity kodu tek bir iş parçacığında çalıştığından, yukarıda bahsedilenlere benzer herhangi bir görev, eşzamanlı olarak gerçekleştirilirse ana iş parçacığının engellenmesine ve dolayısıyla çerçevenin düşmesine yol açacaktır.


Herkese merhaba, adım Dmitrii Ivashchenko ve MY.GAMES'in Geliştirme Ekibinin Başkanıyım. Bu yazımızda bu tür sorunlardan kaçınmanın yollarını konuşacağız. Bu görevleri ayrı bir iş parçacığında gerçekleştirmek için eşzamansız programlama teknikleri önereceğiz, böylece ana iş parçacığını diğer görevleri gerçekleştirmek için serbest bırakacağız. Bu, sorunsuz ve duyarlı bir oyun deneyiminin ve (umarız) oyuncuların memnun kalmasının sağlanmasına yardımcı olacaktır.

Eşyordamlar

Öncelikle koroutinler hakkında konuşalım. Bunlar, .NET'te async / wait ortaya çıkmadan önce bile 2011 yılında Unity'de tanıtıldı. Unity'de eşyordamlar, bir dizi talimatı tek seferde yürütmek yerine birden çok çerçeve üzerinde yürütmemize olanak tanır. Konulara benzerler ancak hafiftirler ve Unity'nin güncelleme döngüsüne entegre edilmişlerdir, bu da onları oyun geliştirme için çok uygun kılar.


(Bu arada, tarihsel olarak konuşursak, eşyordamlar Unity'de eşzamansız işlemleri gerçekleştirmenin ilk yoluydu, bu nedenle İnternet'teki çoğu makale onlar hakkındadır.)


Bir eşyordam oluşturmak için IEnumerator dönüş türüne sahip bir işlev bildirmeniz gerekir. Bu fonksiyon, eşyordamın yürütmesini istediğiniz herhangi bir mantığı içerebilir.


Bir eşyordam başlatmak için MonoBehaviour örneğinde StartCoroutine yöntemini çağırmanız ve eşyordam işlevini argüman olarak iletmeniz gerekir:

 public class Example : MonoBehaviour { void Start() { StartCoroutine(MyCoroutine()); } IEnumerator MyCoroutine() { Debug.Log("Starting coroutine"); yield return null; Debug.Log("Executing coroutine"); yield return null; Debug.Log("Finishing coroutine"); } }


Unity'de WaitForSeconds , WaitForEndOfFrame , WaitForFixedUpdate , WaitForSecondsRealtime , WaitUntil ve diğerleri gibi çeşitli verim talimatları vardır. Bunları kullanmanın tahsislere yol açtığını unutmamak önemlidir, dolayısıyla mümkün olan her yerde yeniden kullanılmaları gerekir.


Örneğin, bu yöntemi belgelerden düşünün:

 IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return new WaitForSeconds(.1f); } }


Döngünün her yinelemesinde yeni bir new WaitForSeconds(.1f) örneği oluşturulacaktır. Bunun yerine, yaratımı döngünün dışına taşıyabilir ve tahsislerden kaçınabiliriz:

 IEnumerator Fade() { Color c = renderer.material.color; **var waitForSeconds = new WaitForSeconds(0.2f);** for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } }


Dikkat edilmesi gereken bir diğer önemli özellik, AsyncOperation s'nin YieldInstruction soyundan gelmesi nedeniyle yield return Unity tarafından sağlanan tüm Async yöntemleriyle kullanılabilmesidir:

 yield return SceneManager.LoadSceneAsync("path/to/scene.unity");

Eşyordamların bazı olası tuzakları

Bütün bunlar söyleniyor, eşyordamların ayrıca dikkat edilmesi gereken birkaç dezavantajı var:


  • Uzun bir operasyonun sonucunu döndürmek imkansızdır. Yine de eşyordama iletilecek ve ondan herhangi bir veri çıkarmak için bittiğinde çağrılacak geri aramalara ihtiyacınız var.
  • Bir eşyordam, onu başlatan MonoBehaviour sıkı sıkıya bağlıdır. GameObject kapatılırsa veya yok edilirse eşyordamın işlenmesi durdurulur.
  • Getiri sözdiziminin varlığı nedeniyle try-catch-finally yapısı kullanılamaz.
  • Bir sonraki kodun yürütülmesine başlamadan önce yield return sonra en az bir kare geçecektir.
  • Lambdanın ve eşyordamın kendisinin tahsisi

Vaatler

Vaatler, eşzamansız işlemleri düzenlemek ve daha okunaklı hale getirmek için kullanılan bir kalıptır. Birçok üçüncü taraf JavaScript kitaplığında kullanılmaları nedeniyle popüler hale geldiler ve ES6'dan bu yana yerel olarak uygulandılar.


Promises'ı kullanırken, eşzamansız işlevinizden hemen bir nesne döndürürüz. Bu, arayanın işlemin çözümünü (veya bir hatasını) beklemesine olanak tanır.


Temel olarak bu, eşzamansız yöntemlerin değerleri döndürebilmesini ve eşzamanlı yöntemler gibi "hareket edebilmesini" sağlar: nihai değeri hemen döndürmek yerine, gelecekte bir zamanda bir değer döndüreceklerine dair bir "söz" verirler.


Unity için çeşitli Promises uygulamaları vardır:


Bir Promise ile etkileşim kurmanın ana yolu geri çağırma işlevleridir .


Bir Söz çözümlendiğinde çağrılacak bir geri çağırma işlevi ve Söz reddedilirse çağrılacak başka bir geri çağırma işlevi tanımlayabilirsiniz. Bu geri aramalar, eşzamansız işlemin sonucunu bağımsız değişkenler olarak alır ve bunlar daha sonra başka işlemler gerçekleştirmek için kullanılabilir.


Promises/A+ organizasyonunun bu spesifikasyonlarına göre bir Promise, üç durumdan birinde olabilir:


  • Pending : başlangıç durumu, eşzamansız işlemin hala devam ettiği ve işlemin sonucunun henüz bilinmediği anlamına gelir.
  • Fulfilled ( Resolved ): çözümlenen duruma, işlemin sonucunu temsil eden bir değer eşlik eder.
  • Rejected : Eşzamansız işlem herhangi bir nedenle başarısız olursa, Sözün "reddedildiği" söylenir. Reddedilen duruma başarısızlığın nedeni eşlik eder.

Promises hakkında daha fazla bilgi

Ek olarak sözler birbirine zincirlenebilir, böylece bir Sözün sonucu başka bir Sözün sonucunu belirlemek için kullanılabilir.


Örneğin, bir sunucudan bazı verileri getiren bir Promise oluşturabilir ve ardından bu verileri, bazı hesaplamaları ve diğer eylemleri gerçekleştiren başka bir Promise oluşturmak için kullanabilirsiniz:

 var promise = MakeRequest("https://some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));


Eşzamansız bir işlem gerçekleştiren bir yöntemin nasıl organize edileceğine dair bir örnek:

 public IPromise<string> MakeRequest(string url) { // Create a new promise object var promise = new Promise<string>(); // Create a new web client using var client = new WebClient(); // Add a handler for the DownloadStringCompleted event client.DownloadStringCompleted += (sender, eventArgs) => { // If an error occurred, reject the promise if (eventArgs.Error != null) { promise.Reject(eventArgs.Error); } // Otherwise, resolve the promise with the result else { promise.Resolve(eventArgs.Result); } }; // Start the download asynchronously client.DownloadStringAsync(new Uri(url), null); // Return the promise return promise; }


Ayrıca eşyordamları bir Promise içine sarabiliriz:

 void Start() { // Load the scene and then show the intro animation LoadScene("path/to/scene.unity") .Then(() => ShowIntroAnimation()) .Then( ... ); } // Load a scene and return a promise Promise LoadScene(string sceneName) { // Create a new promise var promise = new Promise(); // Start a coroutine to load the scene StartCoroutine(LoadSceneRoutine(promise, sceneName)); // Return the promise return promise; } IEnumerator LoadSceneRoutine(Promise promise, string sceneName) { // Load the scene asynchronously yield return SceneManager.LoadSceneAsync(sceneName); // Resolve the promise once the scene is loaded promise.Resolve(); }


Ve elbette, ThenAll / Promise.All ve ThenRace / Promise.Race kullanarak herhangi bir söz yürütme sırası kombinasyonunu düzenleyebilirsiniz:

 // Execute the following two promises in sequence Promise.Sequence( () => Promise.All( // Execute the two promises in parallel RunAnimation("Foo"), PlaySound("Bar") ), () => Promise.Race( // Execute the two promises in a race RunAnimation("One"), PlaySound("Two") ) );

Sözlerin “taviz vermeyen” kısımları

Tüm kullanım kolaylığına rağmen vaatlerin bazı dezavantajları da vardır:


  • Ek Yük : Söz Oluşturmak, eşyordamlar gibi diğer eşzamansız programlama yöntemlerinin kullanılmasıyla karşılaştırıldığında ek yük gerektirir. Bazı durumlarda bu, performansın düşmesine neden olabilir.
  • Hata Ayıklama : Promises'ta hata ayıklama, diğer eşzamansız programlama modellerinde hata ayıklamaktan daha zor olabilir. Yürütme akışını izlemek ve hataların kaynağını belirlemek zor olabilir.
  • İstisna İşleme : İstisna işleme, diğer eşzamansız programlama modelleriyle karşılaştırıldığında Promises ile daha karmaşık olabilir. Promise zincirinde meydana gelen hataları ve istisnaları yönetmek zor olabilir.

Eşzamansız/Bekleme Görevleri

Eşzamansız/beklemede özelliği, sürüm 5.0'dan (2012) beri C#'ın bir parçası olmuştur ve Unity 2017'de .NET 4.x çalışma zamanının uygulanmasıyla tanıtılmıştır.


.NET'in tarihinde aşağıdaki aşamalar ayırt edilebilir:


  1. EAP (Olay Tabanlı Asenkron Model): Bu yaklaşım, bir işlemin tamamlanmasıyla tetiklenen olaylara ve bu işlemi çağıran düzenli bir yönteme dayanmaktadır.
  2. APM (Asynchronous Programming Model): Bu yaklaşım iki yönteme dayanmaktadır. BeginSmth yöntemi IAsyncResult arayüzünü döndürür. EndSmth yöntemi IAsyncResult değerini alır; EndSmth çağrısı sırasında işlem tamamlanmazsa iş parçacığı engellenir.
  3. TAP (Görev Tabanlı Eşzamansız Desen): Bu kavram, eşzamansız/beklemede ve Task ve Task<TResult> türlerinin tanıtılmasıyla geliştirildi.


Son yaklaşımın başarısı nedeniyle önceki yaklaşımlar geçerliliğini yitirdi.

Eşzamansız bir yöntem oluşturmak için yöntemin async anahtar sözcüğüyle işaretlenmesi, içinde bir await içermesi ve dönüş değerinin Task , Task<T> veya void (önerilmez) olması gerekir.

 public async Task Example() { SyncMethodA(); await Task.Delay(1000); // the first async operation SyncMethodB(); await Task.Delay(2000); // the second async operation SyncMethodC(); await Task.Delay(3000); // the third async operation }


Bu örnekte yürütme şu şekilde gerçekleşecektir:


  1. İlk olarak, ilk eşzamansız işleme ( SyncMethodA ) yapılan çağrıdan önceki kod yürütülecektir.
  2. İlk eşzamansız işlem await Task.Delay(1000) başlatıldı ve yürütülmesi bekleniyor. Bu arada asenkron işlem tamamlandığında çağrılacak kod ("devamı") kaydedilecektir.
  3. İlk eşzamansız işlem tamamlandıktan sonra, "devam" — sonraki eşzamansız işleme ( SyncMethodB ) kadar olan kod yürütülmeye başlayacaktır.
  4. İkinci eşzamansız işlem ( await Task.Delay(2000) ) başlatılır ve yürütülmesi beklenir. Aynı zamanda, ikinci eşzamansız işlemi ( SyncMethodC ) takip eden kod olan devamı da korunacaktır.
  5. İkinci eşzamansız işlemin tamamlanmasından sonra SyncMethodC yürütülecek, ardından yürütülecek ve üçüncü eşzamansız işlemin beklenmesi await Task.Delay(3000) .


Bu basitleştirilmiş bir açıklamadır, çünkü asenkron/bekleme, asenkron yöntemlerin uygun şekilde çağrılmasına ve bunların tamamlanmasının beklenmesine izin veren sözdizimsel şekerdir.


Ayrıca WhenAll ve WhenAny kullanarak herhangi bir yürütme emri kombinasyonunu da düzenleyebilirsiniz:

 var allTasks = Task.WhenAll( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); allTasks.ContinueWith(t => { Console.WriteLine("All the tasks are completed"); }); var anyTask = Task.WhenAny( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); anyTask.ContinueWith(t => { Console.WriteLine("One of tasks is completed"); });

IAsyncStateMachine

C# derleyicisi, eşzamansız/beklemede çağrıları, eşzamansız işlemi tamamlamak için gerçekleştirilmesi gereken sıralı bir dizi eylemden oluşan bir IAsyncStateMachine durum makinesine dönüştürür.


Bir bekleme işlemini her çağırdığınızda, durum makinesi işini tamamlar ve o işlemin tamamlanmasını bekler, ardından bir sonraki işlemi yürütmeye devam eder. Bu, asenkron işlemlerin ana iş parçacığını engellemeden arka planda gerçekleştirilmesine olanak tanır ve aynı zamanda asenkron yöntem çağrılarını daha basit ve daha okunaklı hale getirir.


Böylece, Example yöntemi [AsyncStateMachine(typeof(ExampleStateMachine))] ek açıklamasıyla bir durum makinesi oluşturmaya ve başlatmaya dönüştürülür ve durum makinesinin kendisi, bekleme çağrılarının sayısına eşit sayıda duruma sahiptir.


  • Dönüştürülen yöntemin Example Örnek

     [AsyncStateMachine(typeof(ExampleStateMachine))] public /*async*/ Task Example() { // Create a new instance of the ExampleStateMachine class ExampleStateMachine stateMachine = new ExampleStateMachine(); // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); // Set the currentState property of the stateMachine instance to -1 stateMachine.currentState = -1; // Start the stateMachine instance stateMachine.taskMethodBuilder.Start(ref stateMachine); // Return the Task property of the taskMethodBuilder return stateMachine.taskMethodBuilder.Task; }


  • Oluşturulan bir durum makinesi örneği ExampleStateMachine

     [CompilerGenerated] private sealed class ExampleStateMachine : IAsyncStateMachine { public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; private TaskAwaiter taskAwaiter; public int paramInt; private int localInt; void IAsyncStateMachine.MoveNext() { int num = currentState; try { TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; // Call the first synchronous method SyncMethodA(); // Create a task awaiter for a delay of 1000 milliseconds awaiter3 = Task.Delay(1000).GetAwaiter(); // If the task is not completed, set the current state to 0 and store the awaiter if (!awaiter3.IsCompleted) { currentState = 0; taskAwaiter = awaiter3; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } // If the task is completed, jump to the label after the first await goto Il_AfterFirstAwait; case 0: // Retrieve the awaiter from the taskAwaiter field awaiter3 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the first await goto Il_AfterFirstAwait; case 1: // Retrieve the awaiter from the taskAwaiter field awaiter2 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the second await goto Il_AfterSecondAwait; case 2: // Retrieve the awaiter from the taskAwaiter field awaiter = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; break; Il_AfterFirstAwait: awaiter3.GetResult(); // Call the second synchronous method SyncMethodB(); // Create a task awaiter for a delay of 2000 milliseconds awaiter2 = Task.Delay(2000).GetAwaiter(); // If the task is not completed, set the current state to 1 and store the awaiter if (!awaiter2.IsCompleted) { currentState = 1; taskAwaiter = awaiter2; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } // If the task is completed, jump to the label after the second await goto Il_AfterSecondAwait; Il_AfterSecondAwait: // Get the result of the second awaiter awaiter2.GetResult(); // Call the SyncMethodC SyncMethodC(); // Create a new awaiter with a delay of 3000 milliseconds awaiter = Task.Delay(3000).GetAwaiter(); // If the awaiter is not completed, set the current state to 2 and store the awaiter if (!awaiter.IsCompleted) { currentState = 2; taskAwaiter = awaiter; // Set the stateMachine to this ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; } // Get the result of the awaiter awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ } }

Senkronizasyon Bağlamı

AwaitUnsafeOnCompleted çağrısında geçerli senkronizasyon bağlamı SynchronizationContext elde edilecektir. SynchronizationContext , C#'ta bir dizi eşzamansız işlemin yürütülmesini kontrol eden bir bağlamı temsil etmek için kullanılan bir kavramdır. Kodun birden fazla iş parçacığı boyunca yürütülmesini koordine etmek ve kodun belirli bir sırayla yürütülmesini sağlamak için kullanılır. SynchronizationContext'in temel amacı, çok iş parçacıklı bir ortamda eşzamansız işlemlerin zamanlanmasını ve yürütülmesini denetlemenin bir yolunu sağlamaktır.


Farklı ortamlarda SynchronizationContext farklı uygulamaları vardır. Örneğin, .NET'te şunlar vardır:


  • WPF : System.Windows.Threading.DispatcherSynchronizationContext
  • WinForms : System.Windows.Forms.WindowsFormsSynchronizationContext
  • WinRT : System.Threading.WinRTSynchronizationContext
  • ASP.NET : System.Web.AspNetSynchronizationContext


Unity'nin ayrıca PlayerLoop API'sine bağlanma ile eşzamansız işlemleri kullanmamızı sağlayan kendi senkronizasyon bağlamı UnitySynchronizationContext vardır. Aşağıdaki kod örneği, Task.Yield() kullanılarak her karede bir nesnenin nasıl döndürüleceğini gösterir:

 private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }


Bir ağ isteğinde bulunmak için Unity'de eşzamansız/beklemede kullanmanın başka bir örneği:

 using UnityEngine; using System.Net.Http; using System.Threading.Tasks; public class NetworkRequestExample : MonoBehaviour { private async void Start() { string response = await GetDataFromAPI(); Debug.Log("Response from API: " + response); } private async Task<string> GetDataFromAPI() { using (var client = new HttpClient()) { var response = await client.GetStringAsync("https://api.example.com/data"); return response; } } }


UnitySynchronizationContext sayesinde, bu kodun yürütülmesi ana Unity iş parçacığında devam edeceğinden, asenkron bir işlem tamamlandıktan hemen sonra UnityEngine yöntemlerini ( Debug.Log() gibi) güvenle kullanabiliriz.

Görev Tamamlama Kaynağı<T>

Bu sınıf bir Task nesnesini yönetmenize olanak tanır. Eski eşzamansız yöntemleri TAP'a uyarlamak için oluşturuldu, ancak aynı zamanda bir Task bazı olaylar üzerine uzun süren bir işlemin etrafına sarmak istediğimizde de çok kullanışlıdır.


Aşağıdaki örnekte, taskCompletionSource içindeki Task nesnesi başlangıçtan itibaren 3 saniye sonra tamamlanacak ve sonucunu Update yönteminde alacağız:

 using System.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private TaskCompletionSource<int> taskCompletionSource; private void Start() { // Create a new TaskCompletionSource taskCompletionSource = new TaskCompletionSource<int>(); // Start a coroutine to wait 3 seconds // and then set the result of the TaskCompletionSource StartCoroutine(WaitAndComplete()); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the TaskCompletionSource taskCompletionSource.SetResult(10); } private async void Update() { // Await the result of the TaskCompletionSource int result = await taskCompletionSource.Task; // Log the result to the console Debug.Log("Result: " + result); } }

İptal Jetonu

C#'ta bir görevin veya işlemin iptal edilmesi gerektiğini bildirmek için İptal Tokenı kullanılır. Belirteç göreve veya işleme iletilir ve görev veya işlem içindeki kod, görevin veya işlemin durdurulması gerekip gerekmediğini belirlemek için belirteci periyodik olarak kontrol edebilir. Bu, bir görevin veya işlemin aniden sonlandırılması yerine temiz ve zarif bir şekilde iptal edilmesine olanak tanır.


İptal Jetonları, uzun süredir devam eden bir görevin kullanıcı tarafından iptal edilebildiği veya kullanıcı arayüzündeki iptal düğmesi gibi göreve artık ihtiyaç duyulmadığı durumlarda yaygın olarak kullanılır.


Genel desen TaskCompletionSource kullanımına benzer. İlk önce bir CancellationTokenSource oluşturulur, ardından Token eşzamansız işleme aktarılır:

 public class ExampleMonoBehaviour : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource; private async void Start() { // Create a new CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); // Get the token from the CancellationTokenSource CancellationToken token = _cancellationTokenSource.Token; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } private void OnDestroy() { // Cancel the token when the object is destroyed _cancellationTokenSource.Cancel(); } }


İşlem iptal edildiğinde, bir OperationCanceledException oluşturulacak ve Task.IsCanceled özelliği true olarak ayarlanacaktır.

Unity 2022.2'deki yeni eşzamansız özellikler

Task nesnelerinin Unity tarafından değil, .NET çalışma zamanı tarafından yönetildiğini ve görevi yürüten nesne yok edilirse (veya oyun düzenleyicide oynatma modundan çıkarsa), görevin Unity'nin yaptığı gibi çalışmaya devam edeceğini unutmamak önemlidir. iptal etmenin hiçbir yolu yok.


Her zaman await Task karşılık gelen CancellationToken ile eşlik etmeniz gerekir. Bu, bir miktar kod fazlalığına yol açar ve Unity 2022.2'de MonoBehaviour düzeyinde ve tüm Application düzeyinde yerleşik belirteçler ortaya çıktı.


MonoBehaviour nesnesinin destroyCancellationToken kullanıldığında önceki örneğin nasıl değiştiğini görelim:

 using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ExampleMonoBehaviour : MonoBehaviour { private async void Start() { // Get the cancellation token from the MonoBehaviour CancellationToken token = this.destroyCancellationToken; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } }


Artık manuel olarak CancellationTokenSource oluşturmamıza ve görevi OnDestroy yönteminde tamamlamamıza gerek yok. Belirli bir MonoBehaviour ile ilişkili olmayan görevler için UnityEngine.Application.exitCancellationToken kullanabiliriz. Bu, Oynatma Modundan (Editörde) çıkarken veya uygulamadan çıkarken görevi sonlandıracaktır.

Tek Görev

Kullanım kolaylığına ve .NET Görevleri'nin sağladığı yeteneklere rağmen, Unity'de kullanıldıklarında önemli dezavantajlara sahiptirler:


  • Task nesneleri çok hantaldır ve birçok tahsise neden olur.
  • Task Unity iş parçacığı (tek iş parçacığı) ile eşleştirilmiyor.


UniTask kitaplığı, iş parçacığı veya SynchronizationContext kullanmadan bu kısıtlamaları atlar. UniTask<T> yapı tabanlı türünü kullanarak tahsislerin yokluğunu başarır.


UniTask, .NET 4.x komut dosyası oluşturma çalışma zamanı sürümünü gerektirir; Unity 2018.4.13f1, desteklenen en düşük resmi sürümdür.


Ayrıca tüm AsyncOperations uzantı yöntemleriyle UnitTask dönüştürebilirsiniz:

 using UnityEngine; using UniTask; public class AssetLoader : MonoBehaviour { public async void LoadAsset(string assetName) { var loadRequest = Resources.LoadAsync<GameObject>(assetName); await loadRequest.AsUniTask(); var asset = loadRequest.asset as GameObject; if (asset != null) { // Do something with the loaded asset } } }


Bu örnekte LoadAsset yöntemi, bir varlığı eşzamansız olarak yüklemek için Resources.LoadAsync kullanır. AsUniTask yöntemi daha sonra LoadAsync tarafından döndürülen AsyncOperation beklenebilecek bir UniTask dönüştürmek için kullanılır.


Daha önce olduğu gibi, UniTask.WhenAll ve UniTask.WhenAny kullanarak herhangi bir yürütme sırası kombinasyonunu düzenleyebilirsiniz:

 using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private async void Start() { // Start two Tasks and wait for both to complete await UniTask.WhenAll(Task1(), Task2()); // Start two Tasks and wait for one to complete await UniTask.WhenAny(Task1(), Task2()); } private async UniTask Task1() { // Do something } private async UniTask Task2() { // Do something } }


UniTask'ta, daha iyi performans için UnitySynchronizationContext yerine kullanılabilecek UniTaskSynchronizationContext adı verilen başka bir SynchronizationContext uygulaması vardır.

Beklenen API

Unity 2023.1'in ilk alfa sürümünde Awaitable sınıfı tanıtıldı. Awaitable Coroutine'ler, Unity'de çalışmak üzere tasarlanmış, eşzamansız/beklemede uyumlu Görev benzeri türlerdir. .NET Görevlerinden farklı olarak, çalışma zamanı tarafından değil motor tarafından yönetilirler.

 private async Awaitable DoSomethingAsync() { // awaiting built-in events await Awaitable.EndOfFrameAsync(); await Awaitable.WaitForSecondsAsync(); // awaiting .NET Tasks await Task.Delay(2000, destroyCancellationToken); await Task.Yield(); // awaiting AsyncOperations await SceneManager.LoadSceneAsync("path/to/scene.unity"); // ... }


Beklenebilir ve zaman uyumsuz bir yöntemin dönüş türü olarak kullanılabilirler. System.Threading.Tasks ile karşılaştırıldığında daha az karmaşıktırlar ancak Unity'ye özgü varsayımlara dayalı performans artırıcı kısayollar kullanırlar.


.NET Görevleri ile karşılaştırıldığında temel farklar şunlardır:


  • Awaitable nesnesi yalnızca bir kez beklenebilir; birden fazla eşzamansız işlev tarafından beklenemez.
  • Awaiter.GetResults() tamamlanana kadar engellemez. İşlem bitmeden onu çağırmak tanımsız bir davranıştır.
  • Asla bir ExecutionContext yakalamayın. Güvenlik nedeniyle, .NET Görevleri, kimliğe bürünme bağlamlarını eşzamansız çağrılar arasında yaymak için bekleme sırasında yürütme bağlamlarını yakalar.
  • Hiçbir zaman SynchronizationContext yakalamayın. Eşyordam devamları, tamamlamayı artıran koddan eşzamanlı olarak yürütülür. Çoğu durumda bu, Unity ana çerçevesinden olacaktır.
  • Beklentiler, aşırı tahsisleri önlemek için havuzlanmış nesnelerdir. Bunlar referans türleridir, dolayısıyla farklı yığınlarda referans verilebilir, verimli bir şekilde kopyalanabilir vb. ObjectPool eşzamansız durum makineleri tarafından oluşturulan tipik alma/bırakma dizilerindeki Stack<T> sınır denetimlerini önleyecek şekilde geliştirildi.


Uzun bir işlemin sonucunu elde etmek için Awaitable<T> tipini kullanabilirsiniz. AwaitableCompletionSource ve AwaitableCompletionSource<T> kullanarak , TaskCompletitionSource benzer şekilde bir Awaitable tamamlanmasını yönetebilirsiniz:

 using UnityEngine; using Cysharp.Threading.Tasks; public class ExampleBehaviour : MonoBehaviour { private AwaitableCompletionSource<bool> _completionSource; private async void Start() { // Create a new AwaitableCompletionSource _completionSource = new AwaitableCompletionSource<bool>(); // Start a coroutine to wait 3 seconds // and then set the result of the AwaitableCompletionSource StartCoroutine(WaitAndComplete()); // Await the result of the AwaitableCompletionSource bool result = await _completionSource.Awaitable; // Log the result to the console Debug.Log("Result: " + result); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the AwaitableCompletionSource _completionSource.SetResult(true); } }


Bazen oyunun donmasına yol açabilecek devasa hesaplamalar yapmak gerekebilir. Bunun için Awaitable yöntemlerini kullanmak daha iyidir: BackgroundThreadAsync() ve MainThreadAsync() . Ana başlıktan çıkıp ona geri dönmenizi sağlarlar.

 private async Awaitable DoCalculationsAsync() { // Awaiting execution on a ThreadPool background thread. await Awaitable.BackgroundThreadAsync(); var result = PerformSomeHeavyCalculations(); // Awaiting execution on the Unity main thread. await Awaitable.MainThreadAsync(); // Using the result in main thread Debug.Log(result); }


Bu şekilde Awaitables, .NET Görevlerini kullanmanın dezavantajlarını ortadan kaldırır ve ayrıca PlayerLoop olaylarının ve AsyncOperations'ın beklenmesine olanak tanır.

Çözüm

Gördüğümüz gibi Unity'nin gelişmesiyle birlikte asenkron işlemleri organize etmek için giderek daha fazla araç ortaya çıktı:

Birlik

Eşyordamlar

Vaatler

.NET Görevleri

Tek Görev

Yerleşik İptal Jetonları

Beklenen API

5.6





2017.1




2018.4



2022.2


2023.1


Unity'de asenkron programlamanın tüm ana yollarını düşündük. Görevinizin karmaşıklığına ve kullandığınız Unity sürümüne bağlı olarak, oyunlarınızda sorunsuz ve kesintisiz bir oyun deneyimi sağlamak için Coroutines ve Promises'tan Görevler ve Awaitables'a kadar çok çeşitli teknolojileri kullanabilirsiniz. Okuduğunuz için teşekkürler, bir sonraki şaheserlerinizi bekliyoruz.