知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【Unity】UniRx 【6】#89

前回の成果

Updateをストリームに変換するメリットを学んだ。

soramamenatan.hatenablog.com


今回やること

引き続き以下サイト様の手順に習って、UniRxを学んでいきます。

qiita.com


事前準備

アセットストアからUniRxをインポートします。

assetstore.unity.com


コルーチンをIObservableに変換する

コルーチンとUniRxを併用することにより、より便利に使用することができます。

UniRxとコルーチンには、以下の表のメリット、デメリットが挙げられます。
2つを併用することにより、デメリットを消しつつ、メリットを残すことが可能になります。

\ メリット デメリット
UniRx 処理を関数で繋げることで実装でき、可読性が高い 複雑な処理だと、既存の関数だけでは実装不可
コルーチン 好きに記述が出来るので、複雑な処理も実装可能 複雑な記述が増え、可読性が低い


コルーチンの終了を待つ

Observable.FromCoroutineを使用することで、コルーチンの終了タイミングを待つ事ができます。
定義は以下となっています。

Observable.FromCoroutineの定義
/// <summary>
/// コルーチンの終了タイミングを待つ
/// </summary>
/// <param name="coroutine">コルーチン</param>
/// <param name="publishEveryYield">yieldのタイミングでonNextを発行するか、デフォルトはfalse</param>
/// <returns>IObservable<Unit></returns>
public static IObservable<Unit> FromCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false)

実装例は以下となります。

Observable.FromCoroutineのサンプル
using UnityEngine;
using UniRx;
using System.Collections;

public class FromCoroutine : MonoBehaviour {
    [SerializeField]
    private bool _publishEveryYield;

    void Start() {
        ExcuteFromCoroutine();
    }

    /// <summary>
    /// FromCoroutineの実行
    /// </summary>
    private void ExcuteFromCoroutine() {
        // コルーチンの終了タイミングを待つ
        Observable
            .FromCoroutine(LogCoroutine, _publishEveryYield)
            .Subscribe(x => {
                Debug.Log("OnNext");
            }, () => {
                Debug.Log("OnCompleted");
            }).AddTo(gameObject);
    }

    /// <summary>
    /// ログを出すコルーチン
    /// </summary>
    /// <returns></returns>
    private IEnumerator LogCoroutine() {
        Debug.Log("publishEveryYield : " + _publishEveryYield);
        Debug.Log("Coroutine Start");
        // publishEveryYieldがtrueの場合、OnNextを発行
        yield return null;
        yield return null;
        yield return null;
        Debug.Log("Coroutine Finish");
    }
}
publishEveryYieldがfalseの結果

コルーチンの終了タイミングで、OnNextとOnCompletedが発行されています。

f:id:soramamenatan:20210114111051p:plain

publishEveryYieldがtrueの結果

こちらも同じくコルーチンの終了タイミングで、OnNextとOnCompletedが発行されています。
また、yieldのタイミングでOnNextが発行されています。

f:id:soramamenatan:20210114111102p:plain


Subscribe時の注意点

Observable.FromCoroutineはSubscribeされる度に新たにコルーチンを生成してしまうので気をつけてください。

複数回Subscribeするサンプル
/// <summary>
/// 複数回のSubscribe
/// </summary>
private void ExcuteMultiCoroutine() {
    IObservable<Unit> multi = Observable.FromCoroutine(LogCoroutine, _publishEveryYield);
    multi.Subscribe(x => {
        Debug.Log("1回目のSubscribe");
    });
    multi.Subscribe(x => {
        Debug.Log("2回目のSubscribe");
    });
    multi.Subscribe(x => {
        Debug.Log("3回目のSubscribe");
    });
}
結果

複数コルーチンが生成されているのがわかります。

f:id:soramamenatan:20210114125310p:plain


Dispose時

Observable.FromCoroutineで起動したコルーチンはDisposeすると自動的に停止し、検知ができなくなっています。
検知したい場合は、CancellationTokenを渡すことで検知できます。

Disposeを検知するサンプル
/// <summary>
/// FromCoroutineのDisposeの実行
/// </summary>
private void ExcuteDisposeCoroutine() {
    IDisposable dispose = Observable
        .FromCoroutine(x => DisposeCoroutine(x), _publishEveryYield)
        .Subscribe(x => {
            Debug.Log("OnNext");
        }, () => {
            Debug.Log("OnCompleted");
        }).AddTo(gameObject);
    dispose.Dispose();
}

/// <summary>
/// Disposeするコルーチン
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private IEnumerator DisposeCoroutine(CancellationToken token) {
    Debug.Log("Coroutine Start");
    // 例外エラーを投げる
    token.ThrowIfCancellationRequested();
    yield return null;
    Debug.Log("Coroutine Finish");
}
結果

Dispose時に例外エラーを投げています。

f:id:soramamenatan:20210114130119p:plain


yield returnの値を受け取る

Observable.FromCoroutineValue<T>を使用することで、yield returnの値を受け取ることができます。
yield returnは呼び出されると1フレーム停止する性質があります。
ですので、1フレームずつしか値を取得することができないので気をつけてください。

Observable.FromCoroutineValue<T>の定義
/// <summary>
/// yield returnの結果を受け取る
/// </summary>
/// <param name="coroutine">コルーチン</param>
/// <param name="nullAsNextUpdate">nullの時OnNextを発行しないか、デフォルトはtrue</param>
/// <returns>IObservable<T></returns>
public static IObservable<T> FromCoroutineValue<T>(Func<IEnumerator> coroutine, bool nullAsNextUpdate = true)
Observable.FromCoroutineValue<T>のサンプル
using UnityEngine;
using UniRx;
using System.Collections.Generic;

public class FromCoroutineValue : MonoBehaviour {
    private List<int> _list = new List<int>();

    void Start() {
        AddList();
        ExcuteFromCoroutineValue();
    }

    /// <summary>
    /// Listに値を追加
    /// </summary>
    private void AddList() {
        for (int i = 0; i < 5; i++) {
            _list.Add(i);
        }
    }

    /// <summary>
    /// FromCoroutineValueの実行
    /// </summary>
    private void ExcuteFromCoroutineValue() {
        Observable
            // コルーチンから値を取り出す
            .FromCoroutineValue<int>(TakeList)
            .Subscribe(x => {
                Debug.Log("OnNext. Value : " + x);
            }, () => {
                Debug.Log("OnCompleted");
            });
    }

    /// <summary>
    /// Listの値を取り出す
    /// </summary>
    /// <returns></returns>
    private IEnumerator<int> TakeList() {
        return _list.GetEnumerator();
    }
}
結果

Listの値が1つずつ取り出されています。

f:id:soramamenatan:20210114161649p:plain


コルーチン内でOnNextを発行する

Observable.FromCoroutine<T> を使用することにより、コルーチンの内部でOnNextを発行することができます。
これを利用することにより、実装部分では複雑な処理にも対応でき、外部からはストリームとして扱うことができます。

Observable.FromCoroutine<T>の定義
/// <summary>
/// コルーチン内でOnNextを発行する
/// </summary>
/// <param name="coroutine">IObserver<T>を引数とするコルーチン</param>
/// <returns>IObservable<T></returns>
public static IObservable<T> FromCoroutine<T>(Func<IObserver<T>, IEnumerator> coroutine)
Observable.FromCoroutine<T>のサンプル
using UnityEngine;
using UniRx;
using System;
using System.Collections;
using System.Threading;

public class FromCoroutineT : MonoBehaviour {

    [SerializeField]
    private bool _isPause;
    void Start() {
        ExcuteFromCoroutineT();
    }

    /// <summary>
    /// FromCoroutine<T>の実行
    /// </summary>
    private void ExcuteFromCoroutineT() {
        Observable
            .FromCoroutine<int>(x => Counter(x))
            .Subscribe(x => {
                Debug.Log(x);
            }).AddTo(gameObject);
    }

    /// <summary>
    /// カウンター
    /// </summary>
    /// <param name="observer"></param>
    /// <returns></returns>
    private IEnumerator Counter(IObserver<int> observer) {
        int current = 0;
        float deltaTime = 0;

        while(true) {
            yield return null;
            if (_isPause) {
                continue;
            }
            deltaTime += Time.deltaTime;
            if (deltaTime < 1.0) {
                continue;
            }
            // 1秒ごとにOnNext発行
            int integerPart = (int)Mathf.Floor(deltaTime);
            current += integerPart;
            deltaTime -= integerPart;
            observer.OnNext(current);
        }
    }
}
結果

ポーズフラグがONの時は止まって、OFFの時は動いています。


軽量なコルーチンを実行する

Observable.FromMicroCoroutine、もしくはObservable.FromMicroCoroutine<T>を使用することにより、 軽量なコルーチンを使用できます。
このメソッドでは、MicroCoroutineを使用しているためFromCoroutineより軽量となります。
MicroCoroutineの説明は以下で行ったので割愛させて頂きます。

soramamenatan.hatenablog.com

FromMicroCoroutineはコルーチン内でyield return nullしか利用できないので、気をつけてください。

FromMicroCoroutineの定義
/// <summary>
/// 軽量なコルーチンを実行
/// </summary>
/// <param name="coroutine">コルーチン</param>
/// <param name="publishEveryYield">yieldのタイミングでonNextを発行するか、デフォルトはfalse</param>
/// <param name="frameCountType">Update,FixedUpdate,EndOfFrameの3つの内、どれのタイミングにするか</param>
/// <returns>IObservable<Unit></returns>
public static IObservable<Unit> FromMicroCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false, FrameCountType frameCountType = FrameCountType.Update)
FromMicroCoroutineのサンプル
using UnityEngine;
using UniRx;
using System.Collections;

public class FromMicroCoroutine : MonoBehaviour {
    void Start() {
        ExcuteFromMicroCoroutine();
    }

    /// <summary>
    /// FromMicroCoroutineの実行
    /// </summary>
    private void ExcuteFromMicroCoroutine() {
        Observable
            .FromMicroCoroutine(SimpleCoroutine)
            .Subscribe(x => {
                Debug.Log("OnNext");
            }, () => {
                Debug.Log("OnCompleted");
            });
    }

    /// <summary>
    /// nullを返すだけのコルーチン
    /// </summary>
    /// <returns></returns>
    private IEnumerator SimpleCoroutine() {
        yield return null;
    }
}
結果

結果はFromCoroutineと同じです。

f:id:soramamenatan:20210115133506p:plain


IObservableをコルーチンに変換する

次は逆にストリームをコルーチンへと変換します。


ストリームをコルーチンに変換

ToYieldInstructionを使用することにより、ストリームをコルーチンへと変換できます。

ToYieldInstructionの定義
/// <summary>
/// ストリームをコルーチンへと変換
/// </summary>
/// <param name="source">コルーチン</param>
/// <param name="throwOnError">OnErrorが発生した場合に例外を投げるか、省略可能</param>
/// <param name="cancel">処理が中断された場合に引数にtokenを渡す、省略可能</param>
/// <returns>ObservableYieldInstruction<T></returns>
public static ObservableYieldInstruction<T> ToYieldInstruction<T>(this IObservable<T> source, bool throwOnError, CancellationToken cancel)
ToYieldInstructionのサンプル
using UnityEngine;
using UniRx;
using System;
using System.Collections;

public class ObservableToCoroutine : MonoBehaviour {
    void Start() {
        ExcuteTimerCoroutine();
    }

    /// <summary>
    /// タイマーコルーチンの実行
    /// </summary>
    private void ExcuteTimerCoroutine() {
        Observable
            .FromCoroutine(TimerCoroutine)
            .Subscribe(x => {
                Debug.Log("OnNext");
            }, () => {
                Debug.Log("OnCompleted");
            });
    }

    /// <summary>
    /// タイマーコルーチン
    /// </summary>
    /// <returns></returns>
    private IEnumerator TimerCoroutine() {
        Debug.Log("1秒後に終了します");
        yield return Observable
                            .Timer(TimeSpan.FromSeconds(1))
                            .ToYieldInstruction();
    }
}
結果

1秒後にOnNextとOnCompletedが発行されています。

f:id:soramamenatan:20210115141040p:plain


複数のコルーチンを直列で実行する

SelectManyを使用することにより、コルーチンの終了を待ってから別のコルーチンを起動することができます。

SelectManyのサンプル
using UnityEngine;
using UniRx;
using System.Collections;

public class SelectManyCroutine : MonoBehaviour {
    void Start() {
        ExcuteSelectManyCoroutine();
    }

    /// <summary>
    /// SelectManyの実行
    /// </summary>
    private void ExcuteSelectManyCoroutine() {
        Observable
            .FromCoroutine(CoroutineA)
            // CoroutineAの終了を待ってから、CoroutineBを起動
            .SelectMany(CoroutineB)
            // CoroutineBの終了を待ってから、CoroutineCを起動
            .SelectMany(CoroutineC)
            .Subscribe(x => {
                Debug.Log("全てのコルーチンが終了しました");
            });
    }

    private IEnumerator CoroutineA() {
        Debug.Log("コルーチンA開始");
        yield return new WaitForSeconds(1);
        Debug.Log("コルーチンA終了");
    }

    private IEnumerator CoroutineB() {
        Debug.Log("コルーチンB開始");
        yield return new WaitForSeconds(2);
        Debug.Log("コルーチンB終了");
    }

    private IEnumerator CoroutineC() {
        Debug.Log("コルーチンC開始");
        yield return new WaitForSeconds(3);
        Debug.Log("コルーチンC終了");
    }
}
結果

Aが終了してからBを起動、Bが終了してからCを起動しています。
Cが終了すると、OnNextとOnCompletedが発行されます。

f:id:soramamenatan:20210115145331p:plain


複数のコルーチンを並列で実行する

WhenAllを使用することにより、指定した全てのコルーチンを起動して、全てのコルーチンの終了を待つことができます。

WhenAllのサンプル
using UnityEngine;
using UniRx;
using System.Collections;

public class WhenAllCoroutine : MonoBehaviour {
    void Start() {
        ExcuteSelectManyCoroutine();
    }

    /// <summary>
    /// SelectManyの実行
    /// </summary>
    private void ExcuteSelectManyCoroutine() {
        Observable
            // 並列でコルーチンを起動する
            .WhenAll(Observable.FromCoroutine(CoroutineA),
                     Observable.FromCoroutine(CoroutineB),
                     Observable.FromCoroutine(CoroutineC))
            .Subscribe(x => {
                Debug.Log("全てのコルーチンが終了しました");
            });
    }

    private IEnumerator CoroutineA() {
        Debug.Log("コルーチンA開始");
        yield return new WaitForSeconds(1);
        Debug.Log("コルーチンA終了");
    }

    private IEnumerator CoroutineB() {
        Debug.Log("コルーチンB開始");
        yield return new WaitForSeconds(2);
        Debug.Log("コルーチンB終了");
    }

    private IEnumerator CoroutineC() {
        Debug.Log("コルーチンC開始");
        yield return new WaitForSeconds(3);
        Debug.Log("コルーチンC終了");
    }
}
結果

A,B,Cを同時に起動し、全てのコルーチンが終了してからOnNextとOnCompletedを発行しています。

f:id:soramamenatan:20210115150134p:plain

今回は以上となります。
ここまでご視聴ありがとうございました。