
扫雷作为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 界面实现
游戏界面主要分为三个部分:
-
游戏信息区:显示标题、剩余地雷数和用时
-
游戏棋盘:由方格组成的扫雷区域
-
控制区:难度选择按钮和游戏结束提示
html
<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的开发模式,也是学习游戏逻辑开发的好例子。读者可以根据自己的需求进一步扩展和完善这个游戏。
完整代码
html
<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>