EternalWindows
ビットマップ / アイドル時間の処理

前節では、PeekMessageを呼び出して、メッセージキューにメッセージが格納されていないタイミングを捉えることに成功しました。 今回は、このようなアイドル時間を利用して、ゲーム関連の処理を行います。

for (;;) {
	if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
		if (GetMessage(&msg, NULL, 0, 0) > 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
			break;
	}
	else {
		int y = 0;
		HDC hdc;
		
		x += 5;
		if (x > 300)
			x = 5;

		FillRect(hdcBackbuffer, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));

		Rectangle(hdcBackbuffer, x, y, x + 50, y + 50);

		hdc = GetDC(hwnd);
		
		BitBlt(hdc, 0, 0, 640, 480, hdcBackbuffer, 0, 0, SRCCOPY);

		ReleaseDC(hwnd, hdc);
	}
}

上記コードでは、Rectangleでバックバッファに長方形を描画し、 このバックバッファをBitBltでウインドウにコピーしています。 長方形の位置は常に5ずつ加算されていき、FillRectによるバックバッファの塗りつぶしによって前に描画した長方形はクリアされるため、 長方形はアニメーションしているように見えることになります。

上記のようにループ内でゲーム関連のコードを記述すると、 コード量が多くなった場合に大変ですから、一般的には関数化することになります。 このとき、処理の内容毎に関数が存在すればコードの管理がしやすくなりますから、 次のような3つの関数を実装することにします。

for (;;) {
	if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
		if (GetMessage(&msg, NULL, 0, 0) > 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
			break;
	}
	else {
		Move();
		Render();
		Show();
	}
}

Moveという関数には、データを更新するコードを含めることにします。 ここで言う更新というのは、先の例で言えば変数xを更新するコードです。 次のRenderという関数には、バックバッファを更新するコードを含めることにします。 ここで言う更新というのは、先の例で言えばFillRectによる塗りつぶしとRectangleによる長方形の描画です。 最後のShowという関数は、バックバッファをウインドウにコピーするコードを含めることにします。 これは言うまでもなく、GetDCとBitBltの呼び出しです。 このように、目的を明確して関数化しておけば、 たとえば描画に問題が発生した場合に、 Render関数を確認すればよいと直ぐに分かります。

上記のように単純に関数を呼び出す設計にした場合、 メッセージキューにメッセージがない限り、ひたすら長方形が描画されることになりますから、 長方形は恐ろしいほどの速度で描画されることになります。 また、この速度はマシンの性能によって異なるでしょうから、 アニメーションの速度がマシンに依存しているという問題もあります。 よって、一定のペースでアニメーションする方法を考えなければなりません。

ゲームでは、1秒間に何回画面を更新するかが重要であると言われたりします。 この事をFPS(Frame Per Second)と呼びますが、 簡単に述べると、1秒間にフレーム(バックバッファ)をウインドウに描画する回数です。 FPSで厄介なのは、指定したFPSを満たすためにどれくらいのペースで、 バックバッファを描画していけばよいのかというものです。 たとえば、FPSが25ならば40ミリ秒に一回のペースで描画することになるでしょう。 この40という値は、1000(1秒) / 25 という式で求められます。 40ミリ秒置きに描画するわけではないことに注意してください。

DWORD dwCurTime;  // 現在の時刻
DWORD dwNextTime; // 次の時刻

dwNextTime = timeGetTime();

for (;;) {
	if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
		if (GetMessage(&msg, NULL, 0, 0) > 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
			break;
	}
	else {
		dwCurTime = timeGetTime();

		if (dwCurTime > dwNextTime) {
			Move();
			Render();
			Show();

			dwNextTime += dwInterval;
			
			if (dwNextTime < dwCurTime)
				dwNextTime = dwCurTime + dwInterval;
		}
	}
}

timeGetTimeは、システム時刻をミリ秒単位で返す関数です。 dwNextTimeの次の時刻というのは、 バックバッファを描画しなければならない時刻を意味します。 dwCurTimeがdwNextTimeを上回ったときに、実際に描画します。 dwNextTimeにdwIntervalを加算することで、次に描画すべき時刻を求められます。

dwNextTime += dwInterval;

たとえば、dwCurTimeが110でdwNextTimeが100であったとき、dwNextTimeは140となります。 これにより、次の描画は30ミリ秒後になります。 dwCurTimeが110でdwNextTimeが100であるということは、 理想時間から10ミリ秒遅れているため、次の描画タイミングが速くなるのです。 つまり、40ミリ秒に1回の描画というペースを維持しようとするのです。

if (dwNextTime < dwCurTime)
	dwNextTime = dwCurTime + dwInterval;

このif文は、現在時刻が次に描画すべき時刻を、 追い抜いてしまっている状態のときに実行されます。 このようなときは、現在時刻を基に強引に次の時刻を求めます。 しかし、描画のペースが乱れてしまっているため、 期待通りのFPSを得ることができません。

FPSが25であった場合、40ミリ秒に1回のペースで描画を行うことになるわけですが、 この描画というのは、バックバッファをウインドウに表示することです。 40ミリ秒に1回のペースで行うべきなのは、あくまでこの処理です。 となると、dwNextTimeに到達してから、 MoveやRenderを呼び出すのは不自然であるはずです。 dwNextTimeに到達したときに行うべきことは、 本来ならばShowを呼び出してバックバッファをウインドウに表示することですし、 なにより、このときにMoveやRenderを呼び出してしまっては、 その分に費やした時間のためにShowの呼び出しが遅れてしまうのです。 よって、dwNextTimeに到達する前にMoveやRenderの呼び出しは、 済ませておくべきなのです。 つまり、表示のためのバックバッファは完成させておくということです。

BOOL  bMove;      // 更新フラグ
DWORD dwCurTime;  // 現在の時刻
DWORD dwNextTime; // 次の時刻

bMove      = TRUE;
dwNextTime = timeGetTime();

for (;;) {
	if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
		if (GetMessage(&msg, NULL, 0, 0) > 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
			break;
	}
	else {
		if (bMove) {
			Move();
			Render();
			
			bMove = FALSE;
		}

		dwCurTime = timeGetTime();

		if (dwCurTime > dwNextTime) {
			Show();
			
			dwNextTime += dwInterval;
			
			if (dwNextTime < dwCurTime)
				dwNextTime = dwCurTime + dwInterval;
			
			bMove = TRUE;
		}
	}
}

bMoveの更新フラグというのは、 表示するためのバックバッファを更新すべきかどうかです。 Moveでデータが更新され、Renderはそれを基にバックバッファへ描画します。 これにより、表示すべきバックバッファが更新されたので、bMoveをFALSEにします。 dwNextTimeに到達し、実際にバックバッファを表示したら、 今度は次の時刻に表示するためのバックバッファが必要となるので、 bMoveをTRUEにしてMoveとRenderの実行を促します。

timeGetTimeの精度を向上させる関数として、timeBeginPeriodが存在します。 タイマにはどれくらいの精度で時間を返せるとかという分解能が定められていますが、 WindowsNT/2000では既定の精度が非常に曖昧となっています。 大抵は、timeGetTimeが1ミリ秒単位の時刻を返すことを望みますが、 それがWindowsのバージョンによっては叶わないことがあるようです。 timeBeginPeriodに1を指定すれば、多くの場合で1ミリ秒の分解能を期待できると思われますが、 この効果は他のアプリケーションにも影響を及ぼすため、 使用するのは避けた方がよいと思われます。


戻る