有限状態機械(Finite State Machine)

はじめに

有限状態機械とは、
「複数の『状態』を持ち、その『状態』により『振る舞い』が決定される抽象的な機械」
のことです。
 
具体的な例を挙げると、パックマンのモンスターがそれに該当します。
モンスターは、

  • 移動
  • 追跡
  • 逃避

3つの「状態」を持ちます。
 
そして、これらに対応して、

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

状態 振る舞い
初期化 変数の初期化
フェードイン 白色をアルファブレンドしてフェードイン
メイン メインループ(メニューの選択)
スタートボタン押下 選択メニューの点滅
フェードアウト 黒色をアルファブレンドしてフェードアウト
終了 メニューで選択したシーンに遷移

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

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

ということがあるためです。
 
逆にデメリットは、

  • 状態遷移をJump処理で行うため、状態が増えると遷移が複雑になる

ことです。
 

有限状態機械の設計

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

  1. 初期化
    1. プレイヤーのポインタを設定
    2. パワーエサタイマのポインタを設定
    3. 視界を設定
    4. 初期状態(移動)を設定
  2. メインループ
    1. 遷移トリガーチェック→状態遷移
      1. 視界内にプレイヤーがいる and パワーエサが有効でない→追跡
      2. 視界内にプレイヤーがいる and パワーエサが有効である→逃走
      3. それ以外→移動
    2. 状態に基づき、振る舞いを行う
    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 Chase();  // プレイヤーを追いかける
	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といったものを用意する方が、
きれいなソースコードになると思います。
 

ソースコード補足

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

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

がありますので、それらを設定するメンバ関数・メンバ変数が必要になると思います。
 
また、「状態」を外部から見れるようにすると、デバッグがやりやすいので、

	State GetState() {return m_state;}

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