有限状態機械(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;}

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