オブジェクトプール

Instantiate と Destroy の代わりに GameObject インスタンスを再利用し、フレームタイムを安定させます。

Instantiate/Destroy の代わりに Get/Release · 起動時にプリウォームして、ずっと再利用 · no-op 代替: NullObjectPool

CGS は IObjectPoolService によって、オブジェクトの頻繁な生成と破棄のコストを取り除きます。Instantiate(prefab)Destroy(go) を繰り返し呼ぶ代わりに、Get(prefab) で準備済みのインスタンスを受け取り、Release(instance) で返却します。高コストな処理 — アロケーション、Awake 呼び出し、GameObject の登録 — はホットパスから外れ、起動時の一度きりのプリウォームステップ(起動時のインスタンス事前生成)へ移動します。その結果、敵 30 体の同時スポーンも、大量のエフェクトの炸裂も、連射されるプロジェクタイルも、フレームヒッチを起こさなくなります。

できること

  • プレハブごとのプール。 チューニングオプション付きでプレハブを一度登録すると、サービスがそのプレハブの非アクティブインスタンスのスタックを保持します。
  • 生成と破棄の代わりに取得と返却。 Get は非アクティブなインスタンスをポップし(なければ生成し)、アクティブ化して渡します。Release は非アクティブ化して戻します。
  • ライフサイクルフック。 IPoolable を実装したコンポーネントは OnSpawned()OnDespawned() のコールバックを受け取ります — 使用のたびに状態をリセットするための、プール版 Awake/OnDestroy です。
  • プリウォーム。 ロード画面中に N 個のインスタンスを事前生成しておけば、最初の激しい戦闘フレームでアロケーションコストを払うことがありません。
  • 診断。 プレハブごとのアクティブ数・非アクティブ数・合計数をいつでも取得できます。

サービスの取得

一度だけ解決してキャッシュします — Update の中で Resolve を呼んではいけません:

using CommonGameSystem.Core;

var pool = ServiceLocator.Resolve<IObjectPoolService>();

MonoBehaviour の AwakeStart 内で解決する場合は、クラスに [DefaultExecutionOrder(100)] を付けてください。プールは、自動ブートストラッパーがシーンのどの Awake よりも先に登録する 23 のサービスのひとつなので、解決する時点で準備できています。

API リファレンス

登録

void RegisterPrefab(GameObject prefab, PoolOptions options) // Create a pool for the prefab
bool IsRegistered(GameObject prefab)                        // Does this prefab have a pool?
void UnregisterPrefab(GameObject prefab)                    // Destroy the pool and all its instances

同じプレハブを二度登録すると ArgumentException がスローされます — 起動時に一度だけ登録してください。

Get / Release

GameObject Get(GameObject prefab)
GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
bool Release(GameObject instance)

Get は非アクティブなインスタンスをポップし(枯渇ポリシーによっては新規生成し)、アクティブ化して、インスタンスが IPoolable を実装していれば OnSpawned() を呼びます。3 引数のオーバーロードはアクティブ化の前に transform を設定します。Release はインスタンスを非アクティブ化し、OnDespawned() があれば呼び出してプールへ返します — どのプールにも属さないインスタンスの場合は false を返します。

プリウォーム

void Prewarm(GameObject prefab, int count) // Pre-create instances to kill first-use hiccups

診断とクリーンアップ

int CountActive(GameObject prefab)   // Instances currently in use
int CountInactive(GameObject prefab) // Instances resting in the pool
int CountTotal(GameObject prefab)    // Active + inactive
void Clear(GameObject prefab)        // Destroy all inactive instances (registration stays)
void ClearAll()                      // Clear every pool

チューニング(PoolOptions)

オプションデフォルト動作
InitialCapacity0登録時に事前生成するインスタンス数。
MaxSizeint.MaxValue非アクティブインスタンスの上限。上限を超えて返却されたインスタンスは保持されず破棄されます。
OnExhaustedGrowAndDestroy非アクティブなインスタンスがないときの Get の動作(下記参照)。
CallIPoolabletrueOnSpawned() / OnDespawned() フックを呼び出すかどうか。

枯渇ポリシー(PoolExhaustionPolicy)

ポリシープールが空のときの動作
GrowAndDestroyGet は常に新しいインスタンスを生成します(null を返しません)。返却時、MaxSize を超えるインスタンスは破棄されます。安全なデフォルトです。
GrowAndReuseGet は常に生成し、返却は常に保持します — プールは無制限に拡大します。メモリ増加への明示的なオプトインです。
ReturnNullアクティブ数が MaxSize に達すると Getnull を返し、呼び出し側が対応を決めます(たとえばエフェクトをスキップするなど)。

ライフサイクルフック(IPoolable)

プール対象プレハブの任意のコンポーネントに実装します:

void OnSpawned()   // Called right after Get activates the instance — reset state here
void OnDespawned() // Called right before Release deactivates it — clean up here

プールされたインスタンスは初回以降 Awake を実行しないため、使用ごとの初期化は OnSpawned に書きます。

完全な例

弾丸システムの例です。スポナーが登録と発射を行い、弾丸は移動し、寿命をカウントダウンして自身を返却します。

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private GameObject _bulletPrefab;

    private IObjectPoolService _pool;

    private void Awake()
    {
        _pool = ServiceLocator.Resolve<IObjectPoolService>();

        _pool.RegisterPrefab(_bulletPrefab, new PoolOptions
        {
            InitialCapacity = 50,  // Pre-create 50 bullets at startup
            MaxSize = 100,         // Keep up to 100 inactive
            OnExhausted = PoolExhaustionPolicy.GrowAndDestroy
        });
    }

    public void Fire(Vector3 position, Quaternion rotation)
    {
        // With GrowAndDestroy this never returns null.
        _pool.Get(_bulletPrefab, position, rotation);
    }
}

// On the bullet prefab:
[DefaultExecutionOrder(100)]
public class Bullet : MonoBehaviour, IPoolable
{
    [SerializeField] private float _speed = 20f;
    [SerializeField] private float _maxLifetime = 3f;

    private IObjectPoolService _pool;
    private float _lifetime;

    private void Awake()
    {
        _pool = ServiceLocator.Resolve<IObjectPoolService>();
    }

    public void OnSpawned()
    {
        // Reset per-use state — pooled instances do not re-run Awake.
        _lifetime = _maxLifetime;
    }

    public void OnDespawned()
    {
        // Optional cleanup before going back to the pool.
    }

    private void Update()
    {
        transform.Translate(Vector3.forward * _speed * Time.deltaTime);

        _lifetime -= Time.deltaTime;
        if (_lifetime <= 0f)
        {
            _pool.Release(gameObject); // Back to the pool — never Destroy()
        }
    }
}

無効化する

プーリングを無効化するには(デバッグ時や、メモリに余裕のあるプラットフォームで)、null 実装に差し替えます:

ServiceLocator.Replace<IObjectPoolService>(new NullObjectPool());

NullObjectPoolInstantiate / Destroy をそのまま使ってインスタンスを提供します。ゲームコードから見た違いはなく、パフォーマンス上の利点がなくなるだけです。呼び出し側に null チェックも変更も必要ありません。

よくある落とし穴

  • メインスレッド専用。 すべてのプールメソッドは Unity のメインスレッドから呼び出す必要があります。ワーカースレッドからの呼び出しは、Debug ビルドで InvalidOperationException をスローします。

  • 返却はプール経由で。Destroy は禁止。 プール管理下のインスタンスに UnityEngine.Object.Destroy(instance) を呼ぶと、プールのアクティブカウンターが壊れます。常に Release(instance) を使ってください。

  • プレハブは明示的に登録する。 未登録のプレハブに Get(prefab) を呼ぶと、プールはデフォルトオプションで自動登録し、一度だけ警告をログに出します。動作はしますが、InitialCapacityMaxSizeOnExhausted をチューニングする機会を失います — 事前に登録してください。

  • 1 引数の Get は古い transform を保持する。 Get(prefab) は、返却されたときの位置のままインスタンスを渡します。スポーン位置が重要な場合は Get(prefab, position, rotation) を使ってください。

  • フックの例外は封じ込められる。 OnSpawned()OnDespawned() が例外をスローしても、プールは例外をキャッチしてエラーをログに残し、処理を続行します — インスタンスのアクティブ化・非アクティブ化は通常どおり行われます。あなたのバグでプールが壊れることはありません。

  • IL2CPP のストリッピング。 独自の設定型やイベント型でプールを拡張する場合は、IL2CPP ビルドで消されないよう link.xml エントリーを追加してください。コア型(IObjectPoolServiceGameObjectPoolNullObjectPool)はフレームワークがすでに保持しています。

関連ページ

  • Bootstrap — サービスが自動的に起動する仕組み
  • Service Locator — サービスの解決と差し替え
  • Audio — 効果音ソースに内部で同じプーリングの考え方を使用