Unity|オブジェクトプールでパフォーマンス向上

今回はUnityでオブジェクトプールを実装してみます。

はじめに

Unityのバージョンは2022.3.10f1です。

オブジェクトプールとは、オブジェクトの生成、破棄を大量に行う際にCPUにかかる負荷を軽減する方法です。

例えば、シューティングゲームで弾をたくさん発射、敵キャラが大量に出てくる。頻繁にエフェクトが起きるなどの時に利用した方が良いものですね。

下記が公式リファレンスです。

出来るだけ簡単に実装しながら、オブジェクトプール機能の基本を見ていきます。

実装開始

簡単なUIを作成し、オブジェクトプールを利用する場合、利用しない場合を見ていきます。

UI作成

弾を生成して、壁に当たると消える簡単なUIを作成していきます。イメージとしてはシューティングゲームの弾です。

弾を作成

ProjectのAssetsフォルダ内で右クリックして、「Create」→「2D」→「Sprites」→「Circle」を追加。

Hierarchyにドラッグアンドドロップしてスケールを変更します。

名前をbulletに変更して、Rigidbody2DとCircleCollider2Dをアタッチ。Rigidbody2DのGravityScaleを0にします。

下記スクリプトを作成。上方向に進むように速度をセットしています。

using UnityEngine;

public class bullet : MonoBehaviour
{

    private void Start()
    {
        Rigidbody2D rb = GetComponent<Rigidbody2D>();
        rb.velocity = new Vector2(0, 3f);
    }
}

スクリプトをアタッチします。

ProjectのAssetsフォルダ内にドラッグアンドドロップしてプレハブ化。Hierarchyから削除しておきます。これで弾の完成。

弾を管理するオブジェクトを作成

Hierarchyで右クリックして、「CreateEmpty」で空オブジェクトを追加。名前をBulletManagerに変更。

下記スクリプトを作成、スペースキーを押した時に弾を生成しています。

using UnityEngine;

public class bulletManager : MonoBehaviour
{
    [SerializeField] private GameObject BulletPrefab;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(BulletPrefab, this.transform);
        }
    }
}

スクリプトをアタッチして、パラメータに弾のプレハブをセット。

壁の追加

Hierarchyで右クリックして、「2DObject」→「Sprites」→「Square」を追加。

スケールと色を変更。BoxCollider2Dをアタッチします。

下記スクリプトを作成してアタッチ、壁に当たったものを破壊(destroy)しています。

using UnityEngine;

public class wall : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D collision)
    {
        Destroy(collision.gameObject);
    }
}

試しに実行。スペースキーを連打すると、弾が壁に当たり消えます。

Hierarchy上では、スペースキーを押すと「bullet(Clone)」が作成され、壁に衝突すると消えます(Destroy)。

これがオブジェクトプールを利用していない場合です。

オブジェクトプールを利用

次に、オブジェクトプールを利用してスクリプトを書き換えていきます。まずは弾の管理を下記に変更。

using UnityEngine;
using UnityEngine.Pool;

public class bulletManager : MonoBehaviour
{
    [SerializeField] private GameObject BulletPrefab;

    private ObjectPool<GameObject> _pool;
    private bool collectionChecks = true;
    private int maxPoolSize = 10;

    private void Awake()
    {
        _pool = new ObjectPool<GameObject>(
            CreatePooledItem, 
            OnTakeFromPool, 
            OnReturnedToPool, 
            OnDestroyPoolObject, 
            collectionChecks, 
            10, 
            maxPoolSize);
    }
    private GameObject CreatePooledItem()
    {
        GameObject obj = Instantiate(BulletPrefab);
        return obj;
    }

    private void OnTakeFromPool(GameObject obj)
    {
        obj.SetActive(true);
    }

    private void OnReturnedToPool(GameObject obj)
    {
        obj.SetActive(false);
    }

    private void OnDestroyPoolObject(GameObject obj)
    {
        Destroy(obj);
    }

    public void DeleteBullet(GameObject obj)
    {
        _pool.Release(obj);
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = _pool.Get();
            obj.transform.position = this.transform.position;
        }
    }
}

ものすごくコード量が増えましたが、やってることはそれほど難しくないです。

Update()内を「Instantiate」から「ObjectPoolのGet」に変更。オブジェクトを生成するのではなく、ObjectPoolからオブジェクトを取得しています。

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = _pool.Get();
            obj.transform.position = this.transform.position;
        }
    }

次にAwake()。各パラメータに簡単にコメントを追記しました。

    private void Awake()
    {
        _pool = new ObjectPool<GameObject>(
            CreatePooledItem,    // プールが空の場合に新しいインスタンスを作成するために使用
            OnTakeFromPool,      // インスタンスがプールから取得される時に呼び出される
            OnReturnedToPool,    // インスタンスがプールに返される時に呼び出される
            OnDestroyPoolObject, // プールが最大サイズに達した為に要素をプールに戻せない時に呼び出される
            collectionChecks,    // インスタンスがプールに戻される時、既にプール内にある場合、例外スロー
            10,                  // デフォルトの容量
            maxPoolSize);        // プールの最大サイズ
    }

詳しくは下記の公式リファレンス。

流れで言うと、「スペースキーを押す」→「プールからオブジェクトを取得」→「OnTakeFromPoolでオブジェクトがアクティブになる」ですね。

ラムダ式を利用

ラムダ式を利用すると、下記の様な感じに書くこともできます。

    private void Awake()
    {
        _pool = new ObjectPool<GameObject>(
            createFunc: () => Instantiate(BulletPrefab),
            actionOnGet: obj => obj.SetActive(true),
            actionOnRelease: obj => obj.SetActive(false),
            actionOnDestroy: obj => Destroy(obj),
            collectionCheck: true,
            defaultCapacity: 10,
            maxSize: 10);
    }

コード全体は、下記の様な感じにコンパクトにもなります。

using UnityEngine;
using UnityEngine.Pool;

public class bulletManager : MonoBehaviour
{
    [SerializeField] private GameObject BulletPrefab;
    private ObjectPool<GameObject> _pool;

    private void Awake()
    {
        _pool = new ObjectPool<GameObject>(() => Instantiate(BulletPrefab),
            obj => obj.SetActive(true),
            obj => obj.SetActive(false),
            obj => Destroy(obj),
            true,10,10);
    }
    public void DeleteBullet(GameObject obj)
    {
        _pool.Release(obj);
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = _pool.Get();
            obj.transform.position = this.transform.position;
        }
    }
}

他スクリプトの修正

弾のスクリプトはStartで速度を与えているので、OnEnableに変更。

using UnityEngine;

public class bullet : MonoBehaviour
{
    private void OnEnable()
    {
        Rigidbody2D rb = GetComponent<Rigidbody2D>();
        rb.velocity = new Vector2(0, 3f);
    }
}

壁のスクリプトはDestroyからオブジェクトプールのReleaseを呼び出すように変更。

using UnityEngine;

public class wall : MonoBehaviour
{
    [SerializeField] private bulletManager _bulletmanager;

    private void OnCollisionEnter2D(Collision2D collision)
    {
        _bulletmanager.DeleteBullet(collision.gameObject);
    }
}

パラメータをセットして実行してみます。

下記の様にオブジェクトの有効/無効の切り替えになります。

あくまでも基本的な使い方ですが、こんな感じ。

タイトルとURLをコピーしました