paint-brush
Unity 開発のための完全な非同期プログラミング入門by@dmitrii
5,388
5,388

Unity 開発のための完全な非同期プログラミング入門

この記事では、そのような問題を回避する方法について説明します。これらのタスクを別のスレッドで実行する非同期プログラミング手法をお勧めします。これにより、メイン スレッドが他のタスクを実行できるようになります。これにより、スムーズで応答性の高いゲームプレイが保証され、(願わくば) ゲーマーは満足するでしょう。
featured image - Unity 開発のための完全な非同期プログラミング入門
Dmitrii Ivashchenko HackerNoon profile picture
0-item

ゲーム開発の一部のタスクは同期ではなく、非同期です。これは、ゲーム コード内で直線的に実行されないことを意味します。これらの非同期タスクには、完了するまでにかなりの時間がかかるものもあれば、集中的な計算を伴うものもあります。


最も一般的なゲームの非同期タスクの一部を次に示します。

  • ネットワーク リクエストの実行

  • シーン、リソース、およびその他のアセットのロード

  • ファイルの読み取りと書き込み

  • 意思決定のための人工知能

  • 長いアニメーション シーケンス

  • 大量のデータの処理

  • 経路探索


ここで重要なのは、すべての Unity コードが 1 つのスレッドで実行されるため、上記のようなタスクが同期的に実行されると、メイン スレッドがブロックされ、フレーム ドロップが発生することです。


みなさん、こんにちは。Dmitrii Ivashchenko と申します。MY.GAMES の開発チームの責任者です。この記事では、そのような問題を回避する方法について説明します。これらのタスクを別のスレッドで実行する非同期プログラミング手法をお勧めします。これにより、メイン スレッドが他のタスクを実行できるようになります。これにより、スムーズで応答性の高いゲームプレイが保証され、(願わくば) ゲーマーは満足するでしょう。

コルーチン

まず、コルーチンについて話しましょう。それらは 2011 年に Unity に導入されましたが、.NET に async / await が登場する前でさえありました。 Unity では、コルーチンを使用すると、一度にすべての命令を実行するのではなく、複数のフレームにわたって一連の命令を実行できます。これらはスレッドに似ていますが、軽量で Unity の更新ループに統合されているため、ゲーム開発に適しています。


(ちなみに、歴史的に言えば、コルーチンは Unity で非同期操作を実行する最初の方法だったので、インターネット上のほとんどの記事はコルーチンに関するものです。)


コルーチンを作成するには、戻り値の型がIEnumeratorの関数を宣言する必要があります。この関数には、コルーチンで実行する任意のロジックを含めることができます。


コルーチンを開始するには、 MonoBehaviourインスタンスでStartCoroutineメソッドを呼び出し、コルーチン関数を引数として渡す必要があります。

 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 には、 WaitForSecondsWaitForEndOfFrameWaitForFixedUpdateWaitForSecondsRealtimeWaitUntilなど、いくつかの yield 命令が用意されています。それらを使用すると割り当てが発生することを覚えておくことが重要であるため、可能な限り再利用する必要があります。


たとえば、ドキュメントの次のメソッドを検討してください。

 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); } }


ループが繰り返されるたびに、 new WaitForSeconds(.1f)の新しいインスタンスが作成されます。これの代わりに、作成をループの外に移動して、割り当てを回避できます。

 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**; } }


注意すべきもう 1 つの重要なプロパティは、 AsyncOperationYieldInstructionの子孫であるため、Unity が提供するすべてのAsyncメソッドでyield returnを使用できることです。

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

コルーチンの落とし穴

とはいえ、コルーチンには注意すべきいくつかの欠点もあります。


  • 長い操作の結果を返すことはできません。コルーチンに渡され、コルーチンが終了したときに呼び出されてデータを抽出するコールバックが必要です。
  • コルーチンは、それを起動するMonoBehaviourに厳密に関連付けられています。 GameObjectがオフまたは破棄されると、コルーチンの処理が停止します。
  • yield 構文が存在するため、 try-catch-finally構造は使用できません。
  • 次のコードが実行を開始する前に、 yield returnの後、少なくとも 1 つのフレームが通過します。
  • ラムダとコルーチン自体の割り当て

約束

Promise は、非同期操作を整理して読みやすくするためのパターンです。多くのサードパーティの JavaScript ライブラリで使用されているため人気があり、ES6 以降ではネイティブに実装されています。


Promise を使用すると、非同期関数からすぐにオブジェクトが返されます。これにより、呼び出し元は操作の解決 (またはエラー) を待つことができます。


基本的に、これにより、非同期メソッドが値を返し、同期メソッドのように「動作」できるようになります。最終的な値をすぐに返すのではなく、将来いつか値を返すという「約束」を与えます。


Unity にはいくつかの Promises 実装があります。


Promise を操作する主な方法は、コールバック関数を使用することです。


Promise が解決されたときに呼び出されるコールバック関数と、Promise が拒否された場合に呼び出される別のコールバック関数を定義できます。これらのコールバックは、非同期操作の結果を引数として受け取り、それを使用してさらに操作を実行できます。


Promises/A+ 組織これらの仕様によると、Promise は次の 3 つの状態のいずれかになります。


  • Pending : 初期状態。これは、非同期操作がまだ進行中であり、操作の結果がまだ不明であることを意味します。
  • Fulfilled ( Resolved ): 解決された状態には、操作の結果を表す値が伴います。
  • Rejected : 非同期操作が何らかの理由で失敗した場合、Promise は「拒否された」と言われます。拒否された状態には、失敗の理由が伴います。

約束の詳細

さらに、ある Promise の結果を使用して別の Promise の結果を決定できるように、Promise を連鎖させることができます。


たとえば、サーバーからデータをフェッチする Promise を作成し、そのデータを使用して、計算やその他のアクションを実行する別の Promise を作成できます。

 var promise = MakeRequest("https://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; }


Promiseでコルーチンをラップすることもできます:

 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(); }


そしてもちろん、 ThenAll / Promise.AllThenRace / Promise.Raceを使用して、Promise の実行順序を自由に組み合わせて整理できます。

 // 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") ) );

promise の「見込みのない」部分

Promise には便利な使い方がありますが、いくつかの欠点もあります。


  • オーバーヘッド: Promise の作成には、コルーチンなどの非同期プログラミングの他の方法を使用する場合と比較して、追加のオーバーヘッドが伴います。場合によっては、これによりパフォーマンスが低下する可能性があります。
  • デバッグ: Promise のデバッグは、他の非同期プログラミング パターンのデバッグよりも難しい場合があります。実行フローを追跡してバグの原因を特定することは困難な場合があります。
  • 例外処理: 例外処理は、他の非同期プログラミング パターンと比較して、Promise でより複雑になる可能性があります。 Promise チェーン内で発生するエラーや例外を管理するのは難しい場合があります。

非同期/待機タスク

async/await 機能は、バージョン 5.0 (2012) 以降 C# の一部であり、Unity 2017 で .NET 4.x ランタイムの実装とともに導入されました。


.NET の歴史では、次の段階を区別できます。


  1. EAP (イベントベースの非同期パターン): このアプローチは、操作の完了時にトリガーされるイベントと、この操作を呼び出す通常のメソッドに基づいています。
  2. APM (非同期プログラミング モデル): このアプローチは 2 つの方法に基づいています。 BeginSmthメソッドはIAsyncResultインターフェイスを返します。 EndSmthメソッドはIAsyncResultを受け取ります。 EndSmth呼び出し時に操作が完了していない場合、スレッドはブロックされます。
  3. TAP (タスクベースの非同期パターン): この概念は、async/await とタイプTaskおよびTask<TResult>の導入によって改善されました。


最後のアプローチが成功したため、以前のアプローチは廃止されました。

非同期メソッドを作成するには、メソッドをキーワードasyncでマークし、内部にawaitを含め、戻り値をTaskTask<T>またはvoidにする必要があります (推奨されません)。

 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 }


この例では、実行は次のように行われます。


  1. 最初に、最初の非同期操作 ( SyncMethodA ) の呼び出しに先行するコードが実行されます。
  2. 最初の非同期操作await Task.Delay(1000)が起動され、実行されることが期待されます。一方、非同期操作が完了したとき (「継続」) に呼び出されるコードは保存されます。
  3. 最初の非同期操作が完了した後、「継続」 — 次の非同期操作 ( SyncMethodB ) までのコードが実行を開始します。
  4. 2 番目の非同期操作 ( await Task.Delay(2000) ) が開始され、実行されることが期待されます。同時に、継続 — 2 番目の非同期操作 ( SyncMethodC ) に続くコードが保持されます。
  5. 2 番目の非同期操作の完了後、 SyncMethodCが実行され、続いて 3 番目の非同期操作が実行され、 await Task.Delay(3000)待機します。


これは簡単な説明です。実際、async/await は、非同期メソッドを便利に呼び出してその完了を待機できるようにするための構文糖衣です。


WhenAllWhenAnyを使用して、実行順序の任意の組み合わせを編成することもできます。

 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#コンパイラは、async/await 呼び出しをIAsyncStateMachineステート マシンに変換します。これは、非同期操作を完了するために実行する必要がある一連のアクションです。


await 操作を呼び出すたびに、ステート マシンはその作業を完了し、その操作の完了を待ちます。その後、次の操作の実行を続けます。これにより、メイン スレッドをブロックせずにバックグラウンドで非同期操作を実行できるようになり、非同期メソッド呼び出しがより簡単になり、読みやすくなります。


したがって、 Exampleメソッドは、注釈[AsyncStateMachine(typeof(ExampleStateMachine))]を使用してステート マシンを作成および初期化するように変換され、ステート マシン自体には、await 呼び出しの数に等しい数の状態があります。


  • 変換されたメソッドの例Example

     [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; }


  • 生成されたステート マシンの例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呼び出しでは、現在の同期コンテキストSynchronizationContextが取得されます。 SynchronizationContext は、一連の非同期操作の実行を制御するコンテキストを表すために使用される C# の概念です。複数のスレッド間でコードの実行を調整し、コードが特定の順序で実行されるようにするために使用されます。 SynchronizationContext の主な目的は、マルチスレッド環境での非同期操作のスケジューリングと実行を制御する方法を提供することです。


環境が異なれば、 SynchronizationContextの実装も異なります。たとえば、.NET には次のようなものがあります。


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


Unity には独自の同期コンテキストUnitySynchronizationContextもあり、PlayerLoop API へのバインディングで非同期操作を使用できます。次のコード例は、 Task.Yield()を使用して各フレームでオブジェクトを回転する方法を示しています。

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


Unity で async/await を使用してネットワーク リクエストを行う別の例:

 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のおかげで、このコードの実行はメインの Unity スレッドで続行されるため、非同期操作が完了した直後にUnityEngineメソッド ( Debug.Log()など) を安全に使用できます。

TaskCompletionSource<T>

このクラスを使用すると、 Taskオブジェクトを管理できます。これは、古い非同期メソッドを TAP に適応させるために作成されましたが、何らかのイベントで長時間実行される操作をTaskでラップしたい場合にも非常に役立ちます。


次の例では、 taskCompletionSource内のTaskオブジェクトは開始から 3 秒後に完了し、その結果をUpdateメソッドで取得します。

 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# では、キャンセル トークンを使用して、タスクまたは操作をキャンセルする必要があることを通知します。トークンはタスクまたは操作に渡され、タスクまたは操作内のコードはトークンを定期的にチェックして、タスクまたは操作を停止する必要があるかどうかを判断できます。これにより、タスクまたは操作を突然強制終了するのではなく、クリーンで適切なキャンセルが可能になります。


キャンセル トークンは、長時間実行されるタスクをユーザーがキャンセルできる場合や、ユーザー インターフェイスのキャンセル ボタンのように、タスクが不要になった場合によく使用されます。


全体的なパターンは、 TaskCompletionSourceの使用に似ています。まず、 CancellationTokenSourceが作成され、次にそのTokenが非同期操作に渡されます。

 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(); } }


操作がキャンセルされると、 OperationCanceledExceptionがスローされ、 Task.IsCanceledプロパティがtrueに設定されます。

Unity 2022.2 の新しい非同期機能

Taskオブジェクトは Unity ではなく .NET ランタイムによって管理されることに注意することが重要です。タスクを実行しているオブジェクトが破棄された場合 (またはエディターでゲームがプレイ モードを終了した場合)、タスクは Unity と同じように実行され続けます。キャンセルするわけにはいきません。


対応するCancellationTokenawait Taskに常に添付する必要があります。これはコードの冗長性につながり、Unity 2022.2 ではMonoBehaviourレベルとApplicationレベル全体でビルトイン トークンが登場しました。


MonoBehaviourオブジェクトのdestroyCancellationTokenを使用すると、前の例がどのように変化するかを見てみましょう。

 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); } } }


CancellationTokenSource手動で作成し、 OnDestroyメソッドでタスクを完了する必要はなくなりました。特定のMonoBehaviourに関連付けられていないタスクについては、 UnityEngine.Application.exitCancellationTokenを使用できます。これにより、再生モード (エディタ内) を終了するとき、またはアプリケーションを終了するときにタスクが終了します。

ユニタスク

.NET タスクを使用すると便利で機能が提供されますが、Unity で使用すると重大な欠点があります。


  • Taskオブジェクトは扱いにくく、多くの割り当てが発生します。
  • Taskが Unity スレッド (シングルスレッド) に対応していません。


UniTaskライブラリは、スレッドやSynchronizationContextを使用せずにこれらの制限をバイパスします。 UniTask<T>構造体ベースの型を使用することで、割り当てがないことを実現します。


UniTask には .NET 4.x スクリプティング ランタイム バージョンが必要で、Unity 2018.4.13f1 が公式にサポートされている最低バージョンです。


また、拡張メソッドを使用して、すべてのAsyncOperations UnitTaskに変換できます。

 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 } } }


この例では、 LoadAssetメソッドはResources.LoadAsyncを使用してアセットを非同期的に読み込みます。次に、 AsUniTaskメソッドを使用して、 LoadAsyncによって返されたAsyncOperationを待機可能なUniTaskに変換します。


以前と同様に、 UniTask.WhenAllUniTask.WhenAnyを使用して、実行順序の任意の組み合わせを編成できます。

 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 には、パフォーマンスを向上させるためにUnitySynchronizationContextを置き換えるために使用できるUniTaskSynchronizationContextと呼ばれるSynchronizationContextの別の実装があります。

待望のAPI

Unity 2023.1 の最初のアルファ版では、 Awaitableクラスが導入されました。待機可能なコルーチンは、Unity で実行するように設計された非同期/待機互換のタスクのようなタイプです。 .NET タスクとは異なり、ランタイムではなくエンジンによって管理されます。

 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"); // ... }


これらは、非同期メソッドの戻り値の型として待機して使用できます。 System.Threading.Tasksと比較すると、これらは洗練されていませんが、Unity 固有の仮定に基づいてパフォーマンスを向上させるショートカットを使用します。


.NET タスクとの主な違いは次のとおりです。


  • Awaitableオブジェクトは 1 回だけ待機できます。複数の非同期関数で待機することはできません。
  • Awaiter.GetResults()完了するまでブロックしません。操作が完了する前に呼び出すと、未定義の動作になります。
  • ExecutionContextをキャプチャしないでください。セキュリティ上の理由から、.NET タスクは、非同期呼び出し間で偽装コンテキストを伝達するために、待機中に実行コンテキストをキャプチャします。
  • SynchronizationContextをキャプチャしないでください。コルーチンの継続は、完了を発生させたコードから同期的に実行されます。ほとんどの場合、これは Unity メイン フレームからのものです。
  • awaitables は、過剰な割り当てを防ぐためにプールされたオブジェクトです。これらは参照型であるため、異なるスタック間で参照したり、効率的にコピーしたりできます。 ObjectPool改善され、非同期ステート マシンによって生成される一般的な取得/解放シーケンスでのStack<T>境界チェックが回避されました。


時間のかかる操作の結果を取得するには、 Awaitable<T>型を使用できます。 TaskCompletitionSourceと同様に AwaitableCompletionSourceおよびAwaitableCompletionSource<T>を使用してAwaitableの完了を管理できます。

 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); } }


ゲームのフリーズにつながる大規模な計算を実行する必要がある場合があります。このためには、 Awaitable メソッドを使用することをお勧めします: BackgroundThreadAsync()およびMainThreadAsync() 。メインスレッドを終了してメインスレッドに戻ることができます。

 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); }


このように、Awaitables は .NET タスクを使用することの欠点を排除し、PlayerLoop イベントと AsyncOperations の待機も可能にします。

結論

ご覧のとおり、Unity の開発に伴い、非同期操作を整理するためのツールがますます増えています。

団結

コルーチン

約束

.NET タスク

ユニタスク

組み込みのキャンセル トークン

待望のAPI

5.6





2017.1




2018.4



2022.2


2023.1


Unity での非同期プログラミングの主な方法をすべて検討しました。タスクの複雑さと使用している Unity のバージョンに応じて、Coroutines と Promises から Tasks と Awaitables まで幅広いテクノロジを使用して、ゲームでスムーズでシームレスなゲームプレイを確保できます。お読みいただきありがとうございます。次の傑作をお待ちしております。