掃雷作為Windows系統自帶的經典小游戲,承載了許多人的童年回憶。本文將詳細介紹如何使用Uniapp框架從零開始實現一個完整的掃雷游戲,包含核心算法、交互設計和狀態管理。無論你是Uniapp初學者還是有一定經驗的開發者,都能從本文中獲得啟發。
一、游戲設計思路
1.1 游戲規則回顧
掃雷游戲的基本規則非常簡單:
-
游戲區域由方格組成,部分方格下藏有地雷
-
玩家點擊方格可以揭開它
-
如果揭開的是地雷,游戲結束
-
如果揭開的是空白格子,會顯示周圍8格中的地雷數量
-
玩家可以通過標記來標識可能的地雷位置
-
當所有非地雷方格都被揭開時,玩家獲勝
1.2 技術實現要點
基于上述規則,我們需要實現以下核心功能:
-
游戲棋盤的數據結構
-
隨機布置地雷的算法
-
計算每個格子周圍地雷數量的方法
-
點擊和長按的交互處理
-
游戲狀態管理(進行中、勝利、失敗)
-
計時和剩余地雷數的顯示
二、Uniapp實現詳解
2.1 項目結構
我們創建一個單獨的頁面minesweeper/minesweeper.vue
來實現游戲,主要包含三個部分:
-
模板部分:游戲界面布局
-
腳本部分:游戲邏輯實現
-
樣式部分:游戲視覺效果
2.2 核心代碼解析
2.2.1 游戲數據初始化
data() {return {rows: 10, // 行數cols: 10, // 列數mines: 15, // 地雷數board: [], // 游戲棋盤數據remainingMines: 0, // 剩余地雷數time: 0, // 游戲時間timer: null, // 計時器gameOver: false, // 游戲是否結束gameOverMessage: '', // 結束消息firstClick: true // 是否是第一次點擊}
}
2.2.2 游戲初始化方法
startGame(rows, cols, mines) {this.rows = rows;this.cols = cols;this.mines = mines;this.remainingMines = mines;this.time = 0;this.gameOver = false;this.firstClick = true;// 初始化棋盤數據結構this.board = Array(rows).fill().map(() => Array(cols).fill().map(() => ({mine: false, // 是否是地雷revealed: false, // 是否已揭開flagged: false, // 是否已標記neighborMines: 0, // 周圍地雷數exploded: false // 是否爆炸(踩中地雷)})));
}
2.2.3 地雷布置算法
placeMines(firstRow, firstCol) {let minesPlaced = 0;// 隨機布置地雷,但避開第一次點擊位置及周圍while (minesPlaced < this.mines) {const row = Math.floor(Math.random() * this.rows);const col = Math.floor(Math.random() * this.cols);if (!this.board[row][col].mine && Math.abs(row - firstRow) > 1 && Math.abs(col - firstCol) > 1) {this.board[row][col].mine = true;minesPlaced++;}}// 計算每個格子周圍的地雷數for (let row = 0; row < this.rows; row++) {for (let col = 0; col < this.cols; col++) {if (!this.board[row][col].mine) {let count = 0;// 檢查周圍8個格子for (let r = Math.max(0, row - 1); r <= Math.min(this.rows - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(this.cols - 1, col + 1); c++) {if (this.board[r][c].mine) count++;}}this.board[row][col].neighborMines = count;}}}
}
?2.2.4 格子揭示邏輯
revealCell(row, col) {// 第一次點擊時布置地雷if (this.firstClick) {this.placeMines(row, col);this.startTimer();this.firstClick = false;}// 點擊到地雷if (this.board[row][col].mine) {this.board[row][col].exploded = true;this.gameOver = true;this.gameOverMessage = '游戲結束!你踩到地雷了!';this.revealAllMines();return;}// 遞歸揭示空白區域this.revealEmptyCells(row, col);// 檢查是否獲勝if (this.checkWin()) {this.gameOver = true;this.gameOverMessage = '恭喜你贏了!';}
}
2.2.5 遞歸揭示空白區域
revealEmptyCells(row, col) {// 邊界檢查if (row < 0 || row >= this.rows || col < 0 || col >= this.cols || this.board[row][col].revealed || this.board[row][col].flagged) {return;}this.board[row][col].revealed = true;// 如果是空白格子,遞歸揭示周圍的格子if (this.board[row][col].neighborMines === 0) {for (let r = Math.max(0, row - 1); r <= Math.min(this.rows - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(this.cols - 1, col + 1); c++) {if (r !== row || c !== col) {this.revealEmptyCells(r, c);}}}}
}
2.3 界面實現
游戲界面主要分為三個部分:
-
游戲信息區:顯示標題、剩余地雷數和用時
-
游戲棋盤:由方格組成的掃雷區域
-
控制區:難度選擇按鈕和游戲結束提示
<view class="game-board"><view v-for="(row, rowIndex) in board" :key="rowIndex" class="row"><view v-for="(cell, colIndex) in row" :key="colIndex" class="cell":class="{'revealed': cell.revealed,'flagged': cell.flagged,'mine': cell.revealed && cell.mine,'exploded': cell.exploded}"@click="revealCell(rowIndex, colIndex)"@longpress="toggleFlag(rowIndex, colIndex)"><!-- 顯示格子內容 --><text v-if="cell.revealed && !cell.mine && cell.neighborMines > 0">{{ cell.neighborMines }}</text><text v-else-if="cell.flagged">🚩</text><text v-else-if="cell.revealed && cell.mine">💣</text></view></view>
</view>
三、關鍵技術與優化點
3.1 性能優化
-
延遲布置地雷:只在第一次點擊后才布置地雷,確保第一次點擊不會踩雷,提升用戶體驗
-
遞歸算法優化:在揭示空白區域時使用遞歸算法,但要注意邊界條件,避免無限遞歸
3.2 交互設計
-
長按標記:使用
@longpress
事件實現標記功能,符合移動端操作習慣 -
視覺反饋:為不同類型的格子(普通、已揭示、標記、地雷、爆炸)設置不同的樣式
3.3 狀態管理
-
游戲狀態:使用
gameOver
和gameOverMessage
管理游戲結束狀態 -
計時器:使用
setInterval
實現游戲計時功能,注意在組件銷毀時清除計時器
四、擴展思路
這個基礎實現還可以進一步擴展:
-
本地存儲:使用uni.setStorage保存最佳成績
-
音效增強:添加點擊、標記、爆炸等音效
-
動畫效果:為格子添加翻轉動畫,增強視覺效果
-
自定義難度:允許玩家自定義棋盤大小和地雷數量
-
多平臺適配:優化在不同平臺(H5、小程序、App)上的顯示效果
五、總結
通過本文的介紹,我們完整實現了一個基于Uniapp的掃雷游戲,涵蓋了從數據結構設計、核心算法實現到用戶交互處理的全部流程。這個項目不僅可以幫助理解Uniapp的開發模式,也是學習游戲邏輯開發的好例子。讀者可以根據自己的需求進一步擴展和完善這個游戲。
完整代碼
<template><view class="minesweeper-container"><view class="game-header"><text class="title">掃雷游戲</text><view class="game-info"><text>剩余: {{ remainingMines }}</text><text>時間: {{ time }}</text></view></view><view class="game-board"><view v-for="(row, rowIndex) in board" :key="rowIndex" class="row"><view v-for="(cell, colIndex) in row" :key="colIndex" class="cell":class="{'revealed': cell.revealed,'flagged': cell.flagged,'mine': cell.revealed && cell.mine,'exploded': cell.exploded}"@click="revealCell(rowIndex, colIndex)"@longpress="toggleFlag(rowIndex, colIndex)"><text v-if="cell.revealed && !cell.mine && cell.neighborMines > 0">{{ cell.neighborMines }}</text><text v-else-if="cell.flagged">🚩</text><text v-else-if="cell.revealed && cell.mine">💣</text></view></view></view><view class="game-controls"><button @click="startGame(10, 10, 15)">初級 (10×10, 15雷)</button><button @click="startGame(15, 15, 40)">中級 (15×15, 40雷)</button><button @click="startGame(20, 20, 99)">高級 (20×20, 99雷)</button></view><view v-if="gameOver" class="game-over"><text>{{ gameOverMessage }}</text><button @click="startGame(rows, cols, mines)">再玩一次</button></view></view>
</template><script>
export default {data() {return {rows: 10,cols: 10,mines: 15,board: [],remainingMines: 0,time: 0,timer: null,gameOver: false,gameOverMessage: '',firstClick: true}},created() {this.startGame(10, 10, 15);},methods: {startGame(rows, cols, mines) {this.rows = rows;this.cols = cols;this.mines = mines;this.remainingMines = mines;this.time = 0;this.gameOver = false;this.firstClick = true;clearInterval(this.timer);// 初始化棋盤this.board = Array(rows).fill().map(() => Array(cols).fill().map(() => ({mine: false,revealed: false,flagged: false,neighborMines: 0,exploded: false})));},placeMines(firstRow, firstCol) {let minesPlaced = 0;while (minesPlaced < this.mines) {const row = Math.floor(Math.random() * this.rows);const col = Math.floor(Math.random() * this.cols);// 確保第一次點擊的位置和周圍沒有地雷if (!this.board[row][col].mine && Math.abs(row - firstRow) > 1 && Math.abs(col - firstCol) > 1) {this.board[row][col].mine = true;minesPlaced++;}}// 計算每個格子周圍的地雷數for (let row = 0; row < this.rows; row++) {for (let col = 0; col < this.cols; col++) {if (!this.board[row][col].mine) {let count = 0;for (let r = Math.max(0, row - 1); r <= Math.min(this.rows - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(this.cols - 1, col + 1); c++) {if (this.board[r][c].mine) count++;}}this.board[row][col].neighborMines = count;}}}},revealCell(row, col) {if (this.gameOver || this.board[row][col].revealed || this.board[row][col].flagged) {return;}// 第一次點擊時放置地雷并開始計時if (this.firstClick) {this.placeMines(row, col);this.startTimer();this.firstClick = false;}// 點擊到地雷if (this.board[row][col].mine) {this.board[row][col].exploded = true;this.gameOver = true;this.gameOverMessage = '游戲結束!你踩到地雷了!';this.revealAllMines();clearInterval(this.timer);return;}// 遞歸揭示空白區域this.revealEmptyCells(row, col);// 檢查是否獲勝if (this.checkWin()) {this.gameOver = true;this.gameOverMessage = '恭喜你贏了!';clearInterval(this.timer);}},revealEmptyCells(row, col) {if (row < 0 || row >= this.rows || col < 0 || col >= this.cols || this.board[row][col].revealed || this.board[row][col].flagged) {return;}this.board[row][col].revealed = true;if (this.board[row][col].neighborMines === 0) {// 如果是空白格子,遞歸揭示周圍的格子for (let r = Math.max(0, row - 1); r <= Math.min(this.rows - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(this.cols - 1, col + 1); c++) {if (r !== row || c !== col) {this.revealEmptyCells(r, c);}}}}},toggleFlag(row, col) {if (this.gameOver || this.board[row][col].revealed) {return;}if (this.board[row][col].flagged) {this.board[row][col].flagged = false;this.remainingMines++;} else if (this.remainingMines > 0) {this.board[row][col].flagged = true;this.remainingMines--;}},startTimer() {clearInterval(this.timer);this.timer = setInterval(() => {this.time++;}, 1000);},revealAllMines() {for (let row = 0; row < this.rows; row++) {for (let col = 0; col < this.cols; col++) {if (this.board[row][col].mine) {this.board[row][col].revealed = true;}}}},checkWin() {for (let row = 0; row < this.rows; row++) {for (let col = 0; col < this.cols; col++) {if (!this.board[row][col].mine && !this.board[row][col].revealed) {return false;}}}return true;}},beforeDestroy() {clearInterval(this.timer);}
}
</script><style>
.minesweeper-container {padding: 20px;display: flex;flex-direction: column;align-items: center;
}.game-header {margin-bottom: 20px;text-align: center;
}.game-header .title {font-size: 24px;font-weight: bold;margin-bottom: 10px;
}.game-info {display: flex;justify-content: space-around;width: 100%;
}.game-board {border: 2px solid #333;margin-bottom: 20px;
}.row {display: flex;
}.cell {width: 30px;height: 30px;border: 1px solid #ccc;display: flex;justify-content: center;align-items: center;background-color: #ddd;font-weight: bold;
}.cell.revealed {background-color: #fff;
}.cell.flagged {background-color: #ffeb3b;
}.cell.mine {background-color: #f44336;
}.cell.exploded {background-color: #d32f2f;
}.game-controls {display: flex;flex-direction: column;gap: 10px;width: 100%;max-width: 300px;
}.game-over {margin-top: 20px;text-align: center;font-size: 18px;font-weight: bold;
}button {margin-top: 10px;padding: 10px;background-color: #4CAF50;color: white;border: none;border-radius: 5px;cursor: pointer;
}button:active {background-color: #3e8e41;
}
</style>
?