繼續把Qt小游戲寫一下~
整體的代碼結構,游戲邏輯類和游戲界面類分離,采用MVC的思想。
1 定義游戲數據結構
游戲地圖實際上是由一個個方塊組成的二維矩陣,每個方塊存儲數字、雷或者標記的情況,另外還要定義一些游戲的運行狀態枚舉
// ---------------- 全局定義變量 ---------------- //
// 方塊的狀態,未挖掘,翻開,標記,雷出現,錯誤標記
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;
// ----------------------------------------------- //
2 創建游戲邏輯類
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> 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 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++;
}
}
}
}
}
}
}
隨機布雷計算方塊數字
(2)挖雷
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();
}
遞歸挖雷挖到雷游戲結束
(3)標記方塊
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();
}
標記雷,並且當前顯示雷數跟著減標記錯誤了會把游戲狀態設置成FAULT如果挖完了且挖對了游戲就贏了
(4)檢查游戲狀態
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;
}
}
這個函數每次在挖雷和標記雷時都要調用每次檢查都要更新游戲狀態
4 游戲界面類
游戲界面其實就是在window裡面不斷重繪,並且設置鼠標點擊監聽
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(); // 計時
};
5 游戲界面控制
(1)啟動時設置一些元素並初始化游戲模型
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的的信號槽,初始化了界面和游戲模型,設置一個定時器用於游戲計時。
(2)窗口重繪
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);
}
根據游戲的狀態和游戲模型中各方塊的情況、計時器的情況,進行針對性的重繪,實現游戲界面更新。
這裡面用到了位圖偏移量繪制。
(3)鼠標控制
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;
}
}
}
做了簡單的碰撞檢測,左鍵挖雷,右鍵標記。
(4)選擇難度
void MainGameWindow::onLevelChooseClicked()
{
QAction *actionSender = (QAction *)dynamic_cast(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設置參數,方便快捷。
(5)計時
void MainGameWindow::updateTimer()
{
// 計時器計時
game->timerSeconds++;
timeLabel->setText("Time: " + QString::number(game->timerSeconds) + " s");
qDebug() << game->timerSeconds;
}
游戲結束可以看到計時成績


