第五章 简单扫雷游戏程序
wxWidgets开发简单扫雷游戏程序之程序分析
5.2程序分析
本程序主要部分集中在minesweepingFrame类中。下面我先大致的罗列一下。
主要的功能函数有:
void Init(); //游戏场景数据初始化
void DrawGrid(); //绘制游戏场景
void CreateMinesTips(int pos); //生成个数提示数据
int HitTest(wxPoint &point); //鼠标事件区域检测
void HitCheck(int pos); //鼠标点击单元格时是否是地雷的判定
void FindSpaceNearby(int pos); //找出相邻的安全空白区域
void ShowTipsAround(int pos); //鼠标右击提示单元时显示周围可能的雷区
bool EdgeCheck(int pos,int newpos);//场景边缘检测
void OpenAll(); //打开并显示所有单元
另外还有两个给游戏设置对话框的接口函数,以内联的方式写在了头文件里:
void SetLevel(int level=0); //设置游戏等级
int GetLevel(); //获取当前游戏等级
最后是事件处理函数:
void OnQuit(wxCommandEvent& event); //退出菜单事件
void OnAbout(wxCommandEvent& event); //关于菜单事件
void OnMenuStartSelected(wxCommandEvent& event); //开始菜单事件
void OnPaint(wxPaintEvent& event); //窗口重绘事件
void OnLeftDown(wxMouseEvent& event); //鼠标左击按下事件
void OnRightDown(wxMouseEvent& event); //鼠标右击按下事件
void OnRightUp(wxMouseEvent& event); //鼠标右击松开事件
void OnMenuSettintsSelected(wxCommandEvent& event); //设置菜单选中事件
这些函数的定义应该很好理解,就不做过多说明了。下面我把里面的一部分算法拿出来,给大家大致的说明一下。
5.2.1 游戏对象数据结构
首先要介绍的是这个工程里的数据结构。我们要做一个游戏,其中会包含各种各样的元素,比如说场景、人物、动作、关系等等,要完成这样的功能,最好的架构就以面向对象的思想,把它们都以类或者结构体的方式一一实现。我们这个工程虽小,虽然可以灵活一点,但同样离不开这样的思想,因为这样做可以让程序能更好的理解和维护。
在扫雷这个游戏里,最复杂重要的对象莫过于场景中的单元格了,所以就定义了一个游戏场景单元格结构体:
struct Mine{
int flag; //>=10:mines,0<flag<10:mine number around,0:space
int statu; //0:closed,1:opened,2:sweeped
bool tips;
};
这个结构体十分简单,仅仅三个属性,通过注释我们就可以很明白的理解了:
1) flag——用来标识单元格里的内容
Flag>=10时,内容为地雷
0<Flag<10时,内容为地雷旁边的个数提示(因为周围最大地雷数为8,所以定此范围)
Flag=0时,内容为空白(里面啥都没有)
2)statu——用来标识单元格的三种状态
Statu=0时,单元格没有被点开的状态
Statu=1时,单元格被点开的状态
Statu=2时,单元格没被点开时被右击标记为地雷的状态
3)tips——用来记录周围的地雷个数
在定义好这样的结构体后,我们在程序的初始过程中,生成一个结构体数组,就能完全实现整个游戏场景,在整个游戏过程中,我们只要好好维护好这个结构体数组,就能很好的实现游戏的各个功能。
5.2.2 通过WX_DEFINE_ARRAY实现结构体数组
在wxWidgets里,一般通过WX_DEFINE_ARRAY宏来定义对象容器类型,具体写法如下:
WX_DEFINE_ARRAY(对象类型或指针,容器类型名);
WX_DEFINE_ARRAY(Mine *, ArrayOfMines);
在定义完容器类型后,我们就可以通过容器类型来定义实际的对象数组。
容器类型名对象数组名
ArrayOfMines m_arrMines;
定义完对象数组,我们可以先通过Add方法追加,然后通过Item方法对容器里的元素进行读取和更新,还可以通过Remove方法进行删除等等,其操作方法是相当灵活的。
5.2.3 通过wxClientDC进行绘图
在Init方法生成游戏的数据后,最重要的一步是把数据用UI的方式呈现出来。在这个程序中,我们是通过绘图的方式来完成这一个重要功能的。
首先,我们要知道这个wxClientDC,如果你学过MFC就应该知道,它是个设备句柄,并且是客户区设备句柄,通俗一点说,就是我们要绘图的一块黑板。它有个构造函数,里面的参数是wxWindow及其子对象,
1)比如说wxClientDC(this),就是把当前窗体wxFrame或wxDialog的客户区作为整个绘图区域;
2)又比如说我们的程序里有一个wxPanel对象panel1,wxClientDC(panel1)就意味着只在这个panel1中进行绘图;
那这两者有没有什么区别呢,那当然有!wxClientDC既然是在客户区进行绘图,那么wxFrame和wxDialog的客户区只是除标题栏和菜单栏的内容区域,而wxPanel的客户区包括它的全部。
在我们不断深入学习后我们会发现,我们如果想在客户区以外的地方进行绘图,比如说想重绘窗口的标题栏等等,我们可以通过更底层的wxDC基类来实现的。
5.2.4 HitTest鼠标事件坐标检测
在做重绘UI类程序的时候,我们常常要做这样的工作:如何判断鼠标点在哪个单元里。
这一节我们先不要研究我们的实际代码,我打算通过一个更简单的例子来详细的介绍这一点。
首先,假设我们在程序界面上重绘了单元格,它是一个正方形,它的起始坐标和宽高我们是可以知道的,为了方便说明,这里假设它的起始点坐标是(x,y),宽高分别是w、h,下面我们就开始判断鼠标是否点在了这个单元格里。
单元格示意图
在所有鼠标事件中,我们可以通过wxMouseEvent的GetPosition方法轻松获取到事件坐标,这里假设获取到的坐标是(x0,y0),我们把图一画就很清楚的看到,我们只要判断x0是不是大于x且小于x+w,并且y0是不是大于y且小于y+h,就能判断出(x0,y0)是不是在单元格内了。
写成伪代码如下:
START FUNCTION
IF x0>x AND x0<(x+w) THEN
IF y0>y AND y0<(y+h) THEN
RETURN true;
END IF
END IF
RETURN false;
END FUNCTION
大家现在回过头去看看程序里的HitTest函数的实际代码,应该很容易明白了吧?
※思考:程序里的代码和伪代码逻辑上有一点小差别,你发现了吗,想一想为什么呢?
※5.2.5 相邻空白单元寻找算法
这一节要介绍的功能大家应该都很明白,就是要实现点开非地雷单元时,自动打开一些空白的单元。实现这个功能无非是对当前点击单元的周围八个单元进行遍历,如果发现有未点开的空白单元的,继续去基于这个空白单元对周围八个单元进行遍历,依次循环,直到找不到未点开的空白单元为止。虽然算法简单,但里有一个要点,就是在寻找时要检测是否已经到达场景的边缘,因为要防止点到边缘时,另一边缘的单元会被寻找并被点开掉。
这个用图示的方式表达可能大家会更明白一点,比如说下面这个场景(图1)的所有单元都是未打开的状态,然后玩家点了一下标红色方框的那个单元,如果没有边缘检测,会出现图2的这种情况,而实际我们要达到的应该是图3的结果。
我们要防止出现图2的情况,就必须在寻找空白单元的时候,首先判断一下当前单元是不是已经在边缘了,如果在边缘了,我们要防止再往另外一边寻找。而我们的单元是存放在一个连续的一维数组里的,这就要求我们算出另外一边的单元的下标值。
这里把实际代码贴出来,里面还用到了递归,我加了适当的理解注释进去,大家可以自行根据我的思路理解一下,如还有不懂的可以留言提问。
//参数pos即当前单元在数组里的下标值
void minesweepingFrame::FindSpaceNearby(int pos)
{
if(!m_bGameStatu) return; //判断是否正在游戏状态
if(pos<0 || pos>m_nRectSize) return;
int newpos[4]={pos-m_nRectUnit,pos-1,pos+1,pos+m_nRectUnit};
for(int i=0;i<4;i++)
{
if(newpos[i]>=0 && newpos[i]<m_nRectSize)
{
//when in the edge
if(EdgeCheck(pos,newpos[i])) continue;
if(m_arrMines.Item(newpos[i])->flag==0 && m_arrMines.Item(newpos[i])->statu==0)
{
m_arrMines.Item(newpos[i])->statu=1;
FindSpaceNearby(newpos[i]);
}
//show simple 1
if(m_arrMines.Item(newpos[i])->flag==1 && m_arrMines.Item(newpos[i])->statu==0)
{
m_arrMines.Item(newpos[i])->statu=1;
}
}
}
}
bool minesweepingFrame::EdgeCheck(int pos,int newpos)
{
//m_nRectUnit可以理解为游戏场景中每一行的单元格数量
if(pos%m_nRectUnit==0) //left edge
{
if(newpos==pos-(m_nRectUnit+1) || newpos==pos-1 || newpos==pos+(m_nRectUnit-1)) return true;
}
else if((pos+1)%m_nRectUnit==0) //right edge
{
if(newpos==pos-(m_nRectUnit-1) || newpos==pos+1 || newpos==pos+(m_nRectUnit+1)) return true;
}
if(pos-m_nRectUnit<0) //up edge
{
if(newpos==pos-(m_nRectUnit+1) || newpos==pos-m_nRectUnit || newpos==pos-(m_nRectUnit-1)) return true;
}
if(pos+m_nRectUnit>=m_nRectSize) //down edge
{
if(newpos==pos+(m_nRectUnit+1) || newpos==pos+m_nRectUnit || newpos==pos+(m_nRectUnit-1)) return true;
}
return false;
}
这个程序里还有一些知识点,如wxFrame和菜单的使用等,因为太简单这里就不再介绍了,就留给大家直接在源代码里学习吧。
除特别声明为转载内容外,本站所有内容均为作者原创,谢绝任何单位和个人不经许可的复制和转播!
对于确有转载需要的,请先与作者联系,在获得允许后烦请在转载时保留文章出处。
本文出自Lupin's Blog:http://www.cnzui.com/archives/1060
最近工作有些忙,来不及写,更新会比较慢,还请大家多多包涵!