少し前に身内内での勉強会で取り上げられていたのが「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はこちらから入手できます。