Help us understand the problem. What is going on with this article?

UniRxのObjectPoolを利用する

More than 1 year has passed since last update.

ObjectPoolとは

ObjectPool(オブジェクトプール)とは、オブジェクトを使いまわすための機構のことです。

オブジェクトのInstantiateとDestroyは負荷が大きく、大量に呼び出すとフレームレートを著しく下げてしまうことにつながります。
この問題を回避するための仕組みがObjectPoolであり、一度生成したオブジェクトを残しておいて必要になった時に再利用することが可能になります。

特にモバイルゲーム開発においてObjectPoolは当たり前のように利用されており、自前で実装した方は何人もいると思います。

UniRx.Toolkit.ObjectPool

なぜここでUniRxが?と思われるかもしれませんが、実はUniRxはObjectPoolを作成する機構が最初から用意されています。
これを用いことでObjectPoolを簡単に実装することができるようになっています。

エフェクトを再生する例

ボタンがクリックされるごとにEffekseerのエフェクトを再生する、というものを題材に実装を紹介したいと思います

まずはObjectPoolを使わない実装例

ではまず比較のために、ObjectPoolを使わない場合の実装例を紹介します。

動作例

nopool.gif
ボタンがクリックされるたびにエフェクトオブジェクトが生成され、一定時間後に破棄が繰り返されている。

実装例

Effectの再生と破棄を行うスクリプト
using System;
using UniRx;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// エフェクトを再生して一定時間後に削除する
    /// </summary>
    public class ExplosionEffect : MonoBehaviour
    {
        private EffekseerEmitter _effectEmitter;

        private void Start()
        {
            _effectEmitter = GetComponent<EffekseerEmitter>();

            //エフェクトを再生
            _effectEmitter.Play();

            //1秒後に破棄
            Observable.Timer(TimeSpan.FromSeconds(1.0f)).Subscribe(_ => Destroy(gameObject));
        }
    }
}

ボタンが押されたらExplosionEffectのPrefabを作成するスクリプト
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace Samples
{
    /// <summary>
    /// エフェクトを生成する奴
    /// </summary>
    public class EffectManager : MonoBehaviour
    {
        [SerializeField]
        private Button _button;

        [SerializeField]
        private ExplosionEffect _effectPrefab; //エフェクトのPrefab

        void Start()
        {
            int num = 0;


            _button.OnClickAsObservable()
                .Subscribe(_ =>
                {
                    //ランダムな場所
                    var position = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));

                    //ボタンが押されたらエフェクト生成
                    var o = Instantiate(_effectPrefab, position, Quaternion.identity);

                    //生成と破棄をわかりやすくするために名前を連番にする(本来は不要)
                    o.name = o.name + num;
                    num++;
                });
        }
    }
}

prefab.png
prefabのコンポーネント設定

解説

特に解説することもなく、必要なタイミングでInstansiateして一定時間後にDestroyしているだけです。

UniRx.Toolkit.ObjectPoolを使った実装

ではこれをUniRx.Toolkit.ObjectPoolを使った実装例に書き換えてみましょう。

使い方は簡単で次の通りに利用すればOKです。

  1. 対象のコンポーネントTのObjectPoolとして、UniRx.Toolkit.ObjectPool<T>を継承したクラスを作成する
  2. 用意したObjectPoolを利用前にインスタンス化する
  3. ObjectPool.Rent() でオブジェクトを取得する
  4. オブジェクトを使い終わったら ObjectPool.Return(obj) でオブジェクトを返却する
  5. ObjectPool自体を使い終わったらDispose()を呼ぶ

UniRx.ToolKit.ObjectPoolはオブジェクトの貸出・返却時に自動的にGameObjectのSetAtctiveを切り替えてくれます。
この動作が問題になる場合は後述のOnBeforeRentOnBeforeReturnを実装することで対応することができます。

実装例

ObjectPool本体
using UniRx.Toolkit;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// ExplosionEffectのPool
    /// </summary>
    public class EffectPool : ObjectPool<ExplosionEffect>
    {
        private readonly ExplosionEffect _prefab;
        private readonly Transform _parenTransform;

        //コンストラクタ
        public EffectPool(Transform parenTransform, ExplosionEffect prefab)
        {
            _parenTransform = parenTransform;
            _prefab = prefab;
        }

        /// <summary>
        /// オブジェクトの追加生成時に実行される
        /// </summary>
        protected override ExplosionEffect CreateInstance()
        {
            //新しく生成
            var e = GameObject.Instantiate(_prefab);

            //ヒエラルキーが散らからないように一箇所にまとめる
            e.transform.SetParent(_parenTransform);

            return e;
        }
    }
}

ObjectPool自体はMonoBehaviourを継承していないため、Componentとして振る舞うことはできません。


ExplosionEffect
using System;
using UniRx;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// エフェクトを再生して一定時間後に通知する
    /// </summary>
    public class ExplosionEffect : MonoBehaviour
    {
        private EffekseerEmitter _effectEmitter;
        private EffekseerEmitter Emitter
        {
            get
            {
                //遅延初期化に変更
                return _effectEmitter ?? (_effectEmitter = GetComponent<EffekseerEmitter>());
            }
        }

        /// <summary>
        /// エフェクトを再生する
        /// </summary>
        /// <param name="position">再生する座標</param>
        /// <returns>再生終了通知</returns>
        public IObservable<Unit> PlayEffect(Vector3 position)
        {
            transform.position = position;

            //エフェクトを再生
            Emitter.Play();

            //1秒後にエフェクトを止めて終了通知
            return Observable.Timer(TimeSpan.FromSeconds(1.0f))
                .ForEachAsync(_ => Emitter.Stop());
        }
    }
}

Start()で再生していたのを変更し、PlayEffect()を呼び出すことでエフェクトを生成するように変更しました。
また、再生から1秒後に終了通知を発行するようにしました。ForEachAsync()Do()+AsUnitObservable()と同じ挙動という認識で今回は問題ありません

(なおこの実装の場合はPlayEffectが返すIObservableをSubscribeしないとEmitter.Stop()が実行されないので注意が必要です)


ObjectPoolを利用するEffectManager
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.UI;

namespace Samples
{
    /// <summary>
    /// エフェクトを生成する奴
    /// </summary>
    public class EffectManager : MonoBehaviour
    {
        [SerializeField]
        private Button _button;

        [SerializeField]
        private ExplosionEffect _effectPrefab;

        [SerializeField]
        private Transform _hierarchyTransform; //追加

        private EffectPool _effectPool; //追加

        void Start()
        {
            //オブジェクトプールを生成
            _effectPool = new EffectPool(_hierarchyTransform, _effectPrefab);

            //破棄されたときにPoolを解放する
            this.OnDestroyAsObservable().Subscribe(_ => _effectPool.Dispose());

            //ボタンが押されたらエフェクト生成
            _button.OnClickAsObservable()
                .Subscribe(_ =>
                {
                    //ランダムな場所
                    var position = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));

                    //poolから1つ取得
                    var effect = _effectPool.Rent();

                    //エフェクトを再生し、再生終了したらpoolに返却する
                    effect.PlayEffect(position)
                        .Subscribe(__ =>
                        {
                            _effectPool.Return(effect);
                        });
                });
        }
    }
}

ObjectPoolをインスタンス化し、そこからオブジェクトを取得する形式になったのが大きな違いです。

比較

ObjectPoolを使わない
_button.OnClickAsObservable()
    .Subscribe(_ =>
    {
        //ランダムな場所
        var position = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));
        //ボタンが押されたらエフェクト生成
        Instantiate(_effectPrefab, position, Quaternion.identity);
    });
ObjectPoolを使う
//ボタンが押されたらエフェクト生成
_button.OnClickAsObservable()
    .Subscribe(_ =>
    {
        //ランダムな場所
        var position = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));

        //poolから1つ取得
        var effect = _effectPool.Rent();

        //エフェクトを再生し、再生終了したらpoolに返却する
        effect.PlayEffect(position)
            .Subscribe(__ =>
            {
                _effectPool.Return(effect);
            });
    });

このように、オブジェクトが必要になったタイミングでRent()を呼び出すことでオブジェクトを取得することができます。なお、オブジェクトの利用完了した時はReturn()を呼び出してPoolにオブジェクトを手動で返却する必要があります。

動作例

pool1.gif
(2個のオブジェクトを使いまわしてエフェクトを再生している)

pool2.gif
(Poolに貯めてある個数以上のオブジェクトが必要になった場合は都度生成される)

UniRx.ToolKit.ObjectPoolの機能をいくつか抜粋して紹介

プールの中身を空にする

Clear()を呼び出すことでプールサイズを空にすることができます。

似たようなものにDispose()がありますが、こちらはObjectPool自体を破棄する場合に呼び出すメソッドとなっています。
まだプールを使うならClear()、もう使わないのならDispose()を呼ぶとよいでしょう。

プールサイズを事前に拡張する

PreloadAsyncを利用することでプールサイズを拡張する(オブジェクトを事前に生成する)ことができます。
第一引数に目標のサイズを、第二引数に1フレームあたりいくつまでオブジェクトを生成するかを指定します。
(1フレームにまとめて拡張することもでき、何フレームかに分散して拡張することもできるようになっています)

なおPreloadAsyncは非同期実行を前提にしており、Subscribe()を行わないと拡張作業が始まらない点に注意してください。

PreloadAsync利用例
//プールサイズを10まで拡張(1フレームでまとめて生成)
_effectPool.PreloadAsync(10, 10).Subscribe();

//プールサイズを20まで拡張(1フレームあたり2個ずつゆっくりと生成)
_effectPool.PreloadAsync(20, 2).Subscribe();

//OnNext or OnCompleted は拡張が完了したタイミングで実行される
//生成の途中で例外が出た場合はOnErrorとなって中断される
_effectPool.PreloadAsync(100, 1).Subscribe(_ => Debug.Log("拡張終了"), Debug.LogError);

プールサイズを縮小する

一度に縮小する

Shrink()を呼び出すことでプールサイズを縮小(オブジェクトを破棄)することができます。
引数は3つあり、それぞれ「縮小の比率」「最低個数」「OnBeforeRentコールバックの実行の有無」となっています

Shrink
//プールサイズを30%のサイズまで縮小、ただし最低3個は残す、削除されたオブジェクトのOnBeforeRentコールバックは実行しない
_effectPool.Shrink(instanceCountRatio: 0.3f, minSize: 3, callOnBeforeRent: false);

一定時間ごとにShrinkを実行する

StartShrinkTimer()を利用することで定期的にShrink()を実行することができます。
内部実装はObservable.Intervalで定期的にShrink()を呼び出しているだけになっています。

/// <summary>
/// If needs shrink pool frequently, start check timer.
/// </summary>
/// <param name="checkInterval">Interval of call Shrink.</param>
/// <param name="instanceCountRatio">0.0f = clearAll ~ 1.0f = live all.</param>
/// <param name="minSize">Min pool count.</param>
/// <param name="callOnBeforeRent">If true, call OnBeforeRent before OnClear.</param>
public IDisposable StartShrinkTimer(TimeSpan checkInterval, float instanceCountRatio, int minSize, bool callOnBeforeRent = false)
{
    return Observable.Interval(checkInterval)
        .TakeWhile(_ => disposedValue)
        .Subscribe(_ =>
        {
            Shrink(instanceCountRatio, minSize, callOnBeforeRent);
        });
}

(UniRxの実装より抜粋)

オブジェクトの貸出、返却、削除時に処理を挟む

ObjectPoolの実装時にOnBeforeRentOnBeforeReturnOnClearをそれぞれoverrideしてあげることで処理を挟むことができるようになります。

コールバックの実装
using UniRx.Toolkit;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// ExplosionEffectのPool
    /// </summary>
    public class EffectPool : ObjectPool<ExplosionEffect>
    {
        private readonly ExplosionEffect _prefab;
        private readonly Transform _parenTransform;

        //コンストラクタ
        public EffectPool(Transform parenTransform, ExplosionEffect prefab)
        {
            _parenTransform = parenTransform;
            _prefab = prefab;
        }

        /// <summary>
        /// オブジェクトの追加生成時に実行される
        /// </summary>
        protected override ExplosionEffect CreateInstance()
        {
            var e = GameObject.Instantiate(_prefab);

            //ヒエラルキーが散らからないようまとめる
            e.transform.SetParent(_parenTransform);

            return e;
        }

        /// <summary>
        /// オブジェクトの貸出時に実行される
        /// </summary>
        protected override void OnBeforeRent(ExplosionEffect instance)
        {
            //貸し出すオブジェクトのインスタンスIDを出力
            Debug.Log(instance.GetInstanceID());

            //baseではinstance.gameObject.SetActive(true)を実行している
            base.OnBeforeRent(instance);
        }

        protected override void OnBeforeReturn(ExplosionEffect instance)
        {
            //baseではinstance.gameObject.SetActive(false)を実行している
            base.OnBeforeReturn(instance);

            //返却されたオブジェクトのインスタンスIDを出力
            Debug.Log(instance.GetInstanceID());
        }

        protected override void OnClear(ExplosionEffect instance)
        {
            //削除オブジェクトのインスタンスIDを出力
            Debug.Log(instance.GetInstanceID());

            //baseでDestoryされる
            base.OnClear(instance);
        }
    }
}

ObjectPool全体の挙動を非同期にする

今紹介したObjectPoolではRent()およびCreateInstance()が同期処理でした。
これらの処理を非同期処理として振る舞わせることができるAsyncObjectPoolも用意されており、こちらを用いることでオブジェクトの生成と貸出を非同期化することができます。

AsyncObjectPoolを用いた例
using UniRx;
using UniRx.Toolkit;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// EffectPoolのAsync版
    /// </summary>
    public class EffectPoolAsync : AsyncObjectPool<ExplosionEffect>
    {
        private readonly ExplosionEffect _prefab;
        private readonly Transform _parenTransform;

        //コンストラクタ
        public EffectPoolAsync(Transform parenTransform, ExplosionEffect prefab)
        {
            _parenTransform = parenTransform;
            _prefab = prefab;
        }

        /// <summary>
        /// 非同期でインスタンスを生成できる
        /// </summary>
        protected override IObservable<ExplosionEffect> CreateInstanceAsync()
        {
            var e = GameObject.Instantiate(_prefab);
            e.transform.SetParent(_parenTransform);

            //今回の例だと非同期にする要素が無いのでObservable.Returnで値をそのまま返して終了
            return Observable.Return(e);
        }
    }
}
AsyncObjectPoolの利用例
var _asyncPool = new EffectPoolAsync(_hierarchyTransform, _effectPrefab);

//プールに未使用のオブジェクトがある場合はRentAsyncは即値を返す
//オブジェクトが足りない場合はCreateInstanceを実行しその結果を持ってRentAsyncは値を返す
_asyncPool.RentAsync().Subscribe(effect =>
{
    effect.PlayEffect(Vector3.zero).Subscribe(__ => _asyncPool.Return(effect));
});

普段はObjectPool<T>を利用し、CreateInstance中に非同期でリソースの読み込みが必要な場合など本当に非同期にしなくていはいけない場合のみAsyncObjectPool<T>を使うと良さそうです。
UniRxの作者のneueccさんもブログで述べられていますが、同期的な実装の方が圧倒的に使いやすいです。無理に非同期にする必要はなく、できるだけ同期に寄せて書いたほうが考えることが減るので自分もオススメです。

最後に

もしObjectPoolが必要になった場合は、UniRxのObjectPoolを使うと簡単に実装できるよという話でした。

そもそも本当にObjectPoolが必要なのかというと、リリースしようとしているプラットフォームによるかと思います。
PCをターゲットにしているなら、ObjectPoolを使わずに富豪的プログラミングで書いても問題なかったりします。

まずはUnityProfilerで動作を確認して、InstantiateやDestroyのコストが無視できない、GC Allocが発生していてまずいとなってからObjectPoolに差し替えるという方針でよいかなぁと思いました(個人の感想)

(余談)

Effekseer、UnityのShurikenより圧倒的に使いやすいので個人的にはおすすめです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away