クラス内メソッドでも状態遷移できます
C++はオブジェクト指向言語なのでオブジェクトの設計図であるクラスを軸として構成していくコーディングスタイルになります。クラスはある局所的な機能をぎゅっと凝集した物ですから、自然とクラス特有のお仕事が出来てきます。ゲームプログラムでは「Title」とか「MainGame」のような場面単位をクラスにして、それぞれ固有のお仕事をさせる事が多いです。そうなると、一つのクラスが様々な仕事を担うようになり、そこに状態遷移も入ってきたりします。
例えばタイトルを再生する時に、
・フェードイン
・プレイヤーの入力があるまで待機
・ボタンが押されたらフェードアウト
こういう内部状態遷移があるとします。これはタイトルのみの機能なので、タイトルクラスの中だけで完結させたい。こういうクラス内遷移を実現する方法の一つが「クラス内メソッドを使った状態遷移」です。新しい武器ですよ~(^o^)/
ベースは関数ポインタと一緒、でも奇妙でややこしい…
クラスのメソッド(関数)はグローバル関数と同様にアプリが実行されるとそのアドレスが固定的に決まります。ですからそのアドレスと型が分ればその関数を呼び出す事が出来ます。ただし、特殊な呼び出し方をしなければなりません。
具体的に見てみましょう。Titleクラスに状態メソッドとなるfadeIn、idle、fadeOutメソッドを定義し、それらを代入できる状態メソッドポインタ変数state_を宣言します:
class Title {
public:
Title() : state_( &Title::fadeIn ) {}
bool update(); // 更新
private:
// 状態メソッド
void fadeIn(); // フェードイン
void idle(); // 待機
void fadeOut(); // フェードアウト
void (Title::*state_)(); // 状態メソッドポインタ
};
state変数の宣言、奇妙な感じですよねぇ…(-_-;。グローバル関数の関数ポインタ変数は「void (*state)()」と変数を括弧でくくりました。同様にクラスのメソッドポインタ変数も変数名を括弧でくくるのですが、一緒にそのメソッドが所属するクラス名も「Title::」と併記する必要があります。「void (Title::*)()」これがTitleクラス内のメソッドのポインタ型になります。
コンストラクタで最初の遷移先であるfadeInメソッドをstate_に代入する時もスコープ解決演算子が必要です。これを含め型名なので面倒ですがしゃーない。state_はポインタなのでTitle::fadeInメソッドのアドレスを&で渡します。
メソッドポインタの呼び出し方も一癖あります
後はstate_ポインタをメソッドのようにコールすればfadeInメソッドが呼び出せますし、idleメソッドやfadeOutメソッドをstate_に代入すれば状態遷移が起こります。ただ、そのstate_ポインタを通してのメソッドの呼び出し方も中々に独特です。state_はupdateメソッド内で毎フレーム次のように呼び出します:
bool Title::update() {
( this->*state_ )();
return true;
}
えーと…ん?(^-^;
thisは自分自身のポインタです。->はアロー演算子なので矢印の先はメンバ変数かメソッドが来るはずです。所が上のは「->*」とアスタリスクが付いてます。これはメンバアクセス演算子の一つで「ポインタのメンバへのポインタ演算子」という頭が混乱する名前が付いてます(^-^;。まぁあまり深く考え過ぎず、この演算子を使う事で自分自身のstate_に代入されているメソッドを呼び出せるんだなぁ、と飲み込んじゃいましょう。
兎にも角にも上のようにTitleクラス内の同じ型のメソッドをstate_に代入する事でクラス内メソッド状態遷移を実現する事が出来ます。これはこれで十分な武器です。
クラス内メソッド状態遷移のメリット
クラス内メソッド状態遷移の利点は、何と言ってもそのクラス内だけで状態遷移がまとまる所(局所化)、そしてオブジェクトが持つメンバ変数を使ってオブジェクト別に状態を表現できる所(複製)です。グローバル関数+関数ポインタではそういういう局所化や複製が難しいのでが、この方法はクラスを複製するだけで状態遷移するオブジェクトがポンポン出来ます。
素敵(^-^)
クラス内メソッド状態遷移のデメリット
こんな素敵な状態遷移の新しい武器ですが、デメリットもあります。クラスと言えば派生先で機能を追加したりメソッドを変更したりする「多態性(ポリモーフィズム)」が大活躍しますよね。でもこれがあまりうまく活用できないんです。
先のTitleクラスでfadeInの処理を変えたバージョンが必要になり、派生クラスTitleExクラスを新設してfadeInメソッドを再定義して対応しようと考えます。状態遷移で呼んで欲しいのはTitle::fadeInメソッドではなくてTitleEx::fadeInメソッドなんですが…これ、出来ません:
class TitleEx : public Title {
public:
TitleEx() {
state_ = &TitleEx::fadeIn; // 型が違うので代入できない!
}
private:
void fadeIn();
};
型が違うのです。メソッドの型は戻り値の型、引数の型、そして定義されているクラス、この3つがワンセットで定義されるので、上のTitleEx::fadeInはクラスが異なるため別の型になってしまいます。ですからstate_に代入できないんです。んじゃあとfadeInメソッドをvirtualにしてもダメです。state_に入っているアドレスはTitle::fadeInの物なので仮想化が効かないんです。
ではどう回避するか?現実的な方法はTitle::innerFadeInメソッドを用意して、Title::fadeInメソッド内でそれを呼ぶ、という方法かなと思います:
class Title {
protected:
virtual void innerFadeIn(); // フェードイン内部メソッド
void fadeIn() {
innerFadeIn(); // 仮想化されているので派生クラスで上書き出来る
}
};
class TitleEx : public Title {
protected:
virtual void innerFadeIn(); // 上書き可能
};
状態を表す変数でごちゃごちゃに
もう一つのデメリットは、状態を表す変数でクラス内がごちゃごちゃになりやすいという事です。フェードインを3秒で行う時、3秒という目標値aimと現在のフェード中時刻tが必要です。同じ事はフェードアウトにも言えます。フェードアウトは2秒だとするとfadeOutAimが必要ですよね。こんな感じである状態遷移だけで使う変数がどんどん増えちゃうんです。状態が増える程それは肥大化していくので、だんだんクラス内がカオス化していきます。
まぁ諸々デメリットもありますが、状態の局所化というのはクラスにしかできないかなり強力な武器です。これだけで相当しっかりとした状態遷移が作れますので練習してみて下さい。次は入れ子クラスを使った状態遷移です。お楽しみに~(^-^)/