ここでは音ゲーに重要な時間について説明いたします。
まず音楽とは何かからいきましょう。
なんだ知ってるよとか思わないでください。
普段、曲を聞いているだけの人は本当に分かってないのがほとんどですから。
またこれはドラムの音でこれはシンセの音、こっちはギターだなとか、
楽器は知ってる人もいるかもしれませんが、これらの音、単体では決して音楽とはいえません。
音楽とはある一定のタイミングに合わせて、何かを表現した音の集まりなのです。
大切なのは、音の集まりからいろいろな何かを表現することです。
例えばオーケストラであるような「トルコ行進曲」や、
「運命(このタイトルは実は日本が勝手につけた)」のような曲を考えてみましょう。
行進曲は聞くと歩きたくなったり(しないか)、ジャジャジャジャーン♪を聞くとなにか悲しいような、
または何か起こりそうな感じがしませんか?
やはり何かを表現してますね?
さて話は戻りますが音楽としてゲームを作るにはもう一つ重要な事として、
ある一定のタイミングということが課題となります。
普段耳にする曲はテンポと呼ばれるある一定の間隔に合わせて音が合成されて音楽に聞こえます。
小学校でも習うように4/4拍子や3/4拍子、Tempo 120という感じで楽譜上では表記されていますね。
そして1小節というのは4/4拍子なら4回打つまでの間、3/4拍子なら3回打つまでの間を言いますね。
Tempo 120と言われたら、1分間に120回打つ早さですね。
音ゲー作るにはこれを知らないと作れません。
忘れてる人や知らない人はまず音楽の基礎からやるべきです。
ちなみに音ゲーではテンポの事をもっとかっちょよく「BPM」と表現します。
(beat par minute/1分間に打つビートの数)
Tempoとまったく同じ意味ですね。
Windowsのプログラムは、基本的にイベント駆動型であると前に説明しました。
クリックやキー入力により、プログラムのプロシージャが実行される仕組みです。
画面の再描画やマウスの移動といったものは、基本的にWindowsのシステムが必要になったときにのみ発生し、
それぞれのウインドウにメッセージとして送ります。
では、リアルタイムに動くゲームを作りたいと考えます。
その場合はウインドウメッセージに頼った場合、画面の更新やキーの入力はスピードが遅すぎるので使い物になりません。
このためDirectXを使えば、情報をハードウェアから直接受け取ることや、操作することが可能なので処理落ちすることはありません。
DirectXでは画面に描画をする際、毎フレームごとにかならず画面を書き直さないといけません。
しかし、この書き直しするスピードはマシンによって全然違います。
この書き直しするスピードのことをFPS(フリップパーセコンド)と言い、1秒間に何回書き換えられるかを示す単位となります。
ちなみに、家庭用ゲーム機では通常60FPSが使われます。
場合によっては処理落ちしてしまうゲームは強制的に30FPSにしているのもあります。
人間の視覚能力は30FPSではまだ残像として前の画像が残って見えるので通常は60FPSを使用します。
さて、本題に戻って、このFPSの違いを吸収するには、「ある一定のタイミングでイベントを起こす」という、
いわゆるタイマーという概念を使うことになります。
Windowsにも、ウインドウメッセージとしてタイマーメッセージと言うものがあります。
このタイマーは自分で決めたミリ秒に一回、ウインドウプロシージャにメッセージを送ることが出来ます。
タイマーをセットする関数は「SetTimer」という関数です。
この関数では1ミリ秒単位で値をセットすることができ、ウインドウメッセージとして「WM_TIMER」というメッセージが発生します。
これをウインドウプロシージャメッセージに追加し、このメッセージが来た場合のプログラムを記述する必要があります。
(例) ///////////////////////////////////////////////////////////////// // ウインドウメッセージを受け取るコールバック関数 ///////////////////////////////////////////////////////////////// LRESULT CALLBACK mainWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch( uMsg ) { case WM_ACTIVATE: bActive = !((BOOL)HIWORD(wParam)); // アクティブ状態変更 break; case WM_DESTROY: // ALT+F4が押されたら PostQuitMessage( 0 ); break; case WM_LBUTTONDOWN: // マウスの左クリック ・・・ case WM_TIMER: // タイマー発生時の処理 break; default: // その他のメッセージはデフォルトを返す return DefWindowProc( hWnd,uMsg,wParam,lParam ); } return 0L; // 関数のオーバーライド成功 }
アプリケーションを終了する場合はセットしたタイマーを削除しておく必要があります。
その為の関数に「KillTimer」があります。
しかし!!!!早まってはいけません。
ウインドウメッセージとは構造上正確な時間を待ち受けるという事は出来ません。
メッセージが生成されたらシステムはそのメッセージをアプリケーションに渡すまでわずかな時間を要します。
この時間は1ミリ秒以上かかってしまうことがあり、この方法では正確なタイマーを作ることはまず無理です。
さらにSetTimer関数は一応マニュアルでは1msを指定できるようですが、実際やってみると10ms以上の誤差を出してくれます。
そのためこのやり方ではテンポにシビアな音ゲーには使えません。
そこでこれを解決するにはハードウェアでのタイマーを直接取得するという方法をとります。
ウインドウメッセージより正確なタイマーを扱うにはtimeGetTime()関数を使います。
この関数はWindowsを起動してからこの関数を呼び出した瞬間の時間をミリ秒単位で取得します。
タイマーという意味ではまったく考え方が違いますが、その瞬間の時間はかなり精度が高くほぼ100%に近い値が取れます。
以下はこの関数を使ったサンプルです。
void Test( void ) { DWORD old; DWORD now; DWORD keika; old = timeGetTime(); // 処理開始前の時間を取得 // なんかの処理をここで行う now = timeGetTime(); // 処理終了後の時間を取得 keika = now - old; printf( "経過時間 %d[ミリ秒]¥n",keika ); }
こうすればkeika 変数に「なんかの処理」が何ミリ秒かかったのか把握することができます。
なおこちらでも説明していますが、OSによりtimeGetTime()関数には精度の問題があるため、
この関数を使う前にtimeBeginPeriod()で精度を上げておく必要があります。
この関数の戻り値はDWORD値であり32bitの符号なし整数値です。
実はWindowsを起動しっぱなしでいるといつかはこの値を越えて、再び0からのカウントアップになってしまいます。
この間隔はおよそ49日となりますが、サーバーでも無い限りシャットダウンや再起動をするはずなので、
0リセット時の処理はあまり考えなくても問題無いと思われます。
※どうしても気になるのならカウンタが前の値より小さくなったら回りこんだと判断すればOKです
ちなみにこれはOSの時計をいじっても値は変わりません。
ここではこのタイマーを音ゲーにどのように使うのかについて簡単に説明します。
まず音楽にはテンポというのがありました。
そして画面上に譜面を表示するには、そのテンポと時間を計算させて画面の表示を行わなければなりません。
ここではゲーム中画面をスクロールしてくる音符を「オブジェ」と呼んでいますが、
このオブジェの座標をテンポと経過時間から算出するのが結構難しい作業です。
さて、ゲームが始まるとこの座標は時間とともに移動していきます。
ゲームの開始位置というのはプログラマーであれば計算上0であるのが望ましいのは分かると思いますが、
同じくこの開始位置の時間も0であった方が考え方が分かりやすいと思います。
しかしtimeGetTime()関数とは常に加算されていく仕様となっているためこれを0とすることは出来ません。
そこで以下のようにゲーム開始時の時間を取っておき、ゲーム中は現在の時間から開始時の時間を差し引くことで、
開始を0とした時間を求めることが出来ます。
void Game( void ) { DWORD start; DWORD now; DWORD keika; start= timeGetTime(); // 処理開始前の時間を取得 while( TRUE ) { // なんかの処理をここで行う : now = timeGetTime(); // 現在の時間を取得 keika = now - old; // 差分を計算 Trace( "経過時間 : %dミリ秒¥n",keika ); } }
この方法により、変数keikaにはメインループが始まってからの時間が毎ループ求められます。
そしてこの経過時間を使ってオブジェの表示位置を計算したり判定に使います。
また、この方法なら負荷などの影響も関係無く常に正確な時間を取得することが出来るので、
処理落ちしたとしても次の描画のタイミングでは正しい座標値が計算出来ます。
ここではとりあえず簡単に使い方を説明しましたが、timeGetTime()関数でも実はまだ精度があまり高くなく、
スクロール幅を大きくするとオブジェがカクカクしたように見えてしまうことがあります。
人間には1ミリ秒といえば結構小さな値だと思いますが、実は音ゲーを作る時に計算をしてみると
1ミリ秒以下の精度を必要としてしまいます。
※ゲームによっては1000msでも十分な場合があります
そこで次ではさらに細かく取得できる関数を紹介します。