BREW ブロック崩しゲーム 〜 BREW C++ ゲームプログラミング 〜
アニメーション (タイマーの使い方)
衝突判定とアニメーション
このプログラムでは、衝突判定とアニメーションの処理をVoid Block::Animate(Void) という関数の中で行っています。この関数の呼び出しは Void CBAnimate(VoidPtr pdata) 関数によって行われます。そして CBAnimate 関数自体は、タイマーから呼び出されます。(この理由は後程説明します。)
Animate 関数の前半では衝突判定を行っています。衝突判定は、全ての物体を長方形と見做し、それらの長方形が互いにどのように重なっているか(あるいは重なっていないか) を判定することで行います。
この重なり具合の判定は Overlap.hpp と Overlap.cpp で定義された演算子 * で行っています。具体的な処理の内容は、ソース コードを参照してください。
Animate 関数の後半はアニメーション処理です。実際にコードを見てみましょう。
.... // アニメーション // ボール DrawBall(false); _usingBall.ball += _usingBall.velocity; DrawBall(true); // ラケット DrawRacket(false); _racket.racket += _racket.velocity; DrawRacket(); // 得点と残りのボールの数を表示 DrawMessage(); // 画面の更新 _pgraphics->Update(); ...
先にボールやラケットを描画する関数を作っておいたので、随分とすっきりしています。
ここでのポイントは 2 つです。
まず、SFXRectangle や SFXCircle オブジェクトを移動させたいときは+= 演算子を使うと簡単に出来る、ということです。ただしここで言う移動とは画面上の話ではなく、各オブジェクトが保持している座標を変更する点に注意してください。
もう 1 つのポイントは、
_pgraphics->Update();
で画面の更新を忘れずに行うことです。これを忘れるとアニメーションになりませんので。
タイマーの使い方 1
アニメーションは、一定の時間間隔で画面を更新することで実現されます。このブロック崩しでは 1/50 秒毎(Parameters.hpp 中の DRAW_RATE マクロ)に CBAnimate 関数を呼び出すようにしています。更に CBAnimate 関数が Block::Animate(Void) 関数を呼び出します。
まずは実際のコードをご覧ください。
... UInt16 flag = CheckEnd(); SFBShellSmp shell = SFBShell::GetInstance(); // ブロックが動くステージを終了する場合 if (flag != STAGE_END_CONTINUE && _blockMotion) { // 念のためブロック移動のタイマーを解除 shell->CancelTimer(CBMoveBlocks, this); } switch (flag) { case STAGE_END_CONTINUE: // 続行 // タイマーをセット shell->SetTimer(_drawInterval, CBAnimate, this); break; case STAGE_END_MISS: // ミスした if (--_spareBallNumber >= 0) { // そのステージをもう一度 CreateStage(_nowStage); } else { // ゲーム・オーバー ShowGameOver(); } break; case STAGE_END_CLEAR: // そのステージをクリア if (++_nowStage <= MAX_STAGE) { // 次のステージ CreateStage(_nowStage); } else { // 全てのステージをクリア ShowGameClear(); } break; default: break; } ...
この中の shell->CancelTimer(...) については後程説明しますので、今は気にしないでください。
Block クラスのメンバ関数である CheckEnd 関数で、そのステージを終了すべきかどうかを判断していますが、注目して頂きたいのが、
shell->SetTimer(_drawInterval, CBAnimate, this);
の箇所です。これで件のタイマーの設定を行っています。
SFBShell クラスの中に SetTimer というメンバ関数があるのですが、これは次のような定義になっています。
SFCError SetTimer( SInt32 mSecs, // タイマーが満了するまでの時間 (ミリ秒単位) PFNNOTIFY notify, // コール バック関数 VoidPtr data = NULL // ユーザー データ );
これをもう少し判り易く説明すると、「SetTimer を呼び出してから mSecs ミリ秒経ったら、notify(data) を実行する」ということになります。つまりコール バック関数は、VoidPtr を引数にとる関数でなければなりません。
SetTimer の引数の notify は Void 型の関数ポインタです。これがタイマーからは CBAnimate 関数が呼ばれるようにして、その CBAnimate 関数から Block::Animate 関数が呼ばれるようにした理由です。
C++ 言語では、クラスの動的メンバ関数のポインタを取得するのは少々面倒です。C 言語のように簡単には取得できません。
一方静的メンバ関数については、C 言語と同様にしてそのポインタを取得することが出来ます。そのかわり静的メンバ関数ないで使用するメンバ変数も、静的メンバ変数でなければなりません。
しかし BREW の使用上静的メンバ変数は使用できません。(逆に言えば、内部でメンバ変数にアクセスする必要がなければ、静的メンバ関数を使用するのが良いでしょう。)
普通の関数をコール バック関数に使用するのであれば、引数にオブジェクトへのポインタを渡せば良さそうですが、関数内部で非公開メンバにアクセスすることは出来ません。かと言って全てを公開メンバとするのは、C++ 言語のカプセル化の原則に反します。
コール バック関数をそのオブジェクトのフレンド関数にしてしまうことも1 つの手ですが、フレンド関数の使用を極力避けるのであれば、結局は今回のコードのようになります。
因みにコール バック関数 (例えば foo) の定義は、
Void foo(VoidPtr pdata);
です。
また、SetTimer を使うときには予め
SFBShellSmp shell = SFBShell::GetInstance();
として、BREW の IShell インターフェイス を取得しておき、
shell->SetTimer(...);
の形で使用します。
ところで、このタイマーは指定したコール バック関数を 1 回だけしか実行してくれません。そこで今回のコードでは、タイマーから CBAnimate が呼ばれて画面の描画処理が終わった後、再び CBAnimate をタイマーに登録しています。
こうやって画面を一定間隔で更新しています。
タイマーの使い方 2
実はこのブロック崩しでは、もう 1 ヶ所タイマーを使っているところがあります。それはステージ 3 と 4 でブロックを動かすための、MoveBlocks という関数の中です。詳細は Block.cpp を参照して頂くとして、先程 Animate 関数中に登場した CancelTimer について説明します。
Animate 内では
...SFBShellSmp shell = SFBShell::GetInstance(); if (flag != STAGE_END_CONTINUE && _blockMotion) { // ブロックが動くステージを終了する場合shell->CancelTimer(CBMoveBlocks, this); // 念のためブロック移動のタイマーを解除}...
の形で登場していますが、ここでは CBMoveBlocks を呼び出すタイマーを解除しています。(これもアニメーションのときと同様に CBMoveBlocks をタイマーに登録しておき、この関数の中から MoveBlocks を呼び出しています。)
CancelTimer の定義は
SFCError CancelTimer(PFNNOTIFY notify = NULL, // コール バック関数VoidPtr data = NULL // ユーザー データ);
となっています。
CancelTimer では、引数の notify がタイマーに登録されていなくても支障はありません。またタイマー自体は、notify と data の組あわせで区別されています。仮に notify が空ポインタであれば、data に関連付けられた全てのタイマーが解除されます。
このことはサスペンド処理で必要になりますので覚えておいてください。