继续把Qt小游戏写一下~
// ---------------- 全局定义变量 ---------------- // // 方块的状态,未挖掘,翻开,标记,雷出现,错误标记 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 }; // 游戏默认参数 const int kRow = 15; const int kCol = 20; const int kMineCount = 50; const int kTime = 0; // ----------------------------------------------- //
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; // 当前游戏难度 };
void GameModel::createGame(int row, int col, int mineCount, GameLevel level) { // 先清空已经有的游戏地图 gameMap.clear(); // 设置成员变量 mRow = row; mCol = col; totalMineNumber = mineCount; curMineNumber = mineCount; gameState = PLAYING; gameLevel = level; timerSeconds = 0; // 初始化雷方块 for(int i = 0; i < mRow; i++) { //添加每行的block 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++; } } } } } } }
void GameModel::digMine(int m, int n) { // 正常方块且没有被翻开过,标记为已挖 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; } // 检查游戏输赢,并作调整 checkGame(); }
void GameModel::markMine(int m, int n) { // 如果标记错了,就记为错误标记,在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(); }
void GameModel::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; } }
class MainGameWindow : public QMainWindow { Q_OBJECT public: explicit MainGameWindow(QWidget *parent = 0); ~MainGameWindow(); protected: virtual void paintEvent(QPaintEvent *event); // 界面重绘 virtual void mousePressEvent(QMouseEvent *event); // 鼠标控制 private: Ui::MainGameWindow *ui; GameModel *game; // 游戏 QTimer *timer; // 计时器 QLabel *timeLabel; // 计时数字 void handleGameState(GameModel *game); // 处理游戏状态 private slots: void onStartGameClicked(); // 开始游戏 void onLevelChooseClicked(); // 选择游戏难度 void onQuitClicked(); // 退出游戏 void updateTimer(); // 计时 };
MainGameWindow::MainGameWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainGameWindow) { 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); }这里面关联了一些button的的信号槽,初始化了界面和游戏模型,设置一个定时器用于游戏计时。
void MainGameWindow::paintEvent(QPaintEvent *event) { QPainter painter(this); QPixmap bmpBlocks(":/res/blocks.bmp"); QPixmap bmpFaces(":/res/faces.bmp"); QPixmap bmpFrame(":/res/frame.bmp"); QPixmap bmpNumber(":/res/timenumber.bmp"); // 绘制笑脸 switch(game->gameState) { case OVER: painter.drawPixmap((game->mCol * blockSize + offsetX * 2) / 2 - 12, spaceY / 2, bmpFaces, 0 * 24, 0, 24, 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); }根据游戏的状态和游戏模型中各方块的情况、计时器的情况,进行针对性的重绘,实现游戏界面更新。
void MainGameWindow::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"); // 每次重玩都将计时显示为0s 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; } } }做了简单的碰撞检测,左键挖雷,右键标记。
void MainGameWindow::onLevelChooseClicked() { QAction *actionSender = (QAction *)dynamic_cast<QAction *>(sender()); if(actionSender == ui->actionBasic) { qDebug() << "basic"; // 先设置游戏模型 game->createGame(8, 10, 15, BASIC); } else if(actionSender == ui->actionMedium) { qDebug() << "medium"; game->createGame(15, 20, 50, MEDIUM); } else if(actionSender == ui->actionHard) { qDebug() << "hard"; game->createGame(20, 30, 100, HARD); } // 重新计时 timer->start(1000); // 再刷新UI,窗口大小改变会强制重绘 timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s"); timeLabel->setGeometry(game->mCol * blockSize + offsetX * 2 - 80, spaceY / 2, 80, 20); setFixedSize(game->mCol * blockSize + offsetX * 2, game->mRow * blockSize + offsetY * 2 + spaceY); }选择难度里面可以根据信号槽中的信号类型,设置不同的难度,直接对game设置参数,方便快捷。
void MainGameWindow::updateTimer() { // 计时器计时 game->timerSeconds++; timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s"); qDebug() << game->timerSeconds; }游戏结束可以看到计时成绩