拼图游戏开发

本文介绍了作者使用VC6.0开发的一款3×3拼图游戏,包括图形化菜单、音效和自定义图片等功能。文章讨论了如何判断拼图序列是否有解,提出当逆序数和最大数到空白格步数同为偶数或奇数时,拼图有解。并分享了编程实现的关键点,如使用位图函数BitBlt和StretchBlt处理图形,以及如何创建图形化菜单。最后,作者提及通过拦截消息处理键盘输入,提高游戏交互性。

(注)此文章是2年前写的。我一直贴在别的地方,那个地方文章全部删除后我将其搬到了这儿。

前天一时兴起,习惯的打开VC6.0,写了一个拼图游戏——虽然网上有很多可供下载的,但我还是决定自己写一个——因为大一就想写了,一直拖到现在。

        快要考试了,所以没有充裕的时间,但效果还算理想——有图形化菜单、自主选择图片、有音效、自主调节方块数、可调边框颜色……自我感觉还行。也遇到了一些技术、数学方面的难题,所以提出来,一来是对自己的总结,也希望对你有所裨益!大致界面如下,晒晒先:  

主界面

下面讨论技术问题。谁都知道,拼图游戏就相当与一个宫格图。为了简便起见,下面我一律用9宫格来说明——即3×3格式的拼图。所有人都知道:拼图最后的空白格永远在右下角。那么就相当于下面一个序列:

1  2  3
4  5  6
7  8  9
9所在的位置就是空白格。将此序列按从左到右、从上到下的顺序排列成一行:1 2 3 4 5 6 7 8 9。这下你大一学到的线性代数可派上用场了!!先祭出几个定理:
No1. 对于一个排列——就像a1,a2,a3…an,如果对于前面的一个数,它大于后面的数,就称为逆序,所有的逆序总数就称为逆序数。
No2. 如果一个排列的逆序数为偶(包括0),则称其为偶排列,否则称之为奇排列。
No3. 将任意两个数交换称之为对换。对换改变排列的奇偶性。
No4. 奇排列变成自然排列(由小到大排列)的对换次数为奇数,偶排列变成自然排列的对换次数为偶数。

看似有些复杂,其实非常不然。举个例子先:对于一个9宫格排列:
1  2  3  4  7  8  9  6  5
其各个性质怎样呢?看一下:其逆序数为7——显然是奇数。那么这个排列是奇排列。将2和7对换,得到
1  7  3  4  2  8  9  6  5
那么新序列的逆序数为12——为偶数,验证了定理3。将其写成拼图样式:
1  7  3
4  2  8
9  6  5
      设最大的9是其中的空白的格子。自然排列的逆序数为0,显然是偶排列。而上面这个拼图矩阵做成排列是偶排列,可以看出,将9走到最终要到达的位置(当前5所在的地方),最少需要走2(偶数步)步:9-6-5。再多一点呢?比如:9-4-2-8-5,有4步——仍然是偶数步!!结合定理4,可知,这个排列到自然排列有解——也就是说这个拼图序列可以拼完。

但对于下面的排列:
1  2  3
4  5  6
8  7  9
可知其逆序数为1,为奇排列。结合定理4:它到自然排列的步数应为奇数步(①)。

9到空白格(当前9的位置)的最小步数为0,再多一点呢?比如:9-6-5-7-9,为4步。可见这个排列将9放到空白格的步数为偶数——与①处相矛盾!!所以,我们得出这样一个结论:此拼图序列是无解的——也就是永远拼不成!!

从上面我们可以推出我们自己的结论:
推论:对于一个拼图序列,如果其逆序数(D)和最大数到空白格之间的步数(M)均为偶数(或奇数),则此拼图是有解的,否则拼图永远拼不成。
用编程语言描述:

if(D % 2 == M % 2)
{
	……        //有解.
}
else
{
	……        //无解.
}
一个拼图的逆序数显然是好计算的,比如用C++函数描述出来如下:
//
// 其中PArray是指向排列数组的指针.
// ElementCount是排列的元素总数.
//
int GetReverseCount(int* pArray, int ElementCount)
{
	//用来计算逆序数.
	int Reverse_Order = 0;

	for(int i = 0; i != ElementCount - 1; ++i)
	{
		for(int j = i+1; j != ElementCount; ++j)
		{
			if(pPanes[i] > pPanes[j])
			{
				++Reverse_Order;
			}
		}
	}

	return Reverse_Order;
}

那么如何得到排列中最大数到空白格的步数是偶数还是奇数呢?我们知道,不管怎样变换,这个步数永远是一个奇数或偶数。用最大数的当前位置与空白格之间的横向、竖向之差的和来代替。用一个函数:

//
// 若为偶数,则返回TRUE,否则返回FALSE.
// 显然排列中最大的数为ElementCount,如果你习惯从0
// 开始计数,那么这个值为ElementCount-1.
//
bool Get_Parity(int* pArray, int ElementCount)
{
	for(int i=0; i!=sqrt(ElementCount); ++i)
	{
		for(int j=0; j!= sqrt(ElementCount); ++j)
		{
			if(pArray[i*sqrt(ElementCount)+j] == ElementCount)
			{
				if((ElementCount-1 - i) + (ElementCount-1 – j)) % 2 == 0)
				{
					return true;
				}
				else
				{
					return false;
				}
		}
	}

	return false;
}
在这个函数中,我们假设拼图是N×N格式的,所以用了sqrt(ElementCount);如果你将矩阵格式设置成二维数组的话,那么更加简单了!你可以自己实现一下。
那么判断的时候,比如我们用九宫格拼图,这样:
if((GetReverseCount(pArray, 9) % 2 == 0 && Get_Parity(pArray, 9)) ||
	(GetReverseCount(pArray, 9) % 2 != 0 && !Get_Parity(pArray, 9))
{
	…………    //有解.
}
else
{
	…………    //无解.
}
不是很复杂,是不?好了,拼图有无解的问题讨论完了,那么该怎样生成一个随即乱序的初始拼图呢?有两种办法:

1>:①随机填充每个格子
        ②然后判断是否有解,如果有解,下一步。否则,转到①。
2>:  按自然序列将每个格子填充,然后按一定的步数打乱——即让空白格逆序走一遍。
基本的编程方法如下:
虽然用二维数组来存放格子方便许多。但这儿仍然用九宫格一维数组(1-9)来示例。

第一种解决方案代码:

int pArray[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
do
{
	for(int i=0; i!=9; ++i)
	{
		int x = rand() % 9;
		while(pArray[x] != 0)
		{
			x = rand() % 9;
		}

		pArray[x] = i + 1;
	}
} while(!((GetReverseCount(pArray, 9) % 2 == 0 && Get_Parity(pArray, 9)) ||
	((GetReverseCount(pArray, 9) % 2 != 0 && !Get_Parity(pArray, 9))));
这样做很复杂,可以考虑用第二种方案。实际上第二种比第一种简单不少!
第二种解决方案:

// 先按自然排列填充.
int pArray[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 这儿你可以循环比200此多或者少,只要达到效果即可.
for(int i=0; i!=200; ++i)
{
	int x = rand() % 4;  // 空白格随机移动的方向.
	switch(x)
	{
	case 0:
		…………         // 空白方格左移.
		break;
	case 1:
		…………         // 空白方格右移.
		break;
	case 2:
		…………         // 空白方格上移.
		break;
	case 3:
		…………         // 空白方格下移.
		break;
	default:
		VERIFY(FALSE);
		
	}
}
具体实现移动的方法就是交换两数:
void Swap(int &x, int &y)
{
	int SwapValue = x;
	x = y;
	y = x;
}
这样既不需判断拼图是否有解,也不许执行求逆序的函数。只需这两步就初始化了整个拼图序列。很高效,也很有用,是不?
       拼图有无解、如何初始化以及线性代数的方面讨论完了,也就差不多了。下面总结一下在MFC中一些特殊技巧。谁都知道,拼图拼图,必须有图片。要把图片贴到对话框上,很简单。可以用BitBlt,也可以用StretchBlt。这两个函数略有不同,但贴出来的效果大相径庭!

        BitBlt,顾名思义,位对位传送像素数据——也就是你的图片有多大,它贴出来就有多大——当然对于很大的图片你得有足够的区域才能看的完整。而StretchBlt函数,也就是可以将图片贴为任意大小的区域。这样,贴到对话框上的图片有可能很难看。但这儿有一个函数:

SetStretchBltMode(hDC, STRETCH_HALFTONE);

在StretchBlt前加上这句,可以将图片很平滑的贴到任意大小的区域——不说有多好看吧,但至少不至于多么难看。有用的信息是,如果你要求平滑、好看的贴图效果,并且非要用StretchBlt函数不可,那么在StretchBlt调用前加上这句代码是没错的!!
还有图形化菜单,就像这样:
 图形菜单
可以大大提高界面的亲和度。实现起来也不是很困难。用ModifyMenu实现。在OnCreate函数中实现,也就是响应WM_CREATE消息的地方。

// 得到要设置的子菜单指针.
CMenu* pMainMenu = GetMenu()->GetSubMenu(1);
char *hBitmap = (char *)LoadBitmap(
			AfxGetInstanceHandle(),
			MAKEINTRESOURCE(IDB_MENU_PICTURE)
			);
::ModifyMenu(
	pMainMenu->m_hMenu,
	IDC_LOAD_PIC,
	MF_BYCOMMAND | MF_BITMAP,
	IDC_LOAD_PIC,
	hBitmap
	);
IDB_MENU_PICTURE就是你要往菜单上贴的那张图片资源的ID号,IDC_LOAD_PIC是所对应菜单项的ID号。就是如此!!
还有,在MFC对话框中,你的WM_KEYDOWN等消息,如果你想让对话框接受到——而不是子控件。单纯的添加OnKeyDown消息处理函数是不行的。需要在PreTranslateMessage函数中做。像这样:
OOL CPuzzleDlg::PreTranslateMessage(MSG* pMsg)
{
	// 当然,作为一个好玩的游戏,我支持了键盘操作.
	// 用四个方向键来控制游戏.
	if(pMsg->message == WM_KEYDOWN)
	{
		// wParam就是当前按键的虚拟码.
		switch(pMsg->wParam)
		{
		case VK_UP:
			…………
			break;

		case VK_DOWN:
			…………
			break;

		case VK_LEFT:
			…………
			break;

		case VK_RIGHT:
			…………
			break;

		default:
			break;
		}
	}
}

如果你足够有主见,你也可以拦截你感兴趣的消息。比如WM_KEYUP、WM_CHAR等等。这就是我在拼图游戏的开发中的所得,我已和盘托出了。但愿对你有所启发。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值