サンプルコードのダウンロード
では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_02.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_02.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、カーソルキーを押すと、スライム(?)が移動すると思います。
というわけで今回は、キー入力によるキャラクタ移動について解説したいと思います。
キャラクタの表示
では、まずキー入力の前に、キャラクターの表示について簡単に説明します。
まず、キャラクター(slime.bmp 32×32)を作業用バッファ(lpWork)に読み込んでおきます。
画面に表示するだけなら、作業用バッファからプライマリサーフェイスへBltFastとかで転送すれば、それで終わりですが、今回は、キャラクタの座標が変わったら、新しい座標へ移動するようなプログラムを組まなければなりません。
単に毎回キャラクタの座標へキャラクタを表示し続けるだけでは、元の座標に描かれていた絵がそのまま残ってしまいますよね?
そこで、多少工夫(?)し、残骸が残らないようにしてやる必要があります。
今回のサンプルでは、もっとも簡単な方法でそれらを実現しています。
ただ、バックバッファを全部消し、キャラを表示、フリップするだけ。
これなら、毎回バックバッファを全消去しているため、キャラの残骸が残らないと言うわけです。
見れば解りますね(^^;説明するまでもないと思いましたが・・・。
int cx,cy;//キャラクタのx,y座標 略 ClearScreen(lpBack); BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1); Flip();
やってはいけない?
さて、キー入力があった場合にウィンドウプロシージャが呼ばれることは前回解ったと思います。
で、実際にキー入力があった場合にキャラクタを移動させるようなプログラムを組むにはどうすればいいでしょう?
まず、初心者がよく考えそうな事は、キャラクタの座標をグローバル変数で用意し、ウィンドウプロシージャが呼ばれて、特定のキーが押されたら、そこで座標を変更するというもの。
実際に組むと、たぶんこんな感じ。
(キーコードについては前回のプログラムを実行して調べてください(爆))
int cx,cy;//キャラクタのx,y座標 略 //----------[ ウィンドウプロシージャ ]---------------------------------------------------------- HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){ switch(iMessage){ 略 //キー押された case WM_KEYDOWN: if(wParam==37) //「←」キー cx-=5; else if(wParam==39) //「→」キー cx+=5; ・ ・ ・ ClearScreen(lpBack); BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1); Flip(); return(TRUE);
この方法でも、キャラクタは動くには動きます。
さて、テキストで文字を打つときの事を思い浮かべてください。
‘A’キーをずっと押しっぱなしにしていると、’A’と一度表示され、しばらくしてから連続的に’A’が表示されると思います。
ウィンドウプロシージャもこのタイミングで呼ばれるため、キャラクタはカクカク動作する事になり、非常にカッコ悪いし、ゲームにも使えません。
仮想キーマップのお話
というわけで、↑の問題を解決すべく、仮想キーマップなるものを作成します。
まぁ、これは私が勝手に考えたものなので正しい方法とは言い切れません(爆。
では、プログラムを見てみましょう。
/* グローバル変数群 */ char keyg[256]; //仮想キーマップ 略 //----------[ ウィンドウプロシージャ ]---------------------------------------------------------- HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){ 略 //キー押された case WM_KEYDOWN: //pauseキー(キーコード19)が押されたら終了 if(wParam==19) Quit(); keyg[wParam]=1; return(TRUE); //キー離された case WM_KEYUP: keyg[wParam]=0; return(TRUE);
では、まず256個の配列を確保します(keyg)。
キーが押され「WM_KEYDOWN」が送られてきたらkeyg[wParam]に1を代入します(wParamは、キーコードを表します)。
で、キーが離され「WM_KEYUP」が送られてきたらkeyg[wParam]に0を代入します。
これだけです(^^;これで、ウィンドウプロシージャ以外の関数内でもkeyg[キーコード]を参照する事によって
そのキーが押されている(値が1)のか、押されていないか(値が0)を判断出来ると言うわけです。
同期のお話
さて、いつでもキーの状態を取得出来るようになったので、実際にちょっと組んでみましょうか。
こんな感じですかねぇ。
//----------[ メイン関数 ]---------------------------------------------------------------------- int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){ MSG msg; <略> int cx=320-16,cy=240-16,mv=3; //メインループ while(1){ ClearScreen(lpBack); //バックバッファ初期化 DdTextOut(lpBack,0,0,"カーソルキーで移動 pauseキーで終了",255); //キャラクター移動 if(keyg[38]) //↑ cy-=mv; else if(keyg[40]) //↓ cy+=mv; else if(keyg[37]) //← cx-=mv; else if(keyg[39]) //→ cx+=mv; //キャラクタ表示 BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1); Flip(); //フィリップ //メッセージループ while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){ if(!GetMessage(&msg,NULL,0,0)) Quit(); TranslateMessage(&msg); DispatchMessage(&msg); } }
このプログラムには欠点があります。
それは、マシンの処理速度(描画速度)が速ければ速い程、キャラクタの移動速度も
上がってしまうという事です。つまり、処理速度の違うパソコンによってゲーム速度が変わって
しまうというのです(まぁフリップである程度抑えられますが・・・)。
というわけで、疑似タイマー処理(?)を紹介します。以前「DirectDraw基礎 第5回」でも書いたのですが、もうちょっと変更してみます(^^;
プログラムはこんな感じです(今回のサンプルコードと同じ)
/* グローバル変数群 */ DWORD wait_time; //ウェイト用 略 //----------[ ウェイト開始時間設定(謎 ]-------------------------------------------------------- void WaitSet(void){ wait_time=timeGetTime(); } //----------[ ウェイト ]------------------------------------------------------------------------ void Wait(DWORD msec){ MSG msg; do{ while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){ if(!GetMessage(&msg,NULL,0,0)) Quit(); TranslateMessage(&msg); DispatchMessage(&msg); } }while(timeGetTime()<wait_time+msec); } //----------[ メイン関数 ]---------------------------------------------------------------------- int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){ <略> int cx=320-16,cy=240-16,mv=3; //メインループ while(1){ WaitSet(); //現在の時間(単位:ミリ秒)取得 ClearScreen(lpBack); //バックバッファ初期化 DdTextOut(lpBack,0,0,"カーソルキーで移動 pauseキーで終了",255); //キャラクター移動 if(keyg[38]) //↑ cy-=mv; else if(keyg[40]) //↓ cy+=mv; else if(keyg[37]) //← cx-=mv; else if(keyg[39]) //→ cx+=mv; //キャラクタ表示 BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1); Flip(); //フィリップ Wait(1000/60); //メッセージループへ }
さて、まずメインループの最初でWaitSet()関数を呼び出しています。
これは自作関数なのでコードを見てもらえば解ると思いますが、グローバル変数wait_timeに、timeGetTime()関数を呼び出し、現在の時間をミリ秒(1秒=1000ミリ秒)で保存しておきます。
その後、メインループでいろいろな処理をし、最後にWait()関数が呼ばれています。
これも自作関数です。
このWait()関数の内容は主にメッセージループなのですが、現在の時間(timeGetTime())が、最初に記憶しておいたwait_time+Wait()関数の引数msecよりも小さい間はひたすたメッセージループを回しています。
つまり、WaitSet()を呼び出した時間から、Wait()の引数msecミリ秒時間が経つまでWait()関数内で止まっている事になります。
Wait()の引数を1000にすれば、秒間1回更新、500で2回更新・・・。
1000/nでn回更新となります。
ただ、この方法にも欠点があるのですが、それはWaitSet()を呼び出してから、いろいろな処理をし、Wait(DWORD msec);を呼ぶ場合、「いろいろな処理」の時間がWaitの引数「msec」より大きくなってしまうと、
Wait()関数内は、ほぼ素通り(1回はメッセージループの処理をやります)になり、いわゆる「もたつき」だとか「処理落ち」と言った現象になります。
普通、ゲームでは秒間30回更新(1ループ33msec)を目安にしたほうがいいですね。
これより小さいと、動作がカクカクに見えてしまうし、大きいと、処理落ちする可能性が大きくなります。
まぁ場合によりますので、いろいろ試してみてください(^^;
同期のお話 オマケ
さて、上では疑似タイマー処理なるものを紹介しましたが、
タイマー系の処理の種類はいろいろあります。
今回紹介したものは、毎回処理される時間を一定時間に合わせていますね?
この考え方は、よく固定フレームとか言います。
固定フレームでは、プログラムが簡単になる分、「もたつき」が起こりうる可能性が出てしまいます。
また、例えば秒間30回更新(30fps)に設定した場合、とても高性能なマシンで動かしても30FPSです。
ちょっとCPUがもったいない気もしますよね?
まぁ、60FPS設定にしておいて、処理がおいつかない場合は、描画をスキップするなんて事もよくやるようですが・・・。
ちなみに固定フレームに対するものは、変動フレームです。
これは、前回から今回のループに掛かった時間を計測しておき、
「距離=時間×速度」と言ったような式を用いてゲームを進行させる方法です。
この方法では、プログラムが複雑になったり、浮動小数点を使用する必要があったりしますが、マシンが高性能であればある程、ゲームが滑らかになります。
固定か変動かはケースバイケースなので、ゲームに合った方を使用してください。