設計な話
レイアウトアクションを外の人に伝える
前章でレイアウト(HUD)をクリックしたら赤くなるという部分までを設計し実装しました。このワンタイムアクションの結果を外の人に伝えないと面白くないわけです。ではそれをどうするか?試行錯誤は続きます。
@ アクション結果はFunctorに解釈させよう
HUDをクリックして赤くなりました。これはHUD自身の反応です。でも、その反応の結果をどう解釈するかは外の人の自由です。つまり、HUD自身は「マウスのクリックに反応しましたよ」というだけが仕事になります。
反応結果を外に伝える一つの方法に、HUD(Object)内部にフラグを持っていて、それを外の人が取得して活用するというのがあります。確かにこれは伝わります。しかしです!その設計だとObjectクラスがその派生クラスの挙動すべての面倒を見る必要が出てしまいます。つまり、
class Object {
public:
bool isClickLButton(); // マウス左がクリックされた?
};
なんてメソッドをObjectクラスに設けてしまってはいけないわけです。ではどうするか?コールバックオブジェクトFunctor(Observer)を設けるのです!
FunctorというのはFunction(機能)を持った一般的には小さなクラスです。例えばrunメソッドのような実行メソッドを持っていて、これをコールしてもらいます。簡単な例で具体的に説明します:
class Functor {
public:
virtual run() = 0;
};
// 犬ほえるファンクター
class DogHowlingFunctor : public Functor {
public:
virtual run() {
printf("ワン!ワン!");
}
};
// 時計
class TimeCounter {
int curTime;
std::vector<Functor*> functors; // ファンクター
public:
TimeCounter() : curTime() {}
void update() {
Sleep(1000);
curTime++;
printf("%d\n", curTime);
if (curTime % 10) {
for (unsigned i = 0; i < functors.size(); i++)
functors[i].run();
}
}
// ファンクターを登録
void registerFunctor(Functor *functor) {
functors.push_back(functor);
}
};
int main() {
// 犬ほえるファンクターを作成して時計に登録
Functor *functor = new DogHowlingFunctor;
TimeCounter timer;
timer.registerFunctor(functor);
while(1) {
timer.update();
}
return 0;
}
空で書いたのでコンパイルが通らなかったらすみません。Functorクラスは純粋仮想なrunメソッドを持っています。DogHowlingFunctorはそのクラスを継承していて、runメソッドを実装しています。呼ばれれば「ワン!ワン!」と犬が吠えるわけです。TimeCounterクラスはupdateメソッドが呼ばれると内部のカウンタを一つ挙げて1秒待つだけのクラスです。ただ、10回コールされたら自分が保持しているファンクターのrunメソッドを実行します。それによって何が起こるかはTimeCounterクラスは感知しません。ただ単にきっかけを与えているだけです。main関数ではDogHowlingFunctorオブジェクトを作ってタイマーにファンクターとして登録しています。後はTimeCounter::updateメソッドをぐるぐると回すと、10秒ごとに「ワン!ワン!」と吠えるようになるわけです。
Functorを登録する方は、登録先がどういうタイミングでコールしてくれるかその条件を通常良く知っています。ですから具体的に何をするかをFunctorクラスを派生してバンバン書いてしまって構いません。一般にFunctorオブジェクトは小さな仕事を担当します。
HUDの話に戻りましょう。
クリックしたら赤くなるHUD。この時についでにHUDが持っているFunctorのrunメソッドをコールして外部に変わった事を知らせてしまいましょう:
// マウスアクション
void TurnRedObject::mouseAction(Mouse &mouse) {
// クリックされたらパラメータを赤色にする
if (mouse.isDown(Mouse::L) && this->isContain(mouse.getPos())) {
param.color = 0xffff0000;
// ファンクターに連絡
for (unsigned i = 0; i < functors.size(); i++)
functors[i]->run();
}
}
領域内でマウスクリックを検知したら、登録されているファンクターに「クリックされたよー」と連絡します。
連絡を受ける側として、今回はアプリケーションを終了させてみます。アプリ終了ファンクターを作り、メインループの終了条件に仕込みます:
////////////////////////////
// アプリ終了ファンクター
////
class AppEndFunctor : public Functor {
public:
bool bEnd;
AppEndFunctor() : bEnd() {}
virtual void run() {
bEnd = true;
}
};
// アプリ終了ファンクターをHUDオブジェクトに登録
sp<AppEndFunctor> appEndFunctor(new AppEndFunctor);
obj.registerFunctor(appEndFunctor);
// メッセージ ループ
do{
if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){
DispatchMessage( &msg );
}
....
} while( msg.message != WM_QUIT && !appEndFunctor->bEnd);
これでHUDをクリックするとアプリケーションが直ちに終了します!・・・うっ、終了を画像で表現できない・・・(T_T)。サンプルをこちらからダウンロードしてお試し下さい。
HUDが少し機能を持つようになってきました。でも、ゲームの表現としてはあまりに貧弱です。かと言っていきなりすごいのは作れないわけで。ん〜、ゲームのHUDはプレイヤーがクリックするとか選択・決定するなどした場合に見た目のアクションを起こします。それが無いとプレイヤーに選択された事がちゃんと伝わらないのでこれは必須です。このアクションはもう実に多用な表現がされます。その時どうしても導入しなければならないのがアクションの「状態遷移」です。次はその辺りのお話を。