QT简单应用之扫雷游戏
制作扫雷游戏,首先想到先制作一个界面,QT很方便的可以用ui制作;
简单制作一个centralWidget加入菜单栏;
之后开始制作详细内容;
借鉴我们平常玩的扫雷,先考虑游戏逻辑模型,首先主界面会被均分为多个小方块,我们需要考虑方块有两种类型:雷方块和普通方块
而方块又有几种状态:未翻开,已翻开,雷标记(及右键点击方块插旗被认定为雷)
已翻开又有两种状态:数字标记,雷标记
// 方块的状态,未挖掘,翻开,标记,雷出现,错误标记
enum BlockState
{
UN_DIG,
DIGGED,
MARKED,
BOMB,
WRONG_BOMB
};
// 雷方块类
struct MineBlock
{
BlockState curState; // 当前状态
int valueFlag; // 数值,0到8, -1表示雷
};
在考虑游戏状态,有开始,进行中,输,赢
enum GameState
{
PLAYING,
FAULT,
OVER,
WIN
};
借鉴电脑自带扫雷,分为低级,中级,高级,而自定义过于复杂,在这不考虑
enum GameLevel
{
BASIC,
MEDIUM,
HARD
};
设置游戏默认参数,分为行方块个数,列方块个数,雷数,时间;
之后建立游戏模型
:建立五个函数:(翻开,标记,创建游戏,重新游戏,检查游戏状态)
class GameModel
{
public:
GameModel();
virtual ~GameModel();
public:
void digMine(int m, int n); //挖雷, m是行, n是列
void markMine(int m, int n); // 标记雷
void createGame(int row = kRow, int col = kCol, int mineCount = kMineCount, GameLevel level = MEDIUM); // 初始化游戏
void restartGame(); // 按当前参数重新开始游戏
void checkGame(); // 检查游戏输赢
public:
std::vector<std::vector<MineBlock>> gameMap; // 游戏地图
int mRow; // 地图行数
int mCol; // 地图列数
int totalMineNumber; // 雷数
int curMineNumber; // 当前雷数(仅用于显示)
int timerSeconds; // 计时(秒)
GameState gameState; // 当前游戏状态
GameLevel gameLevel; // 当前游戏难度
};
现在考虑复杂事件
首先,初始化雷方块,点击该方块游戏结束
for(int i = 0; i < mRow; i++){
std::vector<MineBlock> lineBlocks;
for(int j = 0; j < mCol; j++)
{
MineBlock mineBlock;
mineBlock.curState = UN_DIG; // 默认都是未挖掘
mineBlock.valueFlag = 0; // 默认都是0
lineBlocks.push_back(mineBlock);
}
gameMap.push_back(lineBlocks);
}
下面是重点:
随机布雷
srand((unsigned int)time(0));
int k = totalMineNumber;
while(k > 0)
{
// 埋雷并防止重叠
int pRow = rand() % mRow;
int pCol = rand() % mCol;
if(gameMap[pRow][pCol].valueFlag != -1)
{
gameMap[pRow][pCol].valueFlag = -1;
k--; // 如果原来就有雷重新循环
}
}
计算雷周围的方块数字
for(int i = 0; i < mRow; i++)
{
for(int j = 0; j < mCol; j++)
{
// 周围八个方块(排除自己,在地图范围内)的数字根据雷的数目叠加
// y为行偏移量,x为列偏移量
// 前提条件是本方块不是雷
if(gameMap[i][j].valueFlag != -1)
{
for(int y = -1; y <= 1; y++)
{
for(int x = -1; x <= 1; x++)
{
if(i + y >= 0
&& i + y < mRow
&& j + x >= 0
&& j + x < mCol
&& gameMap[i + y][j + x].valueFlag == -1
&& !(x == 0 && y == 0))
{
// 方块数字加1
gameMap[i][j].valueFlag++;
}
}
}
}
}
}
}
下面是挖掘(翻开)函数内容:
正常方块且没有被翻开过,标记为已挖
if(gameMap[m][n].valueFlag > 0
&& gameMap[m][n].curState == UN_DIG)
{
gameMap[m][n].curState = DIGGED;
}
// 遇到空白块(数字0)就递归挖雷,如果踩雷就爆掉,游戏结束
if(gameMap[m][n].valueFlag == 0
&& gameMap[m][n].curState == UN_DIG)
{
gameMap[m][n].curState = DIGGED;
for(int y = -1; y <= 1; y++)
{
for(int x = -1; x <= 1; x++)
{
if(m + y >= 0
&& m + y < mRow
&& n + x >= 0
&& n + x < mCol
&& !(x == 0 && y == 0))
{
digMine(m + y, n + x);
}
}
}
}
// 踩雷了
if(gameMap[m][n].valueFlag == -1)
{
gameState = OVER;
gameMap[m][n].curState = BOMB;
}
// 检查游戏输赢,并作调整
检查游戏状态
下面是标记函数内容:
// 如果标记错了,就记为错误标记,在ui层游戏结束时做绘制区分
// 注意这里有个逻辑,如果一个方块标记两次会回到未挖掘的状态
if(gameMap[m][n].curState == UN_DIG)
{
if(gameMap[m][n].valueFlag == -1)
{
gameMap[m][n].curState = MARKED;
}
else
{
gameState = FAULT;
gameMap[m][n].curState = WRONG_BOMB;
}
curMineNumber--; // 挖对了雷就减1
}
else if(gameMap[m][n].curState == MARKED || gameMap[m][n].curState == WRONG_BOMB)
{
gameMap[m][n].curState = UN_DIG;
gameState = PLAYING;
curMineNumber++; // 雷数加回来
}
// 检查游戏输赢,并作调整
checkGame();checkGame()
{
// 游戏结束,显示所有雷
if(gameState == OVER)
{
// 输了就显示所有的雷以及标错的雷
for(int i = 0; i < mRow; i++)
{
for(int j = 0; j < mCol; j++)
{
if(gameMap[i][j].valueFlag == -1)
{
gameMap[i][j].curState = BOMB;
}
}
}
return;
}
// 如果雷排完了,且所有方块都挖出或者标记
if(gameState != FAULT)
{
for(int i = 0; i < mRow; i++)
{
for(int j = 0; j < mCol; j++)
{
if(gameMap[i][j].curState == UN_DIG)
{
gameState = PLAYING;
return;
}
}
}
// 否则既没有错误标记游戏状态又不是输或者进行中,游戏就是赢了
gameState = WIN;
}
}
编辑游戏窗口界面,关联信号槽
ui->setupUi(this); // 创建计时数字标签
timeLabel = new QLabel(this);
// 关联信号槽
connect(ui->actionStart, SIGNAL(triggered(bool)), this, SLOT(onStartGameClicked()));
connect(ui->actionBasic, SIGNAL(triggered(bool)), this, SLOT(onLevelChooseClicked()));
connect(ui->actionMedium, SIGNAL(triggered(bool)), this, SLOT(onLevelChooseClicked()));
connect(ui->actionHard, SIGNAL(triggered(bool)), this, SLOT(onLevelChooseClicked()));
connect(ui->actionQuit, SIGNAL(triggered(bool)), this, SLOT(onQuitClicked()));
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(updateTimer()));
初始化:
game = new GameModel; game->createGame();
setFixedSize(game->mCol * blockSize + offsetX * 2, game->mRow * blockSize + offsetY * 2 + spaceY);
timeLabel->setGeometry(game->mCol * blockSize + offsetX * 2 - 80, spaceY / 2, 80, 20);
timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s");
timer->start(1000);绘制笑脸
switch(game->gameState)
{
case OVER:
painter.drawPixmap((game->mCol * blockSize + offsetX * 2) / 2 - 12, spaceY / 2, bmpFaces, 0 * 24, 0, 24, 24);
break;
case PLAYING:
painter.drawPixmap((game->mCol * blockSize + offsetX * 2) / 2 - 12, spaceY / 2, bmpFaces, 1 * 24, 0, 24, 24);
break;
case WIN:
painter.drawPixmap((game->mCol * blockSize + offsetX * 2) / 2 - 12, spaceY / 2, bmpFaces, 2 * 24, 0, 24, 24);
break;
default:
painter.drawPixmap((game->mCol * blockSize + offsetX * 2) / 2 - 12, spaceY / 2, bmpFaces, 1 * 24, 0, 24, 24);
break;
}
绘制剩余雷数int n = game->curMineNumber;
int posX = (game->mCol * blockSize + offsetX * 2) / 2 - 50; // 最后一位数字的横坐标
if(n <= 0) // 如果雷数为0或者减到0以下,单独绘制
{
painter.drawPixmap(posX, spaceY / 2, bmpNumber, n * 20, 0, 20, 28); // 20是数字的宽,28是高
}
while(n > 0) // 如果是多位数
{
painter.drawPixmap(posX - 20, spaceY / 2, bmpNumber, n % 10 * 20, 0, 20, 28); // 每次从后面绘制一位
n /= 10;
posX -= 20;
}
绘制雷区for(int i = 0; i < game->mRow; i++)
{
for(int j = 0; j < game->mCol; j++)
{
switch(game->gameMap[i][j].curState)
{
// 根据不同的方块状态绘制,算出在bmp中的偏移量
case UN_DIG:
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY , bmpBlocks, blockSize * 10, 0, blockSize, blockSize);
break;
case DIGGED:
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY, bmpBlocks, blockSize * game->gameMap[i][j].valueFlag, 0, blockSize, blockSize);
break;
case MARKED:
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY, bmpBlocks, blockSize * 11, 0, blockSize, blockSize);
break;
case BOMB:
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY, bmpBlocks, blockSize * 9, 0, blockSize, blockSize);
break;
case WRONG_BOMB:
if(game->gameState == PLAYING || game->gameState == FAULT)
{
// 如果还在游戏中就显示旗子
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY, bmpBlocks, blockSize * 11, 0, blockSize, blockSize);
}
else if(game->gameState == OVER)
{
// 如果游戏已经结束,就显示标错了
painter.drawPixmap(j * blockSize + offsetX, i * blockSize + offsetY + spaceY, bmpBlocks, blockSize * 12, 0, blockSize, blockSize);
}
break;
default:
break;
}
}
}
// 处理游戏状态
handleGameState(game);
}
处理游戏状态
handleGameState(GameModel *game){
if(game->gameState == OVER)
{
timer->stop();
qDebug() << "you lose!";
}
else if(game->gameState == WIN)
{
timer->stop();
qDebug() << "CONGRATULATIONS!";
}
}
鼠标点击事件
mousePressEvent(QMouseEvent *event)
{
if(event->y() < spaceY + offsetY)
{
int x = event->x();
int y = event->y();
// 此时判断是否点击笑脸
if(x >= (game->mCol * blockSize + offsetX * 2) / 2 - 12
&& x <= (game->mCol * blockSize + offsetX * 2) / 2 + 12
&& y >= spaceY / 2
&& y <= spaceY / 2 + 24)
{
game->restartGame(); // 重玩
timer->start(1000);
timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s"); // 时间归零
update();
}
}
else if(game->gameState != OVER && game->gameState != WIN)
{
// 游戏没输或没赢才接受点击
// 此时判断点击的是哪个方块
// 获得点击坐标
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->digMine(row, col);
update(); // 每次点击都要重绘
break;
case Qt::RightButton:
game->markMine(row, col);
update();
break;
default:
break;
}
}
}
游戏逻辑大致是这样,不过要想实现还需要许多内容。
比如:
计时器计时
game->timerSeconds++;
timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s");
qDebug() << game->timerSeconds;退出
QCoreApplication::quit();
最后主函数:
main(int argc, char *argv[]){
QApplication a(argc, argv);
MainGameWindow w;
w.show();
return a.exec();
}
效果图: