[unity 高速化]InstantiateとDestroyを減らす工夫 - ObjectPool

シューティングの弾など「同じプレファブが短い時間で大量に発生する」場合、場合によっては秒間数百のオブジェクトが Instantiate され、Destroy されます。

この処理は非常に重いので、

  • Instantiate を SetActive(true)、Destroy を SetActive(false) に見立てればどう?

というアプローチを考えます。弾は Awake 時に同時最大数を設定しておき、それ以上(の数が)必要になった場合は、こっそり最大数を増やします。

最大数を増やす行為は結局 Instantiate になるので、無限に増加する(可能性が高い)場合このアプローチはあまり有用ではありません。(無限に増加という仕様自体を見直すべき、とも言えますが…)

サンプルコード

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    /// <summary>
    /// 使いまわす prefab
    /// </summary>
    [SerializeField]
    GameObject  Prefab = null;
    /// <summary>
    /// 初期最大数
    /// </summary>
    [SerializeField]
    int         ObjectMax = 100;
    /// <summary>
    /// 初期最大数で足りなければ自動的に追加
    /// </summary>
    [SerializeField]
    bool        AutomaticallyAdd = false;

    static List<GameObject> objects;
    static int              objectSearchIndex;

    static int              usedCount;

    /// <summary>
    /// awake
    /// </summary>
    void Awake()
    {
        objects = new List<GameObject>();

        // 初期数のオブジェクトを用意
        for (int i = 0; i < ObjectMax; i++)
        {
            instantiateObject();
        }

        objectSearchIndex = 0;
    }

    /// <summary>
    /// 使用中のオブジェクト数を取得
    /// </summary>
    public int GetUsedCount()
    {
        return usedCount;
    }
    
    /// <summary>
    /// 使用可能なオブジェクト最大数を取得
    /// </summary>
    public int GetMaxCount()
    {
        return objects.Count;
    }
    
    /// <summary>
    /// オブジェクト取得
    /// </summary>
    public GameObject AllocObject()
    {
        int no = objectSearchIndex;

        // foreach で単純に回すと、ObjectMax が増えるほど検索でコストがかかるので、
        // ちょっとした仕掛けで検索が軽くなるよう実装
        for (int i = 0; i < objects.Count; i++)
        {
            if (++no >= objects.Count)
            {
                no = 0;
            }

            var obj = objects[no];

            if (obj.gameObject.activeSelf == false)
            {
                obj.SetActive(true);
                objectSearchIndex = no;
                usedCount++;

                return obj;
            }
        }

        if (AutomaticallyAdd == true)
        {
            // バッファが足りなかった場合、自動的に増やす
            var newobj = instantiateObject();
            newobj.SetActive(true);
            usedCount++;

            return newobj;
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// 確保したオブジェクトを解放
    /// </summary>
    /// <param name="obj">確保したオブジェクト</param>
    public void FreeObject(GameObject obj)
    {
        if (obj.gameObject.activeSelf == true)
        {
            obj.SetActive(false);
            obj.transform.SetParent(this.transform);

            usedCount--;
        }
    }

    /// <summary>
    /// object instantiate
    /// </summary>
    GameObject instantiateObject()
    {
        var obj = Instantiate(Prefab, this.transform);
        obj.SetActive(false);
        objects.Add(obj);
        
        return obj;
    }
}

使い方

動作を確認したい方は Github に unity 環境を用意しました。
Sample.unity を開いてプレイ、マウスをクリックしている間テキストオブジェクトが生成されます。
(生成されたオブジェクトは 3 秒で自動的に消失します)

  • サンプルコードを空 GameObject にアタッチします。
  • インスペクタで Prefab に使いまわしたいプレファブを入れます。
  • AllocObject() を Instantiate の代わりに使います。
  • FreeObject() を Destroy の代わりに使います。

あとがき

Instantiate() の重さ回避としてググれば大量に出てくるアプローチですが、foreach で頭から検索しているコードとか多くてキニナッタため、自分で書いてみました。

とはいえ、プール最大数が数千単位にならないとたいして速度は変わらないかもしれません…?

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA