扫雷作为Windows系统自带的经典小游戏,承载了许多人的童年回忆。本文将详细介绍如何使用Uniapp框架从零开始实现一个完整的扫雷游戏,包含核心算法、交互设计和状态管理。无论你是Uniapp初学者还是有一定经验的开发者,都能从本文中获得启发。
一、游戏设计思路
1.1 游戏规则回顾
扫雷游戏的基本规则非常简单:
-
游戏区域由方格组成,部分方格下藏有地雷
-
玩家点击方格可以揭开它
-
如果揭开的是地雷,游戏结束
-
如果揭开的是空白格子,会显示周围8格中的地雷数量
-
玩家可以通过标记来标识可能的地雷位置
-
当所有非地雷方格都被揭开时,玩家获胜
1.2 技术实现要点
基于上述规则,我们需要实现以下核心功能:
-
游戏棋盘的数据结构
-
随机布置地雷的算法
-
计算每个格子周围地雷数量的方法
-
点击和长按的交互处理
-
游戏状态管理(进行中、胜利、失败)
-
计时和剩余地雷数的显示
二、Uniapp实现详解
2.1 项目结构
我们创建一个单独的页面minesweeper/minesweeper.vue
来实现游戏,主要包含三个部分:
-
模板部分:游戏界面布局
-
脚本部分:游戏逻辑实现
-
样式部分:游戏视觉效果
2.2 核心代码解析
2.2.1 游戏数据初始化
javascript">data() {return {rows: 10, // 行数cols: 10, // 列数mines: 15, // 地雷数board: [], // 游戏棋盘数据remainingMines: 0, // 剩余地雷数time: 0, // 游戏时间timer: null, // 计时器gameOver: false, // 游戏是否结束gameOverMessage: '', // 结束消息firstClick: true // 是否是第一次点击}
}
2.2.2 游戏初始化方法
javascript">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 地雷布置算法
javascript">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 格子揭示逻辑
javascript">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 递归揭示空白区域
javascript">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>