Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK

时间:2021-11-04 17:06:25

1.画图

要做个拼图,第一步当然是把图给画出来。啥也不说,先把昨天那个显示位图的程序照搬过来:

/*-----------------------------------------------------------
   Bricks1.cpp -- LoadBitmap Demostration
------------------------------------------------------------*/

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("Bricks1");
	HWND         hwnd;
	MSG          msg;
	WNDCLASS     wndclass;

	wndclass.style         = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc   = WndProc;
	wndclass.cbClsExtra    = 0;
	wndclass.cbWndExtra    = 0;
	wndclass.hInstance     = hInstance;
	wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH);
	wndclass.lpszMenuName  = NULL;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass (&wndclass))
	{
	MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
	return 0 ;
	}

	hwnd = CreateWindow(szAppName, TEXT ("LoadBitmap Demo"),
					    WS_OVERLAPPEDWINDOW,
					    CW_USEDEFAULT, CW_USEDEFAULT,
					    CW_USEDEFAULT, CW_USEDEFAULT,
					    NULL, NULL, hInstance, NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HBITMAP		hBitmap;
	static int			cxClient, cyClient, cxSource, cySource;
	BITMAP				bitmap;
	HDC					hdc, hdcMem;
	PAINTSTRUCT			ps;
	int					x, y;
	HINSTANCE			hInstance;

	switch (message)
	{
	case WM_CREATE:
		hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));

		GetObject(hBitmap, sizeof(BITMAP), &bitmap);

		cxSource = bitmap.bmWidth;
		cySource = bitmap.bmHeight;

		return 0;

	case WM_SIZE:
		cxClient = LOWORD(lParam);
		cyClient = HIWORD(lParam);
		
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		hdcMem = CreateCompatibleDC(hdc);
		SelectObject(hdcMem, hBitmap);

		for (y = 0; y < cyClient; y += cySource)
			for (x = 0; x < cxClient; x += cxSource)
			{
				BitBlt(hdc, x, y, cxSource, cySource, hdcMem, 0, 0, SRCCOPY);
			}

		DeleteDC(hdcMem);
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		DeleteObject(hBitmap);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


发现有几个问题:

1)窗口大小并不合适 -- 窗口大小要适合图片的大小

2)窗口大小可以改变 -- 窗口大小不应该允许改变,且不应该允许最大化窗口

解决方案:

1)通过获得位图大小后调用MoveWindow设置窗口大小及窗口位置

case WM_CREATE:
		hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));

		GetObject(hBitmap, sizeof(BITMAP), &bitmap);

		// 获取位图宽、高
		cxBitmap = bitmap.bmWidth;
		cyBitmap = bitmap.bmHeight;

		// 获取程序窗口大小及客户区大小
		RECT rcWindow, rcClient;
		GetWindowRect(hwnd, &rcWindow);
		GetClientRect(hwnd, &rcClient);

		// 非客户区的宽、高
		int cxNonClient, cyNonClient;
		cxNonClient = rcWindow.right - rcWindow.left - (rcClient.right - rcClient.left);
		cyNonClient = rcWindow.bottom- rcWindow.top  - (rcClient.bottom- rcClient.top);

		// 修改后的窗口大小
		int cxWindow, cyWindow;
		cxWindow = cxNonClient + cxBitmap;
		cyWindow = cyNonClient + cyBitmap;

		// 显示位置(居中显示)
		int xScreen, yScreen;
		xScreen = GetSystemMetrics(SM_CXSCREEN)/2 - cxWindow/2;
		yScreen = GetSystemMetrics(SM_CYSCREEN)/2 - cyWindow/2;

		// 设置窗口位置和大小
		MoveWindow(hwnd, xScreen, yScreen, cxWindow, cyWindow, TRUE);

		return 0;


2)修改CreateWindow中的参数,使的窗口大小不可改变,并不允许最大化窗口

hwnd = CreateWindow(szAppName, 
						TEXT ("拼图游戏"),
					    WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    NULL, 
						NULL, 
						hInstance, 
						NULL);

修改后运行效果:

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


2.拆分块

1)显示整幅图成功了,那接下来就是把它分成n*n显示了。简单起见,将n暂时固定为4.

2)然后就是随机打乱这16个图像块

3)绘制方格,分隔成16块


数据结构:

用0~15表示16个图像块,用int nGameMap[16]表示游戏盘面上16个块分别对应的图像块。

例如nGameMap[15] = 0 表示游戏盘面最右下角的块应该显示图像块0,即最左上角的图像块。


随机打乱算法采用《算法导论》中的随机洗牌算法:

void InitGameMap(int* map)
{
	for (int i = 0; i < CELLNUM; i++)
		map[i] = i;

	srand((unsigned int)(time(0)));
	for (int i = CELLNUM-1; i > 0; i--)
	{
		int randIndex = rand() % i;
		int tmp = map[i];
		map[i] = map[randIndex];
		map[randIndex] = tmp;
	}
}

绘制图像块,和分隔线:

void DrawGameMap(HDC hdc, HDC hdcMem, int* map, int width, int height)
{
	int cxCell = width / VHNUMS;
	int cyCell = height/ VHNUMS;

	// 绘制图片块
	for (int i = 0; i < VHNUMS; i++)
		for (int j = 0; j < VHNUMS; j++)
		{
			int nCell = map[i*VHNUMS + j];
			int nRow = nCell / VHNUMS;
			int nCol = nCell % VHNUMS;

			BitBlt(hdc, i*cxCell, j*cyCell, cxCell, cyCell, hdcMem, nCol*cxCell, nRow*cyCell, SRCCOPY);
		}

	// 绘制方格,分隔各个图像块
	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, i*cxCell, 0, NULL);
		LineTo(hdc, i*cxCell, height);
	}

	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, 0, i*cyCell, NULL);
		LineTo(hdc, height, i*cyCell);
	}
}

运行效果:

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


忽视了一个问题,忘了留下一个空白方格。

解决方法:

反正图像块已经是打乱的了,就把最后一个方格设成空白的吧~,简单起见,-1表示空白…

void InitGameMap(int* map)
{
	for (int i = 0; i < CELLNUM; i++)
		map[i] = i;

	srand((unsigned int)(time(0)));
	for (int i = CELLNUM-1; i > 0; i--)
	{
		int randIndex = rand() % i;
		int tmp = map[i];
		map[i] = map[randIndex];
		map[randIndex] = tmp;
	}

	// 空白方格
	map[CELLNUM-1] = -1;
}

void DrawGameMap(HDC hdc, HDC hdcMem, int* map, int width, int height)
{
	int cxCell = width / VHNUMS;
	int cyCell = height/ VHNUMS;

	// 绘制图片块
	for (int i = 0; i < VHNUMS; i++)
		for (int j = 0; j < VHNUMS; j++)
		{
			int nCell = map[i*VHNUMS + j];

			if (nCell == -1)	// 空白方格
			{
				Rectangle(hdc, i*cxCell, j*cyCell, i*cxCell+cxCell, j*cyCell+cyCell);
				continue;
			}

			int nRow = nCell / VHNUMS;
			int nCol = nCell % VHNUMS;

			BitBlt(hdc, i*cxCell, j*cyCell, cxCell, cyCell, hdcMem, nCol*cxCell, nRow*cyCell, SRCCOPY);
		}

	// 绘制方格,分隔各个图像块
	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, i*cxCell, 0, NULL);
		LineTo(hdc, i*cxCell, height);
	}

	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, 0, i*cyCell, NULL);
		LineTo(hdc, height, i*cyCell);
	}
}

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


3.鼠标事件

方法:鼠标点击一个块,若它的周围有空白块,则点击的块和空白块互换。

1)获取鼠标点的是哪个块

2)点中一个块后,若它周围有空白块,则移动

3)判断是否胜利

4)补齐空白块

int OutOfMap(int x, int y)
{
	if (x < 0 || y < 0 || x >= VHNUMS || y >= VHNUMS)
		return 1;

	return 0;
}


int LButtonDownAt(int* map, int xPos, int yPos)
{
	int index = xPos + yPos*VHNUMS;
	if (map[index] == -1)
		return -1;

	static int iDirect[4][2] = {
		{0, 1}, {0, -1}, {1, 0}, {-1, 0}
	};

	// 邻近的四个块
	for (int i = 0; i < 4; i++)
	{
		int xNew = xPos + iDirect[i][0];
		int yNew = yPos + iDirect[i][1];
		int newIndex = xNew + yNew*VHNUMS;

		// 没有出界,且是空白块,则移动
		if (!OutOfMap(xNew, yNew) && map[newIndex] == -1)
		{
			int tmp = map[index];
			map[index] = map[newIndex];
			map[newIndex] = tmp;

			return newIndex;
		}
	}

	return -1;
}

int fIsWin(int* map)
{
	int iGone = -1;
	for (int i = 0; i < CELLNUM; i++)
	{
		if (map[i] == -1)
		{
			iGone = i;
		}
		if (map[i] != -1 && map[i] != i)
			return 0;
	}

	map[iGone] = iGone;
	return 1;
}

case WM_LBUTTONDOWN:
		if (iWin)
			return 0;

		xCell = cxBitmap / VHNUMS;
		yCell = cyBitmap / VHNUMS;

		xPos = LOWORD(lParam) / xCell;
		yPos = HIWORD(lParam) / yCell;

		index = LButtonDownAt(nGameMap, xPos, yPos);
		if (index != -1)
		{
			rcCur.left	= xPos*xCell;
			rcCur.right	= xPos*xCell + xCell;
			rcCur.top	= yPos*yCell;
			rcCur.bottom= yPos*yCell + yCell;

			xPos = index % VHNUMS;
			yPos = index / VHNUMS;

			rcNew.left	= xPos*xCell;
			rcNew.right	= xPos*xCell + xCell;
			rcNew.top	= yPos*yCell;
			rcNew.bottom= yPos*yCell + yCell;

			InvalidateRect(hwnd, &rcCur, FALSE);
			InvalidateRect(hwnd, &rcNew, FALSE);

			iWin = fIsWin(nGameMap);
			if (iWin)
			{
				MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
			}

		}
		return 0;

效果:(为了玩起来简单些,VHNUM改成3 了。。)

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDKWindows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


发现有时是拼不起来的。。。

比如:

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


4.键盘消息

暂且不管前面提到的问题,先完成键盘消息

case WM_KEYDOWN:
		if (iWin)
			return 0;

		switch (wParam)
		{
		case VK_LEFT:
			MoveLeft(nGameMap);
			break;

		case VK_RIGHT:
			MoveRight(nGameMap);
			break;

		case VK_UP:
			MoveUp(nGameMap);
			break;

		case VK_DOWN:
			MoveDown(nGameMap);
			break;

		default:
			break;
		}

		GetClientRect(hwnd, &rcClient);
		InvalidateRect(hwnd, &rcClient, FALSE);

		iWin = fIsWin(nGameMap);
		if (iWin)
		{
			MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
		}

		return 0;

int FindBlack(int* map)
{
	for (int i = 0; i < CELLNUM; i++)
		if (map[i] == -1)
			return i;

	return -1;
}


void MoveRight(int* map)
{
	int index = FindBlack(map);
	if (index % VHNUMS == 0)			// 最左边的块
		return;

	int tmp = map[index];
	map[index] = map[index-1];
	map[index-1] = tmp;
}


void MoveLeft(int* map)
{
	int index = FindBlack(map);
	if (index % VHNUMS == VHNUMS-1)		// 最右边的块
		return;

	int tmp = map[index];
	map[index] = map[index+1];
	map[index+1] = tmp;
}


void MoveUp(int* map)
{
	int index = FindBlack(map);
	if (index >= CELLNUM-VHNUMS)		// 最下边的块
		return;

	int tmp = map[index];
	map[index] = map[index+VHNUMS];
	map[index+VHNUMS] = tmp;
}


void MoveDown(int* map)
{
	int index = FindBlack(map);
	if (index < VHNUMS)					// 最上边的块
		return;

	int tmp = map[index];
	map[index] = map[index-VHNUMS];
	map[index-VHNUMS] = tmp;
}

5.改进随机函数

方法为开始后去掉最右下角一个块,然后随机移动若干步,这样肯定能够经过这若干步的逆步骤还原:

void InitGameMap(int* map)
{
	for (int i = 0; i < CELLNUM; i++)
		map[i] = i;

	map[CELLNUM-1] = -1;

	srand((unsigned int)(time(0)));
	for (int i = 0; i < 1000; i++)
	{
		int randNum = rand() % 4;
		switch (randNum)
		{
		case 0:
			MoveLeft(map);
			break;
		case 1:
			MoveRight(map);
			break;
		case 2:
			MoveUp(map);
			break;
		case 3:
			MoveDown(map);
			break;
		default:
			break;
		}
	}
}

现在既能保证拼起来,又能保证基本功能的实现。

6.整理优化

1)将不变量定义成static变量,如hdcMem,程序开始运行时创建,结束时撤销,提高效率

2)用一个变量表示空白块,以免每次寻找

3)整理函数名称、优化函数效率

新的源代码如下:

/*-----------------------------------------------------------
   file:	PinTu.cpp -- 实现一个拼图游戏
   author:	guzhoudiaoke@126.com
   date:	2012-11-14
------------------------------------------------------------*/

#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include "resource.h"

const int VHNUMS = 3;
const int CELLNUM = VHNUMS*VHNUMS;

int		nGameMap[CELLNUM];		// 保存游戏盘面
int		nBlack;					// 保存空白块位置
BOOL	bIsWin;					// 是否已经获胜
int		cxBitmap, cyBitmap;		// bmp的宽、高
int		cxCell, cyCell;			// 每个块的宽、高

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("PinTu");
	HWND         hwnd;
	MSG          msg;
	WNDCLASS     wndclass;

	wndclass.style         = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc   = WndProc;
	wndclass.cbClsExtra    = 0;
	wndclass.cbWndExtra    = 0;
	wndclass.hInstance     = hInstance;
	wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH);
	wndclass.lpszMenuName  = NULL;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass (&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
		return 0 ;
	}

	hwnd = CreateWindow(szAppName, 
						TEXT ("拼图游戏"),
					    WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    NULL, 
						NULL, 
						hInstance, 
						NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

void SetWindowSizeAndPos(HWND hwnd, int width, int height)
{
	// 获取程序窗口大小及客户区大小
	RECT rcWindow, rcClient;
	GetWindowRect(hwnd, &rcWindow);
	GetClientRect(hwnd, &rcClient);

	// 非客户区的宽、高
	int cxNonClient, cyNonClient;
	cxNonClient = rcWindow.right - rcWindow.left - (rcClient.right - rcClient.left);
	cyNonClient = rcWindow.bottom- rcWindow.top  - (rcClient.bottom- rcClient.top);

	// 修改后的窗口大小
	int cxWindow, cyWindow;
	cxWindow = cxNonClient + width;
	cyWindow = cyNonClient + height;

	// 显示位置(居中显示)
	int xScreen, yScreen;
	xScreen = GetSystemMetrics(SM_CXSCREEN)/2 - cxWindow/2;
	yScreen = GetSystemMetrics(SM_CYSCREEN)/2 - cyWindow/2;

	// 设置窗口位置和大小
	MoveWindow(hwnd, xScreen, yScreen, cxWindow, cyWindow, TRUE);
}

// 交换游戏盘面上的两个位置,即实现移动
void Swap(int indexa, int indexb)
{
	int tmp = nGameMap[indexa];
	nGameMap[indexa] = nGameMap[indexb];
	nGameMap[indexb] = tmp;
}

void MoveRight()
{
	if (nBlack % VHNUMS == 0)			// 最左边的块
		return;

	// 移动块
	Swap(nBlack-1, nBlack);
	nBlack = nBlack-1;
}

void MoveLeft()
{
	if (nBlack % VHNUMS == VHNUMS-1)	// 最右边的块
		return;

	// 移动块
	Swap(nBlack+1, nBlack);
	nBlack = nBlack+1;
}

void MoveUp()
{
	if (nBlack >= CELLNUM-VHNUMS)		// 最下边的块
		return;

	// 移动块
	Swap(nBlack+VHNUMS, nBlack);
	nBlack = nBlack+VHNUMS;
}

void MoveDown()
{
	if (nBlack < VHNUMS)				// 最上边的块
		return;

	// 移动块
	Swap(nBlack-VHNUMS, nBlack);
	nBlack = nBlack-VHNUMS;
}

// 初始化游戏
void InitGameMap()
{
	for (int i = 0; i < CELLNUM; i++)
		nGameMap[i] = i;

	nBlack = CELLNUM-1;
	nGameMap[nBlack] = -1;

	srand((unsigned int)(time(0)));
	for (int i = 0; i < 1000; i++)
	{
		int randNum = rand() % 4;
		switch (randNum)
		{
		case 0:
			MoveLeft();
			break;
		case 1:
			MoveRight();
			break;
		case 2:
			MoveUp();
			break;
		case 3:
			MoveDown();
			break;
		default:
			break;
		}
	}
}

// 绘制游戏盘面
void DrawGameMap(HDC hdc, HDC hdcMem)
{
	// 绘制图片块
	for (int i = 0; i < VHNUMS; i++)
		for (int j = 0; j < VHNUMS; j++)
		{
			int nCell = nGameMap[j*VHNUMS + i];

			if (nCell == -1)	// 空白方格
			{
				Rectangle(hdc, i*cxCell, j*cyCell, i*cxCell+cxCell, j*cyCell+cyCell);
				continue;
			}

			int nRow = nCell / VHNUMS;
			int nCol = nCell % VHNUMS;

			BitBlt(hdc, i*cxCell, j*cyCell, cxCell, cyCell, hdcMem, nCol*cxCell, nRow*cyCell, SRCCOPY);
		}

	// 绘制方格,分隔各个图像块
	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, i*cxCell, 0, NULL);
		LineTo(hdc, i*cxCell, cyBitmap);
	}

	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, 0, i*cyCell, NULL);
		LineTo(hdc, cxBitmap, i*cyCell);
	}
}

// 检查是否获胜,若获胜,则将空白块补齐
int CheckIsWin()
{
	for (int i = 0; i < CELLNUM-1; i++)
	{
		if (nGameMap[i] != i)
			return 0;
	}

	bIsWin = TRUE;
	nGameMap[CELLNUM-1] = CELLNUM-1;
	return 1;
}

// 重绘序号为index的块
void RedrawRect(HWND hwnd, int index)
{
	RECT	rcCur;
	int		xPos, yPos;

	xPos = index % VHNUMS;
	yPos = index / VHNUMS;

	// 设置当前块RECT
	rcCur.left	= xPos*cxCell;
	rcCur.right	= rcCur.left + cxCell;
	rcCur.top	= yPos*cyCell;
	rcCur.bottom= rcCur.top + cyCell;

	// 重绘这两个块
	InvalidateRect(hwnd, &rcCur, FALSE);
}


// 鼠标左键点击事件
void WMLbuttonDown(HWND hwnd, LPARAM lParam, WPARAM)
{
	int	xPos, yPos, index, nBlackSave;

	// 玩家已经获胜,则不响应消息
	if (bIsWin)
	{
		return;
	}

	// 获取鼠标点击的块的行、列
	xPos = LOWORD(lParam) / cxCell;
	yPos = HIWORD(lParam) / cyCell;

	index = xPos + yPos*VHNUMS;
	if (nGameMap[index] == -1)	// 点中了空白块
		return;

	// 点中的块不在空白块的周围则返回,因为只有点中了与空白块相邻的块,才移动
	if (index != nBlack-1 && index != nBlack+1 && index != nBlack+VHNUMS && index != nBlack-VHNUMS)
		return;

	// 移动块
	Swap(index, nBlack);
	nBlackSave = nBlack;
	nBlack = index;

	// 重绘这两个块
	RedrawRect(hwnd, nBlackSave);
	RedrawRect(hwnd, nBlack);

	// 检查是否获胜,若获胜,则将空白块补齐
	CheckIsWin();
	// 若获胜,则显示消息框,提示已经获胜
	if (bIsWin)
	{
		MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
	}
}


void WMKeyDown(HWND hwnd, LPARAM lParam, WPARAM wParam)
{
	int	nBlackSave = nBlack;

	// 玩家已经获胜,则不响应消息
	if (bIsWin)
		return;

	switch (wParam)
	{
	case VK_LEFT:	MoveLeft();	break;
	case VK_RIGHT:	MoveRight();break;
	case VK_UP:		MoveUp();	break;
	case VK_DOWN:	MoveDown();	break;
	default:		break;
	}

	// 若发生了移动,重绘这两个块
	if (nBlackSave != nBlack)
	{
		RedrawRect(hwnd, nBlackSave);
		RedrawRect(hwnd, nBlack);
	}

	// 检查是否获胜,若获胜,则将空白块补齐
	CheckIsWin();
	if (bIsWin)
	{
		MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
	}
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HBITMAP		hBitmap;
	BITMAP				bitmap;
	HDC					hdc, hdcMem;
	PAINTSTRUCT			ps;
	HINSTANCE			hInstance;
	

	switch (message)
	{
	case WM_CREATE:
		hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);

		// 获取位图宽、高
		cxBitmap = bitmap.bmWidth;
		cyBitmap = bitmap.bmHeight;

		// 每个块的宽、高
		cxCell = cxBitmap / VHNUMS;
		cyCell = cyBitmap / VHNUMS;

		// 设置窗口大小和位置
		SetWindowSizeAndPos(hwnd, cxBitmap, cyBitmap);

		// 初始化游戏
		InitGameMap();

		return 0;

	case WM_LBUTTONDOWN:
		WMLbuttonDown(hwnd, lParam, wParam);
		return 0;

	case WM_KEYDOWN:
		WMKeyDown(hwnd, lParam, wParam);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		hdcMem = CreateCompatibleDC(hdc);
		SelectObject(hdcMem, hBitmap);
		DrawGameMap(hdc, hdcMem);
		DeleteDC(hdcMem);
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		DeleteObject(hBitmap);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}


7.功能完善

1)对照图片的显示

2)时间、步数统计

3)时间、步数限制


要显示对照图片,则应先扩大客户区大小:

cxWindow = cxNonClient + cxBitmap*2 + SEPWIDTH;
cyWindow = cyNonClient + cyBitmap + PROCESSBARWIDTH*2 + SEPWIDTH*3;

然后绘图:

// 绘制参考图像
BitBlt(hdc, cxBitmap+SEPWIDTH, 0, cxBitmap, cyBitmap, hdcMem, 0, 0, SRCCOPY);

添加进度条,规定最多可用时间、最多可移动步数,超过则判输。

添加进度条,首先再次扩大客户区,在底下显示两个进度条。

键盘、鼠标移动一次,剩余移动次数减一:

InvalidateRect(hwnd, &rcLeftMoveBar, FALSE);

用上面的代码要求重绘,但发现没有达到预定的目的。后来想到,由于进度条是由长变短的,所以重绘剩余的进度条后,原先长的那块冰没有去掉,所以看上去没有重绘。所以需要先用白色重绘两个进度条区域:

// 绘制剩余时间和剩余移动数目进度条
HBRUSH hBrush = CreateSolidBrush(RGB(0, 128, 255));
HBRUSH hBrushWhite = CreateSolidBrush(RGB(255, 255, 255));

FillRect(hdc, &rcProcessBar, hBrushWhite);

FillRect(hdc, &rcLeftTimeBar, hBrush);
FillRect(hdc, &rcLeftMoveBar, hBrush);
DeleteObject(hBrushWhite);
DeleteObject(hBrush);


添加WM_TIMER,每过一秒,剩余时间减一:

SetTimer(hwnd, ID_TIMER, 100, NULL);

case WM_TIMER:
	if (bIsWin || bIsLose)
		return 0;

	// 剩余时间减一
	iTimeLeft--;
	rcLeftTimeBar.right -= iTimeSegLen;
	if (rcLeftTimeBar.right < iTimeSegLen)
		rcLeftTimeBar.right = 0;
	InvalidateRect(hwnd, &rcProcessBar, FALSE);

	if (iTimeLeft == 0)
	{
		bIsLose = TRUE;
		MessageBox(hwnd, TEXT("不好意思,时间到了!"), TEXT("拼图"), MB_ICONINFORMATION);
	}
	return 0;

使进度条颜色改变,起初为蓝色,越来越变成红色,需要修改画刷颜色:

HBRUSH hBrushTime = CreateSolidBrush(RGB(255-255*iTimeLeft/TOTALTIME, 128*iTimeLeft/TOTALTIME, 255*iTimeLeft/TOTALTIME));
HBRUSH hBrushMove = CreateSolidBrush(RGB(255-255*iMoveLeft/TOTALMOVE, 128*iMoveLeft/TOTALMOVE, 255*iMoveLeft/TOTALMOVE));


最终代码:

/*-----------------------------------------------------------
   file:	PinTu.cpp -- 实现一个拼图游戏
   author:	guzhoudiaoke@126.com
   date:	2012-11-14
------------------------------------------------------------*/

#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include "resource.h"

const int VHNUMS = 3;				// 每行、列方格数
const int CELLNUM = VHNUMS*VHNUMS;
const int SEPWIDTH = 8;				// 中间分隔的宽度
const int PROCESSBARWIDTH = 10;		// 进度条的宽度
const int TOTALTIME	= CELLNUM*30;	// 总共可用的时间秒数
const int TOTALMOVE = CELLNUM*8;	// 总共可用的移动次数
const int ID_TIMER	= 1;			// 计时器ID

int		nGameMap[CELLNUM];			// 保存游戏盘面
int		nBlack;						// 保存空白块位置
BOOL	bIsWin;						// 是否已经获胜
BOOL	bIsLose;					// 是否已输
int		cxBitmap, cyBitmap;			// bmp的宽、高
int		cxCell, cyCell;				// 每个块的宽、高
int		iTimeLeft, iMoveLeft;		// 剩余时间、移动次数
RECT	rcLeftTimeBar;				// 剩余时间进度条
RECT	rcLeftMoveBar;				// 剩余移动次数进度条
RECT	rcProcessBar;				// 进度条所在区域
int		iTimeSegLen;				// 时间进度条单位长度
int		iMoveSegLen;				// 剩余移动次数进度条单位长度

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("PinTu");
	HWND         hwnd;
	MSG          msg;
	WNDCLASS     wndclass;

	wndclass.style         = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc   = WndProc;
	wndclass.cbClsExtra    = 0;
	wndclass.cbWndExtra    = 0;
	wndclass.hInstance     = hInstance;
	wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH);
	wndclass.lpszMenuName  = NULL;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass (&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
		return 0 ;
	}

	hwnd = CreateWindow(szAppName, 
						TEXT ("拼图游戏"),
					    WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    NULL, 
						NULL, 
						hInstance, 
						NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

void SetWindowSizeAndPos(HWND hwnd)
{
	// 获取程序窗口大小及客户区大小
	RECT rcWindow, rcClient;
	GetWindowRect(hwnd, &rcWindow);
	GetClientRect(hwnd, &rcClient);

	// 非客户区的宽、高
	int cxNonClient, cyNonClient;
	cxNonClient = rcWindow.right - rcWindow.left - (rcClient.right - rcClient.left);
	cyNonClient = rcWindow.bottom- rcWindow.top  - (rcClient.bottom- rcClient.top);

	// 修改后的窗口大小
	int cxWindow, cyWindow;
	cxWindow = cxNonClient + cxBitmap*2 + SEPWIDTH;
	cyWindow = cyNonClient + cyBitmap + PROCESSBARWIDTH*2 + SEPWIDTH*3;

	// 显示位置(居中显示)
	int xScreen, yScreen;
	xScreen = GetSystemMetrics(SM_CXSCREEN)/2 - cxWindow/2;
	yScreen = GetSystemMetrics(SM_CYSCREEN)/2 - cyWindow/2;

	// 设置窗口位置和大小
	MoveWindow(hwnd, xScreen, yScreen, cxWindow, cyWindow, TRUE);
}

// 交换游戏盘面上的两个位置,即实现移动
void Swap(int indexa, int indexb)
{
	int tmp = nGameMap[indexa];
	nGameMap[indexa] = nGameMap[indexb];
	nGameMap[indexb] = tmp;
}

void MoveRight()
{
	if (nBlack % VHNUMS == 0)			// 最左边的块
		return;

	// 移动块
	Swap(nBlack-1, nBlack);
	nBlack = nBlack-1;
}

void MoveLeft()
{
	if (nBlack % VHNUMS == VHNUMS-1)	// 最右边的块
		return;

	// 移动块
	Swap(nBlack+1, nBlack);
	nBlack = nBlack+1;
}

void MoveUp()
{
	if (nBlack >= CELLNUM-VHNUMS)		// 最下边的块
		return;

	// 移动块
	Swap(nBlack+VHNUMS, nBlack);
	nBlack = nBlack+VHNUMS;
}

void MoveDown()
{
	if (nBlack < VHNUMS)				// 最上边的块
		return;

	// 移动块
	Swap(nBlack-VHNUMS, nBlack);
	nBlack = nBlack-VHNUMS;
}

// 初始化游戏
void InitGameMap()
{
	for (int i = 0; i < CELLNUM; i++)
		nGameMap[i] = i;

	nBlack = CELLNUM-1;
	nGameMap[nBlack] = -1;

	iTimeLeft = TOTALTIME;
	iMoveLeft = TOTALMOVE;

	iTimeSegLen = (cxBitmap*2 + SEPWIDTH) / TOTALTIME + 1;
	iMoveSegLen = (cxBitmap*2 + SEPWIDTH) / TOTALMOVE + 1;

	rcLeftTimeBar.left	= 0;
	rcLeftTimeBar.right = cxBitmap*2 + SEPWIDTH;
	rcLeftTimeBar.top	= cyBitmap + SEPWIDTH;
	rcLeftTimeBar.bottom= rcLeftTimeBar.top + PROCESSBARWIDTH;

	rcLeftMoveBar.left	= 0;
	rcLeftMoveBar.right	= cxBitmap*2 + SEPWIDTH;
	rcLeftMoveBar.top	= rcLeftTimeBar.bottom + SEPWIDTH;
	rcLeftMoveBar.bottom= rcLeftMoveBar.top + PROCESSBARWIDTH;

	rcProcessBar.left	= 0;
	rcProcessBar.right	= cxBitmap*2 + SEPWIDTH;
	rcProcessBar.top	= cyBitmap;
	rcProcessBar.bottom	= cyBitmap + PROCESSBARWIDTH*2 + SEPWIDTH*3;

	srand((unsigned int)(time(0)));
	for (int i = 0; i < 1000; i++)
	{
		int randNum = rand() % 4;
		switch (randNum)
		{
		case 0:
			MoveLeft();
			break;
		case 1:
			MoveRight();
			break;
		case 2:
			MoveUp();
			break;
		case 3:
			MoveDown();
			break;
		default:
			break;
		}
	}
}

// 绘制游戏盘面
void DrawGameMap(HDC hdc, HDC hdcMem)
{
	// 绘制图片块
	for (int i = 0; i < VHNUMS; i++)
		for (int j = 0; j < VHNUMS; j++)
		{
			int nCell = nGameMap[j*VHNUMS + i];

			if (nCell == -1)	// 空白方格
			{
				Rectangle(hdc, i*cxCell, j*cyCell, i*cxCell+cxCell, j*cyCell+cyCell);
				continue;
			}

			int nRow = nCell / VHNUMS;
			int nCol = nCell % VHNUMS;

			BitBlt(hdc, i*cxCell, j*cyCell, cxCell, cyCell, hdcMem, nCol*cxCell, nRow*cyCell, SRCCOPY);
		}

	// 绘制参考图像
	BitBlt(hdc, cxBitmap+SEPWIDTH, 0, cxBitmap, cyBitmap, hdcMem, 0, 0, SRCCOPY);
	
	// 绘制方格,分隔各个图像块
	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, i*cxCell, 0, NULL);
		LineTo(hdc, i*cxCell, cyBitmap);
	}
	for (int i = 0; i <= VHNUMS; i++)
	{
		MoveToEx(hdc, 0, i*cyCell, NULL);
		LineTo(hdc, cxBitmap, i*cyCell);
	}

	// 绘制剩余时间和剩余移动数目进度条
	HBRUSH hBrushTime = CreateSolidBrush(RGB(255-255*iTimeLeft/TOTALTIME, 
											 128*iTimeLeft/TOTALTIME, 
											 255*iTimeLeft/TOTALTIME));
	HBRUSH hBrushMove = CreateSolidBrush(RGB(255-255*iMoveLeft/TOTALMOVE, 
											 128*iMoveLeft/TOTALMOVE, 
											 255*iMoveLeft/TOTALMOVE));
	HBRUSH hBrushWhite = CreateSolidBrush(RGB(255, 255, 255));

	FillRect(hdc, &rcProcessBar, hBrushWhite);

	FillRect(hdc, &rcLeftTimeBar, hBrushTime);
	FillRect(hdc, &rcLeftMoveBar, hBrushMove);
	
	DeleteObject(hBrushWhite);
	DeleteObject(hBrushTime);
	DeleteObject(hBrushMove);
}

// 检查是否获胜,若获胜,则将空白块补齐
int CheckIsWin()
{
	for (int i = 0; i < CELLNUM-1; i++)
	{
		if (nGameMap[i] != i)
			return 0;
	}

	bIsWin = TRUE;
	nGameMap[CELLNUM-1] = CELLNUM-1;
	return 1;
}

// 重绘序号为index的块
void RedrawRect(HWND hwnd, int index)
{
	RECT	rcCur;
	int		xPos, yPos;

	xPos = index % VHNUMS;
	yPos = index / VHNUMS;

	// 设置当前块RECT
	rcCur.left	= xPos*cxCell;
	rcCur.right	= rcCur.left + cxCell;
	rcCur.top	= yPos*cyCell;
	rcCur.bottom= rcCur.top + cyCell;

	// 重绘这个块
	InvalidateRect(hwnd, &rcCur, FALSE);
}


// 鼠标左键点击事件
void WMLbuttonDown(HWND hwnd, LPARAM lParam, WPARAM)
{
	int	xPos, yPos, index, nBlackSave;

	// 玩家已经获胜,则不响应消息
	if (bIsWin || bIsLose)
	{
		return;
	}

	// 获取鼠标点击的块的行、列
	xPos = LOWORD(lParam) / cxCell;
	yPos = HIWORD(lParam) / cyCell;

	index = xPos + yPos*VHNUMS;
	if (nGameMap[index] == -1)	// 点中了空白块
		return;

	// 点中的块不在空白块的周围则返回,因为只有点中了与空白块相邻的块,才移动
	if (index != nBlack-1 && index != nBlack+1 && index != nBlack+VHNUMS && index != nBlack-VHNUMS)
		return;

	// 移动块
	Swap(index, nBlack);
	nBlackSave = nBlack;
	nBlack = index;

	// 重绘这两个块
	RedrawRect(hwnd, nBlackSave);
	RedrawRect(hwnd, nBlack);

	// 剩余移动次数减少一次
	iMoveLeft--;
	rcLeftMoveBar.right -= iMoveSegLen;
	if (rcLeftMoveBar.right < iMoveSegLen)
		rcLeftMoveBar.right = 0;
	InvalidateRect(hwnd, &rcProcessBar, FALSE);

	// 检查是否获胜,若获胜,则将空白块补齐
	CheckIsWin();
	// 若获胜,则显示消息框,提示已经获胜
	if (bIsWin)
	{
		MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
	}

	// 检查是否超过移动次数
	if (iMoveLeft == 0)
	{
		bIsLose = TRUE;
		MessageBox(hwnd, TEXT("不好意思,规定的移动次数到了!"), TEXT("拼图"), MB_ICONINFORMATION);
	}
}


void WMKeyDown(HWND hwnd, LPARAM lParam, WPARAM wParam)
{
	int	nBlackSave = nBlack;

	// 玩家已经获胜,则不响应消息
	if (bIsWin || bIsLose)
		return;

	switch (wParam)
	{
	case VK_LEFT:	MoveLeft();	break;
	case VK_RIGHT:	MoveRight();break;
	case VK_UP:		MoveUp();	break;
	case VK_DOWN:	MoveDown();	break;
	default:		break;
	}

	// 若发生了移动,重绘这两个块
	if (nBlackSave != nBlack)
	{
		RedrawRect(hwnd, nBlackSave);
		RedrawRect(hwnd, nBlack);

		// 剩余移动次数减少一次
		iMoveLeft--;
		rcLeftMoveBar.right -= iMoveSegLen;
		if (rcLeftMoveBar.right < iMoveSegLen)
			rcLeftMoveBar.right = 0;
		InvalidateRect(hwnd, &rcProcessBar, FALSE);

		// 检查是否获胜,若获胜,则将空白块补齐
		CheckIsWin();
		if (bIsWin)
		{
			MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
		}

		// 检查是否超过移动次数
		if (iMoveLeft == 0)
		{
			bIsLose = TRUE;
			MessageBox(hwnd, TEXT("不好意思,规定的移动次数到了!"), TEXT("拼图"), MB_ICONINFORMATION);
		}
	}
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HBITMAP		hBitmap;
	static int			iTickCount;
	BITMAP				bitmap;
	HDC					hdc, hdcMem;
	PAINTSTRUCT			ps;
	HINSTANCE			hInstance;

	switch (message)
	{
	case WM_CREATE:
		hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);

		// 获取位图宽、高
		cxBitmap = bitmap.bmWidth;
		cyBitmap = bitmap.bmHeight;

		// 每个块的宽、高
		cxCell = cxBitmap / VHNUMS;
		cyCell = cyBitmap / VHNUMS;

		// 设置窗口大小和位置
		SetWindowSizeAndPos(hwnd);

		// 初始化游戏
		InitGameMap();

		SetTimer(hwnd, ID_TIMER, 100, NULL);

		return 0;

	case WM_LBUTTONDOWN:
		WMLbuttonDown(hwnd, lParam, wParam);
		return 0;

	case WM_KEYDOWN:
		WMKeyDown(hwnd, lParam, wParam);
		return 0;

	case WM_TIMER:
		if (bIsWin || bIsLose)
			return 0;

		// 剩余时间减一
		iTimeLeft--;
		rcLeftTimeBar.right -= iTimeSegLen;
		if (rcLeftTimeBar.right < iTimeSegLen)
			rcLeftTimeBar.right = 0;
		InvalidateRect(hwnd, &rcProcessBar, FALSE);

		if (iTimeLeft == 0)
		{
			bIsLose = TRUE;
			MessageBox(hwnd, TEXT("不好意思,时间到了!"), TEXT("拼图"), MB_ICONINFORMATION);
		}
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		hdcMem = CreateCompatibleDC(hdc);
		SelectObject(hdcMem, hBitmap);
		DrawGameMap(hdc, hdcMem);
		DeleteDC(hdcMem);
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		DeleteObject(hBitmap);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


8.进一步的完善

功能上已经基本完成了。

若再要改进,就是用户自主控制方面了:

1)菜单选择使用哪个图片

2)菜单选择难度(如3*3,4*4,5*5)

3)可用的时间、移动步数:

思路1:挑战模式,玩家可以设定可用时间、移动步数

思路2:难度模式,可用时间与移动步数与难度相关,改进相关公式

思路3:过关模式,可用时间、移动步数逐步减少

思路4:英雄榜模式,用剩余时间和剩余步数计算一个分数,并实现记录用户数据


/*-----------------------------------------------------------
   file:	PinTu.cpp -- 实现一个拼图游戏
   author:	guzhoudiaoke@126.com
   date:	2012-11-14
------------------------------------------------------------*/

#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include "resource.h"

const int VHNUMS = 5;				// 每行列最大方格数
const int CELLNUM = VHNUMS*VHNUMS;	// 方格总数
const int SEPWIDTH = 8;				// 中间分隔的宽度
const int PROCESSBARWIDTH = 10;		// 进度条的宽度
const int TOTALTIME	= CELLNUM*30;	// 总共可用的时间秒数
const int TOTALMOVE = CELLNUM*8;	// 总共可用的移动次数
const int ID_TIMER	= 1;			// 计时器ID

HINSTANCE hInstance;
HBITMAP	hBitmap;					// 设备相关位图句柄
BITMAP	bitmap;
int		nGameMap[CELLNUM];			// 保存游戏盘面
int		nBlack;						// 保存空白块位置
int		nVHnums;					// 每行每列方格数
int		nCellNums;
BOOL	bIsWin;						// 是否已经获胜
BOOL	bIsLose;					// 是否已输
int		cxBitmap, cyBitmap;			// bmp的宽、高
int		cxCell, cyCell;				// 每个块的宽、高
int		iTimeLeft, iMoveLeft;		// 剩余时间、移动次数
RECT	rcLeftTimeBar;				// 剩余时间进度条
RECT	rcLeftMoveBar;				// 剩余移动次数进度条
RECT	rcProcessBar;				// 进度条所在区域
int		iTimeSegLen;				// 时间进度条单位长度
int		iMoveSegLen;				// 剩余移动次数进度条单位长度

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void SetMenuPic(HWND);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("PinTu");
	HWND         hwnd;
	MSG          msg;
	WNDCLASS     wndclass;

	wndclass.style         = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc   = WndProc;
	wndclass.cbClsExtra    = 0;
	wndclass.cbWndExtra    = 0;
	wndclass.hInstance     = hInstance;
	wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH);
	wndclass.lpszMenuName  = MAKEINTRESOURCE(IDR_MENU1);
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass (&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
		return 0 ;
	}

	hwnd = CreateWindow(szAppName, 
						TEXT ("拼图游戏"),
					    WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    CW_USEDEFAULT, 
						CW_USEDEFAULT,
					    NULL, 
						NULL, 
						hInstance, 
						NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

void SetWindowSizeAndPos(HWND hwnd)
{
	// 获取程序窗口大小及客户区大小
	RECT rcWindow, rcClient;
	GetWindowRect(hwnd, &rcWindow);
	GetClientRect(hwnd, &rcClient);

	// 非客户区的宽、高
	int cxNonClient, cyNonClient;
	cxNonClient = rcWindow.right - rcWindow.left - (rcClient.right - rcClient.left);
	cyNonClient = rcWindow.bottom- rcWindow.top  - (rcClient.bottom- rcClient.top);

	// 修改后的窗口大小
	int cxWindow, cyWindow;
	cxWindow = cxNonClient + cxBitmap*2 + SEPWIDTH;
	cyWindow = cyNonClient + cyBitmap + PROCESSBARWIDTH*2 + SEPWIDTH*3;

	// 显示位置(居中显示)
	int xScreen, yScreen;
	xScreen = GetSystemMetrics(SM_CXSCREEN)/2 - cxWindow/2;
	yScreen = GetSystemMetrics(SM_CYSCREEN)/2 - cyWindow/2;

	// 设置窗口位置和大小
	MoveWindow(hwnd, xScreen, yScreen, cxWindow, cyWindow, TRUE);
}

// 交换游戏盘面上的两个位置,即实现移动
void Swap(int indexa, int indexb)
{
	int tmp = nGameMap[indexa];
	nGameMap[indexa] = nGameMap[indexb];
	nGameMap[indexb] = tmp;
}

void MoveRight()
{
	if (nBlack % nVHnums == 0)			// 最左边的块
		return;

	// 移动块
	Swap(nBlack-1, nBlack);
	nBlack = nBlack-1;
}

void MoveLeft()
{
	if (nBlack % nVHnums == nVHnums-1)	// 最右边的块
		return;

	// 移动块
	Swap(nBlack+1, nBlack);
	nBlack = nBlack+1;
}

void MoveUp()
{
	if (nBlack >= nCellNums-nVHnums)		// 最下边的块
		return;

	// 移动块
	Swap(nBlack+nVHnums, nBlack);
	nBlack = nBlack+nVHnums;
}

void MoveDown()
{
	if (nBlack < nVHnums)				// 最上边的块
		return;

	// 移动块
	Swap(nBlack-nVHnums, nBlack);
	nBlack = nBlack-nVHnums;
}

// 初始化游戏
void InitGameMap()
{
	for (int i = 0; i < nCellNums; i++)
		nGameMap[i] = i;

	nBlack = nCellNums-1;
	nGameMap[nBlack] = -1;

	iTimeLeft = nCellNums*30;
	iMoveLeft = nCellNums*8;

	bIsLose = FALSE;
	bIsWin = FALSE;

	iTimeSegLen = (cxBitmap*2 + SEPWIDTH) / nCellNums / 30 + 1;
	iMoveSegLen = (cxBitmap*2 + SEPWIDTH) / nCellNums / 8 + 1;

	rcLeftTimeBar.left	= 0;
	rcLeftTimeBar.right = cxBitmap*2 + SEPWIDTH;
	rcLeftTimeBar.top	= cyBitmap + SEPWIDTH;
	rcLeftTimeBar.bottom= rcLeftTimeBar.top + PROCESSBARWIDTH;

	rcLeftMoveBar.left	= 0;
	rcLeftMoveBar.right	= cxBitmap*2 + SEPWIDTH;
	rcLeftMoveBar.top	= rcLeftTimeBar.bottom + SEPWIDTH;
	rcLeftMoveBar.bottom= rcLeftMoveBar.top + PROCESSBARWIDTH;

	rcProcessBar.left	= 0;
	rcProcessBar.right	= cxBitmap*2 + SEPWIDTH;
	rcProcessBar.top	= cyBitmap;
	rcProcessBar.bottom	= cyBitmap + PROCESSBARWIDTH*2 + SEPWIDTH*3;

	srand((unsigned int)(time(0)));
	for (int i = 0; i < 1000; i++)
	{
		int randNum = rand() % 4;
		switch (randNum)
		{
		case 0:
			MoveLeft();
			break;
		case 1:
			MoveRight();
			break;
		case 2:
			MoveUp();
			break;
		case 3:
			MoveDown();
			break;
		default:
			break;
		}
	}
}

// 绘制游戏盘面
void DrawGameMap(HDC hdc, HDC hdcMem)
{
	// 绘制图片块
	for (int i = 0; i < nVHnums; i++)
		for (int j = 0; j < nVHnums; j++)
		{
			int nCell = nGameMap[j*nVHnums + i];

			if (nCell == -1)	// 空白方格
			{
				Rectangle(hdc, i*cxCell, j*cyCell, i*cxCell+cxCell, j*cyCell+cyCell);
				continue;
			}

			int nRow = nCell / nVHnums;
			int nCol = nCell % nVHnums;

			BitBlt(hdc, i*cxCell, j*cyCell, cxCell, cyCell, hdcMem, nCol*cxCell, nRow*cyCell, SRCCOPY);
		}

	// 绘制参考图像
	BitBlt(hdc, cxBitmap+SEPWIDTH, 0, cxBitmap, cyBitmap, hdcMem, 0, 0, SRCCOPY);
	
	// 绘制方格,分隔各个图像块
	for (int i = 0; i <= nVHnums; i++)
	{
		MoveToEx(hdc, i*cxCell, 0, NULL);
		LineTo(hdc, i*cxCell, cyBitmap);
	}
	for (int i = 0; i <= nVHnums; i++)
	{
		MoveToEx(hdc, 0, i*cyCell, NULL);
		LineTo(hdc, cxBitmap, i*cyCell);
	}

	// 绘制剩余时间和剩余移动数目进度条
	HBRUSH hBrushTime = CreateSolidBrush(RGB(255-255*iTimeLeft/nCellNums/30, 
											 128*iTimeLeft/nCellNums/30, 
											 255*iTimeLeft/nCellNums/30));
	HBRUSH hBrushMove = CreateSolidBrush(RGB(255-255*iMoveLeft/nCellNums/8, 
											 128*iMoveLeft/nCellNums/8, 
											 255*iMoveLeft/nCellNums/8));
	HBRUSH hBrushWhite = CreateSolidBrush(RGB(255, 255, 255));

	FillRect(hdc, &rcProcessBar, hBrushWhite);

	FillRect(hdc, &rcLeftTimeBar, hBrushTime);
	FillRect(hdc, &rcLeftMoveBar, hBrushMove);
	
	DeleteObject(hBrushWhite);
	DeleteObject(hBrushTime);
	DeleteObject(hBrushMove);
}

// 检查是否获胜,若获胜,则将空白块补齐
int CheckIsWin()
{
	for (int i = 0; i < nCellNums-1; i++)
	{
		if (nGameMap[i] != i)
			return 0;
	}

	bIsWin = TRUE;
	nGameMap[nCellNums-1] = nCellNums-1;
	return 1;
}

// 重绘序号为index的块
void RedrawRect(HWND hwnd, int index)
{
	RECT	rcCur;
	int		xPos, yPos;

	xPos = index % nVHnums;
	yPos = index / nVHnums;

	// 设置当前块RECT
	rcCur.left	= xPos*cxCell;
	rcCur.right	= rcCur.left + cxCell;
	rcCur.top	= yPos*cyCell;
	rcCur.bottom= rcCur.top + cyCell;

	// 重绘这个块
	InvalidateRect(hwnd, &rcCur, FALSE);
}


// 鼠标左键点击事件
void WMLbuttonDown(HWND hwnd, LPARAM lParam, WPARAM)
{
	int	xPos, yPos, index, nBlackSave;

	// 玩家已经获胜,则不响应消息
	if (bIsWin || bIsLose)
	{
		return;
	}

	if (LOWORD(lParam) > cxBitmap)
		return;

	// 获取鼠标点击的块的行、列
	xPos = LOWORD(lParam) / cxCell;
	yPos = HIWORD(lParam) / cyCell;

	index = xPos + yPos*nVHnums;
	if (nGameMap[index] == -1)	// 点中了空白块
		return;

	// 点中的块不在空白块的周围则返回,因为只有点中了与空白块相邻的块,才移动
	if (index != nBlack-1 && index != nBlack+1 && index != nBlack+nVHnums && index != nBlack-nVHnums)
		return;

	// 移动块
	Swap(index, nBlack);
	nBlackSave = nBlack;
	nBlack = index;

	// 重绘这两个块
	RedrawRect(hwnd, nBlackSave);
	RedrawRect(hwnd, nBlack);

	// 剩余移动次数减少一次
	iMoveLeft--;
	rcLeftMoveBar.right -= iMoveSegLen;
	if (rcLeftMoveBar.right < iMoveSegLen)
		rcLeftMoveBar.right = 0;
	InvalidateRect(hwnd, &rcProcessBar, FALSE);

	// 检查是否获胜,若获胜,则将空白块补齐
	CheckIsWin();
	// 若获胜,则显示消息框,提示已经获胜
	if (bIsWin)
	{
		MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
		return;
	}

	// 检查是否超过移动次数
	if (iMoveLeft == 0)
	{
		bIsLose = TRUE;
		MessageBox(hwnd, TEXT("不好意思,规定的移动次数到了!"), TEXT("拼图"), MB_ICONINFORMATION);
	}
}


void WMKeyDown(HWND hwnd, LPARAM lParam, WPARAM wParam)
{
	int	nBlackSave = nBlack;

	// 玩家已经获胜,则不响应消息
	if (bIsWin || bIsLose)
		return;

	switch (wParam)
	{
	case VK_LEFT:	MoveLeft();	break;
	case VK_RIGHT:	MoveRight();break;
	case VK_UP:		MoveUp();	break;
	case VK_DOWN:	MoveDown();	break;
	default:		break;
	}

	// 若发生了移动,重绘这两个块
	if (nBlackSave != nBlack)
	{
		RedrawRect(hwnd, nBlackSave);
		RedrawRect(hwnd, nBlack);

		// 剩余移动次数减少一次
		iMoveLeft--;
		rcLeftMoveBar.right -= iMoveSegLen;
		if (rcLeftMoveBar.right < iMoveSegLen)
			rcLeftMoveBar.right = 0;
		InvalidateRect(hwnd, &rcProcessBar, FALSE);

		// 检查是否获胜,若获胜,则将空白块补齐
		CheckIsWin();
		if (bIsWin)
		{
			MessageBox(hwnd, TEXT("恭喜你赢了"), TEXT("拼图"), MB_ICONINFORMATION);
			return;
		}

		// 检查是否超过移动次数
		if (iMoveLeft == 0)
		{
			bIsLose = TRUE;
			MessageBox(hwnd, TEXT("不好意思,规定的移动次数到了!"), TEXT("拼图"), MB_ICONINFORMATION);
		}
	}
}

void NewGame(HWND hwnd, LPARAM lParam, WPARAM wParam)
{
	// 获取位图宽、高
	cxBitmap = bitmap.bmWidth;
	cyBitmap = bitmap.bmHeight;

	// 每个块的宽、高
	cxCell = cxBitmap / nVHnums;
	cyCell = cyBitmap / nVHnums;

	// 设置窗口大小和位置
	SetWindowSizeAndPos(hwnd);

	// 初始化游戏
	InitGameMap();

	KillTimer(hwnd, ID_TIMER);
	SetTimer(hwnd, ID_TIMER, 100, NULL);
}

void WMCommand(HWND hwnd, LPARAM lParam, WPARAM wParam)
{
	HMENU hMenu = GetMenu(hwnd);
	RECT rcClient;
	GetClientRect(hwnd, &rcClient);

	switch (LOWORD(wParam))
	{
	case ID_NEWGAME:
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	case ID_EXITGAME:
		SendMessage(hwnd, WM_DESTROY, wParam, lParam);
		break;
	case ID_EASY3X3:
		nVHnums = 3;
		nCellNums = nVHnums*nVHnums;
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	case ID_MID4X4:
		nVHnums = 4;
		nCellNums = nVHnums*nVHnums;
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	case ID_HARD5X5:
		nVHnums = 5;
		nCellNums = nVHnums*nVHnums;
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;

	case ID_BAOCHAI:
		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	case ID_DAIYU:
		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP2));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	case ID_SHANDIAN:
		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP3));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);
		NewGame(hwnd, lParam, wParam);
		InvalidateRect(hwnd, &rcClient, FALSE);
		break;
	}
}


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static int			iTickCount;
	HDC					hdc, hdcMem;
	PAINTSTRUCT			ps;

	switch (message)
	{
	case WM_CREATE:
		hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
		nVHnums = VHNUMS;
		nCellNums = nVHnums*nVHnums;
		// 加载位图
		hBitmap = LoadBitmap(hInstance,  MAKEINTRESOURCE(IDB_BITMAP1));
		GetObject(hBitmap, sizeof(BITMAP), &bitmap);
		NewGame(hwnd, lParam, wParam);
		return 0;

	case WM_COMMAND:
		WMCommand(hwnd, lParam, wParam);
		return 0;

	case WM_LBUTTONDOWN:
		WMLbuttonDown(hwnd, lParam, wParam);
		return 0;

	case WM_KEYDOWN:
		WMKeyDown(hwnd, lParam, wParam);
		return 0;

	case WM_TIMER:
		if (bIsWin || bIsLose)
			return 0;

		// 剩余时间减一
		iTimeLeft--;
		rcLeftTimeBar.right -= iTimeSegLen;
		if (rcLeftTimeBar.right < iTimeSegLen)
			rcLeftTimeBar.right = 0;
		InvalidateRect(hwnd, &rcProcessBar, FALSE);

		if (iTimeLeft == 0)
		{
			bIsLose = TRUE;
			MessageBox(hwnd, TEXT("不好意思,时间到了!"), TEXT("拼图"), MB_ICONINFORMATION);
		}
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		hdcMem = CreateCompatibleDC(hdc);
		SelectObject(hdcMem, hBitmap);
		DrawGameMap(hdc, hdcMem);
		DeleteDC(hdcMem);
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		KillTimer(hwnd, ID_TIMER);
		DeleteObject(hBitmap);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

Windows游戏设计(一)- 拼图游戏 - 使用Win32 SDK


现在玩家能选择图像和难度了。

但:

当程序行数达到一定量时,有点乱了;

为了参数简单,定义了大量全局变量;

对Win32 程序该怎么组织还是有点迷惑;


但是,作为自己动手设计、实现的第一个Win32 SDK 程序,我已经尽力做好,暂时到此为止~~