Flutter for OpenHarmony游戏集合App实战之连连看路径连线

通过网盘分享的文件:game_flutter_openharmony.zip

链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

连连看是一个配对消除游戏,点击两个相同的图案,如果能用不超过两个拐角的线连接,就消除。

路径判断是连连看的核心算法,这篇来聊聊怎么实现。这个算法看起来复杂,但拆解开来其实很清晰:先检查直线,再检查一个拐角,最后检查两个拐角。

我在实现这个功能的时候,最开始只考虑了直线连接,后来发现很多明显能连的图案连不上,才意识到还要处理拐角的情况。

连接规则

连连看的连接规则是:两个图案之间的路径最多只能有两个拐角。

也就是说,路径可以是:

  1. 直线: 没有拐角,两点在同一行或同一列
  2. 一个拐角: L形,路径转了一次弯
  3. 两个拐角: 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 + 1maxC - 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;

检查一个位置是否是空的:

  1. 在棋盘范围内(r和c都在有效范围)
  2. 值是-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;

点击了另一个格子,需要判断能否消除:

  1. 图案相同(board值相等)
  2. 能够连接(_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;
}

初始化棋盘:

  1. 生成配对的图案(每种图案两个)
  2. 打乱顺序
  3. 填充到二维数组
  4. 重置得分和选中状态

(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

相关推荐
C系语言1 小时前
python用pip生成requirements.txt
开发语言·python·pip
燃于AC之乐2 小时前
深入解剖STL Vector:从底层原理到核心接口的灵活运用
开发语言·c++·迭代器·stl·vector·源码分析·底层原理
小风呼呼吹儿2 小时前
Flutter 框架跨平台鸿蒙开发 - 社区团购记账应用开发教程
flutter·华为·harmonyos
星火开发设计8 小时前
C++ 数组:一维数组的定义、遍历与常见操作
java·开发语言·数据结构·c++·学习·数组·知识
search79 小时前
前端设计:CRG 3--CDC error
前端
治金的blog9 小时前
vben-admin和vite,ant-design-vue的结合的联系
前端·vscode
TTGGGFF9 小时前
控制系统建模仿真(一):掌握控制系统设计的 MAD 流程与 MATLAB 基础运算
开发语言·matlab
2501_944424129 小时前
Flutter for OpenHarmony游戏集合App实战之贪吃蛇食物生成
android·开发语言·flutter·游戏·harmonyos
不会写代码0009 小时前
Flutter 框架跨平台鸿蒙开发 - 全国景区门票查询应用开发教程
flutter·华为·harmonyos