UniRxのコルーチンについて
こんにちは、Nobollelエンジニアの古屋です。今回はUniRx関連の話です。
UniRxは、.NETのReactive Extensions (Rx) を、neueccさんがUnity向けに移植されたものです。今回はこの中から、Rxに馴染みのない人でも比較的取っ付きやすいであろうコルーチン機能の紹介をしようと思います。
コルーチンの開始
UniRxでコルーチンを開始するには、FromCoroutineでIEnumeratorをIObservable<T>に変換した後、Subscribeします。
void Start() { Observable.FromCoroutine(() => Hoge()).Subscribe(); } IEnumerator Hoge() { yield return new WaitForSeconds(1); Debug.Log("hoge"); }
コルーチン本体 (Hoge) の書き方はUnity標準のコルーチンとまったく同じで、代わりにStartCoroutine(Hoge())のように書いても動きます。
コルーチンの停止
UniRxでコルーチンを止めるには、StopCoroutineの代わりに、Subscribeの戻り値をDisposeします。
IDisposable coroutineDisposable; void Start() { coroutineDisposable = Observable.FromCoroutine(Hoge).Subscribe(); } void AbortCoroutine() { coroutineDisposable.Dispose(); }
IDisposable形式だと、たとえば複数のコルーチンを走らせて一括で止めるような処理が書きやすくなります。
IDisposable coroutinesDisposable; void Start() { IDisposable hogeDisposable = Observable.FromCoroutine(Hoge).Subscribe(); IDisposable fugaDisposable = Observable.FromCoroutine(Fuga).Subscribe(); // 2つのIDisposableをくっつける coroutinesDisposable = StableCompositeDisposable.Create(hogeDisposable, fugaDisposable); } void AbortCoroutines() { // まとめてDispose coroutinesDisposable.Dispose(); }
コルーチンの寿命について
Unity標準のコルーチンは、MonoBehaviourを継承したコンポーネントの中で回ります。コルーチンを回すにはコンポーネント (とゲームオブジェクト) が必須で、Destroy(this)するとコルーチンは止まります。
一方、UniRxのコルーチンは、シーン上に自動生成されるMainThreadDispatcherというシングルトンオブジェクトの中で回ります。なので、MonoBehaviourを継承していないクラスからも利用できるという利点があります。しかし、先ほどのUniRxのコードでは、コンポーネントを破棄してもコルーチンが回り続けてしまうという問題も起こります。もしコルーチンの寿命をStartCoroutineと同じようにしたいなら、OnDestroyでIDisposableを破棄してやれば良いです:
void OnDestroy() {
coroutineDisposable.Dispose();
}
コルーチンの操作
UniRxでは、コルーチンをIObservable<T>として扱えるため、Rxのオペレータを使ってコルーチンを操作することができ便利です。いくつか例を紹介します。
まずはコルーチンの連結です。Hogeが完了したらFugaを開始するコードを、UnityコルーチンとUniRxコルーチンの二通りで書いてみます。
コルーチンの連結 (Unity):
void Start() { StartCoroutine(Piyo()); } IEnumerator Piyo() { yield return Hoge(); yield return Fuga(); Debug.Log("done"); }
コルーチンの連結 (UniRx):
void Start() { Observable.FromCoroutine(Hoge).SelectMany(Fuga).Subscribe(_ => { Debug.Log("done"); }); }
この例だと、Unity標準が割とシンプルに書けるので、むしろUniRxの方が読みづらい気がします。Piyoのような新規メソッドを定義しなくて良い点は便利です。
しかし、もっと複雑な操作になると、UniRxの方が簡単に書くことができます。次はコルーチンの並列実行を書いてみます。
コルーチンの並列実行 (Unity):
bool hogeCompleted = false; bool fugaCompleted = false; void Start() { StartCoroutine(Hoge_()); StartCoroutine(Fuga_()); } void TryComplete() { if (hogeCompleted && fugaCompleted) { Debug.Log("done"); } } IEnumerator Hoge_() { yield return Hoge(); hogeCompleted = true; TryComplete(); } IEnumerator Fuga_() { yield return Fuga(); fugaCompleted = true; TryComplete(); }
コルーチンの並列実行 (UniRx):
void Start() { var hoge = Observable.FromCoroutine(Hoge); var fuga = Observable.FromCoroutine(Fuga); Observable.WhenAll(hoge, fuga).Subscribe(_ => { Debug.Log("done"); }); }
Unityのコルーチンでは呼び出し側から完了を検知できないため、Hoge_やFuga_のようなコルーチンでラップする必要があります (コールバックを使う方法もありますが、同じくコードが煩雑になります)。完了したかどうかを保存しておくbool変数もコルーチンの数だけ用意し、コールバックが来るたびに、すべてのコールバックが呼ばれたかチェックする必要があります。
UniRxの場合、IObservable<T>に変換した時点でコルーチンの完了を検知できる状態になっています。そのため、WhenAllで複数のIObservable<T>の完了をまとめることができます。