【Unity】UniRx 【6】#89
前回の成果
Updateをストリームに変換するメリットを学んだ。
今回やること
引き続き以下サイト様の手順に習って、UniRxを学んでいきます。
事前準備
アセットストアからUniRxをインポートします。
コルーチンを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が発行されています。
publishEveryYieldがtrueの結果
こちらも同じくコルーチンの終了タイミングで、OnNextとOnCompletedが発行されています。
また、yieldのタイミングでOnNextが発行されています。
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"); }); }
結果
複数コルーチンが生成されているのがわかります。
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時に例外エラーを投げています。
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つずつ取り出されています。
コルーチン内で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の説明は以下で行ったので割愛させて頂きます。
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と同じです。
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が発行されています。
複数のコルーチンを直列で実行する
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が発行されます。
複数のコルーチンを並列で実行する
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を発行しています。
今回は以上となります。
ここまでご視聴ありがとうございました。