Nobollel開発者ブログ

Nobollelのエンジニアが、UnityやCocos2d-xの旬な情報・技術を紹介します。

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>の完了をまとめることができます。