知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【Unity】UniRx【2】 #85

前回の成果

UniRxの一連の流れを学んだ。

soramamenatan.hatenablog.com


今回やること

引き続き以下サイト様の手順に習ってUniRxの勉強をします。

qiita.com


事前準備

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

assetstore.unity.com


IObserverインターフェースのメソッド

IObserverインターフェースで定義されている、

  • OnNext
  • OnError
  • OnCompleted

について深堀していきます。


OnNext

メッセージを渡して実行するメソッドとなります。

メッセージを渡す
using UnityEngine;
using UniRx;
using System;

public class OnNext : MonoBehaviour {
    void Start() {
        // イベント発行
        Subject<int> onNextSubject = new Subject<int>();
        // イベント登録
        onNextSubject.Subscribe(x => {
            Debug.Log("渡された値 : " + x);
        });

        // イベント実行
        onNextSubject.OnNext(0);
        onNextSubject.OnNext(10);
        onNextSubject.OnNext(100);
        onNextSubject.OnNext(1000);
    }
}
結果

OnNextの引数の値が表示されているので、メッセージが発行されていることがわかります。

f:id:soramamenatan:20201218120518p:plain


また、Unit型というUniRxで定義されているものを使用することにより、値を発行せずに通知のみ送ることができます。
これはシーン遷移が完了したタイミングや、リソースのロードが完了したタイミング等で利用できます。

空の値を通知
// イベント発行
Subject<Unit> unitSubject = new Subject<Unit>();
// イベント登録
unitSubject.Subscribe(x => {
    Debug.Log("渡された値 : " + x);
});

// イベント実行
unitSubject.OnNext(Unit.Default);
結果

()で空の値を通知しています。

f:id:soramamenatan:20201218120535p:plain


OnError

エラーを通知するメッセージを発行するものになります。
具体的には、例外処理がストリームの途中で発生した際に通知されます。
そして、OnErrorがSubscribeまで到達した場合、そのストリームの購読は終了されて破棄されます

例外をSubscribeまで到達させる
using UnityEngine;
using UniRx;
using System;

public class OnError : MonoBehaviour {
    void Start() {
        // イベント発行
        Subject<string> OnErrorSubject = new Subject<string>();
        // イベント登録
        OnErrorSubject
            // 値を変換する
            .Select(str => int.Parse(str))
            .Subscribe(x => {
                // OnNext
                Debug.Log("処理成功 : " + x);
            }, err => {
                // OnError
                Debug.Log("例外処理 : " + err);
            });

        // イベント実行
        OnErrorSubject.OnNext("1");
        OnErrorSubject.OnNext("2");
        OnErrorSubject.OnNext("Hoge");
        // ストリームの購読が中止される
        OnErrorSubject.OnNext("4");
        OnErrorSubject.OnNext("5");
    }
}
結果

Selectは値を変換するオペレーターとなります。
Hogeをintに変換しようとして、例外処理が発生してしまうのでOnErrorが呼ばれているのがわかります。
また、Hoge以下が呼ばれていないので、ストリームの購読が中止されているのがわかります。

f:id:soramamenatan:20201218120735p:plain


OnErrorRetryオペレーターを使用することによって、例外が発生しても再購読することが出来ます。

// イベント発行
Subject<string> OnErrorSubject = new Subject<string>();
// イベント登録
OnErrorSubject
    // 値を変換する
    .Select(str => int.Parse(str))
    // エラー処理後、Subscribeし直す
    .OnErrorRetry((FormatException err) => {
        Debug.Log("例外が発生したので、再購読します");
    })
    .Subscribe(x => {
        // OnNext
        Debug.Log("処理成功 : " + x);
    }, err => {
        // OnError
        Debug.Log("例外処理 : " + err);
    });

// イベント実行
OnErrorSubject.OnNext("1");
OnErrorSubject.OnNext("2");
OnErrorSubject.OnNext("Hoge");
OnErrorSubject.OnNext("4");
OnErrorSubject.OnNext("5");
結果

OnErrorRetryはOnErrorが来た際にエラー処理をし、一定時間後にSubscribeするオペレーターとなります。
これで再購読されるようになるので、Hoge以下の4と5も呼ばれるようになります。

f:id:soramamenatan:20201218120753p:plain


OnCompleted

ストリームが完了したことを通知するものになります。
OnErrorと同じように、OnCompletedがSubscribeまで到達した場合、そのストリームの購読は終了して破棄されます

using UnityEngine;
using UniRx;
using System;

public class OnCompleted : MonoBehaviour {
    void Start() {
        // イベント発行
        Subject<int> OnCompletedSubject = new Subject<int>();
        // イベント登録
        OnCompletedSubject.Subscribe(x => {
            // OnNext
            Debug.Log("処理成功 : " + x);
        }, () => {
            // OnCompleted
            Debug.Log("Complete");
        });

        // イベント実行
        OnCompletedSubject.OnNext(1);
        OnCompletedSubject.OnNext(10);
        OnCompletedSubject.OnCompleted();
        // 呼ばれない
        OnCompletedSubject.OnNext(100);
    }
}
結果

OnCompletedを呼んで、終了した旨のログが呼ばれていることがわかります。
また、それ以降のOnNextは呼ばれていないこともわかります。

f:id:soramamenatan:20201218121251p:plain


Dispose

Disposeはストリームの購読を終了するメソッドとなります。

ストリームの購読を終了させる
using UnityEngine;
using UniRx;
using System;

public class Dispose : MonoBehaviour {
    void Start() {
        // イベント発行
        Subject<int> subject = new Subject<int>();
        // 保持
        IDisposable dispose = subject.Subscribe(x => {
            Debug.Log("OnNext : " + x);
        }, () => {
            Debug.Log("OnCompleted");
        });

        // イベント実行
        subject.OnNext(1);
        subject.OnNext(10);
        // イベント購読終了
        dispose.Dispose();
        // 呼ばれない
        subject.OnNext(100);
        subject.OnNext(1000);
        // OnCompletedも呼ばれない
        subject.OnCompleted();
    }
}
結果

Dispose以下が呼ばれていないことがわかります。
Disposeで購読を終了した際にOnCompletedは呼ばれないので、注意してください。

f:id:soramamenatan:20201218121409p:plain


Disposeを使用することによって、特定のストリームのみ購読を停止させることができます。

特定のストリームのみ購読停止
// イベント発行
Subject<int> subject = new Subject<int>();
// 保持
IDisposable dispose1 = subject.Subscribe(x => {
    Debug.Log("ストリーム1 : " + x);
}, () => {
    Debug.Log("ストリーム1 OnCompleted");
});
IDisposable dispose2 = subject.Subscribe(x => {
    Debug.Log("ストリーム2 : " + x);
}, () => {
    Debug.Log("ストリーム2 OnCompleted");
});

// イベント実行
subject.OnNext(1);
subject.OnNext(10);
// ストリーム1のみイベント購読終了
dispose1.Dispose();
subject.OnNext(100);
subject.OnCompleted();
結果

ストリーム1のみ購読が終了しているのがわかります。
OnCompletedで購読を終了させているわけではないので、ストリーム2は最後まで購読することができます。

f:id:soramamenatan:20201218121424p:plain


ストリームの寿命

UniRxを使用する際には、ストリームの寿命を意識する必要があります。

以下の画像を見て頂けるとわかるように、ストリームの本体はSubjectになります

f:id:soramamenatan:20201219120456p:plain

UniRx入門 その2 - メッセージの種類/ストリームの寿命 - Qiita:より引用

Subjectが破棄されればストリームも破棄されます。
しかし、Subjectが破棄されなければストリームは動き続けてしまうことになります。
例えば、以下の画像のようにストリームが参照している関数2を破棄して放置してしまうと、エラーが出てしまいます。

f:id:soramamenatan:20201219120502p:plain

実例

実際にエラーが出てしまうケースを実装してみます。

カウントダウンクラス
using UnityEngine;
using UniRx;
using System;
using System.Collections;

/// <summary>
/// カウントダウンクラス
/// </summary>
public class StreamLifeTimer : MonoBehaviour {
    [SerializeField]
    private int _timeLeft = 3;
    private Subject<int> _timerSubject = new Subject<int>();
    public IObservable<int> onTimeChanged {
        get { return _timerSubject; }
    }

    void Awake() {
        StartCoroutine(TimerCoroutine());
        TimerSubscribe();
    }

    /// <summary>
    /// カウントダウンする
    /// </summary>
    /// <returns></returns>
    private IEnumerator TimerCoroutine() {
        yield return null;
        int time = _timeLeft;
        while (time >= 0) {
            _timerSubject.OnNext(time--);
            // 1秒間コルーチンの実行を待つ
            yield return new WaitForSeconds(1);
        }
        // timerが0になったら完了通知
        _timerSubject.OnCompleted();
    }

    /// <summary>
    /// 現在のカウントダウンを表示
    /// </summary>
    private void TimerSubscribe() {
        _timerSubject.Subscribe(x => {
            Debug.Log("NowCount : " + x);
        }, () => {
            Debug.Log("Complete");
        });
    }
}
プレイヤークラス
using UnityEngine;
using UnityEngine.UI;
using UniRx;

/// <summary>
/// プレイヤークラス
/// </summary>
public class StreamLifePlayer : MonoBehaviour {
    [SerializeField]
    private StreamLifeTimer _lifeTimer;
    private float _moveSpeed = 10.0f;

    void Start() {
        ExcuteTimer();
    }

    /// <summary>
    /// タイマーの購読
    /// </summary>
    private void ExcuteTimer() {
        _lifeTimer.onTimeChanged
            .Where(x => x == 0)
            .Subscribe(x => {
                // タイマーが0になったら自身の座標を通知
                Debug.Log("自分の座標 : " + transform.position);
            });
    }

    void Update() {
        DestroyCommand();
    }

    /// <summary>
    /// 自身を破棄する
    /// </summary>
    private void DestroyCommand() {
        if (Input.GetKey(KeyCode.Space)) {
            Debug.Log("破棄された");
            Destroy(gameObject);
        }
    }
}

上記2つのClassを以下の画像のようにアタッチします。

カウントダウンオブジェクト

f:id:soramamenatan:20201219121444p:plain

プレイヤーオブジェクト

f:id:soramamenatan:20201219121435p:plain

Sceneビュー

f:id:soramamenatan:20201219121440p:plain

結果

実行すると以下のようなログになります。
何も問題なく動いているように見えます。

f:id:soramamenatan:20201219121650p:plain

しかし、カウントダウンの途中でスペースボタンを押してGameObjectをDestroyしてみます。
すると、MissingReferenceExceptionのエラーが出てしまいます。

Playerを破棄した時のログ

f:id:soramamenatan:20201219121646p:plain

これは、以下のソースコードの部分でPlayerのtransformを参照しようとしているからになります。
参照しようとしますが、Destroyされているため参照できず、エラーとなってしまいます。

// タイマーが0になったら自身の座標を通知
Debug.Log("自分の座標 : " + transform.position);


対応策

エラーの原因は、ストリームの参照先のオブジェクトが破棄されてしまっているのに購読を続けようとしているからになります。
ですので、参照先のオブジェクトが破棄された場合に購読を止めれば解決できます。

StreamLifePlayerクラスのExcuteTimer関数に以下を追加します。

/// <summary>
/// タイマーの購読
/// </summary>
private void ExcuteTimer() {
    _lifeTimer.onTimeChanged
        .Where(x => x == 0)
        .Subscribe(x => {
            // タイマーが0になったら自身の座標を通知
            Debug.Log("自分の座標 : " + transform.position);
        // 以下を追加
        // 破棄された場合Dispose
        }).AddTo(gameObject);
}

AddToメソッドは、AddToの引数に指定されたオブジェクトが破棄されたときにSubscribeによる購読を終了する(Disposeを呼ぶ)ものになります。
AddToを指定することにより、Playerが破棄された時にストリームの購読も停止されるため、先程のエラーは発生しなくなります。

AddToを追加してPlayerを破棄した時のログ

Playerが破棄されたログが出ても、エラーが出ずにOnCompletedが呼ばれているのがわかります。

f:id:soramamenatan:20201219122904p:plain


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


参考サイト様

qiita.com