通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
连连看是一个配对消除游戏,点击两个相同的图案,如果能用不超过两个拐角的线连接,就消除。
路径判断是连连看的核心算法,这篇来聊聊怎么实现。这个算法看起来复杂,但拆解开来其实很清晰:先检查直线,再检查一个拐角,最后检查两个拐角。
我在实现这个功能的时候,最开始只考虑了直线连接,后来发现很多明显能连的图案连不上,才意识到还要处理拐角的情况。

连接规则
连连看的连接规则是:两个图案之间的路径最多只能有两个拐角。
也就是说,路径可以是:
- 直线: 没有拐角,两点在同一行或同一列
- 一个拐角: L形,路径转了一次弯
- 两个拐角: Z形或U形,路径转了两次弯
为什么是两个拐角?这是连连看的经典规则,太少了游戏太难,太多了游戏太简单。两个拐角刚好平衡了难度和可玩性。
状态变量定义
dart
List<List<int>> board = [];
棋盘数据,二维数组。board[r][c]的值表示该位置的图案类型,-1表示已消除的空位。
dart
int? selectedRow, selectedCol;
当前选中的格子坐标。null表示没有选中任何格子。
dart
int score = 0;
当前得分,每消除一对加10分。
dart
int remaining = 0;
剩余的图案数量,全部消除后游戏胜利。
_canConnect方法
dart
bool _canConnect(int r1, int c1, int r2, int c2) {
// Direct line
if (_directLine(r1, c1, r2, c2)) return true;
// One turn
if (_isEmpty(r1, c2) && _directLine(r1, c1, r1, c2) && _directLine(r1, c2, r2, c2)) return true;
if (_isEmpty(r2, c1) && _directLine(r1, c1, r2, c1) && _directLine(r2, c1, r2, c2)) return true;
// Two turns
for (int r = 0; r < rows; r++) {
if (_isEmpty(r, c1) && _isEmpty(r, c2) && _directLine(r1, c1, r, c1) && _directLine(r, c1, r, c2) && _directLine(r, c2, r2, c2)) return true;
}
for (int c = 0; c < cols; c++) {
if (_isEmpty(r1, c) && _isEmpty(r2, c) && _directLine(r1, c1, r1, c) && _directLine(r1, c, r2, c) && _directLine(r2, c, r2, c2)) return true;
}
return false;
}
这是连连看的核心算法,判断两个点能否连接。方法按照从简单到复杂的顺序检查:直线 → 一个拐角 → 两个拐角。
直线连接
dart
if (_directLine(r1, c1, r2, c2)) return true;
两点在同一行或同一列,中间没有障碍物。这是最简单的情况,直接调用_directLine检查。
如果能直线连接,立即返回true,不需要检查更复杂的路径。这种"提前返回"的写法可以提高效率。
一个拐角
dart
if (_isEmpty(r1, c2) && _directLine(r1, c1, r1, c2) && _directLine(r1, c2, r2, c2)) return true;
if (_isEmpty(r2, c1) && _directLine(r1, c1, r2, c1) && _directLine(r2, c1, r2, c2)) return true;
拐点有两种可能:
- (r1, c2): 第一个点的行,第二个点的列
- (r2, c1): 第二个点的行,第一个点的列
想象一下,从点A到点B画一个L形,拐点要么在A的水平方向、B的垂直方向的交点,要么在A的垂直方向、B的水平方向的交点。
拐点必须是空的(_isEmpty),两段路径都要是直线(_directLine)。三个条件用&&连接,全部满足才能连接。
两个拐角 - 水平中间线
dart
for (int r = 0; r < rows; r++) {
if (_isEmpty(r, c1) && _isEmpty(r, c2) && _directLine(r1, c1, r, c1) && _directLine(r, c1, r, c2) && _directLine(r, c2, r2, c2)) return true;
}
遍历所有行,找一条水平的中间线。
路径是:(r1,c1) → (r,c1) → (r,c2) → (r2,c2)
这是一个Z形或U形的路径,有两个拐点:(r,c1)和(r,c2)。两个拐点都要是空的,三段路径都要是直线。
为什么要遍历所有行?因为中间线可以在任何位置,我们需要找到一条可行的路径。只要找到一条就返回true。
两个拐角 - 垂直中间线
dart
for (int c = 0; c < cols; c++) {
if (_isEmpty(r1, c) && _isEmpty(r2, c) && _directLine(r1, c1, r1, c) && _directLine(r1, c, r2, c) && _directLine(r2, c, r2, c2)) return true;
}
同样的逻辑,遍历所有列,找一条垂直的中间线。
路径是:(r1,c1) → (r1,c) → (r2,c) → (r2,c2)
两种中间线(水平和垂直)覆盖了所有两个拐角的情况。
返回false
dart
return false;
如果所有情况都检查过了,还是找不到可行路径,返回false,表示不能连接。
_directLine方法
dart
bool _directLine(int r1, int c1, int r2, int c2) {
if (r1 == r2) {
int minC = min(c1, c2), maxC = max(c1, c2);
for (int c = minC + 1; c < maxC; c++) if (board[r1][c] != -1) return false;
return true;
}
if (c1 == c2) {
int minR = min(r1, r2), maxR = max(r1, r2);
for (int r = minR + 1; r < maxR; r++) if (board[r][c1] != -1) return false;
return true;
}
return false;
}
这个方法检查两点之间是否能直线连接。直线连接的条件是:两点在同一行或同一列,且中间没有障碍物。
同一行
dart
if (r1 == r2) {
int minC = min(c1, c2), maxC = max(c1, c2);
for (int c = minC + 1; c < maxC; c++) if (board[r1][c] != -1) return false;
return true;
}
检查两点之间的所有格子是否都是空的(-1)。
注意是minC + 1到maxC - 1,不包括两端点。因为两端点是要连接的图案,不是障碍物。
用min和max是因为不知道哪个点在左边,哪个在右边。这样写不管点的顺序如何,都能正确处理。
同一列
dart
if (c1 == c2) {
int minR = min(r1, r2), maxR = max(r1, r2);
for (int r = minR + 1; r < maxR; r++) if (board[r][c1] != -1) return false;
return true;
}
同样的逻辑,检查垂直方向。遍历两点之间的所有行,如果有任何一个格子不是-1(有图案),就返回false。
不在同一行也不在同一列
dart
return false;
不是直线,返回false。这种情况需要通过拐角来连接。
_isEmpty方法
dart
bool _isEmpty(int r, int c) => r >= 0 && r < rows && c >= 0 && c < cols && board[r][c] == -1;
检查一个位置是否是空的:
- 在棋盘范围内(r和c都在有效范围)
- 值是-1(已消除)
这个方法用于检查拐点是否可用。拐点必须是空的,否则路径会被阻挡。
边界检查很重要,因为拐点可能在棋盘外面(比如从边缘绕过去)。如果不检查边界,访问board[-1][0]会报错。
点击处理
dart
void _tap(int row, int col) {
if (board[row][col] == -1) return;
setState(() {
if (selectedRow == null) {
selectedRow = row;
selectedCol = col;
} else if (selectedRow == row && selectedCol == col) {
selectedRow = selectedCol = null;
} else {
if (board[selectedRow!][selectedCol!] == board[row][col] && _canConnect(selectedRow!, selectedCol!, row, col)) {
board[selectedRow!][selectedCol!] = -1;
board[row][col] = -1;
score += 10;
remaining -= 2;
if (remaining == 0) _showWinDialog();
}
selectedRow = selectedCol = null;
}
});
}
这个方法处理玩家的点击操作,是游戏交互的核心。
点击空格
dart
if (board[row][col] == -1) return;
已消除的格子不能点击。-1表示这个位置已经没有图案了,点击无效。
第一次点击
dart
if (selectedRow == null) {
selectedRow = row;
selectedCol = col;
}
如果还没有选中任何格子,记录当前点击的位置。这是配对的第一步。
点击同一个
dart
else if (selectedRow == row && selectedCol == col) {
selectedRow = selectedCol = null;
}
如果点击的是已经选中的格子,取消选中。这是一个常见的交互设计,让玩家可以"反悔"。
点击另一个
dart
if (board[selectedRow!][selectedCol!] == board[row][col] && _canConnect(...)) {
board[selectedRow!][selectedCol!] = -1;
board[row][col] = -1;
score += 10;
remaining -= 2;
if (remaining == 0) _showWinDialog();
}
selectedRow = selectedCol = null;
点击了另一个格子,需要判断能否消除:
- 图案相同(board值相等)
- 能够连接(_canConnect返回true)
两个条件都满足才能消除。消除后:
- 两个格子都设为-1
- 得分加10
- 剩余数量减2
- 检查是否胜利
不管能否消除,都清空选中状态,准备下一轮。
选中高亮
dart
bool selected = r == selectedRow && c == selectedCol;
...
color: value == -1 ? Colors.grey[200] : (selected ? Colors.yellow[200] : Colors.blue[100]),
border: selected ? Border.all(color: Colors.orange, width: 2) : null,
选中的格子用黄色背景和橙色边框高亮。这个视觉反馈很重要,让玩家知道当前选中了哪个格子。
颜色逻辑
dart
color: value == -1 ? Colors.grey[200] : (selected ? Colors.yellow[200] : Colors.blue[100])
三种颜色:
- 灰色: 已消除的空位
- 黄色: 选中的格子
- 蓝色: 普通格子
边框逻辑
dart
border: selected ? Border.all(color: Colors.orange, width: 2) : null
只有选中的格子有边框,2像素宽的橙色边框。橙色和黄色搭配,视觉效果很明显。
图案显示
dart
child: value >= 0 ? Text(
icons[value],
style: const TextStyle(fontSize: 24),
) : null,
如果格子有图案(value >= 0),显示对应的图标;如果是空位(value == -1),不显示任何内容。
icons是一个图标列表,用索引来获取对应的图标。这样设计让图案的添加和修改很方便。
棋盘初始化
dart
void _initGame() {
final random = Random();
List<int> pairs = [];
for (int i = 0; i < (rows * cols) ~/ 2; i++) {
int icon = random.nextInt(icons.length);
pairs.add(icon);
pairs.add(icon);
}
pairs.shuffle();
board = List.generate(rows, (r) =>
List.generate(cols, (c) => pairs[r * cols + c])
);
remaining = rows * cols;
score = 0;
selectedRow = selectedCol = null;
}
初始化棋盘:
- 生成配对的图案(每种图案两个)
- 打乱顺序
- 填充到二维数组
- 重置得分和选中状态
(rows * cols) ~/ 2计算需要多少对图案。~/是Dart的整除运算符。
胜利检查
dart
if (remaining == 0) _showWinDialog();
当剩余图案数量为0时,游戏胜利。remaining在每次消除时减2,最终会变成0。
小结
这篇讲了连连看的路径连线,核心知识点:
- 三种路径: 直线、一个拐角、两个拐角,覆盖所有可能的连接方式
- _directLine: 检查两点之间是否畅通,遍历中间的所有格子
- _isEmpty: 检查拐点是否为空,包含边界检查
- 遍历中间线: 两个拐角时遍历所有可能的中间线,找到一条可行路径
- -1表示空: 消除后的格子值为-1,统一表示空位
- 选中高亮: 黄色背景 + 橙色边框,视觉反馈清晰
- 配对消除: 图案相同且能连接才能消除
- min/max处理: 不依赖点的顺序,代码更健壮
路径判断是连连看的核心算法,理解了三种路径的检查方式,游戏就完成了。这个算法的时间复杂度是O(n),n是棋盘的行数或列数,效率很高。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net