有限状態機械(Finite State Machine)

1.はじめに

有限状態機械とは、

  • 複数の「状態」を持ち、その「状態」により「振る舞い」が決定される抽象的な機械

のことです。

具体的な例を挙げると、パックマンのモンスターがそれに該当します。

モンスターは、

  • 移動
  • 追跡
  • 逃避

3つの「状態」を持ちます。

そして、これらに対応して、

という「振る舞い」を行います。

また、有限状態機械はキャラだけでなく、シーンにも適用することができます。

例えば、タイトルシーンに以下の「状態」を持たせます。

  • 初期化
  • フェードイン
  • メイン
  • スタートボタン押下
  • フェードアウト
  • 終了

さてはて、このような有限状態機械を使うと何が嬉しいのかというと、

  • 分かりやすい
  • 実装しやすい
  • デバッグしやすい(状態が明確なため)

ということがあるためです。

逆にデメリットは、状態遷移をJump処理で行うため、状態が増えると遷移が複雑になることです。 

2.有限状態機械の設計

先ほどのモンスターにおける、有限状態機械の流れは以下のようになります。

  1. 初期化
    1. プレイヤーのポインタを設定
    2. パワーエサタイマのポインタを設定
    3. 視界を設定
    4. 初期状態(移動)を設定
  2. メインループ
    1. 遷移トリガーチェック→状態遷移
      1. 視界内にプレイヤーがいる and パワーエサが有効でない→追跡
      2. 視界内にプレイヤーがいる and パワーエサが有効である→逃走
      3. それ以外→移動
    2. 状態に基づき、振る舞いを行う
    3. メインループに戻る

遷移トリガーチェックというのは、状態遷移するには「何らかのきっかけ」が必要となるためです。ここでは「視界内にプレイヤーがいるかどうか?」などという条件ですね。

あと、ポイントは、「状態遷移のチェック処理」と「振る舞いの実行」が独立していることです。これにより、見通しの良いソースコードになります。

3.有限状態機械の実装

C++のクラスで設計するとこうなります。

/**
 * モンスタークラス
 */
class CMonster
{
public:
    // 状態列挙型
    enum State
    {
        FSM_MOVE,   // 移動
        FSM_CHASE,  // 追跡
        FSM_ESCAPE, // 逃走
    };
private:
    State m_state; // 状態
public:
    void Update(); // 更新関数
private:
    // 遷移トリガー
    bool IsPlayerInRange(); // プレイヤーが視界にいるかどうか
    bool IsPowerFood();     // パワーエサが有効かどうか
    // 振る舞い
    void Move();   // ランダムに移動する
    void Chage();  // プレイヤーを追いかける
    void Escape(); // プレイヤーから逃げる
};
メインループでUpdate関数が呼ばれます。

// =======================================
// 更新関数:毎フレームこの関数が呼ばれる
// =======================================
void CMonster::Update()
{
    // 遷移トリガーチェック
    if(IsPlayerInRange() && !IsPowerFood())
    {
        m_state = FSM_ESCAPE;
    }
    else if(IsPlayerInRange() && IsPowerFood())
    {
        m_state = FSM_CHASE;
    }
    else
    {
        m_state = FSM_MOVE;
    }
    
    // 振る舞いの実行
    switch(m_state)
    {
    case FSM_MOVE:
        Move();
        break;
    case FSM_CHASE:
        Chase();
        break;
    default: // FSM_ESCAPE
        Escape();
        break;
    }
}

読みやすいように、「状態遷移のチェック処理」と「振る舞いの実行」をべた書きしましたが、privateなメンバ関数としてそれぞれChageState関数とDoAction関数を用意する方が、きれいなソースコードになると思います。

4.ソースコード補足

読みやすさを考えてあえて書かなかったのですが、不足している情報として、

  • プレイヤーのポインタ
  • パワーエサタイマのポインタ
  • 視界の広さ

がありますので、それらを設定するメンバ関数・メンバ変数が必要になると思います。

また、「状態」を外部から見れるようにすると、デバッグがやりやすいので、

State GetState() {return m_state;}

というメンバ関数を用意しておくと、何かと便利になります。(デバッグ文字列を取得する関数を用意するのもアリです)