その16 クラス内静的状態遷移なお話
ゲーム制作で状態遷移はぜーーーーったい必要です。だってゲームですから、「何かしたら状態が変わる」が無いとゲームになれません。ゲームのほぼ全域に何らかの状態遷移はあります。
例えば、あるステージを再生する事を考えてみましょう。ステージには様々な物が置かれていますよね。そういうステージを構成する情報を格納したファイルを最初に読み込みます。次にそこからステージを構成する物を実際に作ります。全部作り終わったら画面をフェードインさせて、ステージを開始します。このステージを開始する部分だけでも次のような単方向の状態遷移がある事がわかります:
これをホントーーーにベタに作るとこうなってしまいます:
enum State {
State_LoadingStageConstructFile, // ステージ構成ファイル読み込み中
State_ConstructStage, // ステージ構成物作成中
State_FadeIn, // フェードイン中
State_Game, // ゲーム再生中
};
// グローバル変数
State state = State_LoadingStageConstructFile; // 状態
int curStage = 1; // 現在のステージID
std::vector<ObjInfo> objInfoAry; // ステージ構成物
// ステージファイル名
const char* stageConstFileName[] = {
"stage01.dat", "stage02.dat", "stage03.dat"
};
// ゲーム更新(毎フレーム呼び出し)
void update() {
if (state == State_LoadingStageConstructFile) {
// ステージ構成ファイルを読み込む
loadStageConstructFile(stageConstFileName[curStage]);
} else if (state == State_ConstructStage) {
// ステージ作成
constructStage();
} else if (state == State_FadeIn) {
// フェードイン
fadeIn();
if (fadeVal <= 0.0f) {
fadeVal = 0.0f;
state = State_Game;
}
} else if (state == State_Game) {
updateStage();
}
}
グローバルなupdate関数は毎フレーム呼ばれます。内部はif文の嵐です(^-^;。もし今のステージ(curStage)が1だったら1面のステージ再生を試みます。もし現在の状態(state)がステージ構成ファイルの読込中だったら、ステージ構成ファイルの読み込みを開始します。読み込み後stateをState_ConstructStageに替えます(loadStageConstructFile関数の中で変更)。続いて…と、まぁ、冒頭の遷移図をそのままif文で作ったわけです。
確かにこれでも動くのですが、オブジェクト指向な世の中でさすがにこれはもう無いかなと思うわけです。そこで、これをベースにリファクタリングを繰り返し、クラス化してみようというのが本章の目的です。そこで出てくる「クラス内遷移」が肝となります。
@ リファクタリング:ステージ単位でクラス化
まず、「ステージ」という単位でクラス化するのが常套手段でしょう。ざっくり考えるとこういうクラスになるでしょうか:
class Stage {
enum State {
State_LoadingStageConstructFile, // ステージ構成ファイル読み込み中
State_ConstructStage, // ステージ構成物作成中
State_FadeIn, // フェードイン中
State_Game, // ゲーム再生中
};
std::string stageFileName; // ステージ生成ファイル名
State state; // ステート
std::vector<ObjInfo> objInfoAry; // ステージ構成物
public:
Stage(const char* stageFileName) : stageFileName(stageFileName) {}
virtual ~Stage();
// ゲーム更新(毎フレーム呼び出し)
void update() {
if (state == State_LoadingStageConstructFile) {
// ステージ構成ファイルを読み込む
loadStageConstructFile(stageConstFileName[curStage]);
} else if (state == State_ConstructStage) {
// ステージ作成
constructStage();
} else if (state == State_FadeIn) {
// フェードイン
fadeIn();
if (fadeVal <= 0.0f) {
fadeVal = 0.0f;
state = State_Game;
}
} else if (state == State_Game) {
updateStage();
}
}
};
Stageクラスのコンストラクタにステージ構成ファイル名を渡すと、updateメソッド内の状態遷移の最初でそれがオープンされステージが作られていきます。2面を作りたければ、そういうステージ構成ファイルを作ってStageオブジェクトを新しく作れば良いわけです。たったこれだけの事ですが、随分と「まとまり感」が出ますよね。
さて、とは言うものの、updateメソッド内は相変わらずごちゃごちゃとしています。何が悪いって、言わずもがなですがif文で状態を区切っている所です。「今どの状態になっているのか?」は分かっているのに、その状態を実行するメソッドも呼び出す方が管理しなければならないのが理由です。
オブジェクト指向の基本原則に「依存関係逆転の原則(DIP:the Dependency Inversion Principle)」というのがあります(例えばこちらをご覧くださいhttp://d.hatena.ne.jp/asakichy/20090128/1233144989)。簡単にいえば、「呼び出し元である上位層の人は、下位層の人がする事を気にしてはいけない」という原則です。updateメソッド(上位層)の役目は「ステージを進行しろ」です。進行する際に今どこを進行しているかは知っていても良いかもしれませんが、それを進めるために具体的なメソッド(下位層)の呼び出しを行うのは、下位層がする事を気にしてしまっている状態なわけです。これはオブジェクト指向の依存関係逆転の原則に照らし合わせると「してはいけない事」なんです。
そこで、updateメソッドでは「今すべき事をして下さい」という実装に変更します。擬似プログラムで書くとこんな感じに変わります:
void Stage::update() {
// 今すべき事をしなさい!
curState->run();
};
短っ!
curStateは見てわかるようにメソッド呼び出しをしていますからクラスのオブジェクトです。呼び出しているrunメソッド内ですべき事をします。curStateはStateクラスという一つの状態を表すクラスです。とってもシンプルな次のようなクラスです:
class State {
protected:
State() {}
public:
virtual ~State() {}
// 何かする
virtual void run() = 0;
};
runメソッドは純粋仮想メソッドなので、派生クラスで具体的な実装が必要です。
StageクラスはこのStateクラスのポインタを保持していて、単にそれを再生するだけの人になります:
class Stage {
std::vector<ObjInfo> objInfoAry; // ステージ構成物
State *curState; // 状態オブジェクト
public:
Stage(const char* stageFileName) : curState(new State_LoadingStageConstructFile(stageFileName)) {}
virtual ~Stage();
// ゲーム更新(毎フレーム呼び出し)
void update() {
curState->run();
}
};
最初のクラスからState列挙型が無くなり、代わりにStateクラスのポインタになっています。コンストラクタで最初の状態であるState_LoadingStageConstructFileクラス(Stateクラスの派生型)をセットしています。さらにそのオブジェクトにステージ構成ファイル名を渡しています。ステージ構成ファイル名はこの人以外は知る必要が無いわけです。なので、Stageクラス内でももう保持しません。後はupdateを回すとそのクラスで定義されているrunメソッドが再生されます。
このように、状態をクラス化すると、「その状態だけ知っていれば良い事」をそのクラスの中に閉じ込める事ができます。上位層であるStageクラスは、下位層のちまちまとしたメンバ変数の事など知る必要は無いんです。依存関係逆転の原則に従っていますよね。
A リファクタリング:ステートを伝える
では具体的にState派生クラスであるLoadingStageConstrutFileクラスを作ってみます。実はすぐに詰まってしまうのがわかります:
class LoadingStageConstructFile : public State {
std::string fileName;
public:
LoadingStageConstructFile(const char* fileName) : fileName(fileName) {}
virtual void run() {
// ファイルを開いて何かしたと思いねぇ
// 次のステートに…って、どうやって伝えるべ!?
}
};
runメソッド内でステージ構成ファイルを読み込んで、作成するリソースの情報を色々と収拾したとして、次のステートでその情報を活用する必要が出てきます。また、次のステートに実際に移らなくてもいけません。今のクラスだと、そういう情報を渡したり発したりする方法がそもそもにしてありません。ですから、状態遷移が出来ないんです(^-^;
さて、ではどうやって情報を外に発するか?一つは戻り値に次のステートオブジェクトを渡す方法です:
State* LoadingStageConstructFile::run() {
// ファイルを開いて何かしたと思いねぇ
// 次のステートを作って返す
return new ConstructStage(constructInfo);
};
呼び出し元は戻されたステートを受け取ります:
void Stage::update() {
// 今すべき事をしなさい!
State* newState = curState->run();
if (newState != curState) {
delete curState;
curState = newState;
}
};
戻されたステートが今のステートと異なっていたら、ステートが切り替わったと判断して今のステートを削除し、新しいnewStateを次の再生ステートとしてcurStateに代入しています。渡すべきものはLoadingStageConstructFile::runメソッドの中で解決されていますから、外の人であるStageクラスが気にする事はありません。
この方法は戻り値に必ずステートを返す必要があります。状態によってはもう一度同じ状態を再生したい事もあります(むしろこちらの方が圧倒的に多い)。その場合は自分自身を返します:
State* LoadingStageConstructFile::run() {
// ファイルを開いて何かしたと思いねぇ
// ちょっと次も
if (!finishFileLoading)
return this;
// 次のステートを作って返す
return new ConstructStage(constructInfo);
};
これで同じステートを呼び出し続けることもできます。
B リファクタリング:ステートを「今すぐ」実行したいのか、次に実行したいのか?
ステートを変更する時に、一つ大きく問題になることがあります。それは「切り替えをすぐにしたいのか」それとも「次のフレームでしたいのか」です。先の実装ですと、新しいステートは必ず次の呼び出しから実行されます。でも、例えばステートを実行した結果すでに次のステートに遷移すべき状態になっていたので、自分自身を終了させて「次のステートを直ちに実行して欲しい」という事があります。それに対応するために、State::runメソッドの戻り値で次にして欲しいことを要求します。今返している次のステートは引数側で受け取ることにしましょう:
StateInfo LoadingStageConstructFile::run(State* &newState) {
// ファイルを開いて何かしたと思いねぇ
// ちょっと次も
if (!finishFileLoading)
return StateInfo_Continue;
// 次のステートを直ちに実行せよ!
newState = new ConstructStage(constructInfo);
reutrn StateInfo_Immediate;
};
次のタイミングで良い場合は「StateInfo_Next」を返します。戻り値が3態というわけです。受け取り側もそれに対応します:
void Stage::update() {
// 今すべき事をしなさい!
while(1) {
State *newState = 0;
StateInfo info = curState->run(newState);
if (newState) {
delete curState;
curState = newState;
}
if (info != StateInfo_Immediate)
break;
}
};
これで呼び出し先での状態変更、再生タイミングのちょっとした制御もできるようになりました。
C リファクタリング:作ったリソースは親へ->クラス内ステートの必要性
ステージ構成ファイルからリソースの情報を取り出し、実際にそれを作成するConstructStage::runメソッドを次に考えてみます。
StateInfo ConstructStage::run(State* &newState) {
// 情報ファイルからリソースを作ったと思いねぇ
// 次のステートはフェードインだけど…
newState = new FadeIn(constructObjectAry); // フェーダーがオブジェクト知る必要はないよねぇ…
reutrn StateInfo_Immediate;
};
このクラスのrunメソッド内ではリソースをたくさん作り配列に格納します。作った後、それを使うのは別の人です。しかし、遷移図ではこの次にフェードインが入ってきます。遷移を続けるにはここでFadeステートのオブジェクトを作らないといけません。では、折角作った情報ファイルはどうすれば良いのでしょうか?Fadeオブジェクトに渡す…のはおかし過ぎますよね。
このように、完全にステートのみで行う状態遷移では、どうしてもデータの受け渡しで問題が発生してしまいます。これを解決するには、データを管理する上位層(親)の人がいて、ステートは「親の情報を借りる」という立場にします。今の例ならば、その上位層はStageクラスに他なりません。
ステートに親となるStageオブジェクトの情報を渡すには、runメソッドに親のポインタを渡します:
StateInfo ConstructStage::run(Stage *parent, State* &newState) {
// 情報ファイルからリソースを作ったと思いねぇ
// 親にリソースを渡す
parent->setResources(constructObjectAry);
// 次のステートはフェードインだけど…
newState = new FadeIn; // フェードを作る
reutrn StateInfo_Immediate;
};
この段階で、State::runメソッドの引数がStageクラスに完全に依存している状態になります。これを避けるためには、Stateクラスをテンプレートにするのが手っ取り早いです:
template<class PARENT>
class State {
protected:
State() {}
public:
virtual ~State() {}
// 何かする
virtual void run(PARENT* parent, State<PARENT>* &newState) = 0;
};
すると、めまぐるしくて恐縮ですが、先のConstructState::runメソッドは次のようになります:
StateInfo ConstructStage::run(Stage *parent, State<Stage>* &newState) {
// 情報ファイルからリソースを作ったと思いねぇ
// 親にリソースを渡す
parent->setResources(constructObjectAry);
// 次のステートはフェードインだけど…
newState = new FadeIn<Stage>; // フェードを作る
reutrn StateInfo_Immediate;
};
StageクラスがsetResourcesメソッドを公開しているとして、そこにリソース配列を渡すことで、親に情報を渡した事になります。状態遷移自体は次にフェードインステートに移ります。
さて、親が自分を作るためのsetResourcesメソッドを公開(public)してくれているなら良いのですが、親にとってみるとこれは必ずしも嬉しくありません。親にしてみると「内部でひっそり使いたい」、別の言い方をすれば「メンバはなるべく隠蔽したい」のが本音です。もしsetResourcesメソッドに誰かが適当なリソースを割り当ててしまったら、ステージはいきなりうまく動かなくなるわけです。
では、setResourcesメソッドを非公開(protected, private)にしたとします。すると上のsetResourcesメソッドの呼び出しでコンパイルエラーになってしまいます。さぁ困ったねとなるわけです。公開すると他の人に壊される可能性がある。非公開にするとステート内から設定ができない。欲しいのは「設定する『権利』を持った人だけ設定する」という「限定権利」です。これをする一つの方法は「friend」です。Stageクラス内に自分の状態遷移を構成するステートのクラスをfriendで指定します。そうすると、指定されたクラスは親クラスのメンバやメソッド全てにアクセスできるようになります。
もう一つの別の方法が「入れ子クラス」です。入れ子クラスとはクラスの中に定義されるクラスです。この入れ子クラスもまた親のメンバとメソッドにすべてアクセスする事ができる特別な権利を与えられます。Stageクラスを操作するステートを入れ子クラスにすると次のようになります:
class Stage {
// 各種ステート入れ子クラス
class State_LoadingStageConstructFile : public State<Stage> {
public:
virtual StateInfo run(Stage* parent, State<Stage>* &newState);
};
class State_ConstructStage : public State<Stage> {
public:
virtual StateInfo run(Stage* parent, State<Stage>* &newState);
};
class State_FadeIn : public State<Stage> {
public:
virtual StateInfo run(Stage* parent, State<Stage>* &newState);
};
class State_Game : public State<Stage> {
public:
virtual StateInfo run(Stage* parent, State<Stage>* &newState);
};
std::vector<ObjInfo> objInfoAry; // ステージ構成物
State *curState; // 状態オブジェクト
public:
Stage(const char* stageFileName) : curState(new State_LoadingStageConstructFile(stageFileName)) {}
virtual ~Stage();
// ゲーム更新(毎フレーム呼び出し)
void update() {
curState->run();
}
};
これで、親クラスのメンバやメソッドに自由にアクセスでき、且つ「外部からは見えない」状態遷移クラスが実現できます。完全隠蔽で親の状態遷移に尽くすステートクラスなんです。
入れ子クラスには、その入れ子クラスを外部に公開(public化)したとしても、その派生クラスは親にアクセスできない、という制約があります。例えばFadeInクラスを継承したFadeInExクラスを作ったとしても、そのrunクラスからStageクラスの非公開メンバやメソッドにはアクセスできないわけです。
D 試しに作ってみました
ということで、以上を踏まえ、ステージ構成ファイルを読み込んで、オブジェクトを作り、フェードインして、ゲームを開始する(画面に描画するだけです)一連の状態遷移を内部で行うStageクラスをサンプルとして作りましたのでご参照下さい。