wxWidgets开发简单扫雷游戏程序之程序分析-《跟我学wxWidgets开发》系列教程

第五章 简单扫雷游戏程序
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