tanaka's Programming Memo

プログラミングについてのメモ。

2D Game KitのObject Pooling

2D Game Kitは、プログラムコードを書かなくても結構な横スクロールアクションゲームが作れるUnity公式の素敵アセットです。ゲーム開発に役立つ機能が色々と実装されているのですが、パーティクルやゲームオブジェクトをプールして管理できるVFXController とObject Pooling機能は、スマホゲームなどを作る時にとても有用そうです。公式ドキュメントのObject Poolingを読んだ時のメモ(ほぼ訳)です。目次は私が適当に付けたもので原文にはありません。

(2018/3/5 ObjectPoolとPoolObjectの説明順が直感的ではなかったので、順番を入れ替えました)

目次

Object Poolingのための2つのクラス

2D Game Kitに実装されている拡張可能なObject Poolingシステムを使うには、2つのクラスを作成する必要があります。一つはObjectPoolクラスを継承したクラス、もう一つはPoolObjectクラスを継承したクラスです。ObjectPoolを継承したクラスはプールそのもので、PoolObjectを継承したクラスはプール内の各プレハブごとのラッパーです。

ObjectPoolPoolObjectの宣言

2つのクラスはGenericタイプで関連付けます。ObjectPoolPoolObjectの2つのクラスを型として持ったGenericタイプで宣言する必要があります。例えば以下のような宣言になります。

public class SpaceshipPool: ObjectPool<SpaceshipPool, Spaceship>
{

…

}

public class Spaceship: PoolObject<SpaceshipPool, Spaceship>
{

…

}

これにより、プールは自らが保持するオブジェクトのタイプを知ることができて、オブジェクトは自分がどのタイプのプールに属するかを知ることができます。

3つ目のパラメーター

これらのクラスは3つ目のGenericタイプをオプションとして定義することができます。このオプションは、PoolObjectがプールから取得される時に呼ばれるWakeUp関数に渡したいパラメーターがある場合に使えます。例えばスペースシップを登場させる時に燃料がどれぐらいあるかをfloatの値で指定する場合、以下のように定義します。

public class SpaceshipPool: ObjectPool<SpaceshipPool, Spaceship, float>
{

…

}

public class Spaceship: PoolObject<SpaceshipPool, Spaceship, float>
{

…

}

ObjectPool

PoolObject全体を管理するプールです。

ObjectPoolが持つフィールド

ObjectPoolMonoBehaviourであり、GameObjectにアタッチすることができます。デフォルトでは以下のフィールドを持ちます。

  • Prefab: プールを生成する時に複数回Instantiateされるプレハブへの参照です
  • InitialPoolCount: Startメソッドで最初に生成しておくPoolObjectの数です
  • Pool: PoolObjectのリストです

ObjectPoolが持つ関数

ObjectPoolが持っている関数です。

  • Start: 最初にプールが作成される時に呼び出されます。もしObjectPool内にStart関数を実装していた場合、ベースクラスのStartメソッドが隠されるので、必要に応じて呼び出す必要があることを覚えておいてください
  • CreateNewPoolObject: PoolObjectが生成される時に呼び出されて、SetReferencesを呼び出したら、関数をSleepします。この関数はvirtualではないのでオーバーライドはできませんが、protectedなので継承したクラスから呼び出すことができます
  • Pop: プールからPoolObjectを取得する時に呼び出されます。デフォルトでは、プールからinPoolフラグがtrueになっている最初のオブジェクトを見つけて、それを返します。inPoolフラグがtrueのものが一つも見つからなかった場合は、新しいオブジェクトを生成して返します。この関数は、取り出したPoolObjectWakeUp関数を呼び出します。この関数はvirtualなので、オーバーライドできます
  • Push: この関数は、PoolObjectをプールに戻す時に呼ばれます。デフォルトでは、inPoolフラグをtrueにして、PoolObjectが持つSleepを呼び出すだけです。この関数はvirtualなのでオーバーライドできます

詳しい利用方法については、以降のBulletPoolのドキュメントとスクリプトを見てください。

PoolObject

ゲームオブジェクトと一対一で対応するオブジェクトで、これのリストがObjectPoolで管理されます。

PoolObjectが持つフィールド

デフォルトでは、PoolObjectは以下のフィールドを持っています。

  • inPool: PoolObjectがプールにあるのか、利用されているかを表すbool値です
  • instance: このPoolObjectをラップしたプレハブからInstantiateされたゲームオブジェクトのインスタンスです
  • objectPool: このPoolObjectが所属するオブジェクトプールです。このクラスと同じObjectPoolのタイプを持ちます

PoolObjectが持つvirtual関数

PoolObjectは以下のVirtual関数を持ちます。

  • SetReferences: この関数はPoolObjectが作成された時に一度だけ呼び出されます。これは参照をキャッシュするためのもので、PoolObjectが利用状態になる度に毎回実行する必要がない1回だけ初期化すればよい処理のために利用します
  • WakeUp: PoolObjectがプールから取り出されて、利用状態になるタイミングで毎回呼ばれます。PoolObjectが利用可能になるタイミングで毎回行う必要がある初期化処理のための関数です。クラス定義で3つ目のGenericパラメーターが設定されていた場合、WakeUp関数に指定した型の引数を渡すことができます
  • Sleep: 利用しなくなるPoolObjectをプールに戻す際に毎回呼ばれる関数です。PoolObjectの利用後に毎回行う必要がある解放処理などのために使います
  • ReturnToPool: デフォルトではPoolObjectをプールに返すだけですが、追加の処理をしたい場合はこの関数をオーバーライドします

BulletPoolの解説

概要

BulletPool MonoVehaviourはBullet(=弾)のオブジェクトのプールで、弾プレハブのインスタンスを管理するものです。BulletPoolは主人公のEllenの両方で利用しますが、それぞれで少し違うものを使います。Ellen用のBulletPool MonoBehaviourは、親のゲームオブジェクトのPrefab欄に弾のプレハブを設定することでアタッチされます。敵用のBullet PoolEnemyBehaviourクラスで使われます。GetObjectPoolというstaticな関数を使って、インスタンスの生成をせずにBulletPoolが使えます。

BulletPoolが持つフィールド

BulletPoolクラスは以下のフィールドを持ちます。

  • Prefab: 利用したい弾のプレハブを設定します
  • Initial Pool Count: 開始時にいくつの弾を生成しておくかを設定します。同時に撃てる最大の弾数を設定しておきます。この数より多い弾を発射しようとすると、実行時に弾が生成されます
  • Pool: プール内のBulletObjectです。Inspectorには表示されません

BulletPoolが持つ関数

BulletPoolクラスは以下の関数を持ちます。

  • Pop: プールからBulletObjectを取り出します
  • Push: BulletObjectをプールに戻す時に呼びます
  • GetObjectPool: 指定したPrefabが設定されているBulletPoolを見つけるstatic関数です

BulletObjectが持つフィールド

プールから取得した弾は、BulletObjectのフォームとして渡されます。BulletObjectクラスは以下のフィールドを持ちます。

BulletObjectが持つ関数

BulletObjectは以下の関数を持ちます。

  • WakeUp: Pop関数が呼ばれた時にBulletPoolから呼ばれます
  • Sleep: Push関数が呼ばれた時にBulletPoolから呼ばれます
  • ReturnToPool: 弾の利用を終了する時に呼ぶことになっている関数です。この関数はBulletPoolPush関数を呼び出して、Sleep関数を呼びます

まとめ

公式ドキュメントはこんな感じでした。詳しくは実際のコードを見てください、という感じです。

シーンの開始時に必要なインスタンスをまとめて生成して、その後はそのインスタンスを使い回してくれるので、メモリーの確保と解放を繰り返すことによる速度低下や、メモリーの断片化を避けることが期待できます。予定数をオーバーした場合にも対応していて、超過した時はその時点でInstantiateしてくれる柔軟性を持っています。

WebGLスマホでは、オブジェクトの初回描画時に処理落ちすることがあります。それを避けるためには、シーンの開始時に、必要なオブジェクトを一度画面に描画するウォーミングアップ処理をするとよいようです( 【Unity】パフォーマンスチューニング - KAYAC engineers' blog )。デフォルトのままだとこの辺の対処はありませんが、Start関数に自前でコードを追加すれば対応できそうです。

再利用するオブジェクトは、動作速度を安定させるためには把握しておきたい要素なので、予めObject Poolingを導入しておけば、あとで膨大なプロジェクトの中から再利用しているオブジェクトを探し出す手間がなくなるので楽ができそうです。

参考URL