Unity
UniRx

UniRx.Async(UniTask)機能紹介


UniRx.Async

UniRxが大型アップデートされ、バージョン6.xがリリースされました。

目玉となる機能がUniRx.Asyncという、非同期処理周りの大幅強化です。

今回はこのUniRx.Asyncがどういう機能なのかを紹介します。


導入

まずはAssetStoreよりUniRxを導入します。

image.png

ですが、UniRx.AsyncはUniRxを導入しただけでは利用することができません。

利用するためにはさらに次の2つの操作が追加で必要です。


  • .NET 4.xモードにする

  • Incremental Compilerを導入する


.NET 4.xモードにする

まず、Player Settingsから動作モードを.NET 4.xに切り替える必要があります。

image.png


Incremental Compilerを導入する

Incremental Compiler は次世代のC#コンパイラ「Roslyn」をUnityで利用するために必要なパッケージです。

UniRx.AsyncはC#7の機能に依存しているため、これを導入する必要があります。

Package Managerより導入できます。

image.png

※Incremental Compilerは2018年7月時点ではまだPreview段階です。Stableではないので商用プロダクトでの採用は気をつけましょう。


UniRx.Asyncの機能紹介


Taskより軽量な「UniTask」

UniRx.Asyncでは、非同期処理をUniTaskというクラスで扱えるようにしています。

これはTaskをUnityに最適化する形で実装された非同期処理機構です。

Taskの実装を見てもらうとわかると思うのですが、Taskは結構ヘビーな存在です。

しかもしれっとSynchronizationContextに依存してたり、それの有無によって挙動が変わったりと結構作りがヤベーやつでした。

それをよりUnityに最適化する形で再実装したものがUniTaskであり、Taskより軽量になっています。

またSynchronizationContextへの依存もありません。


asyncとUniTask

このUniTaskですが、なんとasyncの戻り値にすることができます。当然、awaitもできます。

つまり、通常のasync/awaitの返り値をTask<T>からUniTask<T>に書き換えるだけで利用することができるようになります。


asyncの戻り値をUniTaskにする

using System.Threading.Tasks;

using UniRx.Async; //必要
using UnityEngine;

public class Sample1 : MonoBehaviour
{
async void Start()
{
// こっちは普通のTaskのawait
var result = await Work();
Debug.Log(result);

// こっちはUniTaskのawait
var result2 = await Work2();

// ちゃんとメインスレッドに戻ってくる
Debug.Log(result2);

// awaitせずに、非同期処理を投げっぱなしで実行する場合は
// Forget()をつけるとコンパイラのawaitしろ警告を抑圧できる
Work2().Forget();
}

async Task<string> Work()
{
// 適当にマルチスレッド処理
return await Task.Run(() => "Hello!");
}

// 返り値をUniTaskに書き換えただけ
async UniTask<string> Work2()
{
return await Task.Run(() => "Hello!");
}
}



UniTaskの生成

asyncメソッドを使う以外にも、UniTaskを生成する方法があります。


Func<UniTask>からつくる

UniTaskのコンストラクタはFunc<UniTask>を取るようになっています。

これを用いることで、非同期遅延初期化(AsyncLazy)的なことができるようになります。


UniTaskを使った非同期遅延初期化

using UniRx.Async;

using UnityEngine;

namespace Assets
{
class ResourceLoadAsyncSample : MonoBehaviour
{
/// <summary>
/// 初回アクセス時のみ非同期的に初期化する
/// 2度目以降、処理が完了しているならキャッシュを利用する
/// </summary>
public UniTask<Texture> PlayerTexture { get; private set; }

void Awake()
{
PlayerTexture = new UniTask<Texture>(() => LoadAsSprite());
}

async UniTask<Texture> LoadAsSprite()
{
var resource = await Resources.LoadAsync<Texture>("Player");
return (resource as Texture);
}
}
}



UniTaskCompletionSourceからつくる

UniTaskCompletionSourceを使うことで、任意のタイミングで結果を発行するUniTaskを生成できます。

TaskCompletionSource<T>と大体同じです。。Rx的にいうと、Observable.Create<T>()みたいなもんです。

public UniTask<string> WrapMyAsync()

{
var utcs = new UniTaskCompletionSource<string>();

// 何かしらの非同期処理
HogeAsyncAction(result =>
{
// 成功
utcs.TrySetResult(result);
}, ex =>
{
// 失敗
utcs.TrySetException(ex);
});

return utcs.Task; //UniTask<string>を返す
}

/// <summary>
/// 結果をActionで返す非同期処理
/// </summary>
private void HogeAsyncAction(Action<string> onSuccess, Action<Exception> onError)
{
// Do something.
}


いろんなものをawaitできる

UniRx.Asyncの機能の1つとして、Unityの非同期処理をだいたいawaitすることができるようになっています。


AsyncOperationのawait

ResourceRequestなどの、AsyncOperationを継承したものをawaitすることができます。


ResourceRequestのawait

using UniRx.Async; //必要

using UnityEngine;

public class Sample2 : MonoBehaviour
{
async void Start()
{
var texture = await Resources.LoadAsync<Texture>("Player");

gameObject.GetComponent<Renderer>().material.mainTexture = texture as Texture;
}
}



UnityWebRequestAsyncOperationのawait

using UniRx.Async; //必要

using UnityEngine;
using UnityEngine.Networking;

public class Sample3 : MonoBehaviour
{
async void Start()
{
var uri =
"https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";

//テクスチャをダウンロード
var texture = await DownloadTexture(uri);

// テクスチャセット
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
}

// テクスチャをダウンロードする
async UniTask<Texture> DownloadTexture(string uri)
{
// 適当に画像のURL

var r = UnityWebRequestTexture.GetTexture(uri);

await r.SendWebRequest(); // UnityWebRequestをawaitできる

return DownloadHandlerTexture.GetContent(r);
}
}


また、WWWにも使えます。


WWWのawait

using UniRx.Async; //必要

using UnityEngine;

public class Sample4 : MonoBehaviour
{
async void Start()
{
var uri =
"https://2.bp.blogspot.com/-tcLjNKJqOIQ/WkXHUuSC4qI/AAAAAAABJX0/ArQTS8DS9SEOJI4Mb5tvZg4GXuoED8iIQCLcBGAs/s800/otaku_winter.png";

//テクスチャをダウンロード
var texture = await DownloadTexture(uri);

// テクスチャセット
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
}

// テクスチャをダウンロードする
async UniTask<Texture> DownloadTexture(string uri)
{
// 適当に画像のURL

var www = new WWW(uri);

await www; // WWWをawaitできる

return www.texture;
}
}



AsyncOperationのConfigureAwait

AsyncOperationをawaitするときにConfigureAwaitを呼び出すことで、非同期処理の進捗(Progress)を取得することができます。

using System;

using UniRx.Async; //必要
using UnityEngine;
using UnityEngine.Networking;

public class Sample5 : MonoBehaviour
{
async void Start()
{
var request = UnityWebRequest.Get("https://unity3d.com");

//ConfigureAwaitで途中経過取得
var result = await request.SendWebRequest().ConfigureAwait(new Progress<float>(x => Debug.Log(x)));

Debug.Log(result.downloadHandler.text);
}
}


コルーチンのawait

コルーチンを直接awaitできます。


Coroutine

using System.Collections;

using UniRx.Async; //必要
using UnityEngine;

public class Sample6 : MonoBehaviour
{
async void Start()
{
// コルーチン(IEnumerator)をawaitできる
await MoveCoroutine();

Debug.Log("Done!");
}

//数秒間前に進むコルーチン
IEnumerator MoveCoroutine()
{
var start = Time.time;
while (Time.time - start < 2)
{
transform.position += Vector3.forward * Time.deltaTime;
yield return null;
}
}
}



PlayerLoopに同期する

UniRx.AsyncはUnityのPlayerLoopを内部で利用しており、処理を任意のタイミングから実行させることができるようになっています。

UniTask.Yieldに、指定のPlayerLoopタイムを指定することでタイミングの切り替えが可能です。

コルーチンのWaitForFixedUpdateなどに相当します。

using UniRx.Async; //必要

using UnityEngine;

public class Sample7 : MonoBehaviour
{
async void Start()
{
Debug.Log(Time.time);

// 次のFixedUpdateのタイミングを待つ
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);

//↓↓↓これ以降はFixedUpdateと同期して実行される↓↓↓

Debug.Log(Time.time);

// 次のPostLateUpdateを待つ
await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);

//↓↓↓これ以降はPostLateUpdate(WaitForEndOfFrame)と同期して実行される↓↓↓

Debug.Log(Time.time);
}
}


指定フレーム数待つ

UniTask.DelayFrameを使うと任意のフレーム数待機できます。

using UniRx.Async; //必要

using UnityEngine;

public class Sample8 : MonoBehaviour
{
async void Start()
{
Debug.Log(Time.frameCount);

// 5フレーム待機
await UniTask.DelayFrame(5);

Debug.Log(Time.frameCount);

// FixedUpdateを5回分待機
await UniTask.DelayFrame(5, PlayerLoopTiming.FixedUpdate);

// ↓これ以降はFixedUpdateのタイミングに切り替わる点に注意↓

Debug.Log("End");
}
}


Unityコールバックイベントをawaitする

UniRx.Async.Triggersをusingすることで、各種GameObjectのコールバックイベントをUniTaskとして取得することができるようになります。

使い方は、GameObjectまたはComponentに対してGetAsync*Trigger()を呼ぶことで、各種UniTaskを返すAsyncTriggerComponentが取得できます。このコンポーネントから対象のイベントをUniTaskとして取得することができるようになっているため、それawaitすることで、イベント発生を待ち受けることができるようになります。

なお、GetAsync*Trigger()は内部でGetComponentを実行するため、取得したAsyncTriggerComponentはちゃんとキャッシュするようにしておくと良いでしょう。


OnCollisionEnterをawait

using UniRx.Async;

using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
private AsyncCollisionTrigger asyncCollisionTrigger;

void Start()
{
// AsyncTrigger(extends MonoBehaviour)を取得する
asyncCollisionTrigger = this.GetAsyncCollisionTrigger();

DoAsync().Forget();
}

async UniTask DoAsync()
{
Debug.Log("Start");

// OnCollisionEnterが発生するまで待機する
var target = await asyncCollisionTrigger.OnCollisionEnterAsync();

Debug.Log("Hit! " + target.gameObject.name);

// OnCollisionExitが発生するまで待つ
await asyncCollisionTrigger.OnCollisionExitAsync();

Debug.Log("Bye!");
}
}



UnityEventをawaitする

UnityEventは、UnityEvent.GetAsyncEventHandler()を実行するとAsyncEventHandlerというものを取得できます。

このAsyncEventHandler.OnInvokeAsync()を呼び出すことで、UnityEventのawaitができます。

(一度しかawaitしないなら、UnityEvent.OnInvokeAsync()を直接awaitしても大丈夫です。)


UnityEvent

using UniRx.Async;

using UnityEngine;
using UnityEngine.UI;

public class AwaitUnityEvent : MonoBehaviour
{
[SerializeField] private Button button;

void Start()
{
DoAsync().Forget();
}

async UniTask DoAsync()
{
// UnityEventをGetAsyncEventHandlerに変換
var asyncEventHandler = button.onClick.GetAsyncEventHandler();

// ボタンが押されるまで待つ
await asyncEventHandler.OnInvokeAsync();

Debug.Log("1");

// ボタンが押されるまで待つ
await asyncEventHandler.OnInvokeAsync();

Debug.Log("2");
}
}



条件を与えてawaitする

UniTask.Wait*シリーズを使うことで、条件を指定してawaitができます。


Predicate

using UniRx.Async;

using UnityEngine;

public class Sample11 : MonoBehaviour
{
async void Start()
{
// 条件を満たしたら待機終了
await UniTask.WaitUntil(() => transform.position.y < 0);

// 指定オブジェクトの状態が変化するのを待つ
await UniTask.WaitUntilValueChanged(transform, x => x.position.x); //x方向に移動した

// 指定オブジェクトの状態が変化するのを待つ & 対象がDestroyされたら待機終了
await UniTask.WaitUntilValueChangedWithIsDestroyed(transform, x => x.position.x);

// 条件を満たさなくなったら待機終了
await UniTask.WaitWhile(() => transform.position.z < 0);
}
}



ReactivePropertyをawaitする

ReactivePropertyもawaitできるようになっています。

この場合は「値が変化するまで待機」という挙動になります。

なお、ReactivePropertyは直接awaitした方が軽量かつ安全に機能します。

AsObservableしたり、末尾にオペレータをつなぐとObservableのawaitに切り替わってしまい、直接awaitするのと比べるとパフォーマンスが落ちてしまいます。

using UniRx;

using UniRx.Async;
using UnityEngine;

class AwaitReactiveProperty : MonoBehaviour
{
[SerializeField] BoolReactiveProperty isDead = new BoolReactiveProperty(false);

void Start()
{
DoAsync().Forget();
}

async UniTask DoAsync()
{
// 死ぬまで待機
await isDead;

Debug.Log("Dead!");
}

public void Kill()
{
isDead.Value = true;
}
}


Observableをawaitする

厳密に言うとUniRx.Asyncではなく、UniRx側の機能です。

Observableをawaitで待機できます。

この場合はObseravbleが内部でAsyncSubjectに変換されます。

そのためOnCompletedメッセージが発行されるまでずっとawaitし続けてしまうので注意が必要です。


Observable

using UniRx;

using UnityEngine;

public class Sample12 : MonoBehaviour
{
async void Start()
{
var result = await Observable.Return(1);
Debug.Log(result);
}
}



スレッドを切り替える

UniTask.SwitchToThreadPoolまたはUniTask.SwitchToTaskPoolをawaitすることで、以降の処理を別スレッドに切り替えることができます。(ThreadPoolTaskPoolとありますが、ThreadPoolだけ使っていれば問題ないです。Unityの場合はTaskPoolは利用するメリットがありません。)


ChangeThread

using System.Threading;

using UniRx.Async;
using UnityEngine;

public class SampleX : MonoBehaviour
{
async void Start()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);

// TaskPoolに切り替え
await UniTask.SwitchToTaskPool();
// ↓これ以降はTaskPoolで実行される↓

Debug.Log(Thread.CurrentThread.ManagedThreadId);

// メインスレッド(Updateタイミング)に戻す
await UniTask.Yield();
// ↓これ以降はメインスレッドで実行される↓

Debug.Log(Thread.CurrentThread.ManagedThreadId);
}
}



ObservableとUniTaskの相互変換

Observable.ToUniTask()UniTask.ToObservable()でお互いに相互変換できます。

相互変換できるので、非同期処理はとりあえずasync/awaitで書いておいて、状況に応じてObservableに変換すればOKになります。


ObservableとUniTask

using UniRx;

using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class Sample13 : MonoBehaviour
{
[SerializeField] private Button button;
[SerializeField] private Text text;

void Start()
{
// ボタンがクリックされたらHTTPで通信する
// 通信中はボタンを無効化する
button.BindToOnClick(_ =>
{
return GetHtmlAsync().ToObservable()
.ForEachAsync(x => { text.text = x.Substring(0, 100); });
});
}

// HTTPでHTMLをダウンロードする
async UniTask<string> GetHtmlAsync()
{
var uri = "https://unity3d.com/jp";
var r = UnityWebRequest.Get(uri);
var result = await r.SendWebRequest();
return result.downloadHandler.text;
}
}


AsyncReactiveCommandと非同期処理をくっつけた例です。


タイムアウト処理

UniTaskにはタイムアウトの機能がついています。

タイムアウトするとTimeoutExceptionがthrowされます。


Timeout

using System;

using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;

public class Sample14 : MonoBehaviour
{
async void Start()
{
var request = UnityWebRequest.Get("https://unity3d.com");

// 明示的にUniTaskに変換し、Timeoutをくっつける
var result = await request.SendWebRequest().ToUniTask().Timeout(TimeSpan.FromSeconds(3));

Debug.Log(result.downloadHandler.text);
}
}



並列実行&待機

UniTask.WhenAllを使うと、UniTaskの終了を同時に複数待ち受けることができます。

using UniRx.Async;

using UnityEngine;
using UnityEngine.Networking;

public class Sample15 : MonoBehaviour
{
async void Start()
{
var task1 = GetHtmlAsync("https://unity3d.com/jp");
var task2 = GetHtmlAsync("https://github.com");
var task3 = GetHtmlAsync("https://www.yahoo.co.jp");

// まとめて結果をとれる(便利!)
var (unity, github, yahoo) = await UniTask.WhenAll(task1, task2, task3);
}

// HTTPでHTMLをダウンロードする
async UniTask<string> GetHtmlAsync(string uri)
{
var r = UnityWebRequest.Get(uri);
var result = await r.SendWebRequest();
return result.downloadHandler.text;
}
}


キャンセル処理

UniTaskの実行やawaitをキャンセルしたい場合はCancellationTokenを利用する必要があります。

ぶっちゃけここだけはもとTaskのascyn/awaitのころからシンドイ部分ではあります。

キャンセルのやり方は次みたいな感じに。



  1. CancellationTokenSourceを用意する


  2. CancellationTokenSource.TokenCancellationTokenが取得できる

  3. 取得したCancellationTokenを実行するUniTaskに渡していく(シンドイ)

  4. そのとき、定義したasyncメソッド内で逐一キャンセル時の処理を書く(シンドイ)

  5. キャンセルしたくなったタイミングで、CancellationTokenSource.Cancel()を実行する

何度も書きますが、正直めっちゃシンドイです。どうにかならないのかなコレ…。


キャンセル込みの例

using System.Threading;

using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
private AsyncCollisionTrigger asyncCollisionTrigger;

private CancellationTokenSource cancellationTokenSource;

void Start()
{
// CancellationTokenSourceを生成
cancellationTokenSource = new CancellationTokenSource();

// AsyncTrigger(extends MonoBehaviour)を取得する
asyncCollisionTrigger = this.GetAsyncCollisionTrigger();

// 引数にCancellationTokenを渡す
DoAsync(cancellationTokenSource.Token).Forget();
}

void OnDestroy()
{
// GameObject破棄時にキャンセル実行
cancellationTokenSource?.Cancel();
}

async UniTask DoAsync(CancellationToken token)
{
// OnCollisionEnterが発生するまで待機する
var (isCanceled, target) = await asyncCollisionTrigger.OnCollisionEnterAsync()
.WithCancellationWithoutException(token); //tokenでキャンセルできるようにする

if (isCanceled) return; //Finish

Debug.Log("Hit! " + target.gameObject.name);

// OnCollisionExitが発生するまで待つ
(isCanceled, target) = await asyncCollisionTrigger.OnCollisionExitAsync()
.WithCancellationWithoutException(token); //tokenでキャンセルできるようにする

if (isCanceled) return; //Finish

Debug.Log("Bye! " + target.gameObject.name);
}
}



キャンセル用の拡張メソッド

さすがにこのままだとツライので、こういう拡張メソッドを生やしておくとちょっとは便利になる…のかな?


拡張メソッド

using System.Threading;

using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public static class CancellationTokenSourceExtension
{
public static void CancelWith(this CancellationTokenSource cts, Component component)
{
cts.CancelWith(component.gameObject);
}

public static void CancelWith(this CancellationTokenSource cts, GameObject gameObject)
{
gameObject.OnDestroyAsync().ContinueWith(() => cts.Cancel()).Forget();
}
}



使い方

using System.Threading;

using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class AwaitAsyncTrigger : MonoBehaviour
{
private CancellationTokenSource cancellationTokenSource;

void Start()
{
// CancellationTokenSourceを生成
cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelWith(this); //このGameObjectが破棄されたらCancel

// 以下略
}



UniRx.Asyncを使うと何が変わるか

コルーチンをほぼ駆逐することができるようになります。

コルーチンと同等の処理をasync/awaitで記述できるようになる上、コルーチンと違ってUniTask<T>を使えば結果を外に返すことができるようになります。

また、StartCoroutine()GameObjectに依存していたため、MonoBehaviourが無いと実行できないとか、Disableになったらコルーチンごと止まるとかいろいろありましたが、async/await + UniTaskならそういう制約はなくなります。

というわけで、「コルーチンの上位互換」として使える…、かと思いきや途中のキャンセルが結構面倒くさいので一長一短だったりします。 コルーチンの場合はGameObjectDestroyしたら勝手に止まりましたけど、async/awaitの場合は止まりません。

(await対象のオブジェクトがGCで回収されれば勝手にawait処理自体が止まって消え去るので、そこさえGCされてれば最悪大事には至らないかな…?)

Observableとの使い分けですが、非同期処理は基本すべてasync/await+UniTaskで書くことになるかと。

そしてObservableは基本イベント処理(非同期的に複数の値を発行する)への用途がメインになるかと。

とりあえずは思考停止でこの方針で書いてみて、必要に応じて相互変換すればよいかと。


まとめ

すべての機能をまとめきれているわけではないですが、とりあえず目についた部分は書いてみました。

Unityにおける非同期処理がすごいイケてる感じになりそうです!

あ、あと宣伝なのですが、Unity 非同期完全に理解した勉強会を開催します。

大反響で会場のキャパを遥かに超える参加登録が来ており、ちょっとどうするか検討中です。


参考

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合