lime雑記

ゲーム開発、その他雑記。

【Unity】UniRxでのDispose方法3選

この記事でのバージョン

Unity 2021.3.11f1
UniRx 7.1.0

概要

UniRxの主な使い方としてIObservableに対してSubscribeして処理を書く、という使い方をよくしますが、これだけではメモリリークやクラッシュの原因になる可能性があります。
これを防ぐにはSubscribeした戻り値IDisposableに対してDisposeという関数を呼んで購読を解除する必要があります。
このDisposeを呼ぶ方法には何パターンか方法があるため代表的なものをいくつか紹介します。

Observable側のコード

このPlayerクラスのOnJumpObservableに対してSubscribeすることとします。

public class Player : MonoBehaviour {
  private readonly Subject<Unit> _onJumpSubject = new();
  public IObservable<Unit> OnJumpObservable => _onJumpSubject;

  private void Update() {
    if (Input.GetKey(KeyCode.Space)) {
      // ジャンプ処理
      _onJumpSubject.OnNext(Unit.Default);
    }
  }
}

AddToを使うパターン

最も一般的なパターンです。

public class SubscribeTest01 : MonoBehaviour {
  [SerializeField]
  private Player _player;

  private void Awake() {
    _player.OnJumpObservable.Subscribe(_ => {
      Debug.Log("ジャンプした!");
    }).AddTo(this);
  }
}

SubscribeのあとにAddToという関数を呼んでthis(Component)を渡しています。これで、このComponentが付いているGameObjectがDestroyされたときにDisposeする、という意味になります。
処理がMonoBehaviourに紐づいている場合はこちらを使うのがおすすめです。

IDisposableを受け取るパターン

MonoBehaviourを使用していないときによく使用するパターンです。

public class SubscribeTest02 : IDisposable {
  private Player _player;

  private IDisposable _disposable;

  public void Setup(Player player) {
    _player = player;

    _disposable = _player.OnJumpObservable.Subscribe(_ => {
      Debug.Log("ジャンプした!");
    });
  }

  public void Dispose() {
    _disposable?.Dispose();
  }
}

このパターンはSubscribeTest02クラスがMonoBehaviour継承ではないため、AddToの代わりにIDisposableを受け取り、自身でDisposeを行うようにしています。(そのため、自分自身もIDisposableを実装しています)
また_disposable?.Dispose()の部分は_disposableがnullじゃなければDisposeを実行する、という意味になります。

CompositeDisposableを使うパターン

Disposeするものが多いかつMonoBehaviourに紐づいてない場合に使えるパターンです。

public class SubscribeTest03 : IDisposable {
  private Player _player;

  private readonly CompositeDisposable _compositeDisposable = new();

  public void Setup(Player player) {
    _player = player;

    _compositeDisposable.Add(_player.OnJumpObservable.Subscribe(_ => {
      Debug.Log("ジャンプした!");
    }));

    // ↓でもOK
    _player.OnJumpObservable.Subscribe(_ => {
      Debug.Log("ジャンプした!!");
    }).AddTo(_compositeDisposable);
  }

  public void Dispose() {
    _compositeDisposable.Dispose();
  }
}

CompositeDisposableを使うと複数のDisposableをまとめてDisposeすることができます。
上記のように複数のSubscribeを同タイミングでDisposeしたい場合は非常に便利に使うことができます。

Observable側のDisposeについて

本来はObservable側にもDisposeを書いた方がベターです。
(ただちに問題にはなりにくいですが)
最初のコードも以下のように書くとよりよいです。

public class Player : MonoBehaviour {
  private Subject<Unit> _onJumpSubject = new();
  public IObservable<Unit> OnJumpObservable => _onJumpSubject;

  private void Awake() {
    // ① あらかじめAddToしておくパターン
    _onJumpSubject.AddTo(this);
  }

  private void Update() {
    if (Input.GetKey(KeyCode.Space)) {
      // ジャンプ処理
      _onJumpSubject.OnNext(Unit.Default);
    }
  }

  private void OnDestroy() {
    // ② Destroy時にDisposeしてしまうパターン
    _onJumpSubject.Dispose();
  }
}

①か②のどちらか1つで大丈夫です。

まとめ

SubscribeのDispose忘れは必ずエラーになるとは限らず、発見されづらいため後々やっかいな問題になる可能性があります。

まずはAddToを付けるようにする、というところからでよいと思うので、とにかくSubscribeを使用するときはいつDisposeするかを同時に考える、ということを癖づけることをおすすめします。