【C++】ステートパターンでBOSSの状態管理をスッキリ実装する方法|ゲーム制作

サムネイル画像 C++

今回は、C++での「ステートパターン」をゲーム制作のBOSS実装に活用する方法について解説していきます。

この記事を読み終えると、あなたはBOSSの状態管理をスッキリと設計できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

こんな悩みはありませんか?

ゲームのBOSSを実装しようとしたとき、こんな状況になっていませんか?

  • 「待機→攻撃→激怒→死亡」の状態をif文で書いたらコードがぐちゃぐちゃになった
  • 状態が増えるたびにelse ifが増えて、どこを直せばいいか分からない
  • 「ステートパターン」という言葉は聞いたことあるけど、どう使えばいいのか分からない

この記事では、そんな悩みをステートパターンで解決する方法を、BOSSを具体例にしながら解説します!

ステートパターンとは?

ステートパターン(State Pattern)とは、

オブジェクトの「状態」をクラスとして独立させ、状態ごとに振る舞いを切り替える設計パターン

です。

GOF(Gang of Four)が定義したデザインパターンの一つで、特にゲームのキャラクターAIとの相性が抜群です。

ステートパターンを使わない場合、BOSSのupdateはこんな感じになりがちです。

(if/elseの使い方についてはこちらの記事でも解説しています)


// ❌ ステートパターンを使わない場合(こうなりがち)
void Boss::update() {
    if (m_state == State::Idle) {
        // 待機処理...
    } else if (m_state == State::Attack) {
        // 攻撃処理...
        if (m_hp <= 50) {
            m_state = State::Enraged;
        }
    } else if (m_state == State::Enraged) {
        // 激怒処理...
        if (m_hp <= 0) {
            m_state = State::Dead;
        }
    } else if (m_state == State::Dead) {
        // 死亡処理...
    }
    // 状態が増えるたびにここが伸び続ける...
}

状態が4つ5つと増えると、1つの関数が何十行にも膨らんで修正が怖くなります。これをスッキリさせるのがステートパターンです。

BOSSの状態を設計する

今回は、以下の4つの状態を持つBOSSを実装します。

状態クラス意味遷移先
IdleState待機中→ AttackState
AttackState攻撃中HP半分以下で → EnragedState
EnragedState激怒(第2形態)HP0以下で → DeadState
DeadState死亡なし

ステートパターンの構造は次の3つで成り立っています。

  • BossState(基底クラス):全状態の親。updateを純粋仮想関数で定義
  • 各状態クラス(IdleState など):BossStateを継承し、状態ごとの処理を実装
  • Bossクラス(コンテキスト):現在の状態クラスへのポインタを持ち、updateを委譲

実際にコードを書いてみよう

まずは全体を1ファイルにまとめたシンプルな実装を見てみましょう。

なお、ファイルを分割してヘッダーに宣言をまとめる方法については、プロトタイプ宣言の記事で詳しく解説しています。


#include <iostream>

// ===== 前方宣言 =====
class Boss;
class AttackState;
class EnragedState;
class DeadState;

// ===== 基底クラス =====
class BossState {
public:
    virtual ~BossState() = default;
    virtual void update(Boss& boss) = 0;
};

// ===== Bossクラス =====
class Boss {
public:
    int m_hp;
    BossState* m_pState;

    Boss(int hp) : m_hp(hp), m_pState(nullptr) {}
    ~Boss() { delete m_pState; }

    void changeState(BossState* pNew) {
        delete m_pState;
        m_pState = pNew;
    }

    void update() {
        if (m_pState) m_pState->update(*this);
    }

    void takeDamage(int dmg) {
        m_hp -= dmg;
        std::cout << "  ボスHP:" << m_hp << "\n";
    }
};

// ===== 各状態クラスの定義 =====
class IdleState : public BossState {
public:
    void update(Boss& boss) override;
};

class AttackState : public BossState {
public:
    void update(Boss& boss) override;
};

class EnragedState : public BossState {
public:
    void update(Boss& boss) override;
};

class DeadState : public BossState {
public:
    void update(Boss& boss) override;
};

// ===== 各状態の実装(全クラス定義後に書く)=====
void IdleState::update(Boss& boss) {
    std::cout << "[待機] ボスは次の攻撃を狙っている...\n";
    boss.changeState(new AttackState());
}

void AttackState::update(Boss& boss) {
    std::cout << "[攻撃] ボスが斬りかかってきた!\n";
    boss.takeDamage(60);
    if (boss.m_hp <= 50) {
        std::cout << "  → HPが半分以下!激怒状態へ移行\n";
        boss.changeState(new EnragedState());
    }
}

void EnragedState::update(Boss& boss) {
    std::cout << "[激怒] ボスが炎をまとった!\n";
    boss.takeDamage(60);
    if (boss.m_hp <= 0) {
        std::cout << "  → HP0以下!死亡状態へ移行\n";
        boss.changeState(new DeadState());
    }
}

void DeadState::update(Boss& boss) {
    std::cout << "[死亡] ボスは力尽きた...\n";
}

// ===== main =====
int main() {
    Boss boss(100);

    boss.update(); // Idle   → Attack
    boss.update(); // Attack → Enraged
    boss.update(); // Enraged→ Dead
    boss.update(); // Dead(変化なし)

    return 0;
}

実行結果

[待機] ボスは次の攻撃を狙っている...
[攻撃] ボスが斬りかかってきた!
  ボスHP:40
  → HPが半分以下!激怒状態へ移行
[激怒] ボスが炎をまとった!
  ボスHP:-20
  → HP0以下!死亡状態へ移行
[死亡] ボスは力尽きた...

各状態クラスが独立しているため、「激怒状態の攻撃力を上げたい」と思ったら EnragedState だけを修正すればOKです。他の状態に影響がありません!

私が実際にBOSS実装で困った体験談

個人でアクションゲームを自主制作していたときの話です。

最初、BOSSのAIはシンプルだったので、update関数の中にif-else ifを並べて状態を管理していました。「待機中ならこれ、攻撃中ならこれ、HP半分以下ならこれ…」という感じです。

ところが、制作を進めるうちに「第2形態も作りたい」「特殊攻撃パターンも追加したい」と状態がどんどん増えていきました。

結果、1つのupdate関数が100行超えのモンスター関数に…どこを直せばいいか分からなくなり、触るのが怖い状態になってしまいました。

そこで調べてたどり着いたのがステートパターンです。リファクタリングしてみると、各状態が独立したクラスになり、コードが見違えるほどスッキリしました。

「新しい状態を追加したい?じゃあ新しいクラスを1つ作ればいい」という感覚になれたのは、本当に快感でした。設計を変えるだけで、制作のストレスが激減します。

ステートパターンのよくある失敗例と解決策

① changeStateで古い状態をdeleteし忘れる(メモリリーク)

changeStateを呼ぶとき、古い状態をdeleteせずに新しいポインタを代入してしまうと、メモリリークが発生します。


// ❌ 悪い例:deleteを忘れると古いオブジェクトがメモリに残り続ける
void Boss::changeState(BossState* pNew) {
    // delete m_pState; ← これを忘れるとメモリリーク!
    m_pState = pNew;
}

// ✅ 正しい例
void Boss::changeState(BossState* pNew) {
    delete m_pState;   // 古い状態を必ず解放
    m_pState = pNew;   // 新しい状態をセット
}

解決策:changeStateの中でdelete m_pState;を必ず書く習慣をつけましょう。慣れてきたらstd::unique_ptrを使うと自動で解放されて安全です。

② 状態遷移の条件をBoss側に書いてしまう

「HPが半分以下になったら激怒状態へ」という条件を、Boss::update() の中に書いてしまうケースがあります。


// ❌ 悪い例:ボス側で状態遷移を判定している
void Boss::update() {
    m_pState->update(*this);
    if (m_hp <= 50 && /* 現在Attackなら */) {
        changeState(new EnragedState()); // ← ここに書くのはNG
    }
}

// ✅ 正しい例:状態クラスの中で判定・遷移する
void AttackState::update(Boss& boss) {
    // 攻撃処理...
    boss.takeDamage(60);
    if (boss.m_hp <= 50) {
        boss.changeState(new EnragedState()); // ← 状態が自分で判断する
    }
}

解決策「次の状態への遷移は、各状態クラスの中で判断する」のがステートパターンの基本です。BossクラスはupdateをただStateに委譲するだけでOKです。

③ 全状態を1つのファイルに詰め込みすぎる

状態クラスが増えてきたとき、全部を1つのcppに書き続けると見通しが悪くなります。

解決策:状態が4つ以上になってきたら、IdleState.h / IdleState.cppのようにファイルを分割しましょう。ヘッダーファイルの使い方はプロトタイプ宣言の記事を参考にしてみてください。

まとめ

ステートパターンについてまとめます。

  • ステートパターン:状態をクラスとして独立させ、状態ごとの振る舞いをカプセル化する設計パターン
  • BOSSのAI(待機・攻撃・激怒・死亡など)との相性が非常に良い
  • 基底クラス BossState を継承した各状態クラスを作り、Boss クラスがポインタで状態を管理する
  • 状態遷移の判断は「各状態クラスの中」で行うのが基本
  • changeStateでは古い状態の delete を忘れずに
  • 状態クラスが増えてきたらファイルを分割して管理しよう

最初はif-else ifで書いていたBOSSが、ステートパターンを使うことでクラスひとつひとつが独立した美しい設計に変わります。ゲーム制作を続けていく上で、ぜひ身につけておきたいパターンです!

ここまで読んでくださり、ありがとうございました。

この記事が皆様の学習に役立てば幸いです。

関連記事