見出し画像

進んで戻るスタックベース状態遷移

例えばゲームのアイテムメニューにある薬草を捨ててゲームに戻るまでの挙動を考えてみます:

1.  ゲーム画面からアイテムメニューを開く
2. アイテム項目「武器、道具、魔法」から「道具」を選択
3. 道具一覧から薬草を選択
4.「使う、渡す、捨てる」から「捨てる」を選択
5. 「捨てても大丈夫?」という警告ダイアログが出るので「はい」を選択
6. 道具一覧に戻る
7. アイテム項目へ戻る
8. ゲーム画面に戻る

こういう「どんどん先へ進んで末端まで行ったら戻る戻る戻る…」という状態遷移を実現するのがスタックベース状態遷移です。

各状態は「終わったよ~」を告げる

スタックというのは配列やリストのようなコンテナの一つです。追加した最新の物だけに注目するのが特徴で、アクセスも最新の物しか出来ません。そして、今注目している物がいらなくなったらpop(取り出し)すると、その前に追加した物が有効になります。これを状態遷移に利用するんです。

具体的に見てみましょう。C++の場合std::stackテンプレート、C#ならSystem.Collection.Generic.Stackクラスを使います。状態を表すクラスはStateクラスとしときましょう。

Stateクラスのオブジェクトをスタックに追加すると、状態を更新するupdateメソッドが呼ばれ続けるようになります。このメソッドが「もう自分はいらなくなりましたー」と宣言(falseを返す)したら、スタックから取り除かれます。Stateクラスは「継続か終了か」を毎回告げるだけで良いので単純明快(^-^)

もう一つ、Stateクラスの中からスタックに次の状態を積む事が出来ます。これを実現するにはStateクラスが積む方法を知っている必要があります。これには幾つか方法が考えられますが、ここでは「updateメソッドの引数に投げる」という手段を取る事にします。

Stateクラスはこんな感じでしょうか:

class State {
public:
    virtual ~State() {}
    virtual bool update( State **nextState ) = 0;    // 更新
};

シンプル(^-^)/

スタック管理者

Stateのスタックを保持したり状態を更新する管理者が必要です。StateStackManagerクラスとでもしておきましょう。この管理者は毎フレームスタックの先頭にあるStateの更新を行って戻り値と引数を監視します。もしfalseが返ってきたらスタックから状態を取り除き、引数に有効なポインタが戻されたらスタックに積みます:

 #include  <stack>

class StateStackManager {
public:
    StateStackManager( State* firstState ) {
        stack_.push( firstState );
    }
    virtual bool update() {
		if ( stack_.size() == 0 )
			return false;
		State *next = 0;
		if ( stack_.top()->update( &next ) == false ) {
			stack_.pop();
		}
		if ( next != 0 ) {
			stack_.push( next );
		}
		return ( stack_.size() > 0 );
    }
private:
    std::stack< State* > stack_;
};

実装例はこんな感じです。updateメソッドのstack_.top()でスタックの先頭にある対象Stateを更新しています。

この実装、Stateクラスのupdateメソッドの戻り値とnextの有無の組み合わせで振る舞いが色々変わるのが面白い所です。まずupdateメソッドがtrueを返しnextがnullの時、これは既存Stateの継続になります。falseを返してnextもnullの時は既存Stateの破棄、つまり「戻る」に該当する事になります。
updateメソッドがtrueを返しnextに有効なポインタが返った場合、これは状態の追加となり、次のフレームからは追加された状態の更新が走ります。追加した状態が終わるまで今の状態は停止します。
楽しいのがfalseを返しつつnextが有効というパターン。falseなので既存のStateは死にます。でもnextがあるのでそれはスタックに積まれる。これスタックトップの「交換」をしている事になるんです(^-^)。つまりこの技を用いればFSM的な状態遷移もやれるんです。お~~

「薬草誰がどうやって消す」問題

スタックベース状態遷移を使うと、冒頭のアイテムメニューのような進行して戻ってくるような状態遷移を表現できそうです。ただし実用するに注意もあります。

実際に実装して「あれ?」となるのが環境変更への対応です。冒頭の例では進んで進んで最後に「はい」を選択した時に初めて薬草を消す事が確定します。で「んじゃ消しましょう」となった時、今の状態が単なる「YesNoダイアログ」である事に気付きます。消せねぇーー!ってなるんです。

これは消す手段を知らせていない為です。何だかんだで一番簡単な解決方法は、環境をある程度渡してしまう設計です。State::updateメソッドは汎用性の為引数を変えられないので、例えばコンストラクタに環境を渡します。もしくはStateをテンプレートクラスにしてupdateメソッドの引数を汎化する手もあります:

template< class Env >
class State {
public:
    virtual bool update( Env *env, State **next ) = 0;
};
 
class YesNo : public State< ItemEnv > {
public:
    YesNo( Item *item ) : item_( item ) {}
    virtual bool update( Env *env, State **next ) {
        if ( decide_ == true ) {
            env->deleteItem( item_ );
            return false;   // 遷移終了
        }
        return true;
    }
private:
    Item *item_;
};

Envテンプレート引数がupdateメソッドに渡りますので、好きなように扱っていいよ~という実装です。直接消すのではなくてイベントドリブン(イベントだけ発行してそれを受けた人が振る舞うエンジン設計)にするものありです。

という事でスタックベースの状態遷移のお話でした。こちらの話題をリクエスト頂いたtoyboot4eさん、ありがとうございました~(^-^)/

※ 最後までお読み頂きありがとうございます。本コーナーで「こんな事について書いて欲しい」「こういう所がうまくいかないのだけどアイデア下さい」などリクエストやお困りな事がありましたらコメント、Twitter(@Marupeke_IKD)等でお気軽に教えて下さい(^-^)。出来る範囲で恐縮ですが検討させて頂き、記事としてアップ致します!

この記事が気に入ったらサポートをしてみませんか?