Ö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.
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");
MonoBehaviour
sıkı sıkıya bağlıdır. GameObject
kapatılırsa veya yok edilirse eşyordamın işlenmesi durdurulur.try-catch-finally
yapısı kullanılamaz.yield return
sonra en az bir kare geçecektir.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.
Bir Promise ile etkileşim kurmanın ana yolu geri çağırma işlevleridir .
Promises/A+ bu 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.
var promise = MakeRequest("//some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));
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") ) );
.NET'in tarihinde aşağıdaki aşamalar ayırt edilebilir:
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.Task
ve Task<TResult>
türlerinin tanıtılmasıyla geliştirildi.
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:
SyncMethodA
) yapılan çağrıdan önceki kod yürütülecektir.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.SyncMethodB
) kadar olan kod yürütülmeye başlayacaktır.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.SyncMethodC
yürütülecek, ardından yürütülecek ve üçüncü eşzamansız işlemin beklenmesi await Task.Delay(3000)
.
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"); });
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.
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) { /*...*/ } }
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:
System.Windows.Threading.DispatcherSynchronizationContext
System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.WinRTSynchronizationContext
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(); } }
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("//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.
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); } }
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.
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.
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.
Task
nesneleri çok hantaldır ve birçok tahsise neden olur.Task
Unity iş parçacığı (tek iş parçacığı) ile eşleştirilmiyor.
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.
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.
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.
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.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.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.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); }
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 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |