こんにちは。トイロジック新人プログラマーのS.Gです。トイロジックでは毎年新人研修としてiosアプリを開発します。本記事では昨年リリースされた『バクバクバック』で大活躍したオブジェクトプールについて解説したいと思います。
オブジェクトプールとは
私たちが開発したバクバクバックは、大量の敵から逃げながらクイモンにバク食いさせるというアクションゲームです。
そのため大量のオブジェクトを扱うことになるのですが、どのくらい大量かというと…
…このくらいです。
これらのオブジェクトに対して素直に生成と破棄を行ってしまうと、少し遊んだだけでフレーム落ちしてしまいます。そこで私たちはオブジェクトを管理するオブジェクトプール機能を作ることにしました。
オブジェクトプールの仕組み
オブジェクトプールはその名の通り、使い終わったオブジェクトを破棄せずキューに保存しておくシステムのことです。オブジェクトの生成と破棄は処理負荷が高いため、それらを省略すればかなり処理にかかる時間が短縮されます。
今回はエフェクトを管理しているオブジェクトプールを例にあげてご紹介します。
オブジェクトプールの作り方
1.初期化
まずはオブジェクトプールを初期化します。
void EffectObjectPool::Init() {
//エフェクトをすべて管理するオブジェクトプールを作成
Scene* scene = SceneManager::GetCurrentScene();
effect_object_pool_ = scene->CreateSubObject("effect_object_pool");
//エフェクトの名前(char*)からキューを取得するために、マップで対応させる
//このキューにオブジェクトをプールすることになる
object_pool_queue_manager_.insert(std::make_pair("efc_kuimonefc_bite", &que_efc_kuimonefc_bite));
object_pool_queue_manager_.insert(std::make_pair("efc_enemy_deathA", &que_efc_enemy_deathA));
object_pool_queue_manager_.insert(std::make_pair("efc_enemy_death_greenA", &que_efc_enemy_death_greenA));
//エフェクトの名前(char*)から初期化情報を取得するために、マップで対応させる
effect_info_manager_.insert(std::make_pair("efc_kuimonefc_bite", EffectInfo("efc_kuimonefc_bite", 4, 1, LayerGroup::EFFECT_ENEMY_DIE)));
effect_info_manager_.insert(std::make_pair("efc_enemy_deathA", EffectInfo("efc_enemy_deathA", 5, 3, LayerGroup::EFFECT_BUILD)));
effect_info_manager_.insert(std::make_pair("efc_enemy_death_greenA", EffectInfo("efc_enemy_death_greenA", 4, 1, LayerGroup::EFFECT_BUILD)));
//いくつかのエフェクトはゲーム開始時にあらかじめ作成する
int init_pool_size = 1000;
for (int i = 0; i < init_pool_size; i++) {
OnCreate("efc_kuimonefc_bite");
OnCreate("efc_enemy_deathA");
OnCreate("efc_enemy_death_greenA");
}
}
キューはエフェクトごとに宣言しており、それらを*>型のマップで管理しています。ゲーム開始時にあらかじめオブジェクトを生成することで、大量に生成してしまうリスクを軽減しています。
エフェクトを使用したい場合は、それぞれに対応したキューに対して操作を行います。操作のフローは「生成」「取得」「返却」「破棄」からなります。
それぞれ順を追ってみていきましょう。
2.生成
エフェクトオブジェクトを生成してキューに格納する関数です。
void EffectObjectPool::OnCreate(const char* clip_name) {
//オブジェクトの生成
auto* effect_obj_ = effect_object_pool_->CreateSubObject((std::string(clip_name) + "Effect").c_str());
//オブジェクトにエフェクト再生用のコンポーネントを追加
auto* animator = effect_obj_->CreateBehaviour<AnimationController>("effect_animation_controller");
auto* sprite_ = effect_obj_->CreateBehaviour<Sprite>(((std::string(clip_name) + "_0").c_str()));
animator->SetTarget(sprite_);
//コンポーネントを初期化
auto* sprite_info = effect_info_manager_.at(clip_name);
if(sprite_info == nullptr)
{
return;
}
sprite_->SetOrder(sprite_info.layer_);
auto* clip = animator->AddClip(clip_name, sprite_info.size_, sprite_info.frame_interval_);
//エフェクトを最後まで再生終えた時に、オブジェクトプールの返却関数をコールバックさせる
clip->AddCallback(sprite_info.size_ - 1, [this, clip_name, effect_obj_] {OnRelease(clip_name, effect_obj_); });
//機能を停止
effect_obj->enable = false;
//生成したオブジェクトをキューに追加
if(auto* que = object_pool_queue_manager_.at(clip_name))
{
que->push(effect_obj_);
}
}
オブジェクトを生成したのち、必要なコンポーネントをつけて初期化しています。今記事ではエフェクト用のコードを記載していますが、初期化時の処理はプールするものによって異なるので注意してください。キューに格納されている最中はゲームに影響を与えないように機能を停止させています。
3.取得
オブジェクトが格納されているキューから、利用するオブジェクトを取得します。この時キューが空である場合は、新たにオブジェクトを生成します。
GameObject * EffectObjectPool::OnGet(const char * clip_name)
{
//キューを取得
auto* que = object_pool_queue_manager_.at(clip_name);
if(que == nullptr)
{
return nullptr;
}
//キューが空の場合は新しくオブジェクトを追加
if (que->empty()) {
OnCreate(clip_name);
}
//オブジェクトを取得
auto* effect_obj = que->front();
if(effect_obj == nullptr)
{
return nullptr;
}
//機能を有効にする
effect_obj->enable = true;
que->pop();
return effect_obj;
}
4.返却
使い終わったエフェクトはオブジェクトプールのキューに追加されます。
その時はまた機能を停止します。
void EffectObjectPool::OnRelease(const char * clip_name, GameObject * effect_obj)
{
//キューを取得
auto* que = object_pool_queue_manager_.at(clip_name);
if(que == nullptr)
{
return;
}
//オブジェクトの機能を停止して、キューに追加
if(effect_obj)
{
effect_obj->enable = false;
que->push(effect_obj);
}
}
5.破棄
指定のエフェクトを使わなくなる場合は、メモリを圧迫しないようにエフェクトを破棄する関数も必要です。今作では全ステージ同じエフェクトが使いまわされるため破棄の機能は作りませんでしたが、作るとしたらこんな感じになると思います。
void EffectObjectPool::OnDestroy(const char* clip_name)
{
//キューを取得
auto* que = object_pool_queue_manager_.at(clip_name);
if(que == nullptr)
{
return;
}
//キューの要素をすべて破棄する
while(!que->empty())
{
if(auto* effect_obj = que->front())
{
effect_obj->DestroySelf();
}
que->pop();
}
}
最後に
いかがでしたでしょうか。今回はオブジェクトプールのしくみについて解説してみました。バクバクバックではエフェクトの他にも、エネミーや弾の管理にオブジェクトプールを利用しています。覚えておくと結構便利に使えると思います。
最後までお読みいただきありがとうございました!