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