EternalWindows
ビットマップ / ウインドウ処理

ゲームの仕様にもよると思いますが、 どのようなときでも常にゲーム関連の処理を行うとは限りません。 たとえば、そのゲームにポーズ機能があったとして、 ユーザーがポーズを選択するとゲームの進行は停止するはずです。 ゲームの進行が停止するということは、ゲーム関連の処理を行う必要はないでしょうから、 ポーズが解除されるまで待機しておくのがベストだと思われます。 このようにすれば、ポーズ中はCPUを使用することはありません。

if (g_bPause || PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
	if (GetMessage(&msg, NULL, 0, 0) > 0)
		DispatchMessage(&msg);
	else
		break;
}

g_bPauseは、WM_KEYDOWNでポーズキーが押されたときに変化するとします。 この変数がTRUEであるときはポーズ中です。 そのため、GetMessageで待機します。 ゲーム関連のコードが実行されるのは、 ユーザーが再びWM_KEYDOWNでポーズキーを押したときになります。

ゲームの進行を停止するという処理は、ユーザーのウインドウ操作でも起こりうることです。 ウインドウが最小化しているようなときは、ゲーム画面が見えないわけですから、 ゲームを進行することは恐らく好ましくないでしょう。 また、ウインドウが非アクティブになっているようなときは、 ユーザーが他のウインドウで何らかの処理を行っているでしょうから、 このようなときもゲームを続行するべきではないと思われます。

if (g_bPause || g_bLostFocus || PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
	if (GetMessage(&msg, NULL, 0, 0) > 0)
		DispatchMessage(&msg);
	else
		break;
}

g_bLostFocusは、WM_SETFOCUSとWM_KILLFOCUSで変更されます。 この変数がTRUEであるときはウインドウがフォーカスを失っているので、 ゲームの進行を停止させます。

今回のプログラムはゲームループを実装しており、 さらに実践的なゲームに欠かせないウインドウ関連の処理も充実しています。 多くのゲームで使われるであろうコードだけを含んでいるため、 プロトタイプとして働けるのではないかと思います。なお、簡単のため、 バックバッファはCreateCompatibleBitmapで作成しています。

#include <windows.h>

#pragma comment (lib, "winmm.lib")

struct BACKBUFFER {
	HWND    hwnd;
	HDC     hdc;
	HBITMAP hbmp;
	HBITMAP hbmpPrev;
	HBRUSH  hbr;
	int     cx;
	int     cy;
};
typedef struct BACKBUFFER BACKBUFFER;

BOOL       g_bPause = FALSE;
BOOL       g_bLostFocus = FALSE;
BACKBUFFER g_backBuffer = {0};

int  Run(void);
BOOL CreateBackbuffer(HWND hwnd);
void DestroyBackbuffer();
void Move(void);
void Render(void);
void Show(void);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	WNDCLASSEX wc;
	RECT       rc;
	DWORD      dwStyle;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;
	
	dwStyle = WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX;
	SetRect(&rc, 0, 0, 640, 480);
	AdjustWindowRect(&rc, dwStyle, FALSE);

	hwnd = CreateWindowEx(0, szAppName, szAppName, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	return Run();
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_CREATE:
		return CreateBackbuffer(hwnd) ? 0 : -1;
	
	case WM_KEYDOWN:
		if (wParam == VK_PAUSE) {
			g_bPause = !g_bPause;
			if (g_bPause)
				Show();
		}
		return 0;

	case WM_PAINT:
		Show();
		break;

	case WM_NCLBUTTONDOWN:
		Show();
		break;
	
	case WM_ERASEBKGND:
		return 0;
	
	case WM_SETFOCUS:
		g_bLostFocus = FALSE;
		return 0;
		
	case WM_KILLFOCUS:
		g_bLostFocus = TRUE;
		return 0;

	case WM_DESTROY:
		DestroyBackbuffer();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int Run(void)
{
	MSG   msg;
	BOOL  bMove;
	DWORD dwInterval = 40;
	DWORD dwCurTime, dwNextTime;

	bMove      = TRUE;
	dwNextTime = timeGetTime();
	
	for (;;) {
		if (g_bPause || g_bLostFocus || PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
			if (GetMessage(&msg, NULL, 0, 0) > 0)
				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;
			}
			else
				Sleep(dwNextTime - dwCurTime);
		}
	}

	return (int)msg.wParam;
}

BOOL CreateBackbuffer(HWND hwnd)
{
	HDC hdc;

	hdc = GetDC(hwnd);
		
	g_backBuffer.hdc = CreateCompatibleDC(hdc);
	g_backBuffer.hbmp = CreateCompatibleBitmap(hdc, 640, 480);
	if (g_backBuffer.hbmp == NULL) {
		ReleaseDC(hwnd, hdc);
		return FALSE;
	}
	
	g_backBuffer.hbmpPrev = (HBITMAP)SelectObject(g_backBuffer.hdc, g_backBuffer.hbmp);

	ReleaseDC(hwnd, hdc);

	g_backBuffer.hwnd = hwnd;
	g_backBuffer.hbr  = (HBRUSH)GetStockObject(BLACK_BRUSH);
	g_backBuffer.cx   = 640;
	g_backBuffer.cy   = 480;

	return TRUE;
}

void DestroyBackbuffer()
{
	if (g_backBuffer.hdc != NULL) {
		if (g_backBuffer.hbmp != NULL) {
			SelectObject(g_backBuffer.hdc, g_backBuffer.hbmpPrev);
			DeleteObject(g_backBuffer.hbmp);
		}
		DeleteDC(g_backBuffer.hdc);
	}
}

void Move(void)
{

}

void Render(void)
{
	RECT rc;

	SetRect(&rc, 0, 0, g_backBuffer.cx, g_backBuffer.cy);
	FillRect(g_backBuffer.hdc, &rc, g_backBuffer.hbr);
}

void Show(void)
{
	HDC hdc;
	
	hdc = GetDC(g_backBuffer.hwnd);

	BitBlt(hdc, 0, 0, g_backBuffer.cx, g_backBuffer.cy, g_backBuffer.hdc, 0, 0, SRCCOPY);
	
	ReleaseDC(g_backBuffer.hwnd, hdc);
}

プログラムの実行結果は、クライアント領域が黒いだけのウインドウに見えますが、 実際にはゲームループによって更新されたバックバッファが、何度も描画されています。 ゲームループはRunという関数に実装されており、WinMainから呼び出されています。 winmm.libのリンクは、マルチメディア関数(timeGetTimeなど)の利用の際に必要になります。 BACKBUFFER構造体は、バックバッファの情報を管理します。メンバには、バックバッファを コピーすることになるウインドウや、バックバッファ自体を表すメモリデバイスコンテキストの他、 幅と高さやクリアに使うブラシなども含まれています。 WinMainでは、メッセージループのコードの代わりにRunを呼び出しており、 この関数内でゲームループが実装されています。

if (dwCurTime > dwNextTime) {
	Show();

	dwNextTime += dwInterval;

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

	bMove = TRUE;
}
else
	Sleep(dwNextTime - dwCurTime);

このelse文は、前節で紹介したゲームループでは実装していません。 絶対必要というわけではありませんが、CPU使用率を下げる1つの手段になっています。 プログラムはMoveとRenderを呼び終えたら、dwCurTimeがdwNextTimeを超えるまで ひたすらループを繰り返しています。 これは、悪く言えば一種のポーリングであり、CPUの浪費です。 次に描画すべき時間(dwNextTime)というのは分かっているわけですから、 その時間になるまで処理を待機するというのが明らかに得策ではないでしょうか。 Sleepは、指定したミリ秒の分だけコードの実行を停止させる関数で、 この間にはCPUが使用されることはありません。 Sleepが制御を返したときには、dwCurTime > dwNextTime のif文が真になるでしょう。

Sleepには、待機中にメッセージに素早く応答できない点と、 指定したミリ秒経ったとしても、直ぐに制御を返せるとは限らないという点を持っています。 一説によれば、timeBeginPeriodを呼び出すことでSleepの精度が上がるようですが、 どちらにしても、余分に待機してしまう可能性があるということは気に留めておくべきです。

続いて、MoveとRender、そしてShowの内部を見ていきます。

void Move(void)
{

}

Moveで行うべきことは、データの更新です。 しかし、今回はバックバッファを描画するだけのプログラムであるため、 アニメーションなどの処理は一切、行われていません。

void Render(void)
{
	RECT rc;

	SetRect(&rc, 0, 0, g_backBuffer.cx, g_backBuffer.cy);
	FillRect(g_backBuffer.hdc, &rc, g_backBuffer.hbr);
}

Renderで行うべきことは、バックバッファへの描画です。 今回は、バックバッファのクリアのみですが、 本来ならばMoveで更新したデータを基に、ビットマップを描画したりします。

void Show(void)
{
	HDC hdc;
	
	hdc = GetDC(g_backBuffer.hwnd);

	BitBlt(hdc, 0, 0, g_backBuffer.cx, g_backBuffer.cy, g_backBuffer.hdc, 0, 0, SRCCOPY);
	
	ReleaseDC(g_backBuffer.hwnd, hdc);
}

Showで行うべきことは、バックバッファをウインドウに描画することです。 この処理は、どのようなゲームでも必要になるでしょうから、 Showの内部は基本的には変わりません。

さて、ウインドウプロシージャですが、まずはg_bLostFocusの変更部分から見てみましょう。

case WM_SETFOCUS:
	g_bLostFocus = FALSE;
	return 0;
	
case WM_KILLFOCUS:
	g_bLostFocus = TRUE;
	return 0;

WM_KILLFOCUSは、ウインドウが最小化した場合や、非アクティブになった場合に送られます。 このときは、ゲームの進行を止めるためg_bLostFocusをTRUEにします。 WM_SETFOCUSは、ウインドウがアクティブになった場合に送られます。 次に、ポーズ処理を見てみます。

case WM_KEYDOWN:
	if (wParam == VK_PAUSE) {
		g_bPause = !g_bPause;
		if (g_bPause)
			Show();
	}
	return 0;

ここでは、ポーズのキーとしてVK_PAUSEを採用していますが、 VK_SPACEなどとして、スペースキーの押下でポーズ処理を行うのもよいでしょう。 bPauseがTRUEになったときに何故かShowを呼び出していますが、 呼び出さなかったらどうなるのかを考えてみましょう。 今、bPauseがTRUEになったことでゲームの進行は止まっており、 この状態でウインドウを最小化したとします。 そして、元のサイズに戻したとき、ウインドウに描画されている内容が、 最小化前の内容と異なることがあります。 これは、ウインドウの最小化によってWM_PAINTが発行され、 再描画が新しいバックバッファの描画として機能してしまったからです。

新しいバックバッファは、MoveとRenderによって作成されることになります。 問題は、ShowがMoveとRenderの直ぐ後に呼び出されるわけではないので、 ゲームの進行が停止しているときでも呼ばれる可能性があるということです。 この1つの例が、先に述べたポーズ時での再描画の発生です。 再描画で新しいバックバッファが表示されては、 ユーザーからは突然画面が変わったように見えてしまうため、 ゲームの進行が停止した瞬間にShowを呼び出すことによってこれを回避するのです。 似たような処理が、WM_NCLBUTTONDOWNでも行われています。

case WM_NCLBUTTONDOWN:
	Show();
	break;

WM_NCLBUTTONDOWNは、非クライアント領域(タイトルバー等)を押したときに送られます。 このメッセージが送られた時には、ユーザーはウインドウを自由に動かせますから、 ウインドウはディスプレイの外に移動するかもしれません。 このようなときはWM_PAINTが発行されるので、先ほどのポーズと同じ現象が起こりえます。 そのため、ゲームの進行が停止した瞬間にShowを呼び出すのです。

case WM_ERASEBKGND:
	return 0;

WM_ERASEBKGNDは、ウインドウに無効領域が発生したときに送られます。 このメッセージで0を返した場合、無効領域となった部分はウインドウの背景色で 塗りつぶされることはありません(PAINTSTRUCT構造体のfEraseがFALSEになる)。 たとえば、ウインドウの背景色(WinMainのwc.hbrBackground)が白色でWM_ERASEBKGNDを処理しなかった場合、 次のような現象が発生します。

メモ帳のウインドウが自作のウインドウを覆っていたとして、 自作のウインドウをアクティブにしたとします。 無効領域となった部分が背景色で塗りつぶされるため、 一時的にバックバッファの一部が白くなってしまったわけです。 これを回避するには、WM_ERASEBKGNDを自前で処理して0を返すか、 背景色のブラシとしてNULL_BRUSHを選択する方法が考えられます。 プログラムでは念を入れて、両方の処理を行っています。

今回で、ゲームループを利用したプログラムの説明は終了です。 次節では、実践的とまではいきませんが、 簡単なアニメーションを行うプログラムを作成してみます。


戻る