オブジェクトプール
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 の Awake や Start 内で解決する場合は、クラスに [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)
| オプション | デフォルト | 動作 |
|---|---|---|
InitialCapacity | 0 | 登録時に事前生成するインスタンス数。 |
MaxSize | int.MaxValue | 非アクティブインスタンスの上限。上限を超えて返却されたインスタンスは保持されず破棄されます。 |
OnExhausted | GrowAndDestroy | 非アクティブなインスタンスがないときの Get の動作(下記参照)。 |
CallIPoolable | true | OnSpawned() / OnDespawned() フックを呼び出すかどうか。 |
枯渇ポリシー(PoolExhaustionPolicy)
| ポリシー | プールが空のときの動作 |
|---|---|
GrowAndDestroy | Get は常に新しいインスタンスを生成します(null を返しません)。返却時、MaxSize を超えるインスタンスは破棄されます。安全なデフォルトです。 |
GrowAndReuse | Get は常に生成し、返却は常に保持します — プールは無制限に拡大します。メモリ増加への明示的なオプトインです。 |
ReturnNull | アクティブ数が MaxSize に達すると Get は null を返し、呼び出し側が対応を決めます(たとえばエフェクトをスキップするなど)。 |
ライフサイクルフック(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());
NullObjectPool は Instantiate / Destroy をそのまま使ってインスタンスを提供します。ゲームコードから見た違いはなく、パフォーマンス上の利点がなくなるだけです。呼び出し側に null チェックも変更も必要ありません。
よくある落とし穴
-
メインスレッド専用。 すべてのプールメソッドは Unity のメインスレッドから呼び出す必要があります。ワーカースレッドからの呼び出しは、Debug ビルドで
InvalidOperationExceptionをスローします。 -
返却はプール経由で。
Destroyは禁止。 プール管理下のインスタンスにUnityEngine.Object.Destroy(instance)を呼ぶと、プールのアクティブカウンターが壊れます。常にRelease(instance)を使ってください。 -
プレハブは明示的に登録する。 未登録のプレハブに
Get(prefab)を呼ぶと、プールはデフォルトオプションで自動登録し、一度だけ警告をログに出します。動作はしますが、InitialCapacity、MaxSize、OnExhaustedをチューニングする機会を失います — 事前に登録してください。 -
1 引数の
Getは古い transform を保持する。Get(prefab)は、返却されたときの位置のままインスタンスを渡します。スポーン位置が重要な場合はGet(prefab, position, rotation)を使ってください。 -
フックの例外は封じ込められる。
OnSpawned()やOnDespawned()が例外をスローしても、プールは例外をキャッチしてエラーをログに残し、処理を続行します — インスタンスのアクティブ化・非アクティブ化は通常どおり行われます。あなたのバグでプールが壊れることはありません。 -
IL2CPP のストリッピング。 独自の設定型やイベント型でプールを拡張する場合は、IL2CPP ビルドで消されないよう
link.xmlエントリーを追加してください。コア型(IObjectPoolService、GameObjectPool、NullObjectPool)はフレームワークがすでに保持しています。
関連ページ
- Bootstrap — サービスが自動的に起動する仕組み
- Service Locator — サービスの解決と差し替え
- Audio — 効果音ソースに内部で同じプーリングの考え方を使用