(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]



UniRxでUpdateをストリーム化したり、MonoBehaviourを継承していないクラスでUpdateを実行したり、値が変更された時だけUpdateを実行したり【Unity】【UniRx】


このエントリーをはてなブックマークに追加



この記事でのバージョン
Unity 2018.3.4f1


はじめに

今回はタイトル通り、UniRxのUpdate周りについての記事です!

UniRx - Reactive Extensions for Unity - Asset Store




UpdateAsObservable

UniRxではUpdateAsObservableを使うことで簡単にUpdateをストリーム化する事が出来ます。

(FixedUpdateAsObservableやLateUpdateAsObservableもある)

using UniRx;
using UniRx.Triggers; //UpdateAsObservableを使うのに必要
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour{
  
  private void Start() {

    //Updateをストリーム化し、毎フレーム、ログを出すように
    this.UpdateAsObservable()
      .Subscribe(_ => {
          Debug.Log("アップデート中");
        }
      );

  }

}

f:id:kan_kikuchi:20190401064011j:plain


ちなみにUpdateAsObservableはGameObjectが破棄された際に

自動的にOnCompletedが発行されるので、AddToは必要ありません。


また、UpdateAsObservableを実行すると、

ObservableUpdateTriggerというコンポーネントが追加されます。(間違って消さないように)


f:id:kan_kikuchi:20190401065310j:plain


そもそも「Updateをストリーム化出来ると何が嬉しいのか」という話ですが、

例えば「Updateの最初の一回だけ処理したい」みたいな条件を簡単に設定できますし、

this.UpdateAsObservable()
  .FirstOrDefault() //最初の一回実行したら完了させる
  .Subscribe(_ => {
      Debug.Log("最初のアップデート");
    }
  );

f:id:kan_kikuchi:20190402131324j:plain


Update内だけで使う変数を関数外に書かなくても良かったりもします。

private int _sum = 0; //Update内だけでしか使わないが、Update外に書く必要がある

private void Update() {
  _sum++;
  Debug.Log(_sum);
}
int sum = 0; //UpdateAsObservableを使えば同じ関数内に書ける

this.UpdateAsObservable()
  .Subscribe(_ => {
      sum++;
      Debug.Log(sum);
    }
  );

f:id:kan_kikuchi:20190402161440j:plain


他にUpdateAsObservableを複数回実行する事で、異なるUpdate処理を同時に実行出来たり、

好きなタイミングでUpdateを止められたりと、様々な利点があります。


EveryUpdate

UpdateAsObservableはComponentの拡張メソッドなので

MonoBehaviourなどのComponentを継承していないクラスでないと使えません。


と言うことで、次はMonoBehaviourを継承していないクラスでも

Update処理を実行できるEveryUpdateの紹介です。

using UniRx;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour{
  
  private void Start() {
    Sample sample = new Sample();
  }

}

public class Sample {

  public Sample() {

    //MonoBehaviourを継承していなくても使える!
    Observable.EveryUpdate()
      .Subscribe(_ => {
        Debug.Log("EveryUpdate");
      });

  }
  
}

f:id:kan_kikuchi:20190402145614j:plain


なお、EveryUpdateはUpdateAsObservableと違い自動で破棄されたりしないので、

Update処理をやめたい時はSingleAssignmentDisposableのDisposeを使ったり、

//UpdateをやめるためにSingleAssignmentDisposableを保持
var disposable = new SingleAssignmentDisposable();
disposable.Disposable = Observable.EveryUpdate()
  .Subscribe(_ => {
    Debug.Log("EveryUpdate");
  });

//Update処理をやめたい時はDisposeメソッドを使う
//disposable.Dispose();


他のストリーム同様、AddToでGameObjectを設定したりする必要があります。

public Sample(MonoBehaviour monoBehaviour) {

  Observable.EveryUpdate()
    .Subscribe(_ => {
      Debug.Log("EveryUpdate");
    })
    .AddTo(monoBehaviour.gameObject);

}


もちろん、MonoBehaviourを引数で渡すなら、

そのままUpdateAsObservableを使っても大差はありません。

public Sample(MonoBehaviour monoBehaviour) {

  monoBehaviour.UpdateAsObservable()
    .Subscribe(_ => {
        Debug.Log("アップデート中 : Sample");
      }
    );

}


ただし、UpdateAsObservableとEveryUpdateの仕組み自体は異なっているようです。

Observable.EveryUpdate()はUniRxの機能の1つである「マイクロコルーチン」を利用して動作しており、仕組みはUpdateAsObservableと比較して複雑になっています。すごく簡潔にまとめてしまえば、「Observable.EveryUpdate()は呼び出されるたびにシングルトン上でコルーチンを起動する」という動作になっています。このコルーチンは手動で止めない限りずっと動作し続けてしまうため、ストリームの寿命管理をしっかりしないとその2で触れたような問題を引き起こす可能性があります。

ただしその反面、Observable.EveryUpdate()には次のようなメリットも存在します。

シングルトン上で動作するため、ゲームが進行中ずっと存在するストリームを生成できる
大量にSubscribeしてもパフォーマンスが低下しない(マイクロコルーチンの性質)




ObserveEveryValueChanged

Updateをストリーム化すると、条件を簡単に設定できると書きましたが、

よく使いそうな「前フレームから値が変化した場合にだけ実行されるUpdate」

を実装するためのオペレータ、ObserveEveryValueChangedは最初から用意されていたりします。

//この値が変化した時だけUpdate処理をしたい
public int Value = 0;

private void Start() {

  //Valueが変わった時だけそれをログに表示
  this
    .ObserveEveryValueChanged(value => Value)
    .Subscribe(value => Debug.Log("値変更! : " + value));

}

f:id:kan_kikuchi:20190402151956g:plain:w700


ちなみにObserveEveryValueChangedには以下のような特徴があります。

任意のクラスオブジェクトに対して使用できる
対象のインスタンスオブジェクトのプロパティ値(プロパティに限らずに値を返すものなら何でも)が前フレームと比較して変化していた際にその値をOnNextに流す
1フレーム内で発生した複数回の変動は検知できず、前フレームと今フレームの変化のみ検知できる