2022 年 5 月、Alexandre Mutel と Kristyna Hougaard は、投稿「Unity と .NET、次は何ですか?」で発表しました。 Unity は、async-await を使用する利便性など、.NET の機能をさらに採用する予定です。そして、Unity は約束を果たしているようです。 Unity 2023.1 アルファ版では、Awaitable クラスが導入され、非同期コードを記述する機会が増えました。
このセクションでは、Unity の公式ドキュメントに十分な説明があると私が考えるメソッドについては、あまり深く掘り下げません。それらはすべて非同期待機に関連しています。
Awaitable.WaitForSecondsAsync() を使用すると、指定したゲーム時間だけ待機できます。リアルタイムで待機を実行する Task.Delay() とは異なります。違いを明確にするために、コード ブロックの後半に小さな例を示します。
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."); }
この例では、Start() メソッドの開始時に、Time.timeScale を使用してゲーム時間が停止されます。実験のために、RunGameplay() メソッドで 5 秒後にフローを再開するためにコルーチンが使用されます。次に、2 つの 1 秒待機メソッドを起動します。 1 つは Awaitable.WaitForSecondsAsync() を使用し、もう 1 つは Task.Delay() を使用します。 1 秒後、コンソールに「Waiting WaitWithTaskDelay() が終了しました」というメッセージが表示されます。そして 5 秒後に「Waiting WaitWithTaskDelay() が終了しました」というメッセージが表示されます。
Unity の基本的な Player Loop の柔軟性を高めるために、他の便利なメソッドも追加されています。それらの目的は名前から明らかであり、コルーチンを使用するときのアナロジーに対応しています。
コルーチンを初めて使用する場合は、理解を深めるために自分で試してみることをお勧めします。
メソッド Awaitable.FromAsyncOperation() も、古い API AsyncOperation との後方互換性のために追加されました。
コルーチンを使用する便利さの 1 つは、コンポーネントが削除または無効化されると自動的に停止することです。 Unity 2022.2 では、MonoBehaviour に destroyCancellationToken プロパティが追加され、オブジェクトの削除時に非同期実行を停止できるようになりました。 CancellationToken の取り消しによってタスクを停止すると、OperationCanceledException がスローされることに注意してください。呼び出し元のメソッドが Task または Awaitable を返さない場合、この例外をキャッチする必要があります。
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); }
この例では、オブジェクトは Start() ですぐに破棄されますが、その前に Awake() が DoAwaitAsync() の実行を開始することに成功しています。コマンド Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) は 1 秒間待機し、「そのメッセージはログに記録されません」というメッセージを出力する必要があります。オブジェクトはすぐに削除されるため、destroyCancellationToken は OperationCanceledException をスローしてチェーン全体の実行を停止します。このように、destroyCancellationToken により、手動で CancellationToken を作成する必要がなくなります。
ただし、たとえば、オブジェクトの非アクティブ化時に実行を停止するために、これを行うことができます。例を挙げます。
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."); } } }
この形式では、この MonoBehaviour がハングしているオブジェクトがオンになっている限り、「このメッセージは毎秒ログに記録されます」というメッセージが送信されます。オブジェクトをオフにして、再びオンにすることができます。
このコードは冗長に見えるかもしれません。 Unity には、Coroutines や InvokeRepeating() など、同様のタスクをより簡単に実行できる多くの便利なツールが既に含まれています。しかし、これは使用例にすぎません。ここでは、Awaitable のみを扱っています。
Unity では、エディターで再生モードを終了した後でも、非同期メソッドの実行は自動的に停止しません。同様のスクリプトをプロジェクトに追加してみましょう。
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); } } }
この例では、Play Mode に切り替えた後、「This message is logging every second」というメッセージがコンソールに出力されます。再生ボタンを離しても出力され続けます。この例では、Awaitable.WaitForSecondsAsync() の代わりに Task.Delay() が使用されています。ここでは、アクションを表示するために、ゲーム時間ではなくリアルタイムで遅延が必要になるためです。
destroyCancellationToken と同様に、Application.exitCancellationToken を使用できます。これは、再生モードの終了時に非同期メソッドの実行を中断します。スクリプトを修正しましょう。
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); } } }
これで、スクリプトは意図したとおりに実行されます。
Unity では、Start、OnCollisionEnter、OnCollisionExit などの一部のイベント関数をコルーチンにすることができます。しかし、Unity 2023.1 以降では、Update()、LateUpdate、さらには OnDestroy() を含め、すべてを Awaitable にすることができます。
非同期実行を待機しないため、注意して使用する必要があります。たとえば、次のコードの場合:
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()"); }
コンソールでは、次の結果が得られます。
Awake() started OnEnable() Start() Awake() finished
また、非同期コードがまだ実行されている間に、MonoBehaviour 自体またはゲーム オブジェクトが存在しなくなる可能性があることも覚えておく価値があります。このような状況では:
private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }
次のフレームでは、MonoBehaviour は削除されたと見なされます。コンソールでは、次の結果が得られます。
True Flase
これは OnDestroy() メソッドにも当てはまります。メソッドを非同期にする場合は、await ステートメントの後、MonoBehaviour が既に削除されていると見なされることを考慮する必要があります。オブジェクト自体が削除されると、そのオブジェクトにある多くの MonoBehaviour の作業が、この時点で正しく機能しない可能性があります。
イベント関数を操作するときは、実行順序に注意することが重要です。非同期コードは期待どおりの順序で実行されない可能性があるため、スクリプトを設計するときはこの点に留意することが不可欠です。
Awaitable Event Functions はすべての種類の例外をキャッチすることに注意してください。これは予期しない可能性があります。私は彼らが OperationCanceledExceptions だけをキャッチすることを期待していました。ただし、すべてのタイプの例外をキャッチすると、現時点では使用に適していません。代わりに、前の例で示したように、非同期メソッドを実行して、必要なメッセージを手動でキャッチできます。
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); }
コンポーネントは開始時にすぐに削除されるため、DoAwaitAsync() の実行は中断されます。 「そのメッセージはログに記録されません」というメッセージはコンソールに表示されません。 OperationCanceledException() のみがキャッチされ、他のすべての例外がスローされる可能性があります。
今後、この方法が修正されることを願っています。現時点では、Awaitable Event Functions の使用は安全ではありません。
知られているように、ゲーム オブジェクトと MonoBehaviour を使用したすべての操作は、メイン スレッドでのみ許可されます。ゲームのフリーズにつながる大規模な計算を行う必要がある場合があります。メインスレッドの外で実行することをお勧めします。 Awaitable には、BackgroundThreadAsync() と MainThreadAsync() の 2 つのメソッドがあり、メイン スレッドから離れてメイン スレッドに戻ることができます。例を挙げます。
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}"); }
ここで、メソッドが開始されると、追加のスレッドに切り替わります。ここでは、この追加スレッドの ID をコンソールに出力します。 1 はメイン スレッドであるため、1 にはなりません。
その後、スレッドは 10 秒間凍結され (Thread.Sleep(10000))、大規模な計算がシミュレートされます。メイン スレッドでこれを行うと、ゲームは実行中にフリーズしたように見えます。しかし、この状況では、すべてが安定して機能し続けています。これらの計算で CancellationToken を使用して、不要な操作を停止することもできます。
その後、メインスレッドに戻ります。そして今、Unity のすべての機能が再び利用できるようになりました。たとえば、この場合のように、ゲーム オブジェクトを無効にしますが、これはメイン スレッドなしでは実行できませんでした。
結論として、Unity 2023.1 で導入された新しい Awaitable クラスにより、開発者は非同期コードを記述する機会が増え、レスポンシブでパフォーマンスの高いゲームを簡単に作成できるようになります。 Awaitable クラスには、WaitForSecondsAsync()、EndOfFrameAsync()、FixedUpdateAsync()、NextFrameAsync() などのさまざまな待機メソッドが含まれており、Unity の基本的なプレーヤー ループの柔軟性が向上します。 destroyCancellationToken および Application.exitCancellationToken プロパティは、オブジェクトの削除時または再生モードの終了時に非同期実行を停止する便利な方法も提供します。
Awaitable クラスは Unity で非同期コードを記述する新しい方法を提供しますが、最良の結果を得るには、コルーチンや InvokeRepeating などの他の Unity ツールと組み合わせて使用する必要があることに注意することが重要です。さらに、async-await の基本と、パフォーマンスや応答性の向上など、ゲーム開発プロセスにもたらす利点を理解することが重要です。
要約すると、Awaitable クラスは Unity 開発者にとって強力なツールですが、最良の結果を得るには、慎重に使用し、他の Unity ツールや概念と組み合わせて使用する必要があります。その機能と制限をよりよく理解するために、それを試してみることが重要です。