【Unity】UniRx【2】 #85
前回の成果
UniRxの一連の流れを学んだ。
今回やること
引き続き以下サイト様の手順に習ってUniRxの勉強をします。
事前準備
アセットストアからUniRxをインポートします。
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の引数の値が表示されているので、メッセージが発行されていることがわかります。

また、Unit型というUniRxで定義されているものを使用することにより、値を発行せずに通知のみ送ることができます。
これはシーン遷移が完了したタイミングや、リソースのロードが完了したタイミング等で利用できます。
空の値を通知
// イベント発行 Subject<Unit> unitSubject = new Subject<Unit>(); // イベント登録 unitSubject.Subscribe(x => { Debug.Log("渡された値 : " + x); }); // イベント実行 unitSubject.OnNext(Unit.Default);
結果
()で空の値を通知しています。

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以下が呼ばれていないので、ストリームの購読が中止されているのがわかります。

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も呼ばれるようになります。

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は呼ばれていないこともわかります。

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は呼ばれないので、注意してください。

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は最後まで購読することができます。

ストリームの寿命
UniRxを使用する際には、ストリームの寿命を意識する必要があります。
以下の画像を見て頂けるとわかるように、ストリームの本体はSubjectになります。
Subjectが破棄されればストリームも破棄されます。
しかし、Subjectが破棄されなければストリームは動き続けてしまうことになります。
例えば、以下の画像のようにストリームが参照している関数2を破棄して放置してしまうと、エラーが出てしまいます。

実例
実際にエラーが出てしまうケースを実装してみます。
カウントダウンクラス
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を以下の画像のようにアタッチします。
カウントダウンオブジェクト

プレイヤーオブジェクト

Sceneビュー

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

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

これは、以下のソースコードの部分で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が呼ばれているのがわかります。

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