DirectX でゲームを作ってみよう
プログラム開発者向けのページです。
ここに書いてある内容は自分で試行錯誤した結果です。
決してここに書かれているやり方が正解とは限りませんので、
他のサイトなどもいろいろと調べてプログラムしてください。

第 5 回 フリップと移動量計算

§Flip関数を使ってはいけない
DirectDraw のウリのひとつに「ダブルバッファリングによるチラつきのない画面フリップ」というのがあります。
表示したい物( キャラクターや背景など )をいったん目に見えない領域( オフスクリーンサーフェス )に描いておき、すべて描き終えた段階で見えるようにする。これがダブルバッファリングです。
このオフスクリーンサーフェスに描き終えた画面を見えるようにするのに IDirectDrawSurface::Flip 関数を使うのですが、ここに罠があります。

Flip 関数は VSYNC 信号( 垂直帰線期間 )にタイミングを合わせて画面を切り替えます。
Flip 関数を呼ぶと VSYNC 信号が来るのを CPU はじっと待ち、その間処理が止まってしまいます。止まっているくらいなら少しでも多くのキャラクターを描いたり、凝ったエフェクトに時間を使いたいわけで待つなんて事は余計なお世話です。

さらに問題なのはこの VSYNC 信号はマシンによってタイミングが異なる点です。
マシンによって VSYNC 信号( つまり画面切り替え )のタイミングが変わってしまうという事は、1 フレームあたりでキャラクターを何ドット移動させていいのか非常に計算しづらくなります。

キャラクターを単純に 1 フレームに 1 ドットずつ横方向へ移動させる処理を書いたとしましょう。マシンによって異なりますが、リフレッシュレートはだいたい 60Hz から 90Hz の間です。つまり VSYNC 信号が 1 秒間に 60 回から 90 回発生します。
1 フレームに 1 ドットずつ横方向へ 60 ドット移動させた場合、60Hz のマシンでは 1.00 秒掛かりますが、90Hz のマシンでは 0.66 秒で移動を終えてしまいます。
同じプログラムを走らせても、マシンの環境によってこれだけスピードが左右されてしまいます。

もちろんこれがゲームならゲーム全体のスピードにも影響します。60Hz マシンに合わせて作ったゲームを 90Hz のマシンで動かすとゲームスピードも 1.5 倍ということになります。
ダウンロードのページに載せているシューティングゲーム『 Space Bandits 』は VSYNC 信号にタイミングを合わせているたいへん悪い例です( 笑 )。

§Flip関数を使わないダブルバッファリング
というわけで VSYNC 信号みたいなマシンによって異なるタイミングでゲームスピードを決めるのは論外です。よって Flip 関数は使わない方が良さそうです。Flip 関数を使わなくてもダブルバッファリングは可能ですから。

Flip 関数を使わないダブルバッファリングのやり方は、1 フレーム分の画面をオフスクリーンサーフェスに書き終えたら、BltFast 関数でプライマリーサーフェスへ丸ごと転送するだけです。VSYNC 信号のタイミングなど全然関係なしです。
VSYNC 信号を無視して描画をするとチラつくのでは?という心配があります。が、これはやってみれば判りますがチラつきません。なぜだかは知りませんがチラつきません。

もうひとつ、BltFast 関数で画面を丸ごと転送させていては遅くなるのではないか?という心配もあります。Flip 関数はポインタを入れ替えるだけなので命令自体の処理時間はほぼゼロで済む反面 BltFast 関数で転送するには時間が掛かります。
しかし、先ほど書いたように Flip 関数は VSYNC 信号を待ちます。状況とマシン環境にもよりますが最悪の場合で 1/60 秒もの間、何もせずにじっと待ちます。これなら BltFast 関数のほうが確実に速いはずです。

//  Flip関数を使わないフリップ
RECT rc;
SetRect(&rc, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);  //  画面全体を指定
lpPrimarySurface->BltFast(0, 0, lpBackSurface, NULL, DDBLTFAST_NOCOLORKEY);

§キャラクターの移動量を計算する
ゲームスピードを決めるには大きく分けて 2 つのやり方があります。フレームレートを一定にする方法か、更新タイミングによって移動量を変えるやり方です。

まず、フレームレートを一定にするやり方。
これはオフスクリーンサーフェスに描画して、更新すべきタイミングが来るまで待ち、タイミングが来たら BltFast 関数でプライマリーサーフェスへ転送することで実現できます。Flip 関数がやっている事と似ていますが、自分でタイミングを計るため VSYNC 信号とは関係なく自由にフレームレートを決められます。
更新すべきタイミングとは、60fps にしたいのであれば 16.666 ミリ秒毎に 1 フレーム描画です。オフスクリーンサーフェスへの描画に 5 ミリ秒掛かったとしたら、16 - 5 = 11 ミリ秒ウェイトを入れてから BltFast 関数でプライマリーサーフェスへ転送します。
キャラクターの移動量は、フレームレートが固定なので 1 フレームで 10 ドット移動といったように決めるだけで済みます。
但し、このやり方だとオフスクリーンサーフェスへの描画に時間が掛かりすぎたとき( 60fps なら 16 ミリ秒以上掛かった時 )にゲーム全体の速度が落ちます。

もうひとつの方法、移動量を変える方法です。
まず、移動量を決めておきます。例えば、1 秒(= 1000 ミリ秒)で 64 ドット移動するキャラクターがいたとしましょう。
このキャラクターを移動させるには、現在の時間を取得して前フレームからの経過時間を計算します。えー、前フレームからの経過時間が 50 ミリ秒だったとします。1000 ミリ秒では 64 ドット移動するので、50 ミリ秒では 3.2 ドットです。整数化すると 3。よってキャラクターの座標に 3 を加えます。この計算は小学校で習った、距離=速さ×時間という計算と同じです。

この方法で移動量を計算してすべてのキャラクターや背景を書き終えたら BltFast 関数でプライマリーサーフェスに転送します。

このやり方だと高速マシンでも鈍足マシンでも一定のスピードでゲームが進行します。速いマシンだと 1 ドット程度の移動でスムースに移動しますし、遅いマシンだと 1 フレームで何ドットも移動する事になり動きがカクカクしますが、どちらのマシンもキャラクターの移動速度は一定です。

この前者と後者、後者の方が移動量の計算が少しだけ面倒ですが、待つ処理がない分、スムースな描画ができますし、処理効率もいいのでお勧めです。

この 2 つのやり方ともミリ秒単位で時間を計る必要があります。この時間計測には timeGetTime 関数を使ってください。同じような時間計測関数に GetTickCount 関数がありますが、こちらは精度が悪いので画面更新タイミングを計るのには不向きです。
なお、timeGetTime 関数を使うには mmsystem.h をインクルードして winmm.lib をリンクする必要があります。

§実はもう少し補正が必要
先ほど、1000 ミリ秒で 64 ドット移動するキャラクターが 50 ミリ秒では 3.2 ドット移動、整数化して 3 ドット移動と書きました。考え方はこれでいいのですが、実はもう少し補正が必要です。

もし毎フレームの描画に 50 ミリ秒ずつ掛かるとしたら、毎フレーム 3 ドットずつ移動する計算です。
この処理速度で 5 フレーム描いた時を考えてみます。毎フレーム 3 ドット移動が 5 フレームなのでキャラクターは 15 ドット移動することになります。
あれれ? 5 フレームは 50 ミリ秒× 5 フレームなので全体では 250 ミリ秒が経過しています。250 ミリ秒あれば、16 ドット移動するはずなのに 15 ドットしか移動していません。消えた 1 ドットはどこへ行ったのでしょうか?

1 ドットが行方不明になった原因は、3.2 ドット移動するのを整数化して 3 ドットとした所にあります。ここで 0.2 ドットの移動量が消失しているのです。
0.2 ドットが 5 フレーム分消失すると合計 1.0 ドット。消えてしまった 1 ドットと計算が合いますね。

このずれを補正するには消失してしまう小数点以下の値( この場合は 0.2 )を次のフレームで足してやる必要があります。
つまり 2 フレーム目の移動量は、50 ミリ秒分の移動量( 3.2 ドット )に前回消失分の 0.2 ドットを加えた 3.4 ドットを移動させます。もっともこの 3.4 ドットも整数化すると 3 になってしまいますが、3 フレーム目では 50 ミリ秒分の移動量( 3.2 ドット )に前回消失分の 0.4 ドットを加えた 3.6 ドットを移動させます。
こうして消失する小数点以下の部分を次フレームで加えてやれば、5 フレーム目の移動量は 3.2 ドットに前回消失分の 0.8 ドットを加えて移動量が 4 ドットになり、5 フレームでの総移動量が 3 + 3 + 3 + 3 + 4 ドットで合計 16 ドットの移動になります。

この補正を行わないと、1000 ミリ秒で 64 ドット移動するキャラクターも 75 ドット移動するキャラクターも 50 ミリ秒では 3 ドットの移動となり、スピードに違いがなくなります。
もっと極端な例( というよりも現実に起こる問題 )では、フレームレートが 100fps だと、1000 ミリ秒で 64 ドット移動するキャラクターは 1 フレームごとの移動量が 0.64 ドット、つまり整数化すると 0 となり、いつになっても動かなくなります。

§サンプルの解説
今回は、マシンスピードに依存しない一定速度でキャラクターを動かすサンプルです。

それぞれスピードの違う 3 機の飛行機が飛んでいて、画面の左上には現在のフレームレートが表示されています。
この状態で上下矢印キーを押すと、画面更新にウェイトが掛かり擬似的に鈍足マシンを再現します( いい加減な処理をしているため、上下矢印キーの反応が悪いです )。

鈍足マシン状態だとフレームレートが下がりますが、飛行機のスピードは変わらない事に注目してください。
また、このうち 2 機の飛行機のスピードはほとんど同じですが、わずかですが B777 型( 細長いやつ )の方が速い事に注目してください。移動量の端数を正しく扱う事によって微妙なスピードの違いも表現できています。

この sample4.lzh には実行ファイル( sample4.exe )とソースとヘッダー、画像ファイルが格納されています。コンパイルをする際には dxguid.lib と ddraw.lib 、winmm.lib をリンクしてください。
なお、このサンプルは ESC キーで終了します。

( 36.2KB )