【Unity】UniRx【5】 #88
前回の成果
ストリームソースを用意する方法を学んだ。
今回やること
引き続き以下サイト様の手順に習って、UniRxを学んでいきます。
事前準備
アセットストアからUniRxをインポートします。
Updateをストリームに変換
Update関数をストリームへと変換する方法は以下の2つになります。
- Triggers.UpdateAsObservable
- Observable.EveryUpdate
こちらに関しては、以下の記事を参考にしてください。
こちらの2つについて、深堀していきます。
Triggers.UpdateAsObservable
UpdateAsObservableの特徴として、GameObjectが破棄された際に自動でOnCompletedが呼ばれるため、ストリームの寿命を意識しなくても良いことがあります。
UpdateAsObservableのサンプル
/// <summary> /// UpdateAsObservableの実行 /// </summary> private void ExcuteUpdateAsObservable() { this.UpdateAsObservable() .Subscribe(x => { Debug.Log("OnNext"); }, () => { Debug.Log("OnCompleted"); }); // Destroy時に呼ばれる this.OnDestroyAsObservable() .Subscribe(x => { Debug.Log("Destroy"); }); // 1秒後に破棄 Destroy(gameObject, 1.0f); }
結果
Destroy時にOnCompletedが呼ばれているので、寿命を意識しなくても良いのが分かります。
仕組み
UpdateAsObservableメソッドを呼んだタイミングで、対象のGameObjectがnullでは無ければ、ObservableUpdateTriggerコンポーネントをアタッチしています。
ObservableUpdateTriggerのUpdateでOnNextを呼んでいます。
UpdateAsObservableの中身
#region ObservableUpdateTrigger /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary> public static IObservable<Unit> UpdateAsObservable(this Component component) { if (component == null || component.gameObject == null) return Observable.Empty<Unit>(); return GetOrAddComponent<ObservableUpdateTrigger>(component.gameObject).UpdateAsObservable(); } #endregion
using System; // require keep for Windows Universal App using UnityEngine; namespace UniRx.Triggers { [DisallowMultipleComponent] public class ObservableUpdateTrigger : ObservableTriggerBase { Subject<Unit> update; /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary> void Update() { if (update != null) update.OnNext(Unit.Default); } /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary> public IObservable<Unit> UpdateAsObservable() { return update ?? (update = new Subject<Unit>()); } protected override void RaiseOnCompletedOnDestroy() { if (update != null) { update.OnCompleted(); } } } }
またコンポーネントをUniRx側でアタッチしているので、呼び出した際のInspectorにはObservableUpdateTriggerスクリプトがアタッチされています。
ObservableUpdateTriggerスクリプト
Observable.EveryUpdate
基本的には、UpdateAsObservableと同じとなります。
異なる点として、自動でOnCompletedを発行しないので、SubscribeのDisposeを手動で行う必要があります。
EveryUpdateのサンプル
/// <summary> /// EveryUpdateの実行 /// </summary> private void ExcuteEveryUpdate() { Observable .EveryUpdate() .Subscribe(x => { Debug.Log("Update Frame : " + x); }); }
結果
仕組み
UniRxのMicroCoroutineという仕組みを利用して作られています。
MicroCoroutine
MicroCoroutineは大量のコルーチンを扱う上での軽量化する仕組みとなっています。
簡単に説明すると、10000個のStartCoroutineを呼ぶ際に、単純に10000回呼ぶよりも配列に詰めてループで呼んだほうが早くなります。
配列に詰める際にListを使用しているそうなのですが、要素の数が変化する度にRemoveをしていると負荷が高くなってしまいます。
そこで、空いた部分はnullで埋めて数フレーム毎に空きスペースを埋めることによって高速化を図っています。
MicroCoroutineが使われているメソッドはEveryUpdate以外にも以下等があります。
- ObserveEveryValueChanged
- EveryFixedUpdate
- EveryEndOfFrame
- NextFrame
- TimerFrame
- IntervalFrame
- DelayFrame
- SampleFrame
- ThrottleFrame
- ThrottleFirstFrame
- TimeoutFrame
詳しくはこちらのサイト様を参考にしてください。
EveryUpdateのメリット
EveryUpdateはUpdateAsObservableと違いストリームの寿命を管理する必要がありますが、メリットもあります。
シングルトン上で動作するので、実行中ずっと存在するストリームの生成が出来ます。
また、MicroCoroutineを利用しているので大量にSubscribeしてもパフォーマンスが低下しにくいです。
またUniRxでは、MainThreadDispatcherという名前のGameObjectを管理しています。
MainThreadDispatcher
使い分け
両方のストリームは似ているので、ケースバイケースで使い分けていくのが良いです。
UpdateAsObservableを使用するケース
- GameObjectに紐付いたストリームを利用するとき
- ストリームの寿命管理を自動で行うため
EveryUpdateを使用するケース
- MonoBehaviourを継承しないクラスでUpdateを使用したいとき
- シングルトン経由でUpdateを取得できるため
- 実行中に常に存在してほしいストリームが必要なとき
- 手動でDisposeしないと停止しないため
Updateをストリームに変換するメリット
Updateをストリーム化するメリットは主に2つあります。
- オペレーターを利用できる
- 可読性が上がる
1つずつ説明していきます。
オペレーターを利用できる
複雑な処理をしたい場合に、UniRxのオペレータを使用することで簡単に処理を書くことが出来ます。
例えば、Updateの1回目のみ処理をしたい時にストリーム化しないと以下のようなソースになります。
ストリーム化しないサンプル
private bool _isFirstUpdate; void Update() { if (_isFirstUpdate == false) { _isFirstUpdate = true; Debug.Log("初回処理終了"); } }
今回はこれだけなので、特に問題無いですが条件や処理が増えた時に大変です。
そこでUpdateをストリーム化すると以下のように書くことが出来ます。
ストリーム化するサンプル
void Start() { FirstUpdate(); } /// <summary> /// 初回のみUpdate処理 /// </summary> private void FirstUpdate() { this.UpdateAsObservable() .FirstOrDefault() .Subscribe(x => { Debug.Log("初回処理終了"); }); }
ストリーム化することにより、簡単に記述出来ますし、変数も減らすことが出来ました。
可読性が上がる
ゲームの機能を作る際にUpdateに詰め込み、可読性が下がってしまうケースがあります。
その際にもUniRxを使用することでロジックを分け、可読性を上げることが出来ます。
参考サイト様のコードが分かりやすかったので引用させて頂きます。
UniRxを使用せずにロジックを記載
private CharacterController characterController; //ジャンプ中フラグ private bool _isJumping; void Start() { characterController = GetComponent<CharacterController>(); } void Update() { if (!_isJumping) { var inputVector = new Vector3( Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical") ); if (inputVector.magnitude > 0.1f) { var dir = inputVector.normalized; Move(dir); } if (Input.GetKeyDown(KeyCode.Space) && characterController.isGrounded) { Jump(); _isJumping = true; } } else { if (characterController.isGrounded) { _isJumping = false; PlaySoundEffect(); } } } private void Jump() { //Jump処理 } private void PlaySoundEffect() { //効果音の再生 } private void Move(Vector3 direction) { //移動処理 }
UniRxを使用しないと、if文によりネストが多くなってしまったり変数のスコープも曖昧になってしまいます。
UniRxを使用して記載
private CharacterController characterController; //ジャンプ中フラグ private BoolReactiveProperty _isJumping = new BoolReactiveProperty(); void Start() { characterController = GetComponent<CharacterController>(); //ジャンプ中でなければ移動する this.UpdateAsObservable() .Where(_ => !_isJumping.Value) .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) .Where(x => x.magnitude > 0.1f) .Subscribe(x => Move(x.normalized)); //ジャンプ中でないならジャンプする this.UpdateAsObservable() .Where(_ => Input.GetKeyDown(KeyCode.Space) && !_isJumping.Value && characterController.isGrounded) .Subscribe(_ => { Jump(); _isJumping.Value = true; }); //着地フラグが変化したときにジャンプ中フラグを戻す characterController .ObserveEveryValueChanged(x => x.isGrounded) .Where(x => x && _isJumping.Value) .Subscribe(_ => _isJumping.Value = false) .AddTo(gameObject); //ジャンプ中フラグがfalseになったら効果音を鳴らす _isJumping.Where(x => !x) .Subscribe(_ => PlaySoundEffect()); } void Jump() { //Jump処理 } void PlaySoundEffect() { //効果音の再生 } void Move(Vector3 direction) { //移動処理 }
UniRxを使用することにより、機能毎に処理を分割することが出来るので、ネストが減ります。
また、変数のスコープも明確になり明らかに可読性が上がりました。
今回は以上となります。
ここまでご視聴ありがとうございました。