程序框架流程
Win32 SDK C++编写
玩了这么多年的俄罗斯方块,自己也来写一个,为了不太单调,加入了一些Dota的元素和连连看功能呵呵,可真当自己写的时候才发现创造者当时的巧妙的构思,俄罗斯方块中每个方块都是由四个小方块组成,但每个块的形状,变换又不相同,故利用C++继承已经多态来实现比较恰当。
整个程序由一个背景窗口,一个玩家窗口,一个AI窗口和2个分数窗口组成。
俄罗斯方块,顾名思义,即为方块组成,将整个窗口分为若干相同的小方块,采用全局数组来存储窗口中每个块的信息,掉落的方块均为4个小方块,下文中所说的掉落块即为游戏中的7种掉落方块,小块即为整个窗口中的每个小块,定义一个Block类来当做基类,包含4个Cpoint结构用于存放当前掉落方块在整个窗口的位置,另定义各种虚函数,如SwitchBlock来变换掉落块,MoveBlock来移动掉落块,FillBlock来将当前掉落块的信息填充到屏幕,CleanBlock、CleanSame等等。
从Block基类派生出7种方块,然后重载SwitchBlock和AI功能函数实现每个掉落方块的不同功能
通过重载AI函数来实现不同掉落块的AI功能,在Block中加入m_hwnd来存储当前掉落方块属于的窗口句柄,加入全局数组的引用来表示该掉落块属于那个窗口的数组。
为了和玩家窗口独立,采用多线程的方式,将AI功能窗口创建在另一个线程的消息循环中。
程序大体流程如下
玩家窗口通过相应键盘的上下左右按键消息来调用相应的MoveBlock函数来移动掉落快,再MoveBlock中首先通过Check函数来判断该块的位置是否能移动,是否有满行,是否有相同连续的图案需要清除。如需要清除则调用Clean,CleanSame函数来清除相应的块。在Clean,CleanSame中调用Fall函数来实现悬空块的掉落。
图案背景的实现
程序通过全局数组来保存整个窗口的每个小块信息。将整个窗口的背景分成若干小块,每个小块对应数组中的一个元素,数组中存放着每个小块位图的ID,而每个掉落块Block中的Cpoint存放的则是掉落块在数组中的索引,m_BitmapIndex[4]程序存放4个小块的相应的位图ID。FillBlock函数即实现m_BitmapIndex[4]中的ID写到全局数组中,CleanBlock即将掉落块对应位置的数组元素改回成相应背景图片的ID,ShowBlock函数则通过数组中的位图ID,显示出整个窗口的图画。
方块的移动实现和变换
方块的移动通过MoveBlock函数实现。MoveBlock函数首先判断当前掉落块是否可以移动,如是否已到达边界,是否已经到达底部,然后判断当前整个窗口中是否存在满行或者连续的相同图案(连连功能),若存在则清除,然后让悬空的小块下落到相应的位置,最后调用ShowBlock函数刷新画面。若掉落块可移动则调用CleanBlock函数将该掉落块当前的全局数组中的位置的位图ID还原成背景的位图ID,然后4个CPoint成员中的的索引位置往相应的方向变动,再调用FillBlock函数填充当前位置的数组元素,使得该位置的位图变成该方块的图案,最后再调用ShowBlock函数刷新整个窗口。
方块的变换通过虚函数SwitchBlock实现。从Block基类派生出7个掉落块的派生类,重写SwitchBlock虚函数。块的变换的工程和块的移动相似,不同之处在于变换块时改变4个Cpoint的方式。
清除满行块和连连看功能
在MoveBlock中,通过Check函数判断当前块,如果为下移,则遍历当前数组,看是否存在满行的块,即该行所有的元素都不为背景图片ID,若为满行块则调用Clean函数清除,即将数组的该行元素全部改回相应的背景图片ID。然后遍历整个窗口,通过链表寻找每个小块周围的小块,看是否存在2个或者2个以上的相同图案的小块,若存在则清除,清除过程和清除满行块类似。清除完成后会有悬空的小块,调用Fall函数来实现小块的下落,因为下落后可能还会存在满行的块或者图案相同的小块,故采用循环的方式来实现清除块的功能
相应的代码如下
do { if (BFull=Clean()) Sound(KillKeep+=BFull); if (BSame=CleanSame()) Sound(KillKeep+=BSame); } while (BSame||BFull);//当没有满行块和连续的相同图案块时才结束循环
在清除满行块或者连续的相同图案的块后,会有小块悬空,这些小块需要下落,Fall函数即在Clean和CleanSame函数后实现下落功能,Fall函数遍历整个数组,如果数组元素不为背景图片ID即为小块,然后通过定则判断该小块是否悬空,定则如下:
1.一个小块如果左边右边下边都没有别的块,说明该小块悬空
2.一个小块如果左边右边有别的块,但是该块下面一行全部都是空的,说明该小块所在的行悬空
相关代码如下:
for (int N=Length-2;N>=0;N--) for (int M=0;M!=Width;M++) if (st_AllState[M][N]&&!st_AllState[M][N+1]) //不为空的且下面没有块才判断 { BFall=false; Blank_LfRt=false; if (!M||M==Width-1) { if (!M&&!st_AllState[M+1][N]||M==Width-1&&!st_AllState[M-1][N]) Blank_LfRt=true; } else if (!st_AllState[M-1][N]&&!st_AllState[M+1][N]) Blank_LfRt=true; CPoint *SP=NULL; m_hwnd==MainHwnd?SP=PlayerSame:SP=AISame; if (Blank_LfRt) //左右为空,直接下落 BFall=true; else //左右不空,判断是否有地基 { BFoundation=false; for (int l=M;l>=0;l--) //判断左地基 { if (!st_AllState[l][N]) break; if (st_AllState[l][N+1]) BFoundation=true; } for (int r=M;r<=Width-1;r++) //判断右地基 { if (!st_AllState[r][N]) break; if (st_AllState[r][N+1]) BFoundation=true; } if (!BFoundation) BFall=true; } for (int index=0;index<=SameIndex;index++) { if (M==SP[index].x&&N<=SP[index].y) BFall=true; } }
相关代码如下:
if (BFall) { Repeat=true; st_AllState[M][N+1]=st_AllState[M][N]; st_AllState[M][N]=0; FallBlinkStruct *FBall=new FallBlinkStruct(); if (!FBall) MessageBox(0,"内存不足",0,0); FBall->hwnd=m_hwnd; FBall->st_back=st_BackGround; FBall->CurP.x=M; FBall->CurP.y=N; CreateThread(NULL,NULL,FallBlink,(void*)FBall,NULL,NULL); } } if (Repeat) Fall(); }
定义一个全局的Block*指针PBlock,因为所有的掉落块都是派生自Block,故PBlock可以指向任何掉落块。在程序启动的时候使用new Block分配一个掉落块,构造函数中定义了该块的初始位置,属于的窗口,全局数组的引用,背景图片数组的引用,是7个方块中的哪一种掉落块和4个小块的图案都则是随机生成,程序启动时同时分配一个定时器来自动实现方块的下落。玩家可以通过 向下的按键实现快速的下落。当该掉落块不能下移的时候,MoveBlock通过Check得知不可下移会使用
delete Pblock,将该块从内存中释放掉,然后使用相同的方法new一个新块,并将PBlock指向它,就这样一直循环。
AI功能的实现
AI实现人机对战,在程序运行时候,为程序另外创建一个线程,在该线程中也创建一个窗口并且用一个全局数组来记录整个窗口的信息,同样的分配一个新的背景图片ID数组。AI的线程和玩家主线程的实现基本一样,只是AI线程中没有捕获键盘按键和设置定时器,而是通过Block中的AI函数算出该块的移动路径,只是把玩家通过判断后使用键盘操作来移动块的方法换成了自动计算出路径。
AI函数是虚函数,在从Block基类中派生出的7个子类中进行重写。AI函数通过遍历整个数组来计算出当前块的移动路径,具体实现如下
从上到下竖行的遍历整个数组,从而去的每一竖行中最底端的块的位置,然后通过堆排序选出最底的块的位置,根据当前下落块的各种性质(各种变换后的形态),判断该块是否可到达最底的块的位置上方,如果可以则根据计算的路径移动块到该位置,然后delete PBlock并且new一个新的下落块,如果不行则将该元素从堆中移除,重新平衡堆,再次取出最大的元素,如此往复,知道算出路径。
计分功能的实现
玩家和AI各有5个小窗口用于显示各自的分数。在Clean或CleanSame后会加上相应的分数,再各自的分数窗口中显示出来。
每个窗口显示一位数字,每次调用AddScore函数后,为了不影响主程序的执行效率,创建新一个新的线程来实现分数窗口的刷新,并为新的线程传入加的分数作为参数。为了不让加分变得僵硬,采用累加一次刷新一次,而不是一次全加后刷新,使用随机函数来确定每次加的分值。相关代码如下:
while (i<Add) { n=rand()%100; if (n>Add-i) n=Add-i; i+=n; if (Add%10==1) { Score+=n; for (int n=0;n!=5;n++) SendMessage(hPlayerScore[n],WM_PAINT,NULL,NULL); } else { AIScore+=n; for (int n=5;n!=10;n++) SendMessage(hPlayerScore[n],WM_PAINT,NULL,NULL); } }
BakClor=GetPixel(hDC,0,0); //背景像素 WndRgn=CreateRectRgn(0,0,0,0); //创建一个空区域 for (Y=0;Y<56;Y++) { int MX=0; do { int LX=MX; while (LX<50 && BakClor==GetPixel(hDC,LX,Y)) LX++; MX=LX; while (LX<50 && BakClor!=GetPixel(hDC,LX,Y)) LX++; HRGN TmpRgn=CreateRectRgn(MX,Y,LX,Y+1); //确定该像素不为背景像素 CombineRgn(WndRgn,WndRgn,TmpRgn,RGN_OR);//将该像素选入区域中 DeleteObject(TmpRgn); MX=LX; }while (MX<50); } SetWindowRgn(hwnd,WndRgn,TRUE);