【Unity】UniTaskによるasync/await を使った非同期処理

少し前に身内内での勉強会で取り上げられていたのが「UniTask」です。 現状コルーチンを行っている非同期の処理を c#のasync/awaitを使って良い感じでUnityで使えるとのことで、今後新規でゲームを作成するときに利用する機会が多くなるかもしれません。 簡単ですが「UniRx」でどういったことができるのかを調べてみました。

UniTask

「UniTask」はUniRxをプロジェクトへ追加をすれば利用することができます。

導入

現状(2019/1)の最新のUnityバージョンである2018.3.0.f2を使っています。 まずはAssetStoreから「UniRx」をダウンロードし、プロジェクトへインストールします。 https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276

namespace

UniTaskを使うときにはnamespaceを指定します。 [c] using UniRx.Async; [/c]

async/await

今まではコルーチンを使っていた非同期処理がasync/awaitでも実現することができます。

非同期メソッド

頭にasyncをつけたメソッドのこと。 [c] async void Start() {

} [/c]

このメソッド内にawaitを実装することができる。 例えば1000ミリ秒待つときには「UniTask.Delay()」を使う。 [c] async void Start() { await UniTask.Delay(1000); Debug.Log("1000㍉秒待つ"); } [/c]

こちらはコルーチンだと次のような記述で書くこともできました。 [c] private IEnumerator Hoge() { yield return new WaitForSeconds(1.0f); } [/c]

UniTaskにはdelayを含めてたくさんの便利なメソッドが良いされています。

並列実行

UniTask.WhenAll()を使えば、引数で指定した処理を並列に実行してくれます。 すべての処理が終わればその結果を受け取ることができます。 [c] async UniTask DoAsync() { var task1 = GetString("太郎",1); var task2 = GetString("次郎",100); var task3 = GetString("三郎",10); var (name1, name2, name3) = await UniTask.WhenAll(task1, task2, task3); Debug.Log(name1 + name2 + name3);

} [/c]

タプル

ここで見慣れないのはタプル記法で書かれた箇所です。 タプル記法について [c] var (name1, name2, name3) = await UniTask.WhenAll(task1, task2, task3); [/c]

名前のない型がタプルと呼ばれています。 UniTask.WhenAll()を使えば結果をこのタプルで取得ができます。

ラムダ式

UniTask.Run()を使うと、ラムダ式内の処理を実装できます。 戻り値も設定ができます。

[c] var hoge = await UniTask.Run(() => { return " hoge "; }); Debug.Log(hoge); //hogeと出力 [/c]

遅延

UniTask.Delayを使えば、引数に指定したミリ秒処理を待機させることができます。 [c] await UniTask.Delay(1000); [/c]

その他、UniTask.DelayFrameを使えば、引数に指定したフレーム分待機をさせることができます。 [c] Await UniTask.DelayFrame(1f); [/c]

PlayerLoopと同期させる

PlayerLoopに合わせて、特定の処理を実行できます。 [c] await UniTask.Yield(PlayerLoopTiming.FixedUpdate); [/c]

GetAsyncEventHandler

イベントの発火を待機することができます。 例えば、あるボタンが押されるまで待機するなどの処理ができます。 [c] var buttonEvent = _button.onClick.GetAsyncEventHandler(CancellationToken.None); await buttonEvent.OnInvokeAsync(); Debug.Log("ボタン押した"); [/c]

コルーチンを待つ

UniTaskを使えばコルーチンと併用をする機会は少ないかもしれませんが、特定のコルーチンを待つこともできます。 [c] await HogeCoroutine;

private IEnumerator HogeCoroutine() { yield return new WaitForSeconds(1.0f); } [/c]

キャンセル処理

UniTaskの欠点はキャンセル処理が面倒くさいことです。 MonoBehaviourに依存していないので、GameObjectを削除しても消えません。

  • CancellationTokenを発行する
  • CancellationTokenSource.Cancel()を実行する
  • 非同期処理にキャンセル時の挙動を記載する

CancellationTokenを発行する

ローカル変数なりグローバル変数なりでCancellationTokenSourceを定義し、必要な非同期処理にtokenを渡して上げます。 [c] // CancellationTokenを発行する private CancellationTokenSource _tokenSource;

private void Start() { _tokenSource = new CancellationTokenSource();

// 引数にCancellationTokenを渡す CancellTest(_tokenSource.Token).Forget(); } [/c]

CancellationTokenSource.Cancel()を実行する

必要なときにCancel()を呼びます。 [c] void OnDestroy() { _tokenSource?.Cancel(); } [/c]

非同期処理にキャンセル時の挙動を記載する

CancellationTokenSource.Cancel()を呼び出したときに「例外」が発生するようにします。 [c] await UniTask.Delay(TimeSpan.FromSeconds(5), cancellationToken: cancellationToken);

try { Debug.Log("正常"); } catch (OperationCanceledException) { // キャンセル時に実行したい処理があればtry~catchで捕捉 Debug.Log("Canceled"); throw; } [/c]

キャンセル時の挙動は例外を飛ばす方法以外にも、if文でも判定ができたりしますが、複雑な処理には不向きな気がします。

最後に

とりあえず、UniTaskを少し触ってみました。 コルーチンの処理に慣れていると、UniTaskによる非同期処理は複雑な感じがしました(特にキャンセル時) 色々と新しいいことができそうな予感はしているので、早く使えるようになりたい。

UniRxはこちらから入手できます。