先上几张效果图,本人QT刚刚入门一个星期不到,如果你和我一样是个初学者,我觉得这篇文章还有那么点参考价值,如果你是老手,不喜勿喷哈。(全部代码以及工程地址会在最后贴出)
界面比较粗糙,功能也有很多需要完善的,比如计时和分数排名,但是核心的内容应该就是这些了。
如果你感兴趣,下面我就从代码逻辑和界面交互两个方面分别介绍我的编写过程。
- 游戏的内部逻辑:
- 首先分析一下抽象的游戏过程,把一个单元格看作一个数据结构,一个游戏地图看作是一个由各个单元格构成的整体,包含完成各式各样任务的成员函数。
-
一个单元格的互斥状态有:未被挖掘,被标记,被挖掘。同时存储自己是否有地雷的状态,并存储自己周围的地雷数量。根据以上分析,我们定义数据结构如下:
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
- 觉得文章对你有帮助的话,可以扫码关注下下面的公众号哦~我会时不时在公众号中发一些自己的觉得有用的信息、分享自己的收获。