QT5下简易扫雷游戏的编写(过程详细)

QT5下简易扫雷游戏的编写(过程详细)

先上几张效果图,本人QT刚刚入门一个星期不到,如果你和我一样是个初学者,我觉得这篇文章还有那么点参考价值,如果你是老手,不喜勿喷哈。(全部代码以及工程地址会在最后贴出)


界面比较粗糙,功能也有很多需要完善的,比如计时和分数排名,但是核心的内容应该就是这些了。

如果你感兴趣,下面我就从代码逻辑和界面交互两个方面分别介绍我的编写过程。

  1. 游戏的内部逻辑:
    • 首先分析一下抽象的游戏过程,把一个单元格看作一个数据结构,一个游戏地图看作是一个由各个单元格构成的整体,包含完成各式各样任务的成员函数。

    • 一个单元格的互斥状态有:未被挖掘,被标记,被挖掘。同时存储自己是否有地雷的状态,并存储自己周围的地雷数量。根据以上分析,我们定义数据结构如下:

enum State{
    UNDIGGED,
    DIGGED,
    REDFLAG
};

struct unit{
    State curState;
    int bombNum;
    bool isBomb;
    unit():curState(UNDIGGED),bombNum(0),isBomb(false) {}
    ~unit() = default;
};

然后就是游戏地图的设计,首先考虑游戏这个抽象数据结构的数据成员,首先应该包含一组单元格阵列,当前阵列的行数和列数。总的地雷数,剩余的没有被挖掘的单元数(与地雷数比对可以判断是否通过游戏),最后还有一个枚举量,记录当前游戏的状态。
然后分析游戏中的操作,逆向分析,我们可以得到几个基本的动作,比如挖掘dig,标记mark,当然了,还有一些返回私有成员的方法。为了和界面分开测试,我还添加了一个draw()方法,简单的将游戏地图输出。

这么一分析,这个模型还是很简单的。不废话,上代码:

enum gameState{
    PLAYING,
    WIN,
    LOSE
};
class block {
public:
    block(int theRow = 8,int theCol = 15,int theTotalBomb = 20);
    ~block() = default;
public:
    //method
    void draw() const;
    void dig(int theRow,int theCol);
    void mark(int theRow, int theCol);
    bool isBomb(int theRow,int theCol) const;
    int getRow() const;
    int getCol() const;
    int getBombNum() const;
    gameState checkGame();
public:
    vector<vector<unit>> map;
private:
    int row;//行
    int col;//列数
    int totalBomb;
    int restNum;
    gameState curGameState;
private:
    void calBombNum();
    int calBombnum(int theRow,int theCol) const;
};

这里大部分的方法还是很好编写的,因为最后会给出完整工程,所以我就不一一介绍了,主要把最关键的方法思路介绍一下。dig()在挖掘的时候如果遇到周围地雷数量为0的情况会自动向周围扩散,所以需要特别关注如何实现扩散算法。这里我采用的是递归的方式:

void block::dig(int theRow, int theCol)
{
    unit& curUnit = map[theRow][theCol];
    curUnit.curState = DIGGED;
    restNum--;
    if(curUnit.isBomb==1)
    {
        curGameState = LOSE;
        return;
    }
    if(curUnit.bombNum==0)
    {
        for(int i=-1;i<2;++i)
        {
            for(int j = -1;j<2;++j)
            {
                int curRow = theRow + i;
                int curCol = theCol + j;
                if(curRow>=0&&curRow<row &&
                   curCol>=0&&curCol<col &&
                   map[curRow][curCol].curState!=DIGGED)
                        dig(curRow,curCol);
            }
        }
    }
    if(restNum==totalBomb)
    {
        curGameState = WIN;
    }
}

这里,还要时时注意修改当前的游戏状态。这部分我大概花了两个小时完成,还是很简单的,我事先在CLION里面编写的逻辑部分的代码,然后用命令行测试了下。建议你也可以这样做,确保独立的模块能够顺利运行,下面处理的时候就可以安心调试其他模块,省去了很多麻烦。

 

 

2.界面交互设计

界面设计的经验不足,所以也比较粗糙。

首先是图标问题,首先要收集符合要求的图标,这个就很郁闷。这里我推荐一个网站:http://www.iconfont.cn/

这个网站是阿里的一个图标库,很多公开免费的图标资源,个人觉得质量还是不错的,而且下载的时候,还可以对图标进行定制,很方便。当然了,如果你有更好的解决方法,烦请告知,毕竟我也是刚了解这块,还不是很熟悉。

于是我从这上面下载了数字、炸弹红旗、方块等图标,然后开始设计界面。

QT我了解的不多,不敢妄谈,这次也付出了很多试错的代价,首先我新建了widget项目,然后MainWindow类继承QMainWindow类。首先进行模型的分析,作为主窗口,必然包含游戏的内在逻辑交互,那么,他的数据成员就应该有之前定义的block类的实例,以及一些界面间距的固定值,还有方块阵列的行数列数。在游戏的逻辑中,还有游戏难度的选择以及按键的设计,所以还需要存储当前的游戏难度,以及根据难度设计的重启游戏方法与信号槽函数。所以概括起来,就是:

enum gameLevel{
    EASY,
    MEDIUM,
    HARD
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    void restart(int theRow,int theCol,int theTotalBomb);
protected:
    void paintEvent(QPaintEvent *event);
    void mousePressEvent(QMouseEvent *event);
protected:
    QPixmap *pix = new QPixmap;
    int gameRow;
    int gameCol;// = 15;
    int spaceY = 40;
    int blockSize = 30;
    int offsetX = 40;
    int offsetY = 20;
private:
    Ui::MainWindow *ui;
    block *game = new block;
    gameLevel level;
private slots:
    void startEasyGame();
    void startMediumGame();
    void startHardGame();
};

然后,完善成员方法的定义,其实过程不难,要捋清楚逻辑:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    gameRow = game->getRow();
    gameCol = game->getCol();
    level = EASY;
    setFixedSize(gameCol * blockSize + offsetX * 2, gameRow * blockSize + offsetY * 2 + spaceY);
    connect(ui->actionEasy,SIGNAL(triggered(bool)),this,SLOT(startEasyGame()));
    connect(ui->actionMedium,SIGNAL(triggered(bool)),this,SLOT(startMediumGame()));
    connect(ui->actionHard,SIGNAL(triggered(bool)),this,SLOT(startHardGame()));
    connect(ui->actionQuit,SIGNAL(triggered(bool)),this,SLOT(close()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::restart(int theRow, int theCol, int theTotalBomb)
{
    delete game;
    game = new block(theRow,theCol,theTotalBomb);
    gameRow = theRow;gameCol = theCol;
    update();
}



void MainWindow::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QPixmap pngBlockDouble(":/block_double.png");
    QPixmap pngBlockNone(":/block_none.png");
    QPixmap pngRedFlag(":/redFlag.png");
    QPixmap pngBomb(":/bomb.png");
    QPixmap pngOne(":/数字1.png");QPixmap pngTwo(":/数字2.png");QPixmap pngThr(":/数字3.png");
    QPixmap pngFor(":/数字4.png");QPixmap pngFiv(":/数字5.png");QPixmap pngSix(":/数字6.png");
    QPixmap pngSvn(":/数字7.png");QPixmap pngEight(":/数字8.png");
    pngBlockDouble = pngBlockDouble.scaled(QSize(blockSize,blockSize));
    pngBlockNone = pngBlockNone.scaled(QSize(blockSize,blockSize));
    pngRedFlag = pngRedFlag.scaled(QSize(blockSize,blockSize));
    pngBomb = pngBomb.scaled(QSize(blockSize,blockSize));
    pngOne = pngOne.scaled(QSize(blockSize,blockSize));
    pngTwo = pngTwo.scaled(QSize(blockSize,blockSize));
    pngThr = pngThr.scaled(QSize(blockSize,blockSize));
    pngFor = pngFor.scaled(QSize(blockSize,blockSize));
    pngFiv = pngFiv.scaled(QSize(blockSize,blockSize));
    pngSix = pngSix.scaled(QSize(blockSize,blockSize));
    pngSvn = pngSvn.scaled(QSize(blockSize,blockSize));
    pngEight = pngEight.scaled(QSize(blockSize,blockSize));

    for(int i=0;i<gameRow;++i)
    {
        for(int j=0;j<gameCol;++j)
        {
            unit &curUnit = game->map[i][j];
            switch(curUnit.curState)
            {
            case UNDIGGED:
                if(game->checkGame()==LOSE&&curUnit.isBomb==1)
                    painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngBomb);
                else
                    painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngBlockDouble);
                break;
            case DIGGED:
            {
                if(curUnit.isBomb==0)
                {
                    switch(curUnit.bombNum)
                    {
                    case 0:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngBlockNone);
                        break;
                    case 1:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngOne);
                        break;
                    case 2:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngTwo);
                        break;
                    case 3:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngThr);
                        break;
                    case 4:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngFor);
                        break;
                    case 5:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngFiv);
                        break;
                    case 6:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngSix);
                        break;
                    case 7:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngSvn);
                        break;
                    case 8:
                        painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngEight);
                        break;
                    }
                }
                else
                {
                    qDebug() << "you lose" << endl;
                    //QMessageBox::about(this,"warning","You lose!");
                    painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngBomb);
                }
            }
                break;
            case REDFLAG:
                painter.drawPixmap(j*blockSize+offsetX,i*blockSize+offsetY+spaceY,pngRedFlag);
                break;
            default:
                break;
            }
        }
    }

}

void MainWindow::mousePressEvent(QMouseEvent *event)
{

    if(game->checkGame()==PLAYING)
    {
        int px = event->x() - offsetX;int py = event->y() - offsetY - spaceY;
        int row = py/blockSize; int col = px/blockSize;
        switch(event->button())
        {
        case Qt::LeftButton:
            game->dig(row,col);
            qDebug() << "dig:"<<row<<" "<<col<<endl;
            update();
            break;
        case Qt::RightButton:
            game->mark(row,col);
            qDebug() << "mark:"<<row<<" "<<col<<endl;
            update();
            break;
        default:
            break;
        }
    }
}

void MainWindow::startEasyGame()
{
    restart(8,15,20);
    level = EASY;
    setFixedSize(gameCol * blockSize + offsetX * 2, gameRow * blockSize + offsetY * 2 + spaceY);
    qDebug() << "easy mode" << endl;
}

void MainWindow::startMediumGame()
{
    restart(20,25,80);
    level = MEDIUM;
    setFixedSize(gameCol * blockSize + offsetX * 2, gameRow * blockSize + offsetY * 2 + spaceY);
    qDebug() << "medium mode" << endl;
}

void MainWindow::startHardGame()
{
    restart(30,45,100);
    level = HARD;
    setFixedSize(gameCol * blockSize + offsetX * 2, gameRow * blockSize + offsetY * 2 + spaceY);
    qDebug() << "hard mode" << endl;
}

完整工程的代码我上传到github了,详见
GitHub

  • 觉得文章对你有帮助的话,可以扫码关注下下面的公众号哦~我会时不时在公众号中发一些自己的觉得有用的信息、分享自己的收获。
Comments are closed.