【Cocos Creator】アタッチするだけ!ObjectPool (オブジェクトプール)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】ObjectPool の実装:アタッチするだけで「任意のプレハブをまとめてプールし、非表示で再利用」できる汎用スクリプト

弾・エフェクト・敵・UIポップアップなど、頻繁に生成と破棄を繰り返すオブジェクトは、毎回 instantiatedestroy を行うと GC(ガーベジコレクション)の負荷が増え、フレーム落ちの原因になります。

この記事では、任意のプレハブを指定数だけまとめて生成し、「非表示にして再利用する」ための汎用コンポーネント ObjectPool を作ります。
このコンポーネントをシーン内の任意のノードにアタッチし、インスペクタからプレハブと初期数を設定するだけで、コードから簡単にオブジェクトを取得・返却できるようになります。


コンポーネントの設計方針

要件整理

  • 指定したプレハブをあらかじめ複数生成しておき、必要なときに「再利用」する。
  • 使い終わったオブジェクトは「破棄せず、非表示にしてプールへ戻す」。
  • プールが空になった場合の動作を選べる:
    • 自動的に追加生成する(拡張可能なプール)。
    • null を返して呼び出し元で対処する(固定サイズのプール)。
  • 外部の GameManager などには一切依存しない。このコンポーネント単体で完結する。
  • インスペクタから設定可能なパラメータだけで挙動を制御できる。

利用イメージ

ObjectPool をアタッチしたノードから、他のスクリプトで次のように使うことを想定します。

// 例: BulletSpawner.ts からの利用イメージ
const pool = this.poolNode.getComponent(ObjectPool);
if (!pool) return;

// 弾を取得して発射位置に配置
const bullet = pool.spawn();
if (bullet) {
    bullet.setWorldPosition(spawnPos);
    bullet.active = true;
    // ここで弾の初期化処理などを行う
}

// 弾が寿命を迎えたとき
pool.despawn(bullet);

※このガイドでは ObjectPool 自体の実装を中心に説明します。利用側スクリプトは、あなたのゲームに合わせて自由に実装してください。

インスペクタで設定可能なプロパティ設計

ObjectPool コンポーネントに用意するプロパティと役割は次の通りです。

  • prefabPrefab | null
    • プール対象となるプレハブ。
    • 弾、エフェクト、敵など、再利用したい任意のプレハブを指定します。
  • initialSizenumber
    • 起動時にあらかじめ生成しておくインスタンス数。
    • 例:弾用なら 20〜50、エフェクト用なら 10〜30 など。
  • canExpandboolean
    • プールが空になったとき、足りない分を自動生成するかどうか。
    • true:必要に応じて増殖する拡張型プール。
    • false:固定サイズ。空のときは null を返す。
  • expandStepnumber
    • canExpand = true のとき、一度に何個追加生成するか。
    • 例:5 にすると、枯渇したタイミングで 5 個まとめて追加生成。
  • autoParentToPoolboolean
    • 生成したインスタンスの親ノードを、自動的に ObjectPool がアタッチされているノードにするかどうか。
    • true:すべてプールノードの子にまとまり、Hierarchy が整理される。
    • false:親は呼び出し側で自由に設定する前提。
  • deactivateOnDespawnboolean
    • despawn 時に自動的に node.active = false にするか。
    • 通常は true 推奨。
  • reactivateOnSpawnboolean
    • spawn 時に自動的に node.active = true にするか。
    • 発射時の位置調整などをしてから手動で active にしたい場合は false にする。
  • logWarningIfEmptyboolean
    • プールが空で取得できなかった場合に警告ログを出すかどうか。
    • 本番環境でログを減らしたいときは false にする。

これらのプロパティだけで、ほとんどの「弾・エフェクト・敵インスタンスの再利用」パターンに対応できます。


TypeScriptコードの実装

以下が完成版の ObjectPool コンポーネントです。


import { _decorator, Component, Node, Prefab, instantiate, error, warn } from 'cc';
const { ccclass, property } = _decorator;

/**
 * ObjectPool
 * 任意のプレハブをプールし、破棄せずに再利用する汎用コンポーネント。
 *
 * 使用手順:
 * 1. 任意のノードに本コンポーネントをアタッチ。
 * 2. inspector から prefab と initialSize を設定。
 * 3. 他スクリプトから getComponent(ObjectPool) で取得し、spawn / despawn を呼び出す。
 */
@ccclass('ObjectPool')
export class ObjectPool extends Component {

    @property({
        type: Prefab,
        tooltip: 'プールする対象のプレハブを指定します。弾・エフェクトなど任意の Prefab。'
    })
    public prefab: Prefab | null = null;

    @property({
        tooltip: '起動時にあらかじめ生成しておくインスタンス数。\n' +
                 '多すぎると初期ロードが重くなりますが、ランタイム中の GC を減らせます。'
    })
    public initialSize: number = 10;

    @property({
        tooltip: 'プールが空になったとき、自動的にインスタンスを追加生成するかどうか。'
    })
    public canExpand: boolean = true;

    @property({
        tooltip: 'canExpand = true のとき、一度に何個追加生成するか。最低 1。'
    })
    public expandStep: number = 5;

    @property({
        tooltip: '生成したインスタンスの親ノードを、この ObjectPool がアタッチされたノードに自動設定するかどうか。'
    })
    public autoParentToPool: boolean = true;

    @property({
        tooltip: 'despawn 時に node.active = false にするかどうか。通常は true 推奨。'
    })
    public deactivateOnDespawn: boolean = true;

    @property({
        tooltip: 'spawn 時に node.active = true にするかどうか。位置調整後に有効化したい場合は false に。'
    })
    public reactivateOnSpawn: boolean = true;

    @property({
        tooltip: 'プールが空で取得できなかった場合に警告ログを出すかどうか。'
    })
    public logWarningIfEmpty: boolean = true;

    // 実際のプール本体
    private _pool: Node[] = [];

    // デバッグ用: 現在プール内に残っている数
    public get availableCount(): number {
        return this._pool.length;
    }

    // デバッグ用: これまでに生成した総数
    private _totalCreated: number = 0;
    public get totalCreated(): number {
        return this._totalCreated;
    }

    onLoad() {
        // 初期サイズの防御的補正
        if (this.initialSize < 0) {
            warn(`[ObjectPool] initialSize が 0 未満だったため、0 に補正しました。(${this.initialSize})`);
            this.initialSize = 0;
        }

        if (this.expandStep < 1) {
            warn(`[ObjectPool] expandStep は 1 以上である必要があるため、1 に補正しました。(${this.expandStep})`);
            this.expandStep = 1;
        }

        // Prefab 未設定時の警告
        if (!this.prefab) {
            error('[ObjectPool] prefab が設定されていません。このプールは動作しません。Inspector で Prefab を設定してください。');
            return;
        }

        // 初期プール生成
        this._createInstances(this.initialSize);
    }

    /**
     * 内部的にインスタンスを生成し、プールに格納する。
     * @param count 生成する数
     */
    private _createInstances(count: number) {
        if (!this.prefab) {
            // ここに来るのは onLoad 後の想定外ケースなので、強めにエラーを出す
            error('[ObjectPool] _createInstances が呼ばれましたが、prefab が設定されていません。');
            return;
        }

        for (let i = 0; i < count; i++) {
            const node = instantiate(this.prefab);

            if (this.autoParentToPool) {
                node.parent = this.node;
            }

            // プール中は原則非表示にしておく
            node.active = false;

            this._pool.push(node);
            this._totalCreated++;
        }
    }

    /**
     * プールから 1 つインスタンスを取り出す。
     * 足りない場合の挙動は canExpand に依存。
     * @returns 利用可能な Node。取得できない場合は null。
     */
    public spawn(): Node | null {
        // Prefab 未設定時は null を返す
        if (!this.prefab) {
            if (this.logWarningIfEmpty) {
                error('[ObjectPool] spawn が呼ばれましたが、prefab が設定されていません。');
            }
            return null;
        }

        // プールに残っている場合はそれを利用
        if (this._pool.length > 0) {
            const node = this._pool.pop() as Node;

            if (this.reactivateOnSpawn) {
                node.active = true;
            }

            return node;
        }

        // ここから先はプールが空のケース
        if (this.canExpand) {
            // 追加生成してから 1 つ返す
            this._createInstances(this.expandStep);

            const node = this._pool.pop() as Node;
            if (this.reactivateOnSpawn) {
                node.active = true;
            }
            return node;
        } else {
            // 固定サイズモード: 取得できないことを呼び出し側に知らせる
            if (this.logWarningIfEmpty) {
                warn('[ObjectPool] プールが空で、canExpand = false のため新規生成できません。null を返します。');
            }
            return null;
        }
    }

    /**
     * 使用済みインスタンスをプールに戻す。
     * @param node プールに戻したい Node
     */
    public despawn(node: Node | null | undefined): void {
        if (!node) {
            return;
        }

        // 親を戻すかどうかは autoParentToPool に従う
        if (this.autoParentToPool && node.parent !== this.node) {
            node.parent = this.node;
        }

        if (this.deactivateOnDespawn) {
            node.active = false;
        }

        // 二重登録防止(すでにプールにあるノードを戻そうとした場合)
        if (this._pool.indexOf(node) !== -1) {
            if (this.logWarningIfEmpty) {
                warn('[ObjectPool] 同じ Node を複数回 despawn しようとしました。処理をスキップします。');
            }
            return;
        }

        this._pool.push(node);
    }

    /**
     * プールをすべてクリアし、生成済みインスタンスを destroy する。
     * 明示的にプールを破棄したい場合に呼び出してください。
     */
    public clearAndDestroy(): void {
        for (const node of this._pool) {
            // プールに残っているものだけ destroy
            node.destroy();
        }
        this._pool.length = 0;
        this._totalCreated = 0;
    }

    /**
     * コンポーネント破棄時に、プール内のノードも破棄する。
     */
    onDestroy() {
        this.clearAndDestroy();
    }
}

主要メソッドの解説

  • onLoad()
    • initialSizeexpandStep を防御的に補正。
    • prefab が設定されていない場合は error ログを出し、以降の処理をスキップ。
    • _createInstances(initialSize) で初期プールを構築。
  • _createInstances(count)
    • instantiate(prefab) で指定数だけノードを生成し、プールに格納。
    • autoParentToPooltrue の場合、親を ObjectPool ノードに設定。
    • 生成直後は node.active = false にして非表示にしておく。
  • spawn()
    • プールにノードがあれば pop() して返す。
    • reactivateOnSpawntrue なら node.active = true にする。
    • プールが空:
      • canExpand = trueexpandStep 個生成 → 1 つ返す。
      • canExpand = falsenull を返し、必要なら警告ログ。
  • despawn(node)
    • 受け取ったノードをプールに戻す。
    • autoParentToPooltrue なら親を ObjectPool ノードに戻す。
    • deactivateOnDespawntrue なら node.active = false
    • すでにプール内に存在するノードを再度戻そうとした場合は警告して無視。
  • clearAndDestroy() / onDestroy()
    • プールに残っているノードを destroy() し、配列をクリア。
    • ObjectPool コンポーネントが破棄されるとき、自動的にプールも破棄される。

使用手順と動作確認

1. スクリプトファイルを作成

  1. エディタの Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を ObjectPool.ts に変更します。
  4. 作成された ObjectPool.ts をダブルクリックし、エディタ(VS Code など)で開きます。
  5. 中身をすべて削除し、本記事の 実装コード をそのまま貼り付けて保存します。

2. テスト用プレハブを用意

ここでは例として、シンプルな弾プレハブを作ります。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を Bullet にします。
  2. Inspector で Sprite の画像を設定し、サイズや色をお好みに調整します。
  3. Assets パネル内の任意のフォルダに Bullet ノードをドラッグ&ドロップし、プレハブ化します。
    • これで Bullet.prefab が作成されます。
  4. Hierarchy 上の元の Bullet ノードは、テスト用に残しても削除しても構いません。

3. ObjectPool コンポーネントをシーンに配置

  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を BulletPool に変更します。
  2. BulletPool ノードを選択し、InspectorAdd Component ボタンをクリックします。
  3. Custom カテゴリから ObjectPool を選択してアタッチします。
  4. Inspector に表示される ObjectPool のプロパティを次のように設定します。
    • Prefab:先ほど作成した Bullet.prefab をドラッグ&ドロップ。
    • Initial Size20(例として 20 発分)。
    • Can Expandtrue(足りなくなったら自動追加)。
    • Expand Step10(不足時に 10 個ずつ追加)。
    • Auto Parent To Pooltrue(弾ノードを BulletPool の子にまとめる)。
    • Deactivate On Despawntrue
    • Reactivate On Spawntrue
    • Log Warning If Emptytrue(開発中はオン推奨)。

4. 簡単なテストスクリプトで動作確認

ObjectPool 自体はこの時点でもう完成していますが、「本当に再利用されているか」を確認するために、簡単なテストスクリプトを追加してみます。

  1. Assets パネルで右クリック → Create → TypeScript を選び、BulletPoolTester.ts というファイルを作成します。
  2. 以下のコードを貼り付けて保存します。

import { _decorator, Component, Node, Vec3, input, Input, EventKeyboard, KeyCode } from 'cc';
import { ObjectPool } from './ObjectPool';
const { ccclass, property } = _decorator;

/**
 * BulletPoolTester
 * スペースキーを押すたびにプールから弾を取り出して少し移動させ、
 * 1 秒後にプールへ戻すテスト用スクリプト。
 */
@ccclass('BulletPoolTester')
export class BulletPoolTester extends Component {

    @property({
        type: Node,
        tooltip: 'ObjectPool コンポーネントがアタッチされているノードを指定します。'
    })
    public poolNode: Node | null = null;

    @property({
        tooltip: '弾を発射する位置。未指定の場合はこのスクリプトが付いているノードの位置を使用します。'
    })
    public spawnOffsetX: number = 0;

    private _pool: ObjectPool | null = null;

    onLoad() {
        if (!this.poolNode) {
            console.error('[BulletPoolTester] poolNode が設定されていません。Inspector で設定してください。');
            return;
        }

        this._pool = this.poolNode.getComponent(ObjectPool);
        if (!this._pool) {
            console.error('[BulletPoolTester] 指定された poolNode に ObjectPool コンポーネントが存在しません。');
        }

        input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
    }

    private _onKeyDown(event: EventKeyboard) {
        if (event.keyCode === KeyCode.SPACE) {
            this._spawnBullet();
        }
    }

    private _spawnBullet() {
        if (!this._pool) {
            return;
        }

        const bullet = this._pool.spawn();
        if (!bullet) {
            console.warn('[BulletPoolTester] 弾を取得できませんでした。プールが空か、Prefab 未設定の可能性があります。');
            return;
        }

        // 発射位置を設定
        const basePos = this.node.worldPosition;
        bullet.setWorldPosition(new Vec3(basePos.x + this.spawnOffsetX, basePos.y, basePos.z));

        // ここでは簡易的に、1 秒後に despawn するだけ
        this.scheduleOnce(() => {
            this._pool?.despawn(bullet);
        }, 1.0);
    }
}
  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を BulletTester にします。
  2. BulletTester ノードを選択し、Inspector の Add Component → Custom → BulletPoolTester を追加します。
  3. Inspector で BulletPoolTester のプロパティを次のように設定します。
    • Pool Node:先ほど作成した BulletPool ノードをドラッグ&ドロップ。
    • Spawn Offset X0 のままで構いません。

5. ゲームを再生して確認

  1. エディタ右上の ▶(Play) ボタンを押してゲームを再生します。
  2. ゲームビューが開いたら、キーボードの スペースキー を何度か押してみてください。
  3. 設定した位置から弾プレハブが一瞬表示され、1 秒後に消える動作が繰り返されれば成功です。
    • Hierarchy を見ると、BulletPool ノードの子として弾ノードが増えたり減ったりせず、同じノードが再利用されていることが確認できます。
    • Console にエラーや警告が出ていないかも確認してください。

まとめ

この ObjectPool コンポーネントは、

  • 任意のプレハブを指定数だけ事前生成し、破棄せずに非表示で再利用する。
  • プールが枯渇したときの挙動(自動拡張 or 固定サイズ)を canExpand で切り替え可能。
  • 親ノードの管理や active のオン・オフもプロパティで柔軟に制御可能。
  • 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結。

という特徴を持っています。

弾・敵・エフェクト・UI ポップアップなど、生成と破棄が多いオブジェクトをすべてこのプール経由で扱うようにすれば、GC 負荷の低減フレームレートの安定 に大きく貢献します。また、「プレハブと初期数を差し替えるだけ」で別用途のプールとして再利用できるため、プロジェクト全体の設計もシンプルになります。

まずは 1 種類の弾から適用してみて、問題なければエフェクトや敵などにも広げていくと、開発効率とパフォーマンスの両面で効果を実感しやすいはずです。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!