【Unity】UniRx【4】 #87
前回の成果
ストリームソースを用意する方法として、SubjectシリーズとReactivePropertyシリーズを学んだ。
今回やること
引き続き以下サイト様の手順に習って、UniRxを学んでいきます。
事前準備
アセットストアからUniRxをインポートします。
ファクトリメソッドシリーズ
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のログを出しています。
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では不具合が発生してしまう可能性があるので気をつけてください。
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つの場合は停止しない限り発行し続けています。
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と同じように毎フレーム呼ばれています。
コルーチンを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が呼ばれています。
Observableをコルーチンに変換
先程コルーチンをObservableに変換したので、次はObservableをコルーチンに変換します。
変換するにはToYieldInstructionメソッドを使用します。
ToYieldInstructionを使用することで、IObservable<T>をコルーチン内で使用することが出来ます。
事前準備
ボタンを2つ配置します。
各ボタンをSerializeFieldにアタッチします。
コルーチンに変換するサンプル
[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("両方のボタンが押されました"); }
結果
各ボタンが押された時と両方のボタンが押された時にログが出ています。
uGUIのイベントから変換
uGUIのイベントをUniRxで記述することもできます。
Scene上
ボタン1つとスライダー2つを配置します。
ボタン1つとスライダー2つをSerializeFieldにアタッチします。
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が呼ばれています。
また、スライダー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"); }); }
結果
次フレームでメッセージが発行されています。
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でメッセージの発行がされています。
また、ログにはフレーム数が渡っています。
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); }); }
結果
スライダーの値が変化したフレームにメッセージが発行されています。
今回は以上となります。
ここまでご視聴ありがとうございました。