(:3[kanのメモ帳]

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

(:3[kanのメモ帳]



UniRxでオブジェクトプール(ObjectPool)を簡単実装【Unity】【UniRx】


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



この記事でのバージョン
Unity 2019.4.17f1
UniRx 7.1.0


はじめに

Unityではオブジェクトを生成する時にInstantiate、削除する時にDestroyを使いますが、

どちらもそれなりに重い処理なので出来るだけ実行回数を減らしたくなります。


そんな時に使えるのがオブジェクトプールという仕組みで、

使ったオブジェクトを削除せずに非表示にして、必要になったら再度表示して使うというものです。

f:id:kan_kikuchi:20210128065738g:plain


そしてこのオブジェクトプールの仕組みがUniRxにもあるので、

今回はそれを使ってみようという感じの記事です!


なお、そもそもUniRxとはなんぞやという方は以下の記事を参考の事。



また、記事中では以下のアセットを使っています。

Sci-Fi Arsenal | VFX Particles | Unity Asset Store



ObjectPool

まず今回はオブジェクトプールの対象としてパーティクルを使いたいので、

再生終了時に任意の処理を実行出来るようにするため、ParticlePlayerというクラスを作ってみました。

using System;
using UnityEngine;

/// <summary>
/// パーティクルを実行する用のクラス
/// </summary>
[RequireComponent(typeof(ParticleSystem))]
public class ParticlePlayer : MonoBehaviour {
  
  //再生終了時の処理
  private Action<ParticlePlayer> _callback;

  //=================================================================================
  //実行
  //=================================================================================

  public void Play(Action<ParticlePlayer> callback) {
    GetComponent<ParticleSystem>().Play();
    _callback = callback;
  }
  
  //=================================================================================
  //イベント
  //=================================================================================
  
  //パーティクルの再生が終わった時に実行
  private void OnParticleSystemStopped() {
    _callback(this);
  }

}


なお、使い方はParticleSystemと同じオブジェクトにAddして、Playで実行するだけです。

f:id:kan_kikuchi:20210128083959j:plain
particlePlayer.Play(player => Debug.Log("再生後の処理"));


ちなみに、パーティクル再生終了時の処理の仕方については以下の記事を参考の事。



次にオブジェクトプールの実装ですが、

ObjectPool<プールの対象>という形で継承したクラスを作成し、

CreateInstance(インスタンスを作る処理)を実装するだけ。


なお、OnBeforeRentでプールからオブジェクトを取得する前の処理を、

OnBeforeReturnでオブジェクトがプールに戻る前の処理を実装する事も可能です。


実際に先程のParticlePlayer用のオブジェクトプールを実装してみると以下のような感じです。

using UniRx.Toolkit;//ObjectPoolを使うのに必要
using UnityEngine;

/// <summary>
/// ParticlePlayerのオブジェクトプール
/// </summary>
public class ParticlePlayerPool : ObjectPool<ParticlePlayer> {

  private readonly ParticlePlayer _original;

  //=================================================================================
  //初期化
  //=================================================================================

  /// <summary>
  /// オリジナルを渡して初期化
  /// </summary>
  public ParticlePlayerPool(ParticlePlayer original) {
    //オリジナルは非表示に
    _original = original;
    _original.gameObject.SetActive(false);
  }
  
  //インスタンスを作る処理
  protected override ParticlePlayer CreateInstance() {
    //オリジナルを複製してインスタンス作成(オリジナルと同じ親の下に配置)
    return ParticlePlayer.Instantiate(_original, _original.transform.parent);
  }
  
  //=================================================================================
  //イベント
  //=================================================================================
  
  //プールからオブジェクトを取得する前に実行される
  protected override void OnBeforeRent(ParticlePlayer instance) {
    Debug.Log($"{instance.name}がプールから取り出されました");
    base.OnBeforeRent(instance);
  }

  //オブジェクトがプールに戻る前に実行される
  protected override void OnBeforeReturn(ParticlePlayer instance) {
    Debug.Log($"{instance.name}がプールに戻されました");
    base.OnBeforeReturn(instance);
  }

}


後は、このオブジェクトプールからパーティクルを取得したい時にRent

使い終わったらReturnを実行するだけです。

実際にこのオブジェクトプールを使うサンプルも作ってみました。

using UniRx;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;

/// <summary>
/// パーティクルのオブジェクトプールのサンプル
/// </summary>
public class ParticlePoolSample : MonoBehaviour {

  //元のパーティクル(Inspectorから設定)
  [SerializeField]
  private ParticlePlayer _original = null;

  //数値を入力する用のUI(Inspectorから設定)
  [SerializeField]
  private InputField _inputField = null;
  
  //オブジェクトプール
  private ParticlePlayerPool _pool;
  
  //=================================================================================
  //初期化
  //=================================================================================

  private void Awake() {
    //パーティクルを指定してオブジェクトプール作成
    _pool = new ParticlePlayerPool(_original);
  }
  
  //=================================================================================
  //再生処理(uGUIのボタンから実行)
  //=================================================================================

  /// <summary>
  /// パーティクルを再生
  /// </summary>
  public void PlayParticle() {
    //入力された回数だけ実行
    for (var i = 0; i < int.Parse(_inputField.text); i++) {
      //新たなParticlePlayerをプールから取得
      var particlePlayer = _pool.Rent();
    
      //パーティクルの位置をランダムに決定
      particlePlayer.transform.position = new Vector3(Random.Range(-18f, 10f), Random.Range(0, 20f), Random.Range(-18f, 2f));
    
      //パーティクルの再生を実行、再生が終わったらプールに戻す
      particlePlayer.Play(_pool.Return);
    }
  }

}
f:id:kan_kikuchi:20210128065738g:plain


必要になった分だけ新たにパーティクルが生成され、

使用後は非表示になり再度使う時にまた表示されているのが分かるかと思います。


なお、事前にプール内にオブジェクトを生成して置きたい時は、

PreloadAsync(生成数, 1フレームでの生成数)を使い、

既に生成されたインスタンスを削除したい場合は

Shrink(縮小比率, 最低個数)Clearを使います。

//10個のオブジェクトをすぐに作成(PreloadAsyncはSubscribeが必要なので注意)
_pool.PreloadAsync(10, 10).Subscribe().AddTo(gameObject);
//プール内のオブジェクトを5個に減らす
_pool.Shrink(0, 5);
//プール内のオブジェクトを0にする
_pool.Clear();


ちなみにAsyncObjectPoolという非同期用のオブジェクトプールもあります。

using System;
using UniRx;
using UniRx.Toolkit;//ObjectPoolを使うのに必要
using UnityEngine;

/// <summary>
/// ParticlePlayerのオブジェクトプール(非同期)
/// </summary>
public class AsyncParticlePlayerPool : AsyncObjectPool<ParticlePlayer> {

  private readonly ParticlePlayer _original;

  //=================================================================================
  //初期化
  //=================================================================================

  /// <summary>
  /// オリジナルを渡して初期化
  /// </summary>
  public AsyncParticlePlayerPool(ParticlePlayer original) {
    //オリジナルは非表示に
    _original = original;
    _original.gameObject.SetActive(false);
  }
  
  //インスタンスを作る処理(非同期)
  protected override IObservable<ParticlePlayer> CreateInstanceAsync(){
    //オリジナルを複製してインスタンス作成(オリジナルと同じ親の下に配置)
    var particlePlayer =  ParticlePlayer.Instantiate(_original, _original.transform.parent);
    
    //特に非同期にする処理もないのでそのまま返す
    return Observable.Return(particlePlayer);
  }

}
//新たなParticlePlayerをプールから非同期で取得
_pool.RentAsync().Subscribe(particlePlayer =>{
  //パーティクルの位置をランダムに決定
  particlePlayer.transform.position = new Vector3(Random.Range(-18f, 10f), Random.Range(0, 20f), Random.Range(-18f, 2f));
    
  //パーティクルの再生を実行、再生が終わったらプールに戻す
  particlePlayer.Play(_pool.Return);
 });



参考