【Cocos Creator 3.8】ObjectPool の実装:アタッチするだけで「任意のプレハブをまとめてプールし、非表示で再利用」できる汎用スクリプト
弾・エフェクト・敵・UIポップアップなど、頻繁に生成と破棄を繰り返すオブジェクトは、毎回 instantiate と destroy を行うと 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 コンポーネントに用意するプロパティと役割は次の通りです。
- prefab(
Prefab | null)- プール対象となるプレハブ。
- 弾、エフェクト、敵など、再利用したい任意のプレハブを指定します。
- initialSize(
number)- 起動時にあらかじめ生成しておくインスタンス数。
- 例:弾用なら 20〜50、エフェクト用なら 10〜30 など。
- canExpand(
boolean)- プールが空になったとき、足りない分を自動生成するかどうか。
- true:必要に応じて増殖する拡張型プール。
- false:固定サイズ。空のときは
nullを返す。
- expandStep(
number)canExpand = trueのとき、一度に何個追加生成するか。- 例:5 にすると、枯渇したタイミングで 5 個まとめて追加生成。
- autoParentToPool(
boolean)- 生成したインスタンスの親ノードを、自動的に ObjectPool がアタッチされているノードにするかどうか。
- true:すべてプールノードの子にまとまり、Hierarchy が整理される。
- false:親は呼び出し側で自由に設定する前提。
- deactivateOnDespawn(
boolean)despawn時に自動的にnode.active = falseにするか。- 通常は true 推奨。
- reactivateOnSpawn(
boolean)spawn時に自動的にnode.active = trueにするか。- 発射時の位置調整などをしてから手動で active にしたい場合は false にする。
- logWarningIfEmpty(
boolean)- プールが空で取得できなかった場合に警告ログを出すかどうか。
- 本番環境でログを減らしたいときは 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()
initialSizeとexpandStepを防御的に補正。prefabが設定されていない場合はerrorログを出し、以降の処理をスキップ。_createInstances(initialSize)で初期プールを構築。
- _createInstances(count)
instantiate(prefab)で指定数だけノードを生成し、プールに格納。autoParentToPoolがtrueの場合、親を ObjectPool ノードに設定。- 生成直後は
node.active = falseにして非表示にしておく。
- spawn()
- プールにノードがあれば
pop()して返す。 reactivateOnSpawnがtrueならnode.active = trueにする。- プールが空:
canExpand = true:expandStep個生成 → 1 つ返す。canExpand = false:nullを返し、必要なら警告ログ。
- プールにノードがあれば
- despawn(node)
- 受け取ったノードをプールに戻す。
autoParentToPoolがtrueなら親を ObjectPool ノードに戻す。deactivateOnDespawnがtrueならnode.active = false。- すでにプール内に存在するノードを再度戻そうとした場合は警告して無視。
- clearAndDestroy() / onDestroy()
- プールに残っているノードを
destroy()し、配列をクリア。 - ObjectPool コンポーネントが破棄されるとき、自動的にプールも破棄される。
- プールに残っているノードを
使用手順と動作確認
1. スクリプトファイルを作成
- エディタの Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- ファイル名を
ObjectPool.tsに変更します。 - 作成された
ObjectPool.tsをダブルクリックし、エディタ(VS Code など)で開きます。 - 中身をすべて削除し、本記事の 実装コード をそのまま貼り付けて保存します。
2. テスト用プレハブを用意
ここでは例として、シンプルな弾プレハブを作ります。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を
Bulletにします。 - Inspector で Sprite の画像を設定し、サイズや色をお好みに調整します。
- Assets パネル内の任意のフォルダに
Bulletノードをドラッグ&ドロップし、プレハブ化します。- これで
Bullet.prefabが作成されます。
- これで
- Hierarchy 上の元の
Bulletノードは、テスト用に残しても削除しても構いません。
3. ObjectPool コンポーネントをシーンに配置
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
BulletPoolに変更します。 BulletPoolノードを選択し、Inspector の Add Component ボタンをクリックします。- Custom カテゴリから ObjectPool を選択してアタッチします。
- Inspector に表示される ObjectPool のプロパティを次のように設定します。
- Prefab:先ほど作成した
Bullet.prefabをドラッグ&ドロップ。 - Initial Size:
20(例として 20 発分)。 - Can Expand:
true(足りなくなったら自動追加)。 - Expand Step:
10(不足時に 10 個ずつ追加)。 - Auto Parent To Pool:
true(弾ノードをBulletPoolの子にまとめる)。 - Deactivate On Despawn:
true。 - Reactivate On Spawn:
true。 - Log Warning If Empty:
true(開発中はオン推奨)。
- Prefab:先ほど作成した
4. 簡単なテストスクリプトで動作確認
ObjectPool 自体はこの時点でもう完成していますが、「本当に再利用されているか」を確認するために、簡単なテストスクリプトを追加してみます。
- Assets パネルで右クリック → Create → TypeScript を選び、
BulletPoolTester.tsというファイルを作成します。 - 以下のコードを貼り付けて保存します。
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);
}
}
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
BulletTesterにします。 BulletTesterノードを選択し、Inspector の Add Component → Custom → BulletPoolTester を追加します。- Inspector で BulletPoolTester のプロパティを次のように設定します。
- Pool Node:先ほど作成した
BulletPoolノードをドラッグ&ドロップ。 - Spawn Offset X:
0のままで構いません。
- Pool Node:先ほど作成した
5. ゲームを再生して確認
- エディタ右上の ▶(Play) ボタンを押してゲームを再生します。
- ゲームビューが開いたら、キーボードの スペースキー を何度か押してみてください。
- 設定した位置から弾プレハブが一瞬表示され、1 秒後に消える動作が繰り返されれば成功です。
- Hierarchy を見ると、
BulletPoolノードの子として弾ノードが増えたり減ったりせず、同じノードが再利用されていることが確認できます。 - Console にエラーや警告が出ていないかも確認してください。
- Hierarchy を見ると、
まとめ
この ObjectPool コンポーネントは、
- 任意のプレハブを指定数だけ事前生成し、破棄せずに非表示で再利用する。
- プールが枯渇したときの挙動(自動拡張 or 固定サイズ)を
canExpandで切り替え可能。 - 親ノードの管理や active のオン・オフもプロパティで柔軟に制御可能。
- 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結。
という特徴を持っています。
弾・敵・エフェクト・UI ポップアップなど、生成と破棄が多いオブジェクトをすべてこのプール経由で扱うようにすれば、GC 負荷の低減 と フレームレートの安定 に大きく貢献します。また、「プレハブと初期数を差し替えるだけ」で別用途のプールとして再利用できるため、プロジェクト全体の設計もシンプルになります。
まずは 1 種類の弾から適用してみて、問題なければエフェクトや敵などにも広げていくと、開発効率とパフォーマンスの両面で効果を実感しやすいはずです。




