学习FLTK图形库,拿扫雷游戏做例子

[0]起因

花了一个星期左右的时间学习了下FLTK图形库,拿扫雷游戏作为学习FLTK图形库的原因有二:

  1. 扫雷游戏算法很简单;
  2. 之前写过一个Win32API的扫雷游戏(自己回头去看看,感觉以前的按过程式的方法设计的代码不便于理解,结构不清晰)

 

[1]设计一个可以载入图片的GameObject类

 

[1.1]类的声明如下:

class GameObject
{
public:
    GameObject(void);
    GameObject(const char* filename);
    GameObject(const GameObject& a_rightSide);
    GameObject& operator=(const GameObject& a_rightSide);
    ~GameObject(void) {}//没有需要删除的

    void setImage(const char* filename);

    float getX(void)const {return m_pBox->x();}
    float getY(void)const {return m_pBox->y();}
    float getWidth(void)const {return m_pBox->w();}
    float getHeight(void)const {return m_pBox->h();}
    void setPosition(float a_x, float a_y);
    void setSize(float a_width, float a_height);

private:
    auto_ptr<Fl_Box> m_pBox;//图片框
    auto_ptr<Fl_Image> m_pImage;//图片
    Fl_Shared_Image* m_pSharedImage;//共享区图片
};

[1.2]类的定义

这里使用了组合的方式,将Fl_Box,Fl_Image组合在一起(使用了智能指针方便管理内存)。而且自己定义了拷贝构造函数和赋值运算符,实现自定义的赋值操作(主要是方便存放在vector里面)。方法的定义如下:

inline GameObject::GameObject(void)
:m_pSharedImage(NULL)
{
    m_pBox.reset(new Fl_Box(0,0,0,0));
}
inline GameObject::GameObject(const char* filename)
:m_pSharedImage(NULL)
{
    m_pBox.reset(new Fl_Box(0,0,0,0,0));
    setImage(filename);//设置图片
}
inline GameObject::GameObject(const GameObject& a_rightSide)
{
    m_pBox.reset(new Fl_Box(a_rightSide.getX(),a_rightSide.getY(),
        a_rightSide.getWidth(),a_rightSide.getHeight()));//构建一个新的大小位置相同的box
    m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//获取相同的sharedImage
    m_pImage.reset(m_pSharedImage->copy(a_rightSide.getWidth(),a_rightSide.getHeight()));//构建一幅大小相同的Image
    m_pBox->image(m_pImage.get());
}
inline GameObject& GameObject::operator=(const GameObject& a_rightSide)
{
    m_pBox->resize(a_rightSide.getX(),a_rightSide.getY(),
        a_rightSide.getWidth(),a_rightSide.getHeight());
    m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());
    m_pImage.reset(m_pSharedImage->copy(a_rightSide.getWidth(),a_rightSide.getHeight()));//构建一幅大小相同的Image
    m_pBox->image(m_pImage.get());
    return *this;
}

//设置图片
inline void GameObject::setImage(const char *filename)
{
    fl_register_images();
    m_pSharedImage = Fl_Shared_Image::get(filename);
    m_pImage.reset(m_pSharedImage->copy());
    m_pBox->image(m_pImage.get());
}

//设定位置和大小
inline void GameObject::setPosition(float a_x, float a_y)
{
    m_pBox->position(a_x,a_y);
}
inline void GameObject::setSize(float a_width, float a_height)
{
    m_pBox->size(a_width,a_height);
    m_pImage.reset(m_pSharedImage->copy(a_width,a_height));
    m_pBox->image(m_pImage.get());//重新修改image,因为之前的image会自动销毁
}

这样实现对于做控件的背景图片还是可以的,但是有个重要的缺点是响应事件的问题。如果不是继承含有handle()方法的类实现事件响应必须使用回调函数callback()(这只是我这几天看FLTK1.3.0帮助文档发现的,或许还有更好的方法我没有发现)。所以有了下面的修改,就可以重写父类的handle()方法来实现自定义的事件响应。

[1.3]使用到的FLTK类

Fl_Shared_Image

Fl_Image

Fl_Box

[2]改进的图片框ImageBox类

[2.1]类的实现

//用于放置图片的图片框
class ImageBox:public Fl_Box
{
public:
    ImageBox(void):Fl_Box(0,0,0,0,0) {}
    ImageBox(const char* filename):Fl_Box(0,0,0,0,0)
    {
        setImage(filename);
    }
    ImageBox(const ImageBox& a_rightSide)
    :Fl_Box(a_rightSide.getX(),a_rightSide.getY(),a_rightSide.getWidth(),a_rightSide.getHeight(),0)
    {
        m_x = a_rightSide.getX();
        m_y = a_rightSide.getY();
        m_width = a_rightSide.getWidth();
        m_height = a_rightSide.getHeight();
        m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//获取相同的sharedImage
        m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//构建一幅大小相同的Image
        image(m_pImage.get());
        redraw();
    }
    ImageBox& operator=(const ImageBox& a_rightSide)
    {
        m_x = a_rightSide.getX();
        m_y = a_rightSide.getY();
        m_width = a_rightSide.getWidth();
        m_height = a_rightSide.getHeight();
        resize(m_x,m_y,m_width,m_height);
        m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//获取相同的sharedImage
        m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//构建一幅大小相同的Image
        image(m_pImage.get());
        redraw();
        return *this;
    }

    //设置图片
    void setImage(const char *filename)
    {
        fl_register_images();
        m_pSharedImage = Fl_Shared_Image::get(filename);
        m_pImage.reset(m_pSharedImage->copy());
        image(m_pImage.get());
        redraw();
    }

    int getX(void)const {return x();}
    int getY(void)const {return y();}
    int getWidth(void)const {return w();}
    int getHeight(void)const {return h();}
    void setPosition(int a_x, int a_y) {position(a_x,a_y);}
    void setSize(int a_width, int a_height)
    {
        m_width = a_width;
        m_height = a_height;
        size(m_width,m_height);
        m_pImage.reset(m_pSharedImage->copy(m_width,m_height));
        image(m_pImage.get());//重新修改image,因为之前的image会自动销毁
        redraw();
    }

    //Event处理
    int handle(int event)
    {
        if (!m_enable) return (0);//点开数字后失去操作权
        using std::cout;
        using std::endl;
        switch (event)
        {
        case FL_PUSH:
            switch (Fl::event_button())
            {
            case FL_LEFT_MOUSE:
                cout << "你点击了鼠标左键" << endl;
                cout << "m_width=" << m_width << endl;
                m_pSharedImage = Fl_Shared_Image::get("res/images/bg.jpg");
                m_pImage.reset(m_pSharedImage->copy(m_width,m_height));
                image(m_pImage.get());//重新修改image,因为之前的image会自动销毁
                redraw();
                break;
            }
            break;
        }
        return 0;
    }

private:
    auto_ptr<Fl_Image> m_pImage;//图片
    Fl_Shared_Image* m_pSharedImage;//共享区图片
    int m_x,m_y;
    int m_width,m_height;
    bool m_enable;//是否可以操作(当点开数字后就设为false)
};

这里包含了类的定义。在这里需要提到一个重要的问题,每当更换image后需要调用redraw()函数(第一个GameObject类没有使用,所以它更换图片的时候不会自动刷新的)。

[2.2]这种实现方式的优点

  • 可以很方便的处理事件;
  • 继承FLTK的Fl_Box类,结构更清晰。
  • 注:c++primer里有句经典的话,“拷贝构造函数、重载赋值运算符、析构函数三个中只要定义了其中一个就需要定义其他两个”。在这里由于使用了智能指针auto_ptr的缘故,并不需要自己定义析构函数。

[2.3]handl()函数

使用handle()函数时,里面调用方法的过程大概是这样的(扫雷游戏的基本鼠标操作):

  • 单击左键事件处理(完成点开格子的过程,调用扫雷算法的方法,完成image的修改,并置这些图片为不可再响应事件);
  • 单击右键事件处理(该过程只是改变image状态,右键第一次后改为红旗,右键第二次改为问号,右键第三次改为原样(前两个状态不能响应左键事件))。

[2.4]测试函数

下面是两个测试函数:

void Test::test_copy_construdtor(GameObject& t1, GameObject& t2)
{
    //测试拷贝构造函数
    t2.setSize(800,500);
    t1 = t2;
    t2.setSize(300,400);
//    t1.setSize(800,300);
/*    t2 = t1;
    t1.setSize(700,500);
    t1 = t2;
    t2.setSize(700,400);
    */
}

void Test::test_ImageBox(ImageBox& t)
{
    t.setSize(250,250);
}

[2.5]使用到的FLTK类

Fl_Shared_Image

Fl_Image

Fl_Box

注:后面由于扫雷游戏用到图片框主要是每个格子使用图片框,我就把名字改成了GridBlock类,定义和ImageBox差不多,只是后面慢慢的有些改进。

[3]进一步对扫雷游戏的设计

[3.1]设计单击鼠标左键后的事件处理流程

  • 传递该消息(行号和列号)给GUI (注:GUI类是控制主窗体的类,里面包含各种其他组件)
  • GUI调用核心算法获取点开的所有格子和对应的数字
  • 根据地雷分布图调用setImage(image[k]).k属于[0,9],并置格子为不可响应事件setDisable()(格子点开之后必须禁止其继续响应事件)
  • GUI自身有一张数字地图,9表示初始态,0~8表示点开后的数字。各个数字对应着各个图片,10表示点到了地雷=》GameOver

 

如何处理左击事件?

  • 置该格子为不可响应事件状态=》调用核心算法(传递行列值)=》获取更新后的地图状态=》更换图片

[3.2]定义一个消息结构体——获取左键点开的格子和对应的数字

用于传递点开的格子的位置和数字的消息结构体

struct OpenGridMsg 
{
    int i;
    int j;
    int stat;//图片状态
};

核心算法中应该提供一个这样的接口

//核心算法中给GUI提供的接口

void clickOpen(int i, int j, vector<OpenGridMsg>& msg);

返回的msg存储着点开(i.j)位置后递归点开的位置和其状态,点到雷的情况是msg[0].stat=9;

 

为了消除魔数还需要为类的状态设定一个枚举类型:

//格子最后显示状态的枚举类型

enum ImageName

{

zero=0,one=1,two=2,three=3,four=4,five=5,six=6,seven=7,eight=8,

mineBoom=9,mineInit=10,mineOut=11,mark=12,markWrong=13,markUnknow=14

};

[3.3]完成鼠标右键基本处理后的效果

wps_clip_image-2953

右键事件处理的相关代码:

鼠标的右键事件已经完成。
//处理右击事件:状体转换,更换图片。如果是“红旗”和“问号”状体则置不可响应左击事件,否则置可响应左击事件
inline void GridBlock::rightClick(void)
{
    m_rightClickNumber++;
    if (m_rightClickNumber>2)
    {
        m_rightClickNumber=0;
    }
    setImage(imageName[markImage[m_rightClickNumber]]);
    //在标记状态下停止响应左击事件
    if (m_rightClickNumber == 0)
    {
        m_isableLeftClick = true;
    }
    else
    {
        m_isableLeftClick = false;        
    }
}

[3.4]设计鼠标左键单击事件的处理

为了方便实现鼠标左击事件,我接下来把GUI类设计为单例模式,这样的话在GridBlock::leftClick() 中容易调用GUI中的方法GUI::clickOpen(m_lineNumber,m_columnNumber);

调用过程很简单,就一句话:

wps_clip_image-4311[6]

下面是GUI的单例模式定义:

wps_clip_image-9713[6]

实现如下:

wps_clip_image-4877[6]

[4]GUI类改进创建格子的过程

当知道大小的时候,使用vector时优先使用resize()。

inline void GUI::createGameObjects(void)
{
    //知道大小的情况下,优先使用resize()
    m_grid.resize(m_numberLines);
    std::vector<GridBlock> grids;
    grids.resize(m_numberColumns);
    for (int i=0; i<m_numberLines; i++)
    {
        for (int j=0; j<m_numberColumns; j++)
        {
            GridBlock gridBlock(imageName[mineInit]);
            gridBlock.setPosition(j*initBlockWidth,i*initBlockHeight);
            gridBlock.setSize(initBlockWidth,initBlockHeight);
            gridBlock.setLineColumn(i,j);//设置行号和列号
            cout << "i=" << i << ",j=" << j << endl;
            grids[j] = gridBlock;
        }
        m_grid[i] = grids;
    }
}

为了要让这个函数顺利进行,还需要修改GridBlock类的默认构造函数,拷贝构造函数,重载复制运算符

inline GridBlock::GridBlock(void)
:Fl_Box(0,0,0,0,0),m_pSharedImage(NULL),m_x(0),m_y(0),m_width(0),m_height(0),
m_enable(true),m_isableLeftClick(true),m_lineNumber(0),m_columnNumber(0),
m_rightClickNumber(0)
{
…
…
}

首先把pSharedImage置为NULL。我调试的时候发现的问题,这个问题需要记住,当 pSharedImage不存在的时候是不能调用它的方法的。(一般调试了就能找到)

inline GridBlock& GridBlock::operator=(const GridBlock& a_rightSide)
{
    m_x = a_rightSide.getX();
    m_y = a_rightSide.getY();
    m_width = a_rightSide.getWidth();
    m_height = a_rightSide.getHeight();
    m_enable = a_rightSide.m_enable;
    m_isableLeftClick = a_rightSide.m_isableLeftClick;
    m_lineNumber = a_rightSide.m_lineNumber;
    m_columnNumber = a_rightSide.m_columnNumber;
    m_rightClickNumber = a_rightSide.m_rightClickNumber;
    resize(m_x,m_y,m_width,m_height);
    if (a_rightSide.m_pSharedImage != NULL)//Fl_Shared_Image不存在的情况下不能调用它的方法
    {
        m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//获取相同的sharedImage
        m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//构建一幅大小相同的Image
        image(m_pImage.get());
        redraw();
    }
    return *this;
}

然在赋值运算符重载函数中给个判断 if (a_rightSide.m_pSharedImage != NULL)

这样的话就不会在resize时出现Fl_Shared_Image不存在的情况下调用它的方法的错误问题了。

[5]扫雷算法类Mines类的设计

仅仅是一个矩阵(二维数组),操作只有这个地雷矩阵图的简单方法。GUI操作Mines类的接口见MinesInterface类。

[5.1]Mines类的的声明

class Mines
{
public:
    Mines(void);
    void initMines(int a_lines, int a_columns);
    bool isMine(int a_line, a_column);
    int countRoundMines(int a_line, int a_column);//统计周围的地雷数量

private:
    vector<vector<bool> > m_minesMap;//雷的布局图
    int m_numberLines;//行数
    int m_numberColumns;//列数
};

当时我为设计每个元素为int型还是bool型纠结了一番的,最终选择了bool类型(有过几次想换为int型的冲动,结果还是没换)。

[5.2]初始化雷区地图

关于随机放置雷的问题,采用如下方法:

inline void Mines::initMines(int a_lines, int a_columns, int a_minesCount)
{
    assert(a_minesCount<a_lines*a_columns);
    m_minesCount = a_minesCount;
    m_minesMap.resize(a_lines);
    vector<bool> minesMapLines;
    minesMapLines.resize(a_columns);
    for (int i=0; i<a_lines; i++)
    {
        for (int j=0; j<a_columns; j++)
        {
            minesMapLines[j] = false;
        }
        m_minesMap[i] = minesMapLines;
    }
    for(int i=0; i<m_minesCount; i++)
    {    //随机填充
        srand(time(NULL));
        int index = rand() % (a_lines*a_columns);//[0,a_lines*a_columns-1]
        if(m_minesMap[index/a_columns][index%acolumns] )
            i--;
        else
            m_minesMap[index/a_columns][index%a_columns] = true;
    }
}

这样的话每次放置只取一次随机数,如果行列各取一次很容易出现相同的情形的。i--是为了防止已经放置了雷的地方重新放的问题。结果我又开始纠结了,随机生成地图慢啊!发现两个缺陷(后面会有改进方案)

  • 初始化所有元素为false可以更快(但这不是慢的原因)
  • 随机生减少重复

[5.3]计算周围地雷个数

为了方便计算周围的地雷个数,这样设计isMines2方法。(内部使用,可以越界一格)

inline bool Mines::isMine2(int a_line, int a_column)
{
    assert(a_line>-2);
    assert(a_column>-2);
    assert(a_line<m_numberLines+1);
    assert(a_column<m_numberColumns+1);
    if (a_line==-1 || a_line==m_numberLines
        || a_column==-1 || a_column==m_numberColumns)//边界情况
    {
        cout << "边界情况" << endl;
        return false;
    }
    return m_minesMap[a_line][a_column];
}

这样就可以很简单的写countRoundMines方法了。countRoundMines()方法定义如下:

inline int Mines::countRoundMines(int a_line, int a_column)
{
    int count=0;
    if (isMine2(i-1,j-1))        count++;
    if (isMine2(i,j-1))        count++;
    if (isMine2(i+1,j-1))    count++;
    if (isMine2(i-1,j))        count++;
    if (isMine2(i+1,j))        count++;
    if (isMine2(i-1,j+1))    count++;
    if (isMine2(i,j+1))        count++;
    if (isMine2(i+1,j+1))    count++;
    return count;
}

外部调用的isMine方法如下:(外部使用,不能出现越界情况)

inline bool Mines::isMine(int a_line, int a_column)
{
    assert(a_line>-1);
    assert(a_column>-1);
    assert(a_line<m_numberLines);
    assert(a_column<m_numberColumns);
    return m_minesMap[a_line][a_column];
}

[6]设计SweepInterface类

用于GUI使用Mines类的接口类

[6.1]定义和实现

class SweepInterface
{
public:
    void initMines(int a_lines, int a_columns, int a_minesCount);
    void clickOpen(int i, int j, vector<OpenGridMsg>& msg);

private:
    Mines m_mines;
};

inline void SweepInterface::clickOpen(int i, int j, vector<OpenGridMsg>& msg)
{
    OpenGridMsg amsg;
    if (m_mines.isMine(i,j))//踩到雷了
    {
        amsg.i = i;
        amsg.j = j;
        amsg.state = mineBoom;//
        msg.push_back(amsg);
        return;
    }
    amsg.i = i;
    amsg.j = j;
    amsg.state = m_mines.countRoundMines(i,j);
    msg.push_back(amsg);
    if (amsg.state == 0)//如果周围没有雷,继续点开周围的
    {
        clickOpen(i-1,j-1,msg);
        clickOpen(i,j-1,msg);
        clickOpen(i+1,j-1,msg);
        clickOpen(i-1,j,msg);
        clickOpen(i+1,j,msg);
        clickOpen(i-1,j+1,msg);
        clickOpen(i,j+1,msg);
        clickOpen(i+1,j+1,msg);
    }
}

现在发现问题了,边界的问题,怎么解决了,我现在想到了两个方法。

  • 第一个是修改Mines类的isMine()方法,如果是雷返回1,不是雷返回0,是边界外返回-1。
  • 第二个是修改下面的if(amsg.state==0){/***/}大括号里面如果越界就不clickOpen()。

使用方法一需要进行比较大的修改,我选择方法二:

if (amsg.state == 0)//如果周围没有雷,继续点开周围的
    {
        if (i-1>=0 && j-1>=0)                        clickOpen(i-1,j-1,msg);
        if (j-1>=0)                                    clickOpen(i,j-1,msg);
        if (i+1<m_numberLines && j-1>=0)            clickOpen(i+1,j-1,msg);
        if (i-1>=0)                                    clickOpen(i-1,j,msg);
        if (i+1<m_numberLines)                        clickOpen(i+1,j,msg);
        if (i-1>=0 && j+1<m_numberColumns)            clickOpen(i-1,j+1,msg);
        if (j+1<m_numberColumns)                        clickOpen(i,j+1,msg);
        if (i+1<m_numberLines && j+1<m_numberColumns)    clickOpen(i+1,j+1,msg);
    }

原理很简单,就是迭代到了边界就不在迭代。

[6.2]到此为止发现的两个问题分析

现在又出现新问题了,每次迭代的时候会迭代到以前的位置。这样的话,需要在每次迭代前检测位置是否已经检查过。现在遇到两个问题:

  • 迭代不完全。(空白区域不能继续往下迭代)
  • 初始化太慢了,那个随机生成Map的需要修改。

初始化15个雷用的循环次数太多了

wps_clip_image-9389

第1个问题的原因:

wps_clip_image-9533

行数和列数初始化的时候没有设置。所以引起右下角部分不能成功继续迭代。(记住,包含数据成员的类一定要自己建立构造函数来初始化数据成员)

分析第一个问题的原因,没吃调用srand里面的时间在循环里面几乎是一样的,所有每次求出的index值在短时间是一样的。然后就需要很长时间才能找出不同的位置来放置地雷。我给出的解决方案如下,在srand的参数里面加入变量i来干扰随机数,这样的效果很好。几乎m_minesCount次就能完成。

wps_clip_image-9922

wps_clip_image-27092

wps_clip_image-24675

 

但是这样还是不够好,毕竟i大小有限制的。下面的这种更好。因为count的数字只增不减。

wps_clip_image-10013

wps_clip_image-367wps_clip_image-584wps_clip_image-2816

[6.3]修正随机生成地图

上面的方法有问题,当玩行列很大的时候就很慢了。还有就是srand()一般只需要用一次的。下面的结果很好了。

unsigned int count=0;
    unsigned int alpha=0;
    srand(time(NULL));
    for(int i=0; i<m_minesCount; i++)
    {    //随机填充
        int index = (rand())%(m_numberLines*m_numberColumns);//[0,m_numberLines*m_numberColumns-1]
        if(m_minesMap[index/m_numberColumns][index%m_numberColumns])
        {
            i--;
            alpha++;//统计重复的次数
        }
        else
        {
            m_minesMap[index/m_numberColumns][index%m_numberColumns] = true;
        }
        count++;
    }
    cout << "初始化雷区用来" << count << "次循环(雷数量:" << m_minesCount << ")" << endl;
    cout << "重复次数:" << alpha << endl;
}

wps_clip_image-10274

wps_clip_image-8271

[7]试玩一把

到这里,已经完成了扫雷的基本要求,所以我就玩了一把

wps_clip_image-10477

[8]对话框设计

[8.1]重来对话框的设计

wps_clip_image-10532

void GUI::replayDialog(void)//GameOver对话框
{
    int hotspot = fl_message_hotspot();
    fl_message_hotspot(0);
    fl_message_title("Game Over!");
    int rep = fl_choice("重新开始?",
        "Level", "Replay", "Exit");
    fl_message_hotspot(hotspot);
    if (rep==2) 
        exit(0);
    else if (rep == 1)
    {
        m_coreMines.initMines(m_numberLines,m_numberColumns,m_minesCount);
        for (int i=0; i<m_numberLines; i++)
        {
            for (int j=0; j<m_numberColumns; j++)
            {
                m_grid[i][j].init();
                m_grid[i][j].setImage(imageName[mineInit]);
            }
        }
    }
}

FLTK提供的对话框很方便就能调用。Fl_ask.h文件中定义了所以对话框调用函数。下面是我用到的对话框函数:

FL_EXPORT void fl_message(const char *,...) __fl_attr((__format__ (__printf__, 1, 2)));
FL_EXPORT void fl_alert(const char *,...) __fl_attr((__format__ (__printf__, 1, 2)));
FL_EXPORT int fl_ask(const char *,...) __fl_attr((__format__ (__printf__, 1, 2), __deprecated__));
FL_EXPORT int fl_choice(const char *q,const char *b0,const char *b1,const char *b2,...) __fl_attr((__format__ (__printf__, 1, 5)));
FL_EXPORT const char *fl_input(const char *label, const char *deflt = 0, ...) __fl_attr((__format__ (__printf__, 1, 3)));

[8.2]退出对话框

wps_clip_image-11247

void window_callback(Fl_Widget*, void*)
{
    int hotspot = fl_message_hotspot();
    fl_message_hotspot(0);
    fl_message_title("Exit");
    int rep = fl_ask("确定退出?");
    fl_message_hotspot(hotspot);
    if (rep==1) 
        exit(0);
}

[8.3]等级设置

苦恼的问题,需要记住,FLTK中,如果删除了Fl_Window,它里面的东西也将会被删除。所以,在更换等级前必须做的事情是:先删除m_grid;然后删除窗体。这个问题是由于自己粗心,忘记了又这回事,最后通过测试函数:(慢慢思索,才找出原因的。)

void Test::test_GridBlock(vector<vector<GridBlock> >& vv)
{
    vv.clear();
    vv.resize(10);
    vector<GridBlock> v;
    v.resize(10);
    for (int i=0; i<10; i++)
    {
        for (int j=0; j<10; j++)
        {
            GridBlock t(imageName[mineBoom]);
            t.setPosition(initBlockWidth,initBlockHeight);
            t.setSize(initBlockWidth,initBlockHeight);
            v[j] = t;
        }
        vv[i] = v;
    }
}
void GUI::setLevel(int a_level)
{
    m_minesCount = initMinesCount*a_level;
    m_numberLines = initLines*a_level;//行数
    m_numberColumns = initColumns*a_level;//列数
    m_width=initBlockWidth*m_numberColumns;
    m_height=initBlockHeight*m_numberLines;
    m_pWindow->size(m_width,m_height);
    m_pWindow->begin();//往窗体里面添加对象
        createGameObjects();
    m_pWindow->end();
    m_pWindow->show();
}

如果是这样的话,因为没有删除窗体,所以m_grid里的内容不会被删除。这解决办法比上面的好多了。

[8.4]菜单设计

仿造下面的菜单设计

wps_clip_image-12178

我设计的菜单如下

wps_clip_image-15904

扫雷英雄榜对话框的设计

wps_clip_image-15282

我的设计师这样的

wps_clip_image-31545

现在又发现FLTK的一个问题了,在写汉字的时候如果出现异常可以在异常汉字前后加全角的空格来消除异常。wps_clip_image-14370

wps_clip_image-25182

 

[9]三大界面问题

[9.1]自定义雷区对话框

wps_clip_image-12289

我自己画的对话框

wps_clip_image-20902

处理事件有点麻烦,两个按钮的事件回调函数如下:

void DialogWindow::OkCB(Fl_Widget* w)
{
    DialogWindow* dialog = DialogWindow::getInstance();
    dialog->m_strH = dialog->m_pInputs[0]->value();
    dialog->m_strW = dialog->m_pInputs[1]->value();
    dialog->m_strC = dialog->m_pInputs[2]->value();
    dialog->hide();
    GUI* gui = GUI::getInstance();
    int H,W,C;
    H = atoi(m_strH.c_str());
    W = atoi(m_strW.c_str());
    C = atoi(m_strC.c_str());
    gui->setLevelSelf(H,W,C);
}
void DialogWindow::CancelCB(Fl_Widget* w)
{
    DialogWindow* dialog = DialogWindow::getInstance();
    dialog->m_pInputs[0]->value("");
    dialog->m_pInputs[1]->value("");
    dialog->m_pInputs[2]->value("");
    dialog->hide();
}

这样运行时可以了,但是少个对输入数据的判断,接下来加个判断进去:

wps_clip_image-12710

void DialogWindow::OkCB(Fl_Widget* w)
{
    DialogWindow* dialog = DialogWindow::getInstance();
    string strH(dialog->m_pInputs[0]->value());
    string strW(dialog->m_pInputs[1]->value());
    string strC(dialog->m_pInputs[2]->value());
    int H,W,C;
    H = atoi(strH.c_str());
    W = atoi(strW.c_str());
    C = atoi(strC.c_str());
    if (H>20 || H<5 || W>30 || W<5 || C>W*H)
    {
        fl_alert(dialogWindowStr[6]);
        dialog->m_pInputs[0]->value("");
        dialog->m_pInputs[1]->value("");
        dialog->m_pInputs[2]->value("");
        return;
    }
    dialog->hide();
    GUI* gui = GUI::getInstance();
    gui->setLevelSelf(H,W,C);
    dialog->m_pInputs[0]->value("");
    dialog->m_pInputs[1]->value("");
    dialog->m_pInputs[2]->value("");
}

调用的那个setLevelSelf()函数比setLevel()更简单:

void GUI::setLevel(int a_level)
{
    m_level = a_level;
    switch(m_level)
    {
    case 1:
        m_minesCount = initMinesCount;
        m_numberLines = initLines;//行数
        m_numberColumns = initColumns;//列数
        break;
    case 2:
        m_minesCount = initMinesCount*4;
        m_numberLines = initLines*2;//行数
        m_numberColumns = initColumns*2;//列数
        break;
    case 3:
        m_minesCount = initMinesCount*8;
        m_numberLines = initLines*2;//行数
        m_numberColumns = initColumns*3;//列数
        break;
    default:
        assert("a_level must be [1~3]");
        return;
    }
    updateWindow();
}
void GUI::updateWindow(void)
{
    m_width=initBlockWidth*m_numberColumns;
    m_height=initBlockHeight*m_numberLines;
    m_pWindow->size(m_width,m_height+menuBarHeight);
    m_pMenuBar->size(m_width,menuBarHeight);
    m_pWindow->begin();//往窗体里面添加对象
    createGameObjects();
    m_pWindow->end();
    m_pWindow->show();
    updateMap();
}

void GUI::setLevelSelf(int a_H, int a_W, int a_C)
{
    m_level=0;
    m_minesCount = a_C;
    m_numberLines = a_H;//行数
    m_numberColumns = a_W;//列数
    updateWindow();
}

[9.2]游戏胜利对话框,包括记录的保存

由于这个操作需要在右键单击时候进行判断,所以,我在GUI类中加一个rightClick()方法:

void GUI::rightClick(int a_line, int a_colum)
{
    if (strcmp(m_grid[a_line][a_colum].getImageName(),
        imageName[markUnknow]) != 0)//如果不是问号就做判断
    {
        int i=0,j=0;
        for (; i<m_numberLines; i++)
        {
            j=0;
            for (; j<m_numberColumns; j++)
            {
                if (strcmp(m_grid[i][j].getImageName(),imageName[mineInit]) == 0)
                {//如果还有没标记点开或者没标记的
                    break;
                }
                if (strcmp(m_grid[i][j].getImageName(),imageName[mark]) == 0
                    && !m_coreMines.isMine(i,j))//该位置被标记,且该位置不是雷
                {
                    break;
                }
                if (strcmp(m_grid[i][j].getImageName(),imageName[markUnknow]) == 0)//如果有问号
                {
                    break;
                }
            }
            if (j<m_numberColumns) break;//很重要的一句话,跳出双重循环
        }
        if (i==m_numberLines && j==m_numberColumns)
        {//胜利
            winnerDialog();//胜利对话框
        }
    }
}

这是对胜利的条件的判断,如果判断胜利了,就调用winnerDialog()方法。胜利对话框用一个输入对话框来做会简单些,设计如下:

wps_clip_image-13083

现在我使用的是把数据保存到文件中,所以需要用来对文件的写入操作(如果使用SQLite数据库的话可能更方便后续添加网络模块)。对于这个数据保存数据半夜搞了两个小时没搞定,结果花了一上午搞定了。

void GUI::winnerDialog(void)
{
    int hotspot = fl_message_hotspot();
    fl_message_hotspot(0);
    fl_beep(FL_BEEP_MESSAGE);
    fl_message_hotspot(hotspot);
    fl_message_title(GUIStr[8]);
    int useTime = 102;//下一步的任务是计算时间
    char name[20]="";
    if (m_level != 0)
    {
        const char* str = fl_input(GUIStr[9],"",useTime);
        if (strlen(name)==0)
        {
            strcpy(name,GUIStr[12]);//匿名
        }
        else
        {
            strcpy(name,str);
        }
    }
    else
    {
        fl_message(GUIStr[10],useTime);
        return;
    }
    saveRecord(useTime,name);//保存记录
}

时间相关的等下一个时间显示问题做好了再添加上去,暂时用了个常数代替

文件格式:

%d\t%s

文件有且仅有三行

保存文件相对来说是比较麻烦的,主要是格式问题和记录的覆盖问题,这个问题花了一上午才解决的,要记住,读文件的时候先把内容全部读出了再操作(内容较少的情况下)。

void GUI::saveRecord(int a_useTime, const char* a_userName)
{
    assert(m_level!=0);
    using std::ofstream;
    using std::ifstream;
    ofstream writeFile;
    ifstream readFile;

    //备份
    string text;
    string str;
    readFile.open(GUIStr[11]);
    while (getline(readFile,str))
    {
        text += str+"\n";
    }
//    cout <<"text:"<< text;
    readFile.close();

    writeFile.open(GUIStr[11]);
    for (int i=1; i<4; i++)
    {
        int b = text.find_first_of('\n',0);
        str = text.substr(0,b);
//        cout <<"str:"<< str;
        text = text.substr(b+1,text.length()-b);
//        cout <<"text:"<< text;
//        cout << i << ":" << str << endl;
        if (i==m_level)
        {
//            cout << m_level << "级:" << str;
            if (str.length()==0)
            {
                writeFile<<a_useTime<<"\t"<<a_userName<<"\n";
            }
            else
            {
                int index = str.find('\t');
                string s = str.substr(0,index);//记住后面的参数是num
                int time = atoi(s.c_str());
//                cout << "old:" << time << endl;
//                cout << "new:" << a_useTime << endl;
                if (time>a_useTime)//更新记录
                {
                    writeFile<<a_useTime<<"\t"<<a_userName<<"\n";
                }
                else
                {
                    writeFile<<str<<"\n";
                }
            }
        }
        else
        {
            writeFile<<str<<"\n";
        }
    }
    writeFile.close();
}

也可以看到上面的cout部分都加了注释,是调试的时候用的。String操作中有个要注意的地方就是取子串的时候,第二个参数是子串的长度。现在文件保存弄好了,接下来就要修改wps_clip_image-13906

这个对话框的载入记录的内容了。

//排行榜对话框
void GUI::RankingsDialog(void)
{
    int hotspot = fl_message_hotspot();
    fl_message_hotspot(0);
    fl_message_title(GUIStr[7]);

    using std::ifstream;
    ifstream readFile(GUIStr[11]);//读取文件中的数据
    string text;
    string str;
    while (getline(readFile,str))
    {
        text += str+"\n";
    }
    readFile.close();
    cout << text;

    string textout;
    //找到str为m_level行
    for (int i=0; i<3; i++)
    {
        int b = text.find_first_of('\n',0);
        str = text.substr(0,b);
        text = text.substr(b+1,text.length()-b);
        int index = str.find('\t');
        string time = str.substr(0,index);//记住后面的参数是num
        string name = str.substr(index+1,str.length()-index);
        cout << "time,name:" << time << "," << name << endl;
        textout+=GUIStr[13+i]+time+GUIStr[16]+name+"\n";
    }
/*        "初级: ",                //13
        "中级: ",                //14
        "高级: ",                //15
        " 秒 \t",                //16
*/
    fl_message("%s",textout.c_str());
}

到现在,也许已经注意到了许多GUIStr[]这样的字符串数组的使用了吧,主要是为了后续的本地化。需要本地化的时候只需要修改这个数组就行了。

[9.3]时间显示和雷数显示

雷数量的显示分两个:剩余雷数和已经标记的雷数

wps_clip_image-14033

这两个显示的话比较简单,在右键事件中加入就行了。

inline void TimeShowBox::setRemainMines(int a_num)
{
    if (a_num<0) a_num=0;
    sprintf(m_str,TimeShowBoxStr[0],a_num);
    m_pBox[0]->copy_label(m_str);
    m_pBox[0]->redraw_label();
}

inline void TimeShowBox::setMarkMines(int a_num)
{
    sprintf(m_str,TimeShowBoxStr[1],a_num);
    m_pBox[1]->copy_label(m_str);
    m_pBox[1]->redraw_label();
}

关于时间的显示还是有点难度的。我有点不理解FLTK自带的那个Fl_Timer。所以我就自己弄了个全局变量控制。

具体怎么控制这个g_useTime呢?首先在主函数中使用

void callback(void*)
{
    useTime++;
    cout << "time:" << useTime << endl;
    Fl::repeat_timeout(1.0, callback);
}

int main(void)
{
    GUI* gui = GUI::getInstance();
    Fl::add_timeout(1.0, callback);
    return Fl::run();
}

这个的作用是,useTime每秒自增一次。为了防止溢出也可以加个判断在回调函数里面的。

然后在GUI创建的时候置g_sseTime为0。在更新地图的时候也置g_useTime为0。

//更新地图
void GUI::updateMap(void)
{
    g_useTime=0;
    m_coreMines.initMines(m_numberLines,m_numberColumns,m_minesCount);
...
}

为了再弹出对话框的时候使时间暂停,我加了一个变量 g_isPauseTime=false;

控制是否暂停。需要暂停时间的弹出的对话框有:显示排行榜对话框。

刚刚试了一下5X5的时候,上面的显示不好看了。

wps_clip_image-14360

所以我改下自定义的范围。 if (H>20 || H<3 || W>40 || W<10 || C>W*H)

{

" 高度(3~20) ",

" 宽度(10~40) ",

[10]结束语

终于完工了,可以发布了,洗洗睡吧!

FLTK中还有强大的Event功能,这个扫雷游戏中只使用的了很基础的Event功能。

忘记了一件重要的事情:公布源代码!代码已经push到github了,想看看,想玩玩的可以去下载,图片素材用的是别人的:http://www.cnblogs.com/jacky87/archive/2010/08/03/1791395.html
代码下载地址:https://github.com/hanxi/MineSweeping

posted @ 2012-09-02 22:20  涵曦  阅读(6816)  评论(20编辑  收藏  举报