有限状態機械(Finite State Machine)
はじめに
有限状態機械とは、
「複数の『状態』を持ち、その『状態』により『振る舞い』が決定される抽象的な機械」
のことです。
具体的な例を挙げると、パックマンのモンスターがそれに該当します。
モンスターは、
- 移動
- 追跡
- 逃避
3つの「状態」を持ちます。
そして、これらに対応して、
という「振る舞い」を行います。
また、有限状態機械はキャラだけでなく、シーンにも適用することができます。
状態 | 振る舞い |
---|---|
初期化 | 変数の初期化 |
フェードイン | 白色をアルファブレンドしてフェードイン |
メイン | メインループ(メニューの選択) |
スタートボタン押下 | 選択メニューの点滅 |
フェードアウト | 黒色をアルファブレンドしてフェードアウト |
終了 | メニューで選択したシーンに遷移 |
さてはて、このような有限状態機械を使うと何が嬉しいのかというと、
- 分かりやすい
- 実装しやすい
- デバッグしやすい(状態が明確なため)
ということがあるためです。
逆にデメリットは、
- 状態遷移をJump処理で行うため、状態が増えると遷移が複雑になる
ことです。
有限状態機械の設計
先ほどのモンスターにおける、有限状態機械の流れは以下のようになります。
- 初期化
- プレイヤーのポインタを設定
- パワーエサタイマのポインタを設定
- 視界を設定
- 初期状態(移動)を設定
- メインループ
- 遷移トリガーチェック→状態遷移
- 視界内にプレイヤーがいる and パワーエサが有効でない→追跡
- 視界内にプレイヤーがいる and パワーエサが有効である→逃走
- それ以外→移動
- 状態に基づき、振る舞いを行う
- メインループに戻る
- 遷移トリガーチェック→状態遷移
遷移トリガーチェックというのは、
状態遷移するには「何らかのきっかけ」が必要となるためです。
ここでは「視界内にプレイヤーがいるかどうか?」などという条件ですね。
あと、ポイントは、「状態遷移のチェック処理」と「振る舞いの実行」が独立していることです。
これにより、見通しの良いソースコードになります。
有限状態機械の実装
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といったものを用意する方が、
きれいなソースコードになると思います。