1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】オブジェクトプールの効果をメモリの数値を見ながら試してみた

Posted at

はじめに

エフェクトの再生機能などはよくオブジェクトプールの仕組みを使用して作られることが多かったり、そうしたほうが良いとされています。

メリットとして大体上がるのが

  • オブジェクトを使いまわすのでメモリを一定にできる
  • オブジェクトを使いまわすのでオブジェクト破棄でのFPSのカクツキを抑えることができる
    などがあると思います。

言われていることは結構簡単なことなので実際に検証などすることなくなんとなくでオブジェクトプールを作成していたのですが本当にメリットがあるのかメモリプロファイラーを見ながら検証してみたいと思います。

必要なもの

UnityのMemoryProfiler
こちらはPackageMangerでインストール必要があります。
任意のタイミングでスナップショットを取り、その瞬間にどのような要素でどれぐらいメモリを確保しているのか詳しく見ることができます。

メモリ計測のための準備

エフェクトを用意する

image.png

こんな感じのほぼデフォルトのエフェクトを作成します。
作ったら後程たくさんInstantiateするコードを作成するのでPrefabにします。

エフェクトを沢山出す処理を作る

メモリが増えたことを確かめたいのでエフェクトを沢山出す処理を書きます。

public class Test : MonoBehaviour
{
    /// <summary>
    /// パーティクルを出す最大数
    /// </summary>
    private static readonly int MAX_PARTICLE_COUNT = 100;
    
    /// <summary>
    /// 生成するエフェクトのオブジェクト
    /// </summary>
    [SerializeField] 
    private GameObject _particleObj;

    /// <summary>
    /// パーティクルの数
    /// </summary>
    private int _particleCount = 0;

    /// <summary>
    /// パーティクルシステム
    /// </summary>
    private ParticleSystem _particleSystem = null;
    void Start()
    {
        
    }

    void Update()
    {
        // Nullまたは再生が停止していたら生成する
        if (_particleSystem == null || !_particleSystem.isPlaying)
        {
            if (_particleCount < MAX_PARTICLE_COUNT)
            {
                var obj = Instantiate(_particleObj);
                _particleSystem = obj.GetComponent<ParticleSystem>();
                _particleCount++;
            }
        }
    }
}

パーティクルのアニメーションが終わったら次のパーティクルが生成される処理です。
これで100個生成されます。

image.png

これを適当なオブジェクトにつけてPlayします。

image.png

沢山生成されていそうです。
これで準備は完了です。

メモリを計測する

UnityをPlayし、パーティクルが出たら一時停止します。

image.png

Window > Analysis > Memory Profiler でメモリプロファイラーを開きます。

image.png

この画面の左上のCaptureボタンでスナップショットを取ります。一時停止している状態のメモリを計測できます。
※UnityEditorでのメモリをキャプチャーするので結構余分なメモリが蓄積されていると思います。実機(iOSやAndroidなど)でビルドして計測したほうが正確ですが今回はパーティクル関係のオブジェクトに関してみたいので無視します。

image.png

なんの要素でどれぐらいメモリ消費がされているのか結果が表示されます。
ParticleSystemは赤枠の部分で5.5MB使っているようです。
上のSummaryタブをUnityObjectsタブに変更してコンポーネントでどのようにメモリ確保されているのか詳細を見てみます。

image.png

ParticleSystemは105個存在しているようです。(赤枠に個数がかかれています)
パーティクルは100個出したので5つ多いですがこの5つはスクリプトに張り付けたPrefabが持っているParticleSystemだったり、スクリプトで確保した変数の分だと思います。

image.png

気になるコンポーネントをクリックするとどんな要素からそれが参照されているのか詳細を見ることができます。
ParticleSystem(Clone)はTransform(Clone)から参照されていることが確認できるためこれはInstantiateで生成されたGameObjectについているParticleSystemだと特定できます。
このParticleSystem(Clone)は100個あり、一つあたり53.7KB消費しているようです。

image.png

逆にParticleSystemはTestクラスやPrefabから参照されていることがわかるためこれはスクリプトにアタッチしたPrefabが持っているコンポーネントだと特定できます。

オブジェクトプールをする

単純に100個生成するとParticleSystemが100個以上作られて無駄が多いのでオブジェクトプールを作り、それで生成するように変更します。

オブジェクトプールを使ったコード

public class Test : MonoBehaviour
{
    /// <summary>
    /// パーティクルを出す最大数
    /// </summary>
    private static readonly int MAX_PARTICLE_COUNT = 100;
    
    /// <summary>
    /// 生成するエフェクトのオブジェクト
    /// </summary>
    [SerializeField] 
    private GameObject _particleObj;

    /// <summary>
    /// パーティクルの数
    /// </summary>
    private int _particleCount = 0;

    /// <summary>
    /// パーティクルシステム
    /// </summary>
    private ParticleSystem _particleSystem = null;

    private List<GameObject> _particleObjs = new List<GameObject>();

    void Start()
    {
        int poolSize = 3;
        for (int i = 0; i < poolSize; i++)
        {
            var particleObj = Instantiate(_particleObj);
            particleObj.SetActive(false);
            _particleObjs.Add(particleObj);
        }
    }

    void Update()
    {
        // パーティクルの再生が終わっていたら非表示にする
        InActiveStopParticle();
        
        // エフェクトを作成する
        if (Input.GetKeyDown(KeyCode.Q))
        {
            if (_particleCount < MAX_PARTICLE_COUNT)
            {
                var obj = ParticlePool();
                obj.SetActive(true);
                _particleCount++;
            }
        }
        
        // 100個作ったらログを出す
        if (_particleCount == MAX_PARTICLE_COUNT - 1)
        {
            Debug.Log("100個生成した");
        }
    }

    /// <summary>
    /// パーティクルのオブジェクトプール
    /// </summary>
    private GameObject ParticlePool()
    {
        var firstInactiveObj = _particleObjs.FirstOrDefault(obj => !obj.activeSelf);
        if (firstInactiveObj != null)
        {
            return firstInactiveObj;
        }
        else
        {
            var newParticleObj = Instantiate(_particleObj);
            _particleObjs.Add(newParticleObj);
            return newParticleObj;
        }
    }

    /// <summary>
    /// 止まったエフェクトがあれば非アクティブにする
    /// </summary>
    private void InActiveStopParticle()
    {
        foreach (var obj in _particleObjs)
        {
            if (!obj.activeSelf)
            {
                continue;
            }
            
            var particleSystem = obj.GetComponent<ParticleSystem>();
            if (!particleSystem.isPlaying)
            {
                obj.SetActive(false);
            }
        }
    }
}

最初にいくつかパーティクルを作成し、Listに追加、生成するときに非アクティブなものがあるかを探し、非アクティブのものがあれば使用します。
なかったら新規で作成する簡単な構造です。

プールされた結果のメモリ

image.png

このシステムによってParticleSystemの生成数を5個(1つは元々のPrefabについているもの)に抑えることができました。
ParticleSystemの総使用メモリは262KBまで少なくすることができました。1/20までに抑えることができています。

おわりに

オブジェクトプールを使用するとオブジェクトの生成を抑えることができる
といったあいまいな知識しかありませんでしたが実際に使用されているメモリを見ることで効果を実感することができました。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?