EternalWindows
ビットマップ / ゲームループによるアニメーション

今回のプログラムは、タイマによるアニメーションの節で取り上げた内容をゲームループで実装したものです。

#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;

struct OBJECT {
	HDC     hdc;
	HBITMAP hbmp;
	HBITMAP hbmpPrev;
	BOOL    bUp;
	int     nBright;
	int     x;
	int     y;
	int     cx;
	int     cy;
};
typedef struct OBJECT OBJECT;

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

int  Run(void);
BOOL CreateBackbuffer(HWND hwnd);
void DestroyBackbuffer();
BOOL CreateObject(HWND hwnd);
void DestroyObject();
void Move(void);
void Render(void);
void Show(void);
BYTE ArrangeBright(BYTE color, BYTE bright);
void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, BYTE bright);
LPBYTE GetBits(HBITMAP hbmp, int x, int y);
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:
		if (!CreateBackbuffer(hwnd))
			return -1;
		if (!CreateObject(hwnd))
			return -1;
		return 0;
	
	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();
		DestroyObject();
		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;
	LPVOID           lp;
	BITMAPINFO       bmi;
	BITMAPINFOHEADER bmiHeader;

	ZeroMemory(&bmiHeader, sizeof(BITMAPINFOHEADER));
	bmiHeader.biSize      = sizeof(BITMAPINFOHEADER);
	bmiHeader.biWidth     = 640;
	bmiHeader.biHeight    = 480;
	bmiHeader.biPlanes    = 1;
	bmiHeader.biBitCount  = 24;

	bmi.bmiHeader = bmiHeader;

	g_backBuffer.hbmp = CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, &lp, NULL, 0);
	if (g_backBuffer.hbmp == NULL)
		return FALSE;

	hdc = GetDC(hwnd);
		
	g_backBuffer.hdc = CreateCompatibleDC(hdc);
	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);
	}
}

BOOL CreateObject(HWND hwnd)
{
	HDC    hdc;
	BITMAP bm;

	hdc = GetDC(hwnd);
	
	g_object.hdc = CreateCompatibleDC(hdc);
	g_object.hbmp = (HBITMAP)LoadImage(NULL, TEXT("sample.bmp"), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE);
	if (g_object.hbmp == NULL) {
		ReleaseDC(hwnd, hdc);
		return FALSE;
	}

	g_object.hbmpPrev = (HBITMAP)SelectObject(g_object.hdc, g_object.hbmp);

	g_object.bUp = FALSE;
	g_object.nBright = 0;

	GetObject(g_object.hbmp, sizeof(BITMAP), &bm);
	g_object.x  = 0;
	g_object.y  = 0;
	g_object.cx = bm.bmWidth;
	g_object.cy = bm.bmHeight;

	ReleaseDC(hwnd, hdc);

	return TRUE;
}

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

void Move(void)
{
	int nSpeed = 1;

	if (GetAsyncKeyState(VK_SPACE) < 0)
		nSpeed = 3;

	if (g_object.bUp) {
		g_object.nBright += 2 * nSpeed;
		if (g_object.nBright >= 255) {
			g_object.bUp = FALSE;
			g_object.nBright = 255;
		}
	}
	else {
		g_object.nBright -= 2 * nSpeed;
		if (g_object.nBright <= 0) {
			g_object.bUp = TRUE;
			g_object.nBright = 0;
		}
	}
}

void Render(void)
{
	RECT rc;

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

	CopyBits(g_backBuffer.hbmp, g_object.x, g_object.y, g_object.cx, g_object.cy, g_object.hbmp, (BYTE)g_object.nBright);
}

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);
}

BYTE ArrangeBright(BYTE color, BYTE bright)
{
	if (bright > 128)
		color += ((255 - color) * (bright - 127)) / 128;
	else
		color = color * bright / 128;

	return color;
}

void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, BYTE bright)
{
	int    x, y;
	LPBYTE lpSrc, lpDest;

	for (y = 0; y < cy; y++) {
		lpSrc  = GetBits(hbmpSrc, 0, y);
		lpDest = GetBits(hbmpDest, xStart, yStart + y);
		for (x = 0; x < cx; x++) {
			lpDest[0] = ArrangeBright(lpSrc[0], bright);
			lpDest[1] = ArrangeBright(lpSrc[1], bright);
			lpDest[2] = ArrangeBright(lpSrc[2], bright);

			lpSrc  += 3;
			lpDest += 3;
		}
	}
}

LPBYTE GetBits(HBITMAP hbmp, int x, int y)
{
	BITMAP bm;
	LPBYTE lp;
	
	GetObject(hbmp, sizeof(BITMAP), &bm);

	lp = (LPBYTE)bm.bmBits;
	lp += (bm.bmHeight - y - 1) * ((3 * bm.bmWidth + 3) / 4) * 4;
	lp += 3 * x;

	return lp;
}

OBJECT構造体は、バックバッファに描画するオブジェクト(ビットマップ)の情報を格納しています。 これは、CreateObjectで初期化されます。

BOOL CreateObject(HWND hwnd)
{
	HDC    hdc;
	BITMAP bm;

	hdc = GetDC(hwnd);
	
	g_object.hdc = CreateCompatibleDC(hdc);
	g_object.hbmp = (HBITMAP)LoadImage(NULL, TEXT("sample.bmp"), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE);
	if (g_object.hbmp == NULL) {
		ReleaseDC(hwnd, hdc);
		return FALSE;
	}

	g_object.hbmpPrev = (HBITMAP)SelectObject(g_object.hdc, g_object.hbmp);

	g_object.bUp = FALSE;
	g_object.nBright = 0;

	GetObject(g_object.hbmp, sizeof(BITMAP), &bm);
	g_object.x  = 0;
	g_object.y  = 0;
	g_object.cx = bm.bmWidth;
	g_object.cy = bm.bmHeight;

	ReleaseDC(hwnd, hdc);

	return TRUE;
}

bUpはオブジェクトの輝度が上昇する場合にTRUEになり、nBrightはオブジェクトの輝度を示す値です。 これらは、Moveで更新されることになります。 xからcyは、オブジェクトの描画位置とサイズを格納します。 これらは、Renderでオブジェクトが描画される際に参照されます。

輝度の変更速度は、spaceキーが押下されている場合に上昇します。 Moveで呼び出しているGetAsyncKeyStateが0より低い値を返した場合は、 第2引数のキーが押されていることを意味するため、 このときにはnSpeedの値を変更すれば、輝度の変更速度は上昇します。 WM_KEYDOWNにおけるキーの検出は若干の遅延があるため、 ゲーム中でのキーの押下はGetAsyncKeyStateを使用します。

実際のゲーム開発を想定すると、 今回のようなMoveとRenderの使い方はあまり好ましくありません。 Moveでは実際にアニメーションを行うための関数を呼び出し、 その関数は他のソースファイルで管理したほうが効率的です。 このようにすれば、ウインドウ関連のコードの汎用性も高まるでしょう。 次に、実装の一例を示します。

enum SCEAN {
	scean_opening, 
	scean_battle,
	scean_ending
};
typedef enum SCEAN SCEAN;

SCEAN g_scean;

void Move(void)
{
	if (g_scean == scean_opening)
		MoveOpening();
	else if (g_scean == scean_battle)
		MoveBattle();
	else if (g_scean == scean_ending)
		MoveEnding();
	else
		;
}

上記コードは、ゲームをシーンという単位で分け、 そのシーン専用のデータ更新の関数を呼び出しています。 Renderの処理でも、関数名の違いはあれ上記のようになるでしょう。 実際には各シーンはMoveとRenderの他に、シーンの切り替えで発生するであろう 作成と破棄のための関数も実装することになるはずです。 ゲームの作り方というものはゲームによるでしょうが、 個々の処理をコード化していく前にまず全体像を把握して、 必要となるであろう関数を列挙しておくのが有効だと思われます。


戻る