概要
シューティングゲームの弾、敵の群れ、破壊されたときのエフェクト... ゲーム中には、短時間で大量のオブジェクトを生成し、不要になったら破棄する、という処理が頻繁に発生します。多くの初心者は、この処理をInstantiate()(生成)とDestroy()(破棄)メソッドを使って素直に実装します。
しかし、このInstantiate/Destroyの頻繁な呼び出しは、Unityにおける二大パフォーマンスキラーです。これらのメソッドは、 メモリの確保(ヒープアロケーション)と解放を伴うため、CPUに大きな負荷をかけるだけでなく、ガベージコレクション(GC)を引き起こし、ゲームに深刻なカクつき(スパイク)をもたらす最大の原因となります。
この問題を解決するための、プロのゲーム開発現場では必須とされているテクニックがオブジェクトプーリング (Object Pooling) です。この記事では、オブジェクトプーリングの概念と、その具体的な実装方法について詳しく解説します。
オブジェクトプーリングとは?
オブジェクトプーリングの考え方は、「一度作ったオブジェクトは捨てずに、使い回す」という非常にシンプルなものです。
- 事前生成 (Pre-population): ゲーム開始時など、負荷が問題にならないタイミングで、必要になるであろう数のオブジェクト(例: 弾30個)を予め
Instantiate()しておきます。生成したオブジェクトは、すべて非アクティブ状態 (SetActive(false)) にして、「プール」と呼ばれるリストやキューに保管しておきます。 - 取得 (Get/Rent): オブジェクトが必要になったとき(例: 弾を発射するとき)、
Instantiate()で新しく生成する代わりに、プールから非アクティブなオブジェクトを一つ取り出します。そして、それをアクティブ状態 (SetActive(true)) にし、位置や向きを再設定して使います。 - 返却 (Return/Release): オブジェクトが不要になったとき(例: 弾が敵に当たった、画面外に出たとき)、
Destroy()で破棄する代わりに、再び非アクティブ状態にしてプールに戻します。これにより、オブジェクトは次の出番を待つことになります。
このサイクルを繰り返すことで、ゲームプレイ中のInstantiate()とDestroy()の呼び出しをゼロにすることができ、パフォーマンスを劇的に改善できます。
シンプルなオブジェクトプーリングの実装
それでは、汎用的なオブジェクトプーリングシステムを実装してみましょう。
ステップ1: プール本体のクラスを作成する
まず、特定のPrefabに対応するオブジェクトプールを管理するクラスを作成します。ここでは、Queue(キュー)を使って、非アクティブなオブジェクトを効率的に管理します。
// ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public GameObject objectToPool; // プールする対象のPrefab
public int amountToPool; // 最初に生成しておく数
private Queue<GameObject> pooledObjects;
void Start()
{
pooledObjects = new Queue<GameObject>();
for (int i = 0; i < amountToPool; i++)
{
GameObject obj = Instantiate(objectToPool);
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
// プールからオブジェクトを取得するメソッド
public GameObject GetPooledObject()
{
// プールに利用可能なオブジェクトがあれば、それを取り出す
if (pooledObjects.Count > 0)
{
GameObject obj = pooledObjects.Dequeue();
obj.SetActive(true);
return obj;
}
// プールが空の場合(予備が尽きた場合)
// ここではnullを返すか、あるいは動的に新しいオブジェクトを生成する選択肢もある
// return Instantiate(objectToPool); // 動的に拡張する場合
return null;
}
// オブジェクトをプールに戻すメソッド
public void ReturnObjectToPool(GameObject obj)
{
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
ステップ2: プールを利用するクラスの実装
次に、このObjectPoolを利用して弾を発射するスクリプトを作成します。
// PlayerShooter.cs
using UnityEngine;
public class PlayerShooter : MonoBehaviour
{
public ObjectPool bulletPool; // Inspectorで弾のプールをセット
public Transform firePoint;
public float fireRate = 0.5f;
private float nextFireTime = 0f;
void Update()
{
if (Input.GetButton("Fire1") && Time.time >= nextFireTime)
{
nextFireTime = Time.time + fireRate;
Fire();
}
}
void Fire()
{
// プールから弾を取得
GameObject bullet = bulletPool.GetPooledObject();
if (bullet != null)
{
bullet.transform.position = firePoint.position;
bullet.transform.rotation = firePoint.rotation;
// 弾のスクリプトにプールへの参照を渡すなどして、
// 弾自身が自分をプールに戻せるようにする
Bullet bulletScript = bullet.GetComponent<Bullet>();
if (bulletScript != null)
{
bulletScript.SetPool(bulletPool);
}
}
}
}
ステップ3: プールに戻る処理の実装
最後に、弾自身が壁に当たったり、一定時間経過したらプールに戻る処理を実装します。
// Bullet.cs
using UnityEngine;
public class Bullet : MonoBehaviour
{
private ObjectPool myPool;
public void SetPool(ObjectPool pool)
{
myPool = pool;
}
void OnCollisionEnter(Collision collision)
{
// 何かに衝突したらプールに戻る
if (myPool != null)
{
myPool.ReturnObjectToPool(gameObject);
}
else
{
// プールが設定されていない場合のフォールバック
Destroy(gameObject);
}
}
}
より高度なプーリングシステムへ
上記は基本的な実装ですが、実際のプロジェクトではさらに以下のような機能を備えた、より汎用的なプーリングマネージャーを作成することが多いです。
- 複数の異なるPrefabを一つのマネージャーで管理する機能(
Dictionary<string, Queue<GameObject>>などを使用)。 - プールが空になったときに、動的に新しいオブジェクトを生成してプールを拡張する機能。
- オブジェクト取得時(
Get)や返却時(Return)に、特定の初期化・クリーンアップ処理を呼び出すためのイベントやインターフェース。
まとめ
オブジェクトプーリングは、一見すると複雑に見えるかもしれませんが、その原理は単純明快で、得られるパフォーマンス上のメリットは計り知れません。
InstantiateとDestroyは重い処理であり、GCの原因となる。- オブジェクトプーリン グは、オブジェクトを事前に生成し、再利用することでこれらの呼び出しを避けるテクニック。
- 「事前生成」「取得」「返却」がプーリングの3つの基本ステップ。
特にモバイルゲーム開発においては、オブジェクトプーリングは「推奨テクニック」ではなく「必須テクニック」と言っても過言ではありません。ゲームのパフォーマンスに問題を感じたら、まずはInstantiateを多用している箇所がないかを確認し、オブジェクトプーリングの導入を検討してみてください。