Unity公式が公開しているプログラミングパターンを読み取ってみた！～その7. Object Pool～

# はじめに
Unity公式がプログラミングパターンのデモを説明書と共に全12種公開していたので、1つずつ読み取っていきたいと思います！
英語の説明書でしたので間違った解釈をしているかもしれませんが、ご了承下さい！
最初の5種は、SOLID原則に沿ったデモとなっており、コードのみが公開されています。
残り7種は、Scene付きのデモが入っていますので、ZIPファイルをダウンロードして試してみてください。
今回は、7つ目のObject Poolについてです。

https://github.com/Unity-Technologies/game-programming-patterns-demo

https://resources.unity.com/games/level-up-your-code-with-game-programming-patterns

# 環境
OS : Windows10
Unity バージョン : 2021.3.8f1

# Object Poolとは
大量のGameObjectを作成、破棄するときに使用する最適化手法です。大量に生成するGameObjectは、ロード画面中などのユーザーが気づかない箇所で生成してしまい、後は必要に応じてGameObjectを表示・非表示させます。Object Poolを使用することで、CPUに負荷が掛かりにくくなります。

# デモ
デモは、マウスでフィールド内をクリックすると、大砲の向いている方向に弾丸が発射されるというシンプルなゲームです。
![20230218_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/70421655-a14d-3c74-46b5-1e81e9c7a4cc.gif)

ここで大量に発生するGameObjectは、弾丸になります。
Hierarchy上には空のオブジェクトObjectPoolが作成してあり、弾丸を生成して溜めておくObjectPool.csがアタッチされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/990e1ee6-5603-791e-9a2d-d37d0c026319.png)
ObjectPool.csは、ゲーム起動すると指定数(initPoolSize)弾丸を生成し、stack内にプールしておくようになっています。

```C#
public class ObjectPool : MonoBehaviour
{
    [SerializeField] private uint initPoolSize;
    [SerializeField] private PooledObject objectToPool;
    
    private Stack<PooledObject> stack;
    private void Start()
    {
        SetupPool();
    }
    // ゲーム起動と共に、initPoolSize分弾丸が生成され、stack内にプールされる
    private void SetupPool()
    {
        stack = new Stack<PooledObject>();
        PooledObject instance = null;
        for (int i = 0; i < initPoolSize; i++)
        {
            instance = Instantiate(objectToPool);
            instance.Pool = this;
            instance.gameObject.SetActive(false);
            stack.Push(instance);
        }
    }
```
ゲームを起動すると弾丸が以下のように生成されます。Projectileというのが弾丸です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/bc8eb0ce-8604-c6b3-989e-8fb91f69e4cb.png)

大砲(TurretGunBase)オブジェクトにはExampleGun.csというスクリプトがアタッチされていて、マウスクリックしたときの処理が書かれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/5a137868-9a4f-3e82-c7ad-1d53afc46586.png)

ExampleGun.csは、マスクをクリックするとプール内にある弾丸を表示させ、一定時間が経つと非表示になるようになっています。

```C#
public class ExampleGun : MonoBehaviour
{
    [Tooltip("Prefab to shoot")]
    [SerializeField] private GameObject projectile;
    [Tooltip("Projectile force")]
    [SerializeField] float muzzleVelocity = 700f;
    [Tooltip("End point of gun where shots appear")]
    [SerializeField] private Transform muzzlePosition;
    [Tooltip("Time between shots / smaller = higher rate of fire")]
    [SerializeField] float cooldownWindow = 0.1f;
    [Tooltip("Reference to Object Pool")]
    [SerializeField] ObjectPool objectPool;

    private float nextTimeToShoot;

    private void FixedUpdate()
    {
        if (Input.GetButton("Fire1") && Time.time > nextTimeToShoot && objectPool != null)
        {
            // プール内にある弾丸を表示させる
            GameObject bulletObject = objectPool.GetPooledObject().gameObject;

            if (bulletObject == null)
                return;

            bulletObject.SetActive(true);

            bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation);

            bulletObject.GetComponent<Rigidbody>().AddForce(bulletObject.transform.forward * muzzleVelocity, ForceMode.Acceleration);
            
            // 時間が経つと弾丸が非表示になる処理
            ExampleProjectile projectile = bulletObject.GetComponent<ExampleProjectile>(); 
            projectile?.Deactivate();

            nextTimeToShoot = Time.time + cooldownWindow;

        }
    }
}
```

2021年以降のUnityバージョンを使用している場合は、独自のPooledObjectまたはObjectPoolクラスを作る必要がないそうです。

# UnityのObjectPoolを使用したデモ
ObjectPoolは、よく使用されるためUnity2021バージョン以降は、独自のUnityEngine.Pool.APIをサポートするようになりました。

```c#
using UnityEngine.Pool;
public class RevisedGun : MonoBehaviour
{
    private IObjectPool<RevisedProjectile> objectPool;
    [SerializeField] private bool collectionCheck = true;
    [SerializeField] private int defaultCapacity = 20;
    [SerializeField] private int maxSize = 100;
    private void Awake()
    {
        objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
        OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
        collectionCheck, defaultCapacity, maxSize);
    }
    // ゲーム起動と共に、弾丸をプールしておく
    private RevisedProjectile CreateProjectile()
    {
        RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
        projectileInstance.ObjectPool = objectPool;
        return projectileInstance;
    }
    // 弾丸が非表示
    private void OnReleaseToPool(RevisedProjectile pooledObject)
    {
        pooledObject.gameObject.SetActive(false);
    }
    // プール内から弾丸を表示させる
    private void OnGetFromPool(RevisedProjectile pooledObject)
    {
        pooledObject.gameObject.SetActive(true);
    }
    // 上限を超えると弾丸を破棄
    private void OnDestroyPooledObject(RevisedProjectile pooledObject)
    {
        Destroy(pooledObject.gameObject);
    }
```

ほとんど、前の例と同じですが、以下機能がデフォルトで入っています。

- 最初に弾丸をプールする
- 弾丸をプールに戻す
- 上限を設定でき、超えると弾丸を破棄する

個人的には、こちらの方がシンプルに見えて好きかなと思います。