[HMC]年間5250円で独自ドメイン付属、ウィルスチェックありのレンタルサーバ!
Last update : 1999/12/13
ゲーム制作基礎 第2回
4つのキー入力によってキャラクタを4方向へ移動させる
サンプルコードのダウンロード
  では、まずサンプルコードをダウンロードし、解凍して下さい。 私はVisual C++6.0でコンパイルしているので、をお持ちの方は Visual C++でプロジェクトファイルを開いて下さい(「game_02.dsw」をダブルクリックすれば開けます)。 圧縮ファイルに含まれる「game_02.exe」をダブルクリックし、実行してみて下さい。どうでしょう?画面が切り替わり、フルスクリーン化すると思います。 そして、カーソルキーを押すと、スライム(?)が移動すると思います。 というわけで今回は、キー入力によるキャラクタ移動について解説したいと思います。
キャラクタの表示
  では、まずキー入力の前に、キャラクターの表示について簡単に説明します。 まず、キャラクター(slime.bmp 32x32)を作業用バッファ(lpWork)に読み込んでおきます。 画面に表示するだけなら、作業用バッファからプライマリサーフェイスへBltFastとかで転送すれば、それで終わりですが、 今回は、キャラクタの座標が変わったら、新しい座標へ移動するようなプログラムを組まなければなりません。 単に毎回キャラクタの座標へキャラクタを表示し続けるだけでは、元の座標に描かれていた絵がそのまま残ってしまいますよね? そこで、多少工夫(?)し、残骸が残らないようにしてやる必要があります。

  今回のサンプルでは、もっとも簡単な方法でそれらを実現しています。ただ、バックバッファを全部消し、キャラを表示、フリップ(爆。 これなら、毎回バックバッファを全消去しているため、キャラの残骸が残らないと言うわけです。

見れば解りますね(^^;説明するまでもないと思いましたが・・・。
やってはいけない(爆
  さて、キー入力があった場合にウィンドウプロシージャが呼ばれることは前回解ったと思います。 で、実際にキー入力があった場合にキャラクタを移動させるようなプログラムを組むにはどうすればいいでしょう? まず、初心者がよく考えそうな事は、キャラクタの座標をグローバル変数で用意し、 ウィンドウプロシージャが呼ばれて、特定のキーが押されたら、そこで座標を変更するというもの。

実際に組むと、たぶんこんな感じ。 (キーコードについては前回のプログラムを実行して調べて下さい(爆))
  この方法でも、キャラクタは動くには動きます。 さて、テキストで文字を打つときの事を思い浮かべて下さいな。 'A'キーをずっと押しっぱなしにしていると、'A'と一度表示され、しばらくしてから連続的に'A'が 表示されると思います。ウィンドウプロシージャもこのタイミングで呼ばれるため、キャラクタは カクカク動作する事になり、非常にカッコ悪ぅだし、ゲームにも使えません(たぶん。

仮想キーマップのお話
  というわけで、↑の問題を解決すべく、仮想キーマップなるものを作成します。まぁ、これは私が勝手に考えた ものなので正しい方法とは言い切れません(爆。

では、プログラムを見てみましょう。
  では、まず256個の配列を確保します(keyg)。キーが押され「WM_KEYDOWN」が送られてきたら keyg[wParam]に1を代入します(wParamは、キーコードを表します)。で、キーが離され「WM_KEYUP」 が送られてきたらkeyg[wParam]に0を代入します。 これだけです(^^;これで、ウィンドウプロシージャ以外の関数内でもkeyg[キーコード]を参照する事によって そのキーが押されている(値が1)のか、押されていないか(値が0)を判断出来ると言うわけです。
同期のお話
  さて、いつでもキーの状態を取得出来るようになったので、実際にちょっと組んでみましょうか。 こんな感じですかねぇ。
  このプログラムには欠点があります。 それは、マシンの処理速度(描画速度)が速ければ速い程、キャラクタの移動速度も 上がってしまうという事です。つまり、処理速度の違うパソコンによってゲーム速度が変わって しまうというのです(まぁフリップである程度抑えられますが・・・)。

  というわけで、疑似タイマー処理(?)を紹介します。まぁ以前「
DirectDraw基礎 第5回」 でも書いたのですが、もうちょっと変更してみます(^^;

プログラムはこんな感じです(今回のサンプルコードと同じ)
  さて、まずメインループの最初で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設定にしておいて、処理がおいつかない場合は、描画をスキップするなんて 事もよくやるようですが・・・。
  ちなみに固定フレームに反するものは、変動フレームです。 これは、前回から今回のループに掛かった時間を計測しておき、 「距離=時間×速度」と言ったような式を用いてゲームを進行させる方法です。 この方法では、プログラムが複雑になったり、浮動小数点を使用する必要があったりしますが、 マシンが高性能であればある程、ゲームが滑らかになります。
  ま、固定か変動かはケースバイケースなので、ゲームに合った方を使用して下さい(^^;