知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【Unity】UniRx【4】 #87

前回の成果

ストリームソースを用意する方法として、SubjectシリーズとReactivePropertyシリーズを学んだ。

soramamenatan.hatenablog.com


今回やること

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

qiita.com


事前準備

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

assetstore.unity.com


ファクトリメソッドシリーズ

UniRxで用意している、ストリームソースを構築するメソッドのことです。
これを使用することで、Subjectだけでは作りにくい複雑なストリームを制作することが出来ます。

Observable.Create

observerが渡ってくるので、自由にストリームを制作することができます。
Disposableを返すため、破棄時の処理を書くことによりCompleteやErrorの処理を書くこともできます。

Observable.Createのサンプル
/// <summary>
/// ObservableCreateの実行
/// </summary>
private void ExcuteObservableCreate() {
    IObservable<int> create = Observable.Create<int>(x => {
        Debug.Log("Start");
        for (int i = 0; i <= 20; i+= 10) {
            x.OnNext(i);
        }
        x.OnCompleted();
        return Disposable.Create(() => {
            // 終了時処理
            Debug.Log("Dispose");
        });
    });

    // イベント登録
    create.Subscribe(x => {
        Debug.Log("OnNext : " + x);
    }, () => {
        Debug.Log("OnCompleted");
    });
}
結果

基本的なストリームの処理をし、終了時にDisposeのログを出しています。

f:id:soramamenatan:20210108130626p:plain


Observable.Start

別スレッドで実行し、OnNextとOnCompletedが1回だけ呼ばれるものになります。
非同期で画像をDLして、完了したら通知する時等に使用できます。

Observable.Startのサンプル
/// <summary>
/// ObservableStartの実行
/// </summary>
private void ExcuteObservableStart() {
    // 別スレッドで実行
    IObservable<Unit> start = Observable.Start(() => {
        Debug.Log("Start");
        // 1秒待つ
        Thread.Sleep(1000);
        Debug.Log("Finish");
    });

    start
        // メインスレッドに戻す
        .ObserveOnMainThread()
        .Subscribe(x => {
            Debug.Log("OnNext");
        }, () => {
            Debug.Log("OnComplete");
        });
}
結果

Finishのログが表示された後に、OnNextとOnCompletedが発行されているのがわかります。
Observable.Startは処理を別スレッドで実行し、その別スレッドからSubscribeします
ですので、スレッドセーフではないUnityでは不具合が発生してしまう可能性があるので気をつけてください。

f:id:soramamenatan:20210108130747p:plain


Observable.Timer / Observable.TimerFrame

TimerとTimerFrameはどちらも指定時間後にメッセージを発行するメソッドとなっています。
TimerとTimerFrameの違いは以下となります。

名称 機能
Timer 実時間
TimerFrame Unityのフレーム数

また、どちらのメソッドも引数の数に応じて挙動が変化します。

// 1秒後に1回のみ発行
Observable.Timer(TimeSpan.FromSeconds(1))
// 1秒後に発行、その後2秒おきに発行し続ける
Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))
Observable.Timerのサンプル
/// <summary>
/// ObservableTimerの実行
/// </summary>
private void ExcuteObservableTimer() {
    // 実時間で指定し、経過後発火
    Observable
        .Timer(TimeSpan.FromSeconds(1))
        .Subscribe(x => {
            Debug.Log("1秒経ちました");
        });

    // フレームで指定し、経過後発火
    Observable
        .TimerFrame(1)
        .Subscribe(x => {
            Debug.Log("1フレーム経ちました");
        });

    Observable
        // 停止させない限り、動き続ける
        .Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2))
        .Subscribe(x => {
            Debug.Log("1秒経ちました、停止されるまで実行します。");
        });
}
結果

TimerもTimerFrameも指定した時間に発行されています。
また、引数が2つの場合は停止しない限り発行し続けています。

f:id:soramamenatan:20210108131010p:plain


UniRx.Triggersシリーズ

UnityのコールバックイベントをUniRxのIObservableに変換しているメソッドです。
数が多いのでよく使いそうなものを紹介します。
詳しくは以下サイト様を参照してください。

UniRx.Triggers · neuecc/UniRx Wiki · GitHub

また、GameObjectがDestroyされた時にOnCompletedを自動で発行してくれるので、ストリームの寿命を意識しなくても良いです。

UniRx.Triggersのサンプル
/// <summary>
/// UniRxTriggersの実行
/// </summary>
private void ExcuteUniRxTriggers() {
    // Updateでメッセージを発行
    this.UpdateAsObservable()
        .Subscribe(x => {
            Debug.Log("Update");
        });

    // OnCollisionEnterが呼ばれた時にメッセージを発行
    this.OnCollisionEnterAsObservable()
        .Where(x => x.gameObject.tag == "Enemy")
        .Subscribe(x => {
            Debug.Log("敵と接触");
        });

    // OnTriggerEnterが呼ばれた時にメッセージを発行
    this.OnTriggerEnterAsObservable()
        .Where(x => x.gameObject.tag == "Water")
        .Subscribe(x => {
            Debug.Log("泳ぎ判定");
        });
}
結果

UnityのUpdateと同じように毎フレーム呼ばれています。

f:id:soramamenatan:20210108131139p:plain


コルーチンをObservableに変換

コルーチンとObservableは相互に変換することができ、併用することによりシンプルに書けるケースも存在します。
コルーチンをObservableに変換した場合、コルーチンが終了するとOnNextとOnCompleteが呼ばれます。

Observableに変換するサンプル
/// <summary>
/// ObservableFromCoroutineの実行
/// </summary>
private void ExcuteFromCoroutine() {
    Observable
        // コルーチンの実行
        .FromCoroutine<int>(x => TimerCoroutine(x, 3))
        .Subscribe(x => {
            Debug.Log("Timer : " + x);
        }, () => {
            Debug.Log("OnCompleted");
        });
}

/// <summary>
/// タイマー
/// </summary>
/// <param name="observer"></param>
/// <param name="count"></param>
/// <returns></returns>
private IEnumerator TimerCoroutine(IObserver<int> observer, int count) {
    int currentCount = count;
    while (currentCount > 0) {
        observer.OnNext(currentCount--);
        yield return new WaitForSeconds(1);
    }
    observer.OnNext(0);
    observer.OnCompleted();
}
結果

コルーチンが終了した際にOnNextとOnCompleteが呼ばれています。

f:id:soramamenatan:20210108131346p:plain


Observableをコルーチンに変換

先程コルーチンをObservableに変換したので、次はObservableをコルーチンに変換します。
変換するにはToYieldInstructionメソッドを使用します。
ToYieldInstructionを使用することで、IObservable<T>をコルーチン内で使用することが出来ます。

事前準備

ボタンを2つ配置します。

f:id:soramamenatan:20210108131715p:plain

各ボタンをSerializeFieldにアタッチします。

f:id:soramamenatan:20210108131816p:plain

コルーチンに変換するサンプル
[SerializeField]
private Button _buttonA;
[SerializeField]
private Button _buttonB;

void Start() {
    ExcuteCoroutine();
}

/// <summary>
/// コルーチンの実行
/// </summary>
private void ExcuteCoroutine() {
    StartCoroutine(BothButtonClick());
}

/// <summary>
/// 両方のボタンが押された時にログを出す
/// </summary>
/// <returns></returns>
private IEnumerator BothButtonClick() {
    Debug.Log("ボタンAが押されるのを待っています");
    yield return _buttonA
        .OnClickAsObservable()
        .FirstOrDefault()
        // Observableをコルーチンに変換
        .ToYieldInstruction();

    Debug.Log("ボタンBが押されるのを待っています");
    yield return _buttonB
        .OnClickAsObservable()
        .FirstOrDefault()
        // Observableをコルーチンに変換
        .ToYieldInstruction();

    // 両方のボタンが押された時
    Debug.Log("両方のボタンが押されました");
}
結果

各ボタンが押された時と両方のボタンが押された時にログが出ています。

f:id:soramamenatan:20210108131940p:plain


uGUIのイベントから変換

uGUIのイベントをUniRxで記述することもできます。

Scene上

ボタン1つとスライダー2つを配置します。

f:id:soramamenatan:20210108132534p:plain

ボタン1つとスライダー2つをSerializeFieldにアタッチします。

f:id:soramamenatan:20210108132530p:plain

uGUIのイベントから変換するサンプル
[SerializeField]
private Button _button;
[SerializeField]
private Slider _sliderA;
[SerializeField]
private Slider _sliderB;

/// <summary>
/// uGUIイベントの実行
/// </summary>
private void ExcuteUGUIAsObservable() {
    // ボタン押下時
    _button
        .OnClickAsObservable()
        .Subscribe(x => {
            Debug.Log("ボタンが押されました");
        });

    // Sliderの値変更時、初期値あり
    _sliderA
        .OnValueChangedAsObservable()
        .Subscribe(x => {
            Debug.Log("SliderA Value : " + x);
        });

    // Sliderの値変更時、初期値なし
    _sliderB
        .onValueChanged
        .AsObservable()
        .Subscribe(x => {
            Debug.Log("SliderB Value : " + x);
        });
}
結果

ボタンが押された時、スライダーの値が変更した際にonNextが呼ばれています。

f:id:soramamenatan:20210108132804p:plain

また、スライダーAとBの違いは初期値を発行するか否かになります。

// 初期値を発行する
_sliderA
    .OnValueChangedAsObservable()
    .Subscribe(x => {
    });

// 初期値を発行しない
_sliderB
    .onValueChanged
    .AsObservable()
    .Subscribe(x => {
    });


その他のストリーム

他にもストリームソースを制作する手段があるので、いくつか紹介します。

Observable.NextFrame

次のフレームでメッセージを発行するものになります。
メッセージ発行のタイミングはUpdateではなく、コルーチンのタイミングとなるので使用する際には注意してください。

Observable.NextFrameのサンプル
/// <summary>
/// NextFrameの実行
/// </summary>
private void ExcuteNextFrame() {
    // 次のフレームで実行
    Observable
        .NextFrame()
        .Subscribe(x => {
            Debug.Log("Next Frame");
        });
}
結果

次フレームでメッセージが発行されています。

f:id:soramamenatan:20210108132941p:plain


Observable.EveryUpdate

毎Updateのタイミングを通知するストリームソースとなります。
TriggersのUpdateAsObservableと似ていますが、こちらには以下の特徴があります。

  • staticメソッドとして定義されているので、MonoBehavior以外でも使用できる
  • ストリームには、Subscribeしてからのフレーム数が渡る
  • SubscribeのDisposeは手動で行う必要がある
Observable.EveryUpdateのサンプル
/// <summary>
/// EveryUpdateの実行
/// </summary>
private void ExcuteEveryUpdate() {
    // Updateのタイミングを通知
    Observable
        .EveryUpdate()
        .Subscribe(x => {
            Debug.Log("Frame : " + x);
        });
}
結果

毎Updateでメッセージの発行がされています。
また、ログにはフレーム数が渡っています。

f:id:soramamenatan:20210108133032p:plain


ObserveEveryValueChanged

任意のオブジェクトのパラメータを毎フレーム監視して、変化があった時にメッセージを発行します。
変化した時に通知ではなく、毎フレーム監視になります。
ですので、1フレームの間に変化して元の数値に戻った場合等はメッセージは発行されません

ObserveEveryValueChangedのサンプル
[SerializeField]
private Slider _slider;


/// <summary>
/// ObserveEveryValueChangedの実行
/// </summary>
private void ExcuteObserveEveryValueChanged() {
    _slider
        .ObserveEveryValueChanged(x => x.value)
        .Subscribe(x => {
            Debug.Log("Slider Value : " + x);
        });
}
結果

スライダーの値が変化したフレームにメッセージが発行されています。

f:id:soramamenatan:20210108133211p:plain

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


参考サイト様

qiita.com