専用クラスで状態遷移をフレームワーク化しよう
ゲームの状態遷移を良く見ると、そこに「状態の更新と遷移を担当する人」と「状態そのものな人」の2タイプの登場人物がいる事が分かります。前回の入れ子クラスによる状態遷移では、親クラスが更新と遷移を担当する人、入れ子クラスが状態そのものな人です。
この方法の場合、内部で状態遷移したい親クラスは都度updateメソッドでの更新部分を記述する必要があり、状態そのものである入れ子クラスは都度親クラス内で作成したState抽象クラスから派生する必要があります。これ毎回ほぼ同じ事を書きます。正直シンドイ。そこで状態遷移に特化したクラスを作ってしまい、状態遷移をフレームワーク化してしまおうと画策するのが「状態遷移専用クラスによる状態遷移」です。
最初にお断りを。この状態遷移専用クラス、設計方法は実に様々千差万別。以下はそのほんの一例一アイデアなのでご注意下さい。
テンプレ的な部分を基底クラスに集約
状態遷移を司る基底クラスをStateBaseクラスとします。StateBaseクラスに入れ込む物、つまり状態遷移に共通する物を以下で考えてみます。
更新メソッドはvirtualにしてはいけない!
ゲームの状態遷移に絶対必要なもの、それは「更新」です。基本状態維持が日課ですから。よってupdateメソッドは必須でしょう。ただ具体的にどういう状態を維持するかは分かりません。「なら派生クラスで具体的な実装をしよう」。クラス設計としてはそう考えますよね。では、以下のようにしますでしょうか?
class StateBase {
public:
virtual void update() = 0; // 更新は純粋仮想?
...
};
これNOです。なぜか?StateBaseクラスは状態遷移専用なので、更新するだけでなく中で状態を遷移しなければなりません。そこも共通項です。しかし上のようにすると、更新も状態遷移も派生クラスに任せる事になってしまいます。それだとこのクラスを設ける意味がありません。
これは例えば次のようにします:
class StateBase {
public:
// 更新エントリー
void update() {
innerUpdate(); // 内部更新
// 遷移指示?
if ( bTrans ) {
// 次の状態へ変更 //
...
}
}
protected:
virtual void innerUpdate() = 0; // 具体的な更新はここを実装で
};
updateメソッドは毎フレーム3つの仕事をこなします。「状態の更新」「状態遷移の監視」そして「遷移」。この仕事は共通項で固定的です。ですからupdateメソッドをvirtualにはせず固定化してしまいます。ただし状態の更新は派生先で変えたいのでその具体的な実装をvirtualなinnerUpdateメソッドに投げてしまうんです。こうすると派生クラスは状態更新に注力できて、遷移を気にする必要が殆ど無くなります。こういうお決まりな仕事をあるメソッドで固定的に呼び出し、その内容は仮想メソッドに投げる型を「Template Methodパターン」と言います。
状態遷移を起こすルール
次に状態遷移を起こすルールを決めます。これ、実に悩むんですよ…ホントに。
innerUpdateメソッドは通常は状態維持に努めます。でも何らかのきっかけでその中で状態を移す局面が来ます。親のStateBaseはそれを捉える必要があります。では、次のような設計はどうでしょうか?
class Idle : public StateBase;
bool Idle::innerUpdate() {
return true; // trueを返したら状態遷移
}
innerUpdateがtrueを返したら状態遷移だ!これはトリガーにはなっていますよね。でも、これを受け取るStateBase::updateメソッドは「どこに遷移したらいいの?」となってしまいます。トリガーは引いているけども遷移先を指定していないんですね。このままでは遷移は出来ません。
「どうせなら遷移する先を返したらいいんじゃない?」という設計も考えられます:
StateBase* Idle::innerUpdate() {
return new Walk(); // 歩きに遷移
}
これはトリガーと遷移先を満たしているのでありと言えばありです。ただしnewした物を返しているのでStateBaseクラスがdeleteする責務を負います:
class StateBase {
public:
void update() {
if ( cur_ != 0 ) {
StateBase* pre = cur_; // バックアップ
StateBase* cur_ = cur_->innerUpdate(); // 更新
if ( cur_ != pre ) {
// 状態遷移発動
delete pre; // 前の状態はさよなら
}
}
}
protected:
virtual StateBase* innerUpdate() = 0;
private:
StateBase* cur_;
};
cur_には現在の状態オブジェクトが入ります。cur_のinnerUpdateメソッドを実行した結果、cur_と異なるオブジェクトが返ってきたら状態遷移が発動した合図なので、cur_のバックアップであるpreポインタをdeleteしてメモリから消してしまいます。もし同じポインタ(this)が返ってきたら継続です。そしてnullが返ってきたら一連の状態遷移は終了、以後updateメソッド内は何もしなくなります。
これ、cur_に最初の状態オブジェクトを登録する仕組みを入れればちゃんと状態遷移します。ただし「すべての状態遷移オブジェクトは絶対にnewしてね」というルールが必須です。
状態遷移出来てるのに何も動作しない!
上の実装、実際に状態遷移出来ますが、実は非常に大切な観点がすっぽりと抜けています。それは「データアクセス」です。
Idle、Walk、Dash、etc...。これらはキャラクタの状態オブジェクトは対象となるキャラクタに対して働きかけなければなりません。所が上の実装だとそのキャラクタの存在が無いので幽霊のように中身空っぽで状態だけが独り歩きしてしまいます。では各状態オブジェクトの中からキャラクタのデータにどうやってアクセスしたら良いのでしょうか?
1つの解決方法はキャラクタオブジェクトを状態オブジェクトに渡し続ける、通称「たらい回し作戦」です。一番最初のIdleオブジェクトに対象となるキャラクタを渡します。IdleからWalkなどに変わる時、newしたWalkにそのキャラクタを渡します。これを続けていけばそのキャラクタをWalk内でも操作出来ます。ただしキャラクタクラスは操作に必要なメソッドを全て公開する必要があります。
実際はキャラクタだけでなくゲーム環境とかサウンド云々も扱えないとダメだったりします。このアクセス問題は状態遷移がオブジェクト指向的になる程深刻になってきます。まぁ環境もたらい回しする設計もありますが、個人的に良い思い出が無いので、素直にシングルトンかなぁ…っと僕は思います。
設計は様々なので創意工夫を。ただし難しくし過ぎない事
専用クラスを作ると設計ルールの下で状態遷移を起こせるようになります。最初にも述べたようにこの設計は実に様々です。創意と工夫でいかようにも出来ます。ただ、難し過ぎる設計はあまりお勧めしません。これも経験上の話なんですが、面倒臭くなって使わなくなるんです。なので最初に色々作ってみて自分やチームが使いやすい所に落ち着けるのが良いのかなと思います。
さて、大分武器が溜まってきました。今回の武器は斧のようながっしり系ですが、もっと軽量で気楽に書きたい状態遷移もあります。そこで次回はその場でざざっとかける状態遷移を考えてみたいと思います。