Unity Term Book
高度な技術

Object Pooling

Object Pooling は毎回の Instantiate/Destroy の代わりにインスタンスを再利用します。弾・FX・敵ウェーブなどで GC スパイクとカクつきを抑え、フレームを安定させます。図解とシミュレーターでプールの流れと負荷差を比較しながら、実践的にしっかり学べます。自学にも最適です。

想像してみてください...

毎回新しいコーヒーカップを買って → 使って → 捨てる(Instantiate → 使用 → Destroy)代わりに、レストランは食器プールを使います: 20 杯用意し、1 つ取り、ラックに返す。買い足しも廃棄もなし——Garbage Collection ゼロ。Unity では GC.Collect() がラッシュ時に厨房へ突入する掃除人——ドロップフレームです。Pooling がそれを止めます。

概念の詳細

Instantiate/Destroy の問題:Instantiate() は新しい GameObject を確保し、コンポーネントデータをコピーし、シーンに登録します。 Destroy() はメモリを GC 対象にします。シューターは毎秒 30–50 発——頻繁な GC が典型的なカクつき(数秒ごとの「突然のラグ」)を生みます。

Object Pool パターン: ロード時に固定数を事前生成。必要時は生成ではなくプールから取り出し(非アクティブだったもの)。使い終わったら無効化して戻す。メモリは平坦で、GC は走りません。

Unity 2021+ から UnityEngine.PoolObjectPool<T>LinkedPool<T>GenericPool<T> を同梱——手書きキュー不要。コールバック: onCreateonGetonReleaseonDestroy(プール満杯時)。

典型用途: 弾、ヒット/爆発 FX、浮かぶダメージ数字、敵/NPC スポーン、足音 AudioSource、UI トースト、パーティクル。1 分に 1 回未満しか出ないオブジェクトはプール不要——管理オーバーヘッドが割に合いません。

プールの仕組み

Get()
Pool→ dequeue inactive object → SetActive(true) → returnGame Object
Release()
Game Object→ Reset state → SetActive(false) → enqueue back to poolPool
Pool full?
Release()プール満杯(countAll == maxSize)→ Destroy(object) かプール拡張🗑 Destroy

ハンズオン手順

1

IPoolable インターフェースを作る

OnGet()(プールから取るときの状態リセット)と OnRelease()(戻す前のクリーンアップ)。

2

ObjectPool を作成

new ObjectPool<T>(onCreate, onGet, onRelease, onDestroy, collectionCheck, defaultCapacity, maxSize) —— 2021+ 標準。

3

pool.Get()/pool.Release() を使う

Instantiate を pool.Get()、Destroy を pool.Release() に置き換え。onGet で位置/速度/状態をリセット。

4

ロード時にプールをプリウォーム

開始時に N 個を生成してすぐ Release。空プールでの最初のフレームスパイクを防ぎます。

5

pool.CountAll/pool.CountActive を監視

統計をログしてプールサイズが足りるか確認。countActive/countAll が高い → defaultCapacity を上げる。

インタラクティブシミュレーター

Instantiate/Destroy と Object Pool を比較——Fire を押して GC の差を確認しましょう。

Pool: 5/12

Allocations

0

GC Events

0

Active Objects

0

コード例

基本

Queue によるシンプルな弾プール——Unity パッケージ不要。

using UnityEngine;
using System.Collections.Generic;

public class BulletPool : MonoBehaviour
{
  public GameObject bulletPrefab;
  public int poolSize = 30;
  readonly Queue<GameObject> pool = new Queue<GameObject>();

  void Awake()
  {
      // Pre-warm: create and disable
      for (int i = 0; i < poolSize; i++)
      {
          var go = Instantiate(bulletPrefab);
          go.SetActive(false);
          go.transform.SetParent(transform);
          pool.Enqueue(go);
      }
  }

  public GameObject Get(Vector3 pos, Quaternion rot)
  {
      var go = pool.Count > 0
          ? pool.Dequeue()
          : Instantiate(bulletPrefab); // Expand if empty
      go.transform.SetPositionAndRotation(pos, rot);
      go.SetActive(true);
      return go;
  }

  public void Release(GameObject go)
  {
      go.SetActive(false);
      pool.Enqueue(go);
  }
}

コード例

上級

UnityEngine.Pool.ObjectPool<T>(Unity 2021+)——標準の自動拡張とコールバック。

using UnityEngine;
using UnityEngine.Pool;

public class AdvancedBulletPool : MonoBehaviour
{
  public Bullet bulletPrefab;

  ObjectPool<Bullet> pool;

  void Awake()
  {
      pool = new ObjectPool<Bullet>(
          createFunc:   () => Instantiate(bulletPrefab, transform),
          actionOnGet:  b => { b.gameObject.SetActive(true); b.OnGet(); },
          actionOnRelease: b => { b.gameObject.SetActive(false); b.OnRelease(); },
          actionOnDestroy: b => Destroy(b.gameObject), // When the pool is full
          collectionCheck: false,  // Skip duplicate checks (production)
          defaultCapacity: 20,
          maxSize:         50
      );
  }

  public Bullet Spawn(Vector3 pos, Vector3 dir)
  {
      var bullet = pool.Get();
      bullet.transform.position = pos;
      bullet.Init(dir, pool); // Bullet calls pool.Release(this) when done
      return bullet;
  }

  public void LogStats()
  {
      Debug.Log($"Pool: {pool.CountActive} active / {pool.CountAll} total");
  }
}

📌 要点

  • Pool = オブジェクト再利用、GC アロケーション回避
  • ロード時にプリウォームして初フレームスパイクを防ぐ
  • UnityEngine.Pool.ObjectPool<T> —— 2021+ 標準
  • 状態リセットは onGet、Update ではない
  • collectionCheck=true は Editor のデバッグ専用

⚠️ よくあるミス

  • ❌ すでにアクティブなオブジェクトを Release(二重リリース)

    二重 Release → プール破損 → ちらつきやクラッシュ

    ✅ 開発時は collectionCheck=true、Release() 前に null ガード

  • ❌ Get 時に状態をリセットしない

    前回の速度・色・HP が残る

    ✅ actionOnGet コールバックで完全にリセットする