スポンサーリンク
UnityUnityメモ

Unityでオブジェクトプールを実装する方法と最適化ポイント

Unity

1. はじめに

Unityでゲームを作っていると、必ずといっていいほど直面するのが「大量のオブジェクトをどう管理するか」という問題です。特にシューティングやアクションゲームでは、敵キャラや弾、エフェクトなどが次々に生成・削除されるため、気づかないうちに処理の重さや動作のカクつきにつながってしまうこともあります。

実はその原因の多くは、Instantiate()Destroy() を頻繁に使うことにあります。これらは便利ですが、繰り返し呼び出すとパフォーマンス低下やGC(ガベージコレクション)による処理落ちを引き起こすことがあるんです。

そこで登場するのが「ObjectPoolパターン(オブジェクトプール)」です。必要なオブジェクトをあらかじめ用意しておき、使い終わったら破棄せず再利用することで、ゲームを軽快かつ安定して動かすことができます。

この記事では、ObjectPoolの基本的な仕組みから、Unity公式クラスを活用した実装例、さらに応用的なテクニックまでを体系的に解説していきます。Unity初心者から中級以上の方まで、知っておくと必ず役立つ知識なので、ぜひ最後までチェックしてみてくださいね。

📘 作って学べる Unity本格入門 [Unity 6対応版]
✅ Amazonでチェックする✅ 楽天でチェックする


2. ObjectPoolパターンの基本原理

まずは「ObjectPoolパターンってそもそも何?」というところから見ていきましょう。 ObjectPoolは、ゲームでよく使われるデザインパターン(設計の考え方)のひとつで、簡単にいうと「使い終わったオブジェクトを捨てずに再利用する仕組み」です。

仕組みの流れ

  1. プール(在庫)を用意
    あらかじめ弾や敵キャラなどのPrefabを複数生成しておき、非アクティブな状態で「待機」させておきます。
  2. 必要になったら取り出す
    ゲーム中に弾を撃つときなどは、新しくInstantiate()するのではなく、プールから1つ取り出してSetActive(true)にします。
  3. 使い終わったら返す
    弾が画面外に出た、敵が倒されたといったタイミングで、DestroyせずにSetActive(false)にしてプールへ戻します。
  4. 再利用
    その後また必要になれば、プールから同じオブジェクトを取り出して使います。

つまり「作る(Instantiate)」「消す(Destroy)」の繰り返しを避け、再利用することで処理を軽くするのがObjectPoolの狙いです。




3. 実装の基本手順

ここからは実際にObjectPoolをコードで実装していく流れを見ていきましょう。 最もシンプルな形は「在庫を管理するクラス」と「在庫になるオブジェクト」を分けて作ることです。

クラスの役割

クラス名役割
ObjectPoolオブジェクトの在庫を管理(生成・貸し出し・返却)
PooledObject管理される側のオブジェクト(弾や敵など)

実装の流れ

① 在庫の準備

まず、ゲーム開始時にあらかじめ一定数のオブジェクトを生成しておきます。 生成したオブジェクトはSetActive(false)で非アクティブ状態にし、Stackなどのコレクションに入れておきます。


// 在庫の初期化
void SetupPool(int initCount) {
    for (int i = 0; i < initCount; i++) {
        var obj = Instantiate(objectPrefab);
        obj.SetActive(false);
        stack.Push(obj);
    }
}

② 在庫から取り出す

オブジェクトが必要になったら、在庫から1つ取り出してSetActive(true)にします。 もし在庫が足りなければ、新しくInstantiate()して補充する仕組みを作ります。


GameObject GetPooledObject() {
    if (stack.Count > 0) {
        var obj = stack.Pop();
        obj.SetActive(true);
        return obj;
    }
    // 在庫が空なら新規生成
    var newObj = Instantiate(objectPrefab);
    return newObj;
}

③ 使用後に返却する

弾が画面外に出た時や敵が倒れた時は、Destroy()せずにSetActive(false)にして再び在庫に戻します。


public void ReturnToPool(GameObject obj) {
    obj.SetActive(false);
    stack.Push(obj);
}

この「準備 → 取り出し → 返却」の流れがObjectPoolの基本サイクルです。 ゲーム内で繰り返し利用することで、処理の重さを大幅に軽減できます。




4. 応用的な実装(ジェネリック・抽象・Factoryパターン)

基本のプールが動いたら、次は再利用性と保守性を高める設計にアップグレードしていきましょう。ここでは「抽象クラス」「ジェネリック」「Factoryパターン」を組み合わせ、弾・敵・エフェクトなど種類が違うオブジェクトでも同じ仕組みで管理できる形にします。

設計のゴール

  • 管理対象ごとにスクリプトを複製しない(ObjectPool<T>で統一)。
  • 「生成の仕方」をプール本体から分離(Factory導入)。
  • 返却処理(Release)をオブジェクト自身から安全に呼べるようにする。

① 在庫側の抽象クラス:APooledObject

プールに返すときのコールバック(Action<APooledObject>)を登録できるようにして、Release()でいつでも返却できる設計にします。


using System;
using UnityEngine;

public abstract class APooledObject : MonoBehaviour
{
    private Action&lt;APooledObject&gt; _onRelease;

    // プールから取り出されたときに呼ばれる初期化
    public virtual void OnTakenFromPool() { }

    // プールに戻される直前に呼ばれるクリーンアップ
    public virtual void OnReturnedToPool() { }

    // プール管理側から登録されるコールバック
    public void SetReleaseAction(Action&lt;APooledObject&gt; action)
    {
        _onRelease = action;
    }

    // 自分自身をプールへ返す
    public void Release()
    {
        _onRelease?.Invoke(this);
    }
}

例えば「弾」は寿命タイマーや命中時にRelease()を呼べばOK。返却処理が一箇所にまとまるので、Destroyの混入を防止できます。


② 生成を分離する:IFactory<T>

プール本体からInstantiate()の詳細を切り離すために、Factoryを導入します。こうしておくと、Prefab以外の生成(アドレス可能、ネットワーク同期、特殊初期化など)にも柔軟に対応できます。


public interface IFactory&lt;T&gt; where T : APooledObject
{
    T Create();
}

// 代表例:Prefabを使うファクトリ
using UnityEngine;

public class PrefabFactory&lt;T&gt; : IFactory&lt;T&gt; where T : APooledObject
{
    private readonly T _prefab;
    private readonly Transform _parent;

    public PrefabFactory(T prefab, Transform parent = null)
    {
        _prefab = prefab;
        _parent = parent;
    }

    public T Create()
    {
        return Object.Instantiate(_prefab, _parent);
    }
}



③ 在庫管理の汎用化:ObjectPool<T>

T : APooledObjectの制約をつけたジェネリックプールです。
貸し出し(Get)時と返却(Return)時のフックを持たせ、SetActiveと初期化/クリーンアップを一元管理します。


using System.Collections.Generic;
using UnityEngine;

public class ObjectPool&lt;T&gt; where T : APooledObject
{
    private readonly Stack&lt;T&gt; _stack = new Stack&lt;T&gt;();
    private readonly IFactory&lt;T&gt; _factory;
    private readonly int _maxSize;

    public ObjectPool(IFactory&lt;T&gt; factory, int defaultCapacity = 16, int maxSize = 256)
    {
        _factory = factory;
        _maxSize = Mathf.Max(defaultCapacity, maxSize);
        // 初期在庫
        for (int i = 0; i &lt; defaultCapacity; i++)
        {
            var inst = CreateNew();
            Return(inst);
        }
    }

    private T CreateNew()
    {
        var inst = _factory.Create();
        inst.gameObject.SetActive(false);
        // 返却コールバックを在庫側に注入
        inst.SetReleaseAction(obj =&gt; Return(obj as T));
        return inst;
    }

    public T Get()
    {
        var inst = _stack.Count &gt; 0 ? _stack.Pop() : CreateNew();
        inst.gameObject.SetActive(true);
        inst.OnTakenFromPool();
        return inst;
    }

    public void Return(T inst)
    {
        inst.OnReturnedToPool();
        inst.gameObject.SetActive(false);
        if (_stack.Count &lt; _maxSize)
        {
            _stack.Push(inst);
        }
        else
        {
            // 上限超過時は破棄(設計ポリシー次第)
            Object.Destroy(inst.gameObject);
        }
    }
}

④ 使用例:弾オブジェクトを汎用プールで管理

弾のふるまい(寿命やヒット時の返却)はAPooledObjectを継承したクラスにまとめます。


using UnityEngine;

public class Bullet : APooledObject
{
    [SerializeField] float _lifeTime = 2f;
    float _timer;

    void OnEnable() { _timer = 0f; }

    void Update()
    {
        _timer += Time.deltaTime;
        if (_timer &gt;= _lifeTime)
        {
            Release(); // 自分でプールに戻る
        }
    }

    private void OnCollisionEnter(Collision other)
    {
        // 命中したら返却(エフェクトは別処理など)
        Release();
    }

    public override void OnTakenFromPool()
    {
        // 速度や見た目を初期化
        _timer = 0f;
    }

    public override void OnReturnedToPool()
    {
        // 状態クリア(トレイルやパーティクル停止など)
    }
}

発射側のスクリプトでは、プールを初期化してGet()で取り出すだけです。


using UnityEngine;

public class Shooter : MonoBehaviour
{
    [SerializeField] Bullet bulletPrefab;
    [SerializeField] Transform muzzle;

    ObjectPool&lt;Bullet&gt; _bulletPool;

    void Awake()
    {
        var factory = new PrefabFactory&lt;Bullet&gt;(bulletPrefab, null);
        _bulletPool = new ObjectPool&lt;Bullet&gt;(factory, defaultCapacity: 32, maxSize: 256);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            var b = _bulletPool.Get();
            b.transform.SetPositionAndRotation(muzzle.position, muzzle.rotation);
            // 発射速度や初期値をここで設定
        }
    }
}

実装のコツ

  • 初期化/リセット徹底OnTakenFromPool()OnReturnedToPool()で状態を必ず戻す。
  • 返却の一元化Release()を通すことで、誤ってDestroy()を混ぜる事故を防ぐ。
  • 可視化:在庫数・貸出数をInspectorで確認できるよう統計を持たせると運用が楽。

スクリプトの作成・アタッチ手順
・新規スクリプトは、プロジェクトウィンドウを右クリック「Create」→「C# Script」を選んで作成し、(スクリプト名)と名前を付けます。
・作成したスクリプトは、対象のGameObjectへドラッグ&ドロップしてアタッチします。




5. Unity公式のObjectPool<T>クラスの使い方

Unity 2021.1以降は UnityEngine.Pool.ObjectPool<T> が用意されています。これを使うと、createFunc / actionOnGet / actionOnRelease / actionOnDestroy / maxSize などを渡すだけで、堅牢なプールがすぐ使えます。

使いどころ

  • 弾・敵・エフェクト・UI項目(リサイクルするリストアイテム)など、短命で大量に出入りするオブジェクト
  • Instantiate/Destroyの多用によるGCやスパイクを避けたいとき。

準備(デモ環境)

  1. 管理対象オブジェクト(例:EnemyObject)のPrefabを1つ用意します。
    ※ 3Dオブジェクトを作る場合は、ヒエラルキー(Hierarchy)で右クリック3D ObjectSphere などを選び、必要に応じてPrefab化します。
  2. プロジェクトウィンドウを右クリック「Create」→「C# Script」を選んで、新しいスクリプトを作成し、「EnemyObject」と名前を付けます。
  3. 同様に「EnemyObjectPool」というスクリプトを作成します。
  4. 作成したスクリプトは、対象のGameObjectへドラッグ&ドロップしてアタッチします。

管理対象クラス(EnemyObject)

返却のフックを持たせ、一定時間で自動的にプールへ戻る例です。


using System;
using UnityEngine;

public class EnemyObject : MonoBehaviour
{
    private Action&lt;EnemyObject&gt; _onDisableCallback;
    [SerializeField] private float _lifeTime = 2f;
    private float _timer;

    public void Initialize(Action&lt;EnemyObject&gt; onDisableCallback)
    {
        _onDisableCallback = onDisableCallback;
        _timer = 0f;
        // ここでHPや速度などの初期化を行う
    }

    private void OnEnable()
    {
        _timer = 0f;
    }

    private void Update()
    {
        _timer += Time.deltaTime;
        if (_timer &gt;= _lifeTime)
        {
            // 自分でプールへ返却を通知
            _onDisableCallback?.Invoke(this);
        }
    }

    private void OnCollisionEnter(Collision other)
    {
        // ぶつかったら返却する例
        _onDisableCallback?.Invoke(this);
    }
}



プール管理クラス(EnemyObjectPool)

ObjectPool<T> を生成し、コールバックで取出時/返却時の処理を集中管理します。


using UnityEngine;
using UnityEngine.Pool;

public class EnemyObjectPool : MonoBehaviour
{
    [SerializeField] private EnemyObject _enemyPrefab;
    [SerializeField] private int _defaultCapacity = 16;
    [SerializeField] private int _maxSize = 256;

    private ObjectPool&lt;EnemyObject&gt; _enemyPool;

    private void Start()
    {
        _enemyPool = new ObjectPool&lt;EnemyObject&gt;(
            createFunc: OnCreateObject,
            actionOnGet: OnTakeFromPool,
            actionOnRelease: OnReturnedToPool,
            actionOnDestroy: OnDestroyObject,
            defaultCapacity: _defaultCapacity,
            maxSize: _maxSize
        );
    }

    private EnemyObject OnCreateObject()
    {
        var inst = Instantiate(_enemyPrefab);
        inst.gameObject.SetActive(false);
        return inst;
    }

    private void OnTakeFromPool(EnemyObject enemy)
    {
        // 取得時:初期化+表示
        enemy.Initialize(() =&gt; _enemyPool.Release(enemy));
        enemy.gameObject.SetActive(true);
    }

    private void OnReturnedToPool(EnemyObject enemy)
    {
        // 返却時:非表示+クリーンアップ(必要なら)
        enemy.gameObject.SetActive(false);
        // Trail/Particle/残存状態のリセットなど
    }

    private void OnDestroyObject(EnemyObject enemy)
    {
        // 上限超過時やClear()で呼ばれる
        Destroy(enemy.gameObject);
    }

    public EnemyObject GetEnemy()
    {
        return _enemyPool.Get();
    }

    public void ClearEnemy()
    {
        // 非アクティブ在庫を破棄(OnDestroyObjectが呼ばれる)
        _enemyPool.Clear();
    }
}

挙動のポイント

  • 在庫不足createFuncactionOnGet の順で呼ばれる。
  • 在庫ありcreateFuncは呼ばれず、actionOnGetだけ実行。
  • 上限超過:返却時にプールへ戻せない場合は actionOnDestroy が呼ばれ、破棄される。
  • Clear():非アクティブ在庫に対して actionOnDestroy が呼ばれて削除。

よくある落とし穴
・返却後もほかのスクリプトが参照を握っていると不整合が発生します。
・返却前にコルーチン/タイマー/パーティクルを確実に停止しましょう。
SetActive()は軽くはありません。1フレーム内の大量切替は避け、バッチ化やスケジューリングを検討しましょう。

運用をラクにするおすすめアセット

Inspectorの整備やログ解析を強化すると、プールの挙動確認・チューニングが格段にやりやすくなります。

最小導入チェックリスト

  • actionOnGet初期化(位置/回転/速度/HPなど)を必ず行う。
  • actionOnReleaseクリーンアップ(エフェクト停止/フラグ解除/親子関係の戻し)。
  • 返却は常に同じ経路(コールバック)を通す。Destroy()混入を禁止。
  • maxSizeはピークを観測して余裕を持って設定。Editor上で在庫数を可視化して調整。



6. 注意点と最適化のポイント

ObjectPoolパターンはとても便利ですが、「導入すればすべて解決!」というわけではありません。正しく運用しないと逆にメモリを無駄に使ったり、不具合の原因になったりします。ここでは代表的な注意点と最適化のコツを整理します。

① 初期化とリセットの徹底

オブジェクトは再利用されるため、前回の使用状態が残ったまま次に使われることがあります。例えば「敵がダメージを受けた状態のまま復活する」「弾が予期せぬ位置に出てしまう」といった不具合です。 これを避けるには、actionOnGetOnTakenFromPool() 内で必ず初期化処理を行いましょう。

② メモリリークの防止

プールにオブジェクトを溜め込みすぎると、使われないオブジェクトが非アクティブのまま大量にメモリを占有することがあります。 maxSizeを設定し、上限を超えた分はDestroy()するなど、バランスを取ることが大切です。

③ SetActiveの多用に注意

SetActive()Instantiate()/Destroy() より軽い処理ですが、それでも大量に連発すると負荷がかかります。特に1フレーム内で数百オブジェクトを一斉に切り替えるような処理は避けましょう。 対策としては「アクティブ化のタイミングを分散する」「更新をまとめて行う」などがあります。

④ デバッグと可視化

プールの利用状況を確認できないと、「在庫が足りているか」「返却漏れがないか」がわかりづらくなります。 Inspectorの拡張やログ管理を行っておくと、チューニングがとても楽になります。

⑤ プロファイラで実測確認

「プールを入れたけど効果を感じられない…」という場合は、UnityのProfilerでGC Allocやフレーム時間をチェックしましょう。

GCが減っているか、フレームのスパイクが抑えられているかを確認することで、チューニングの方向性が見えてきます。




7. まとめ

今回はUnityにおけるObjectPoolパターンについて、基本の仕組みから公式クラスの使い方、応用的な実装方法、そして注意点までを解説しました。 シューティングやアクションのように「短命オブジェクトが大量に出入りするゲーム」では、この仕組みを導入するだけで処理落ちの改善やGCの削減といった効果が期待できます。

本記事のポイントおさらい

  • Instantiate()Destroy()の乱用はパフォーマンス低下を招く。
  • ObjectPoolパターンは「作る→使う→戻す」を循環させるシンプルな仕組み。
  • ジェネリック・抽象・Factoryを組み合わせることで汎用性アップ。
  • Unity公式のObjectPool<T>を使えば実装が大幅に簡単になる。
  • 初期化・リセット、メモリ管理、SetActive多用の回避などが安定運用の鍵。

「とりあえず弾や敵をプールで管理する」ところから始めてみると、違いをすぐに体感できるはずです。慣れてきたらFactoryやカスタム管理を導入して、より複雑なオブジェクトにも対応してみましょう。

📘 作って学べる Unity本格入門 [Unity 6対応版]
✅ Amazonでチェックする✅ 楽天でチェックする


あわせて読みたい

ObjectPoolはUnityの最適化手法のひとつですが、他にもゲームを軽くしたり効率化するテクニックがたくさんあります。合わせて読むと理解がさらに深まりますよ。


よくある質問(FAQ)

Q
小規模なゲームでもObjectPoolは必要ですか?
A

必ずしも必要ではありません。例えば「敵や弾が数十個程度しか出ない」「生成や破棄がごく稀」というケースでは、パフォーマンスへの影響はほぼ無視できます。 一方で「弾が数百発出るシューティング」「短命のエフェクトを多用するRPG」では効果が大きいので導入がおすすめです。

Q
自作のプールとUnity公式のObjectPool、どちらを使えばいいですか?
A

初心者や小規模プロジェクトではUnity公式のObjectPool<T>を使うのが手軽です。安全でシンプルに導入できます。 一方で「生成ロジックをカスタマイズしたい」「特殊な初期化や依存関係がある」場合は、自作のジェネリックObjectPoolやFactoryパターンを使うと柔軟に対応できます。

Q
SetActiveの多用による処理落ちはどう避ければいいですか?
A

SetActive()は比較的軽い処理ですが、1フレーム内で大量に切り替えると負荷になります。 対策としては:

  • 一度に切り替える数を抑え、フレームを分散させる
  • 必要な時だけ切り替え、無駄にアクティブ/非アクティブを繰り返さない
  • ProfilerでGC Allocやフレーム時間を確認して調整

これらを意識するだけで、安定した動作に近づけます。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク