Skip to content
第02回 プレイヤーの弾とオブジェクトプール

2 / 4 PlayerShotManagerで弾を管理する

このステップの目的

弾を固定数の配列でまとめ、Gameクラスから独立した形で管理する

動的なメモリ確保を避ける

シューティングゲームでの弾は連射されるものですが、発射のたびに生成と破棄を繰り返すとメモリの確保と解放の処理が高頻度で行われることになります。

メモリ確保の処理は負荷が重く、高頻度で行われるとゲームのフレームレートを下げる原因になりかねません。
また、動的なメモリ確保を繰り返すとメモリの断片化が発生し、長時間のプレイでメモリ不足に陥る危険もあります。

動的なメモリ確保自体は問題ではありませんが、頻度が問題です。

オブジェクトプールの仕組み

シューティングゲームの弾は寿命が短く、同時に存在する数はある程度決まっています。
上限が決まっているのであれば、最初から必要な数だけ生成しておき、使い回す方が効率的です。
あえて上限を設けることで、処理負荷やゲームバランスを制御できるメリットもあります。

そこで、最初から決められた数のPlayerShotを配列で所有し、停止中の要素を次の弾へ再利用することにします。

あらかじめ用意したオブジェクトを使い回すオブジェクトプールと呼ばれる仕組みが、シューティングゲームの弾の管理に適しています。

オブジェクトプールは、使用済みオブジェクトを破棄せず再利用するデザインパターンです。 生成と破棄の処理負荷を軽減し、パフォーマンスを最適化するために使われます。

PlayerShotManager.hcpp
class PlayerShotManager
{
public:
    bool Spawn(const Vector2& startPosition);
    void Update(float deltaTime);
    void Draw(const ImageManager& images) const;
    void Reset();

private:
    // 弾の配列
    static constexpr std::size_t Capacity = 48;
    std::array<PlayerShot, Capacity> shots = {};
};

std::array

std::arrayは固定長の配列を表す標準テンプレートライブラリのコンテナです。

C言語やC++には組み込みの配列がありますが、配列の長さを得る標準的な方法が無いことや、範囲外アクセスの危険性があること、コピーや代入ができないことなどの難点があります。

std::arrayは素の配列と違い、コピーや代入が可能で、範囲for文やstd::size()などの標準ライブラリの機能を利用でき、パフォーマンスも素の配列と同等という優れたコンテナです。
この教材では組み込み配列ではなくstd::arrayを使って弾の配列を管理します。

std::arrayはC++11以降で利用可能です。

空き要素を再利用する

PlayerShotManager::Spawn()は、配列の先頭から停止中の弾を探します。
停止中の弾が見つかったら、その弾のPlayerShot::Spawn()を呼び出し、弾の初期化を行います。

空きがなければfalseを返し、発射できなかったことを呼び出し側へ伝えます。

PlayerShotManager.cppcpp
bool PlayerShotManager::Spawn(const Vector2& startPosition)
{
    for (PlayerShot& shot : shots)
    {
        if (shot.IsActive())
            continue;

        shot.Spawn(startPosition);
        return true;
    }

    return false;
}

まとまった弾をひとつのオブジェクトとして扱う

GameクラスはPlayerShotManagerを所有しますが、弾の操作にあたってPlayerShotManagerの公開関数を呼び出す以外のことはしません。

Game.cppcpp
void Game::Draw() const
{
    player.Draw(images);
    playerShots.Draw(images);
}

弾の管理の実装をPlayerShotManagerにまとめることで、Gameは弾の個数や配列の所有方法を意識せずに済むようになります。 例えば弾の最大数を48発から変更しても、Gameは変更の影響を受けません。 まとまった機能をひとつのオブジェクトとして扱うことで、プログラムの構造を簡潔に保ち、変更に強い設計にすることができます。

こうしたカプセル化と責務の分離はオブジェクト指向プログラミングの基本的な考え方です。
ゲームプログラミングに限らずソフトウェア開発全般で重要な設計の指針となります。

オブジェクト指向プログラミング

オブジェクト指向プログラミング(Object Oriented Programming, OOP)は、データと、それを操作する処理をひとまとまりのオブジェクトとして扱い設計するプログラミング手法です。

プログラムを役割ごとに分割しやすくなり、再利用や機能追加、修正がしやすくなる優れた手法です。
すべての領域でオブジェクト指向プログラミングが最適というわけではありませんが、様々な状態を抱えたキャラクターを多数同時に操作するようなゲームプログラミングでは特に有効です。

チェックリスト
  • GamePlayerShotを直接所有していないことを理解した
  • PlayerShotManagerstd::array<PlayerShot>で弾を保持していることを理解した
  • 空き要素だけをSpawn()で再利用していることを理解した
  • 弾の描画がPlayerShotManager::Draw()経由になっていることを理解した