14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

大量のオブジェクト生成にはオブジェクトプールを使おう! - レアゾン・ホールディングス 2025新卒エンジニア ゲーム研修1 -

Last updated at Posted at 2025-07-10

はじめに

こんにちは!株式会社ルーデル25卒の猪俣です。
本記事では、Unityにおける「オブジェクトプール」という技術についてご紹介します。
私がレアゾンホールディングスの新卒エンジニア研修のうち、ゲーム研修で学んだ内容を基に作成しています。

ゲーム研修について

ゲーム研修では、下記の内容の講義を受けました。

  • 環境構築
  • Unity基礎
  • インゲーム
  • ビルド
  • アウトゲーム
  • ゲームの改造

本記事はインゲームの中でも、オブジェクトプールに着目します。

オブジェクトプールとは?

今回、記事のテーマとして挙げさせていただいた「オブジェクトプール」とは、オブジェクトをあらかじめ生成してプール(保管)しておき、必要な時に取り出して使い回す仕組みのことです。

シューティングゲームの弾や、アクションゲームで大量に出現する敵など、たくさんのGameObjectを量産して使用したい!
そんな時におすすめなのがオブジェクトプールです!!

オブジェクトプールのここがすごい!

ゲームを開発している中で、こんな状況になったことはありませんか?

敵を沢山出したいぞ!!
まずは敵のGameObjectをInstantiate() で沢山生成して、、、
倒せたら Destroy() で破棄して、、

その処理、ちょっと待った!!

大量のInstantiate() , Destroy()の繰り返しは、ゲームのパフォーマンスが悪くなってしまいます!😱
この二つの関数を何度も呼び出すと、そのたびにメモリを確保し、使い終わった後には掃除をする、という処理が裏で頻繁に走ります。
この「確保」と「掃除」のコストが積み重なることで、ゲーム全体の動作が重くなってしまうのです。

そこで頼りになるのがオブジェクトプール!!オブジェクトプールは、 Instantiate()Destroy() の呼び出し回数を最小限に抑え、パフォーマンスを軽量化できてしまうテクニックです!同じ種類のオブジェクトを頻繁に生成・破棄する時は特に有効になります!

オブジェクトプールの仕組み

では、オブジェクトプールがどのようにしてパフォーマンスを良くしているか説明します。

  1. ゲーム開始時などに、あらかじめ一定数のオブジェクトを生成し、非アクティブな状態(画面に見えない状態)で保管する

  2. オブジェクトが必要になったら、プールから一つ取り出し、アクティブな状態にして使用する(Instantiate() の代わり)

  3. 使い終わったら再び非アクティブな状態にしてプールに戻す(Destroy() の代わり)

このサイクルで、無駄なメモリの確保や解放処理を減らすことができるのです!

オブジェクトプールを実装してみよう!

では、オブジェクトプールを実装していきましょう!
4つのパートと「おまけ」に分けて実装していきます。
最終的には、一定時間ごとに敵が現れ、クリックすると消える(プールに戻る) という動作を完成させます。

  • Part1:プールの「器」を作ろう
  • Part2:プールから敵を取り出せるようにしよう
  • Part3:使った敵をプールに戻せるようにしよう
  • Part4:プールが空になっても対応できるようにしよう
  • おまけ:敵を自動で出現させてみよう

事前準備:敵プレハブの作成

⚠️「Box Collider 2D」のアタッチもしましょう!

簡単な敵プレハブの用意の手順(ここをクリックで展開)
まず事前準備として敵プレハブの作成をお願いします

以下簡単に手順の説明を記載します

1. 敵として表示したい画像スプライトを用意しProjectビューにインポート

2. 1の画像をヒエラルキーウィンドウにドラッグドロップしシーン上にGameObjectとして配置

最重要 
3. シーンに配置した敵オブジェクトを選択しInspectorウィンドウでAdd Componentをクリック
検索窓にBox Collider 2Dと入力しコンポーネントを追加
キャラクターの形が円形ならCircle Collider 2Dでも構いません

4. 3の設定が完了したGameObjectをヒエラルキーからProjectビューにドラッグドロップするとプレハブが完成

5. 作成できたらシーン上に残っている元のGameObjectは不要なので削除しましょう


これでクリックに反応できる敵のプレハブが完成しました
オブジェクトプールの実装を始めましょう

[参考画像]今回、私が用意した敵のプレハブはこちらです!

Part1:プールの「器」を作ろう

まずは、オブジェクトプールの機能を管理する空のGameObjectをシーンに用意します。
GameObjectの名前は「EnemyPoolManager」など、分かりやすいものに設定しましょう。

EnemyPoolManager.cs ファイルを作成

次に、このオブジェクトにアタッチするためのスクリプト、EnemyPoolManager.csを作成します。
EnemyPoolManager.csを、シーンに作成したEnemyPoolManagerオブジェクトにアタッチしてください。

ゲームが始まったときに、敵をあらかじめ生成し、待機させる、言い換えるとオブジェクトプールの本体のEnemyPoolManager.cs の中身を書いていきます。

必要な変数を3つ用意します。
_enemyPrefab, _initialPoolSize, _enemyPool を作りましょう。
なお、_enemyPoolは、作った敵をしまっておくための箱で、データ構造はQueueを使用します。
StackListでも作れますが、Queueの「先に入れたものから先に出す(FIFO)」という性質により、オブジェクトの再利用を公平に行えるため、今回はQueueを選択しました。

EnemyPoolManager.cs
using System.Collections.Generic;
using UnityEngine;

public class EnemyPoolManager : MonoBehaviour
{
    // プーリング対象となる敵のプレハブ
    [SerializeField] private GameObject _enemyPrefab;

    // 最初にプールしておくオブジェクトの数。Inspectorから変更できるよう、[SerializeField]にしておくと便利です。
    [SerializeField] private int _initialPoolSize = 100;

    // プールされたオブジェクトを格納するキュー
    private readonly Queue<GameObject> _enemyPool = new Queue<GameObject>();

    private void Awake()
    {
        // プールを初期化する
        InitializePool();
    }

    // プールを指定されたサイズで初期化します。
    private void InitializePool()
    {
        //  _initialPoolSize の数だけ、敵を作ってプールに入れていく。
        for (int i = 0; i < _initialPoolSize; i++)
        {
            var newEnemy = CreateNewEnemy();
            // 作った敵をキューに追加。
            _enemyPool.Enqueue(newEnemy); 
        }
    }

    // 実際に敵を1体作る処理です。
    private GameObject CreateNewEnemy()
    {
        // Instantiateで敵を生成。
        var newEnemy = Instantiate(_enemyPrefab);
        // 作った敵は非表示にして、プールされている状態にする。
        newEnemy.SetActive(false);       
        return newEnemy;
    }
}

コードが書けたら、Unityエディタで EnemyPoolManager オブジェクトのインスペクターを開きましょう。Enemy Prefab の欄に
Projectビューから事前に用意していた 敵のプレハブ をドラッグ&ドロップしてアタッチしてください。

これで器の完成です!!Part2にいきましょう!

Part2:プールから敵を取り出せるようにしよう

1. 敵をプールから取り出す命令 GetEnemy() を作る

次に、EnemyPoolManager.cs に、プールから敵を取り出すための GetEnemy() メソッドを追加します。この命令は、他のスクリプトから呼ばれることを想定して public にしておきます。

enemypoolmanager.cs
// Part1のコードに続けて追記

    // プールから敵オブジェクトを1体取得する命令
    public GameObject GetEnemy()
    {
        // プールに待機中のオブジェクトがあれば、それを取り出す
        GameObject enemyToGet = _enemyPool.Dequeue();

        // 敵を見えるようにする
        enemyToGet.SetActive(true); 

        return enemyToGet;
    }

まずは取り出す機能だけをシンプルに実装します!

2. 動作確認用にEnemySpawner.csを作る

実装したGetEnemy()が正しく機能するか、テスト用のスクリプトを作って確認してみましょう。
中身はSpaceキーで敵を生成するシンプルなものにします。

シーンに、空のGameObjectをEnemySpawner という名前で作成しましょう。そしてEnemySpawner.csを作成し、EnemySpawnerにアタッチします。

EnemySpawner.cs
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private EnemyPoolManager _poolManager;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
    {
        var enemy = _poolManager.GetEnemy();

        // 敵が重ならないよう、出現位置をランダムにする
        // ※値はカメラに映る範囲で適宜調整してください
        float randomX = Random.Range(-4f, 4f);
        float randomY = Random.Range(-4f, 4f);
        enemy.transform.position = new Vector2(randomX, randomY);
    }
    }
}

Unityエディタで EnemySpawner オブジェクトのインスペクターを開きましょう。_poolManager の欄に、シーンから EnemyPoolManager (EnemyPoolManager.csがアタッチされているオブジェクト)をドラッグ&ドロップしてアタッチすれば完了です。

ゲームを再生し、Spaceキーを連打してみましょう。敵が画面にどんどん現れれば成功です!

[参考画像]Spaceキーを押すごとに、わらわら発生する敵👾


Part3:使った敵をプールに戻せるようにしよう

敵が「使用済み」になったことを判定し、プールに戻す処理を作りましょう。
今回は、簡単に 「敵キャラクターがクリックされたら、使用済みとみなしてプールに戻す」 という仕様にします。

1. 敵をプールに戻す命令 ReturnEnemy() を作る

まず EnemyPoolManager.cs に、敵をプールに戻す ReturnEnemy() メソッドを追加します。

EnemyPoolManager.cs
// Part2のコードに続けて追記

    // 使用済みの敵オブジェクトをプールに戻す命令
    public void ReturnEnemy(GameObject enemy)
    {
        // 敵を再び非表示にする
        enemy.SetActive(false);
        // キューの最後尾に戻す
        _enemyPool.Enqueue(enemy);
    }

2. 敵がクリックされたことを検知する Enemy.cs を作る

次に、敵自身がクリックされたことを知るための新しいスクリプト Enemy.cs を作成し、敵のプレハブにアタッチしておきましょう。

Enemy.cs
using UnityEngine;

public class Enemy : MonoBehaviour
{
    // 自身を管理してくれているプールの情報を入れておくための変数
    public EnemyPoolManager PoolManager { get; set; }

    // オブジェクトがクリックされた時に自動で呼ばれるメソッド
    private void OnMouseDown()
    {
        // 自分自身(のGameObject)をプールに戻すようお願いする
        if (PoolManager != null)
        {
            PoolManager.ReturnEnemy(gameObject);
        }
    }
}

3. 取り出す敵に情報を渡す

Enemy.cs には、自分(敵自身)がどのプールに戻ればいいかを知るために EnemyPoolManager の情報が必要です。
そこで、GetEnemy() メソッドを少し修正して、敵を取り出す際にこの情報を渡してあげます。

EnemyPoolManager.cs
// Part2で作成した GetEnemy() を修正

    public GameObject GetEnemy()
    {
        // プールに待機中のオブジェクトがあれば、それを取り出す
        GameObject enemyToGet = _enemyPool.Dequeue();
        enemyToGet.SetActive(true); 

        // ★ここから追記★
        // 取り出した敵にアタッチされている Enemy.cs を取得
        var enemyComponent = enemyToGet.GetComponent<Enemy>();
        // Enemy.cs に、自分自身(このEnemyPoolManager)の情報を渡す
        enemyComponent.PoolManager = this;
        // ★ここまで追記★

        return enemyToGet;
    }

これで、画面に登場した敵をクリックすると、Enemy.csOnMouseDown() が呼ばれ、EnemyPoolManagerReturnEnemy() を通じてプールに戻る、という流れが完成しました。

ゲームを再生して確認してみましょう!

Spaceキーで敵を出現させる → その敵をクリック

をすると、ヒエラルキービューで、敵がプールから順番に出る(Active)、クリックするとプールに戻る(非Active)といったサイクルが確認できると思います!

【確認】
クリックしても敵が消えない場合は、以下の点を確認しましょう!

1.敵のプレハブに Box Collider 2D などのColliderがアタッチされているか

2.敵のプレハブに Enemy.cs がアタッチされているか


Part4:プールが空になっても対応できるようにしよう

最初に100体の敵を用意したけど、101体目が必要になったらどうするの?

そんな時もあるでしょう!
今のままだと、最初に用意した100体の敵をすべて画面に出し切ってしまうと、GetEnemy()Dequeue() しようとした際に誰もいなくてエラーになってしまいます。

そこで、GetEnemy() をもっと賢くし、イレギュラーにも対応しましょう!
プールが空っぽだった場合は、その場で新しい敵を作って貸し出すように処理を変更します。

EnemyPoolManager.cs
// Part3で作成した GetEnemy() を書き換え

    public GameObject GetEnemy()
    {
        GameObject enemyToGet;

        // もしプールの中に1体でも敵が残っていたら…
        if (_enemyPool.Count > 0)
        {
            enemyToGet = _enemyPool.Dequeue();
        }
        // もし1体も残っていなかったら…
        else
        {
            // 緊急で新しい敵を生成!
            // パフォーマンスのために、普段はここを通らない方が望ましい
            Debug.LogWarning("プールが空です。新しい敵を動的に生成します。Initial Pool Sizeを増やすことを検討してください。");
            enemyToGet = CreateNewEnemy();
        }
        
        enemyToGet.SetActive(true);

        var enemyComponent = enemyToGet.GetComponent<Enemy>();
        enemyComponent.PoolManager = this;

        return enemyToGet;
    }

これで、想定以上の数の敵が必要になった場合でも、エラーで止まることなくゲームを続けることができます。
ただ、Debug.LogWarningにもあるように、ここで動的に生成が大量に発生すると、結局処理が重くなってしまいます。
オブジェクトプールの意味がなくなってしまうので、あまりにもプールが空になる場合はInitial Pool Sizeの調整しましょう!

オブジェクトプールを有効活用には、Initial Pool Sizeの適切な設定がポイントになります!

これでオブジェクトプールの完成です!!!


おまけ:敵を自動で出現させてみよう

最後におまけです!これまではテストのためにSpaceキーで手動で敵を出現させていましたが、ゲームらしく自動で出現するように改造してみましょう。
EnemySpawner.csを以下のように書き換えます。

EnemySpawner.cs
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    // InspectorからEnemyPoolManagerをアタッチするための変数
    [SerializeField] private EnemyPoolManager _poolManager;

    // 敵を出現させる間隔(秒)
    [SerializeField] private float _spawnInterval = 0.5f;

    void Start()
    {
        // _spawnIntervalごとに SpawnEnemy メソッドを繰り返し呼び出す
        InvokeRepeating(nameof(SpawnEnemy), 1f, _spawnInterval);
    }

    private void SpawnEnemy()
    {
        // プールから敵を1体もらう
        var enemy = _poolManager.GetEnemy();

        // もしプールから敵が取得できなかった場合は何もしない
        if (enemy == null) return;

        // 敵の出現位置をランダムに設定
        float randomX = Random.Range(-4f, 4f);
        float randomY = Random.Range(-4f, 4f);
        enemy.transform.position = new Vector3(randomX, randomY, 0);
    }
}

ゲームを再生すると、敵が次々と出現し、クリックすると消える(プールに戻る)様子が確認できるはずです!

付録:完成コード一覧

EnemyPoolManager.cs(最終形)
EnemyPoolManager.cs
using System.Collections.Generic;
using UnityEngine;

public class EnemyPoolManager : MonoBehaviour
{
    // プーリング対象となる敵のプレハブ
    [SerializeField] private GameObject _enemyPrefab;

    // 最初にプールしておくオブジェクトの数。Inspectorから変更できるよう、[SerializeField]にしておくと便利です。
    [SerializeField] private int _initialPoolSize = 100;

    // プールされたオブジェクトを格納するキュー
    private readonly Queue<GameObject> _enemyPool = new Queue<GameObject>();

    // AwakeはStartよりも先に一度だけ呼ばれるライフサイクルメソッド。
    private void Awake()
    {
        // プールを初期化する
        InitializePool();
    }

    // プールを指定されたサイズで初期化します。
    private void InitializePool()
    {
        //  _initialPoolSize の数だけ、敵を作ってプールに入れていく。
        for (int i = 0; i < _initialPoolSize; i++)
        {
            var newEnemy = CreateNewEnemy();
            // 作った敵をキューに追加。
            _enemyPool.Enqueue(newEnemy); 
        }
    }

    // 実際に敵を1体作る処理です。
    private GameObject CreateNewEnemy()
    {
        // Instantiateで敵を生成。
        var newEnemy = Instantiate(_enemyPrefab);
        // 作った敵は非表示にして、プールされている状態にする。
        newEnemy.SetActive(false);       
        return newEnemy;
    }
    
    // プールから敵オブジェクトを1体取得する命令
    public GameObject GetEnemy()
    {
        GameObject enemyToGet;

        // もしプールの中に1体でも敵が残っていたら…
        if (_enemyPool.Count > 0)
        {
            enemyToGet = _enemyPool.Dequeue();
        }
        // もし1体も残っていなかったら…
        else
        {
            // 緊急で新しい敵を生成!
            // パフォーマンスのために、普段はここを通らない方が望ましい
            Debug.LogWarning("プールが空です。新しい敵を動的に生成します。Initial Pool Sizeを増やすことを検討してください。");
            enemyToGet = CreateNewEnemy();
        }
        
        enemyToGet.SetActive(true);

        var enemyComponent = enemyToGet.GetComponent<Enemy>();
        enemyComponent.PoolManager = this;

        return enemyToGet;
    }
  
    // 使用済みの敵オブジェクトをプールに戻す命令
    public void ReturnEnemy(GameObject enemy)
    {
        // 敵を再び非表示にする
        enemy.SetActive(false);
        // キューの最後尾に戻す
        _enemyPool.Enqueue(enemy);
    }
}


Enemy.cs(最終形)
Enemy.cs
using UnityEngine;

public class Enemy : MonoBehaviour
{
    // 自身を管理してくれているプールの情報を入れておくための変数
    public EnemyPoolManager PoolManager { get; set; }

    // オブジェクトがクリックされた時に自動で呼ばれるメソッド
    private void OnMouseDown()
    {
        // 自分自身(のGameObject)をプールに戻すようお願いする
        if (PoolManager != null)
        {
            PoolManager.ReturnEnemy(gameObject);
        }
    }
}


EnemySpawner.cs(おまけ適用後)
EnemySpawner.cs
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    // InspectorからEnemyPoolManagerをアタッチするための変数
    [SerializeField] private EnemyPoolManager _poolManager;

    // 敵を出現させる間隔(秒)
    [SerializeField] private float _spawnInterval = 0.5f;

    void Start()
    {
        // _spawnIntervalごとに SpawnEnemy メソッドを繰り返し呼び出す
        InvokeRepeating(nameof(SpawnEnemy), 1f, _spawnInterval);
    }

    private void SpawnEnemy()
    {
        // プールから敵を1体もらう
        var enemy = _poolManager.GetEnemy();
        
		// もしプールから敵が取得できなかった場合は何もしない
        if (enemy == null) return;

        // 敵の出現位置をランダムに設定
        float randomX = Random.Range(-4f, 4f);
        float randomY = Random.Range(-4f, 4f);
        enemy.transform.position = new Vector3(randomX, randomY, 0);
    }
}


まとめ

今回の研修を通して学んだ、Unityにおけるパフォーマンス改善の基本テクニック「オブジェクトプール」について解説しました。

  • Instantiate() / Destroy() の多用はパフォーマンス低下の原因になる
  • オブジェクトプールは、オブジェクトを使い回すことで負荷を軽減する仕組み
  • Queue を使うと、先入れ先出しで公平にオブジェクトを再利用できる
  • プールが空になった場合も想定しておくと、より堅牢なシステムになる

この仕組みは、大量に生成・破棄されるあらゆるものに応用できます。
ぜひ、ご自身のゲーム開発に活かしてみてください。

最後までお読みいただき、ありがとうございました。


▼採用情報

レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。

現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。

14
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?