Flutter for OpenHarmony游戏集合App实战之扫雷数字显示
本文介绍了Flutter for OpenHarmony扫雷游戏中数字显示的实现方式。文章说明数字代表格子周围的地雷数量,并通过Cell结构的adjacentMines属性存储。初始化时使用_countAdjacentMines方法遍历相邻格子并进行边界检查,预计算每个格子的周围雷数。显示时通过_getCellContent根据格子状态返回旗帜、地雷图标或彩色数字,颜色由_getNumberCol
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言

玩过Windows扫雷的都知道,点开一个格子后,如果周围有雷,格子里会显示一个数字,告诉你周围8个格子里有几颗雷。而且这个数字是有颜色的:1是蓝色,2是绿色,3是红色……
这个彩色数字是扫雷游戏的灵魂,玩家就是靠这些数字来推理哪里有雷。这篇就来聊聊怎么实现这个数字显示功能。
数字的含义
先搞清楚这个数字是怎么来的。
扫雷的棋盘是一个二维网格,每个格子要么是雷,要么不是雷。当玩家点开一个非雷格子时,需要显示这个格子周围8个方向有多少颗雷。
所谓周围8个方向,就是上、下、左、右、左上、右上、左下、右下这8个相邻的格子。如果某个方向超出了棋盘边界,就不算。
比如一个格子周围有2颗雷,就显示数字2;周围没有雷,就什么都不显示(空白)。
Cell数据结构
每个格子用一个Cell对象表示:
class Cell {
bool isMine = false;
bool isRevealed = false;
bool isFlagged = false;
int adjacentMines = 0;
}
四个属性各有用途:
- isMine: 这个格子是不是雷
- isRevealed: 这个格子有没有被翻开
- isFlagged: 这个格子有没有被插旗标记
- adjacentMines: 周围有几颗雷(这就是要显示的数字)
adjacentMines在游戏初始化时就计算好了,不是每次显示时才算。这样点击格子时直接读取就行,不用重复计算。
计算周围雷数
游戏初始化时,随机放置完地雷后,要遍历每个非雷格子,计算它周围的雷数:
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (!grid[i][j].isMine) {
grid[i][j].adjacentMines = _countAdjacentMines(i, j);
}
}
}
两层循环遍历所有格子。如果这个格子不是雷(!grid[i][j].isMine),就调用_countAdjacentMines计算周围雷数,存到adjacentMines属性里。
💡 为什么雷格子不用算? 因为雷格子被点开后游戏就结束了,不需要显示数字。所以只有非雷格子才需要计算。
_countAdjacentMines方法
这个方法是计算的核心:
int _countAdjacentMines(int row, int col) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
int r = row + i;
int c = col + j;
if (r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c].isMine) {
count++;
}
}
}
return count;
}
逻辑拆解一下:
外层两个循环i和j都是从-1到1,组合起来就是9种情况:
- i=-1, j=-1:左上
- i=-1, j=0:上
- i=-1, j=1:右上
- i=0, j=-1:左
- i=0, j=0:自己(会被跳过,因为自己不是雷的话不会计数)
- i=0, j=1:右
- i=1, j=-1:左下
- i=1, j=0:下
- i=1, j=1:右下
r = row + i和c = col + j计算出相邻格子的坐标。
然后是边界检查:
if (r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c].isMine) {
r >= 0:行号不能小于0r < rows:行号不能超出棋盘c >= 0:列号不能小于0c < cols:列号不能超出棋盘grid[r][c].isMine:这个相邻格子是雷
只有同时满足这5个条件,才把计数器加1。
💡 边界检查很重要。 如果格子在棋盘边缘或角落,它的某些方向是没有相邻格子的。比如左上角的格子,它的左边、上边、左上都超出了棋盘。不做边界检查会导致数组越界崩溃。
显示格子内容
格子的内容由_getCellContent方法决定:
Widget _getCellContent(Cell cell) {
if (cell.isFlagged && !cell.isRevealed) {
return const Icon(Icons.flag, color: Colors.red, size: 20);
}
if (!cell.isRevealed) {
return const SizedBox();
}
if (cell.isMine) {
return const Icon(Icons.brightness_7, color: Colors.black, size: 20);
}
if (cell.adjacentMines > 0) {
return Text(
'${cell.adjacentMines}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getNumberColor(cell.adjacentMines),
),
);
}
return const SizedBox();
}
这个方法根据格子的状态返回不同的Widget,逻辑是从上到下依次判断:
第一个判断:插旗状态
if (cell.isFlagged && !cell.isRevealed) {
return const Icon(Icons.flag, color: Colors.red, size: 20);
}
如果格子被插了旗(isFlagged)而且还没被翻开(!isRevealed),显示一个红色的旗帜图标。
第二个判断:未翻开状态
if (!cell.isRevealed) {
return const SizedBox();
}
如果格子还没翻开,返回一个空的SizedBox,什么都不显示。格子的背景色会显示它是未翻开的状态。
第三个判断:是雷
if (cell.isMine) {
return const Icon(Icons.brightness_7, color: Colors.black, size: 20);
}
如果是雷,显示一个黑色的太阳图标(Icons.brightness_7看起来像爆炸的雷)。这种情况只有游戏结束时才会出现,因为点到雷游戏就结束了,会把所有雷都翻开。
第四个判断:有相邻雷
if (cell.adjacentMines > 0) {
return Text(
'${cell.adjacentMines}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getNumberColor(cell.adjacentMines),
),
);
}
如果周围有雷(adjacentMines > 0),显示数字。数字用Text组件,样式是加粗的,颜色由_getNumberColor方法根据数字决定。
最后:周围没雷
return const SizedBox();
如果以上都不满足,说明这是一个翻开的、周围没有雷的空白格子,返回空的SizedBox。
数字颜色映射
经典扫雷的数字颜色是有讲究的,每个数字对应固定的颜色:
Color _getNumberColor(int number) {
switch (number) {
case 1: return Colors.blue;
case 2: return Colors.green;
case 3: return Colors.red;
case 4: return Colors.purple;
case 5: return Colors.brown;
case 6: return Colors.cyan;
case 7: return Colors.black;
case 8: return Colors.grey;
default: return Colors.black;
}
}
这个配色方案是还原Windows扫雷的经典配色:
- 1 - 蓝色:最常见的数字,用冷静的蓝色
- 2 - 绿色:比较安全的感觉
- 3 - 红色:开始有点危险了,用警示的红色
- 4 - 深紫色:更危险
- 5 - 棕色:很少见,周围一半都是雷
- 6 - 青色:非常少见
- 7 - 黑色:极其罕见
- 8 - 灰色:理论上的最大值,周围全是雷
💡 为什么要用不同颜色? 一是为了好看,二是为了快速识别。玩家扫一眼就能知道这是几,不用仔细看数字。颜色比数字更容易被大脑识别。
default分支理论上不会执行,因为adjacentMines的值只可能是0-8。但加上它是个好习惯,防止意外情况。
数字的样式
再看一下数字的TextStyle:
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getNumberColor(cell.adjacentMines),
),
- fontWeight: FontWeight.bold:加粗显示,让数字更醒目
- color:根据数字获取对应的颜色
没有设置fontSize,用的是默认字号。因为格子大小是根据屏幕自适应的,默认字号在大多数情况下都合适。如果格子特别小,可能需要调小字号。
格子的构建
数字显示是格子的一部分,看看完整的格子构建:
Widget _buildCell(int row, int col) {
Cell cell = grid[row][col];
return GestureDetector(
onTap: () => _revealCell(row, col),
onLongPress: () => _toggleFlag(row, col),
child: Container(
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: cell.isRevealed
? (cell.isMine ? Colors.red : Colors.grey[300])
: Colors.blue[400],
borderRadius: BorderRadius.circular(4),
格子用GestureDetector包裹,处理点击(翻开)和长按(插旗)事件。
Container设置了格子的外观:
- margin: 格子之间留1像素间隙,不会挤在一起
- color: 根据状态显示不同颜色
- 未翻开:蓝色(
Colors.blue[400]) - 翻开且是雷:红色
- 翻开且不是雷:浅灰色(
Colors.grey[300])
- 未翻开:蓝色(
- borderRadius: 圆角4像素,看起来更柔和
格子的内容用Center包裹,让数字/图标居中显示:
child: Center(
child: _getCellContent(cell),
),
未翻开格子的阴影
未翻开的格子还有一个细节——阴影效果:
boxShadow: cell.isRevealed
? null
: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(1, 1),
blurRadius: 2,
),
],
只有未翻开的格子才有阴影(cell.isRevealed ? null : [...])。
阴影的参数:
- color: 黑色,20%透明度,不会太重
- offset: 向右下偏移1像素,模拟光源在左上方
- blurRadius: 模糊半径2像素,阴影边缘柔和
这个阴影让未翻开的格子看起来像是凸起的按钮,翻开后阴影消失,变成平的,视觉上有"按下去"的感觉。
为什么数字要预计算
有人可能会问:为什么要在初始化时就算好所有格子的adjacentMines,而不是显示时再算?
两个原因:
1. 性能考虑
每次setState后,所有格子都会重新build。如果每次build都要算一遍周围雷数,100个格子就要算100次,每次算要检查8个方向,总共800次检查。虽然现代手机算这点东西不费劲,但能省则省。
预计算的话,初始化时算一次就够了,后面显示时直接读取。
2. 逻辑清晰
把"计算雷数"和"显示数字"分开,代码更清晰。初始化负责准备数据,显示负责渲染UI,各司其职。
如果混在一起,_getCellContent方法会变得很复杂,又要判断状态,又要计算雷数,不好维护。
空白格子的连锁翻开
还有一个相关的功能:当翻开一个周围没有雷的格子(adjacentMines == 0)时,会自动翻开周围的格子,如果周围也有空白格子,继续连锁翻开。
if (grid[row][col].adjacentMines == 0) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
_revealCell(row + i, col + j);
}
}
}
这是一个递归调用。翻开当前格子后,如果是空白,就对周围8个格子也调用_revealCell。如果周围的格子也是空白,会继续递归,直到遇到有数字的格子为止。
这就是为什么扫雷里点一个空白区域,会"哗"地一下翻开一大片。
小结
这篇讲了扫雷游戏中数字显示的实现,核心知识点:
- Cell数据结构存储格子状态,
adjacentMines记录周围雷数 - _countAdjacentMines遍历周围8个方向,统计雷的数量,注意边界检查
- _getCellContent根据格子状态返回不同的Widget:旗帜、空白、雷、数字
- _getNumberColor用switch语句映射数字到颜色,还原经典扫雷配色
- 预计算雷数而不是实时计算,性能更好,逻辑更清晰
- 阴影效果让未翻开的格子有立体感
数字显示看起来简单,但涉及到数据结构设计、算法实现、UI渲染等多个方面。把这些搞清楚,扫雷游戏的核心就完成一大半了。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)