知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【Unity】UniRx【5】 #88

前回の成果

ストリームソースを用意する方法を学んだ。

soramamenatan.hatenablog.com


今回やること

引き続き以下サイト様の手順に習って、UniRxを学んでいきます。

qiita.com


事前準備

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

assetstore.unity.com


Updateをストリームに変換

Update関数をストリームへと変換する方法は以下の2つになります。

  • Triggers.UpdateAsObservable
  • Observable.EveryUpdate

こちらに関しては、以下の記事を参考にしてください。

soramamenatan.hatenablog.com

こちらの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が呼ばれているので、寿命を意識しなくても良いのが分かります。

f:id:soramamenatan:20210114101538p:plain


仕組み

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スクリプト

f:id:soramamenatan:20210114101730p:plain


Observable.EveryUpdate

基本的には、UpdateAsObservableと同じとなります。
異なる点として、自動でOnCompletedを発行しないので、SubscribeのDisposeを手動で行う必要があります

EveryUpdateのサンプル
/// <summary>
/// EveryUpdateの実行
/// </summary>
private void ExcuteEveryUpdate() {
    Observable
        .EveryUpdate()
        .Subscribe(x => {
            Debug.Log("Update Frame : " + x);
        });
}
結果

f:id:soramamenatan:20210114101845p:plain


仕組み

UniRxのMicroCoroutineという仕組みを利用して作られています。

MicroCoroutine

MicroCoroutineは大量のコルーチンを扱う上での軽量化する仕組みとなっています。
簡単に説明すると、10000個のStartCoroutineを呼ぶ際に、単純に10000回呼ぶよりも配列に詰めてループで呼んだほうが早くなります。
配列に詰める際にListを使用しているそうなのですが、要素の数が変化する度にRemoveをしていると負荷が高くなってしまいます。
そこで、空いた部分はnullで埋めて数フレーム毎に空きスペースを埋めることによって高速化を図っています。


MicroCoroutineが使われているメソッドはEveryUpdate以外にも以下等があります。

  • ObserveEveryValueChanged
  • EveryFixedUpdate
  • EveryEndOfFrame
  • NextFrame
  • TimerFrame
  • IntervalFrame
  • DelayFrame
  • SampleFrame
  • ThrottleFrame
  • ThrottleFirstFrame
  • TimeoutFrame

詳しくはこちらのサイト様を参考にしてください。

blogs.unity3d.com

neue.cc


EveryUpdateのメリット

EveryUpdateはUpdateAsObservableと違いストリームの寿命を管理する必要がありますが、メリットもあります。
シングルトン上で動作するので、実行中ずっと存在するストリームの生成が出来ます。
また、MicroCoroutineを利用しているので大量にSubscribeしてもパフォーマンスが低下しにくいです。

またUniRxでは、MainThreadDispatcherという名前のGameObjectを管理しています。

MainThreadDispatcher

f:id:soramamenatan:20210114102028p:plain


使い分け

両方のストリームは似ているので、ケースバイケースで使い分けていくのが良いです。

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を使用することにより、機能毎に処理を分割することが出来るので、ネストが減ります。
また、変数のスコープも明確になり明らかに可読性が上がりました。

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


参考サイト様

qiita.com

kan-kikuchi.hatenablog.com