Flutter for OpenHarmony游戏集合App实战之消消乐下落填充

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

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

前言

消消乐是一个三消游戏,交换相邻方块,三个或更多相同颜色的方块连成一线就消除。

消除后,上面的方块要下落填充空位,顶部要生成新方块。这篇来聊聊下落填充的实现。

下落填充是消消乐的核心机制之一,它让游戏有了连锁反应的乐趣。一次好的交换可能触发多次消除,得分翻倍。我在实现这个功能的时候,最开始没有处理连锁消除,结果下落后形成的新三连不会自动消除,玩家体验很差。

状态变量定义

dart 复制代码
List<List<int>> board = [];

棋盘数据,二维数组。board[r][c]的值表示该位置方块的颜色索引,-1表示空位(已消除)。

dart 复制代码
int? selectedRow, selectedCol;

当前选中的方块坐标。null表示没有选中任何方块。

dart 复制代码
int score = 0;

当前得分,每消除一个方块加10分。

dart 复制代码
bool processing = false;

是否正在处理消除和下落。为true时禁止玩家操作,防止状态混乱。

dart 复制代码
final int size = 8;

棋盘大小,8x8的方格。

消除标记

dart 复制代码
void _processMatches() {
  bool found = true;
  while (found) {
    found = false;
    List<List<bool>> toRemove = List.generate(size, (_) => List.filled(size, false));

用一个布尔数组标记要消除的方块。toRemove[r][c]为true表示该位置的方块需要消除。

while (found)循环处理连锁消除------消除后下落可能形成新的三连,需要继续处理直到没有新的匹配。

List.generate创建一个size行的列表,每行是一个size长度的false数组。这样初始化后,所有位置都标记为"不消除"。

水平匹配

dart 复制代码
for (int r = 0; r < size; r++) {
  for (int c = 0; c < size - 2; c++) {
    if (board[r][c] >= 0 && board[r][c] == board[r][c+1] && board[r][c] == board[r][c+2]) {
      toRemove[r][c] = toRemove[r][c+1] = toRemove[r][c+2] = true;
      found = true;
    }
  }
}

检查每一行,连续三个相同就标记消除。

外层循环遍历所有行,内层循环遍历每行的列。注意内层循环到size - 2就停止,因为要检查连续三个,最后两个位置不需要作为起点。

board[r][c] >= 0确保不是已消除的空位(-1)。空位不参与匹配,否则会出现"三个空位也算匹配"的bug。

三个条件用&&连接:当前位置有方块,且和右边两个位置的颜色相同。满足条件就把三个位置都标记为消除。

垂直匹配

dart 复制代码
for (int c = 0; c < size; c++) {
  for (int r = 0; r < size - 2; r++) {
    if (board[r][c] >= 0 && board[r][c] == board[r+1][c] && board[r][c] == board[r+2][c]) {
      toRemove[r][c] = toRemove[r+1][c] = toRemove[r+2][c] = true;
      found = true;
    }
  }
}

同样的逻辑,检查每一列。外层循环遍历列,内层循环遍历行。

水平和垂直匹配是独立的,一个方块可能同时参与水平和垂直的匹配(比如T形或L形的交叉点)。用toRemove数组标记而不是立即消除,就是为了处理这种情况。

执行消除

dart 复制代码
int removed = 0;
for (int r = 0; r < size; r++) {
  for (int c = 0; c < size; c++) {
    if (toRemove[r][c]) { board[r][c] = -1; removed++; }
  }
}
score += removed * 10;

遍历toRemove数组,把标记的方块设为-1(空),同时计数。

每消除一个方块得10分。removed记录本轮消除的方块数,乘以10就是本轮得分。

这种"先标记后消除"的两阶段处理是游戏开发中的常见模式,可以避免在遍历过程中修改数据导致的问题。

下落填充

dart 复制代码
void _dropAndFill() {
  final random = Random();
  for (int c = 0; c < size; c++) {
    List<int> column = [];
    for (int r = size - 1; r >= 0; r--) {
      if (board[r][c] >= 0) column.add(board[r][c]);
    }
    while (column.length < size) column.add(random.nextInt(colors.length));
    for (int r = size - 1; r >= 0; r--) {
      board[r][c] = column[size - 1 - r];
    }
  }
}

这是下落填充的核心方法,让方块像真实物理世界一样下落,空位被上面的方块填充,顶部生成新方块。

逐列处理

dart 复制代码
for (int c = 0; c < size; c++) {

每一列独立处理,方块只在列内下落,不会跨列移动。这符合重力的物理规律。

收集非空方块

dart 复制代码
List<int> column = [];
for (int r = size - 1; r >= 0; r--) {
  if (board[r][c] >= 0) column.add(board[r][c]);
}

从下往上遍历,收集所有非空方块(值>=0)。

为什么从下往上?因为下落后,原来在下面的方块还是在下面。从下往上收集,column列表的顺序就是下落后从下到上的顺序。

比如一列是[A, 空, B, C, 空](从上到下),收集后column是[C, B, A](从下往上收集的非空方块)。

填充新方块

dart 复制代码
while (column.length < size) column.add(random.nextInt(colors.length));

如果column不够size个,说明有空位需要填充。用随机颜色的新方块填充。

新方块从顶部"掉下来",所以添加到column的末尾。random.nextInt(colors.length)生成0到颜色数量-1的随机整数,作为新方块的颜色索引。

写回棋盘

dart 复制代码
for (int r = size - 1; r >= 0; r--) {
  board[r][c] = column[size - 1 - r];
}

把column的内容写回棋盘。

column[size - 1 - r]的映射关系:

  • r=size-1(最底行)对应column[0](最先收集的,原来最下面的)
  • r=0(最顶行)对应column[size-1](最后添加的,新生成的)

这个映射确保了方块的相对顺序正确。

连锁消除

dart 复制代码
while (found) {
  // 标记消除
  // 执行消除
  // 下落填充
}

下落填充后可能形成新的三连,所以用while循环继续处理。

直到没有新的匹配为止。这就是连锁消除的实现,也是消消乐游戏乐趣的来源之一。

连锁消除的流程:

  1. 检查是否有三连,标记要消除的方块
  2. 执行消除,方块变成空位
  3. 下落填充,上面的方块下落,顶部生成新方块
  4. 回到步骤1,检查新的三连

每次循环都会更新found变量。如果这一轮没有找到任何三连,found保持false,循环结束。

检查是否有匹配

dart 复制代码
bool _hasMatches() {
  for (int r = 0; r < size; r++) {
    for (int c = 0; c < size - 2; c++) {
      if (board[r][c] >= 0 && board[r][c] == board[r][c+1] && board[r][c] == board[r][c+2]) return true;
    }
  }
  for (int c = 0; c < size; c++) {
    for (int r = 0; r < size - 2; r++) {
      if (board[r][c] >= 0 && board[r][c] == board[r+1][c] && board[r][c] == board[r+2][c]) return true;
    }
  }
  return false;
}

这个方法只检查是否存在三连,不执行消除。用于判断交换后是否形成有效的三连。

和_processMatches的检查逻辑一样,但找到一个就立即返回true,不需要标记所有的匹配。这样效率更高。

交换逻辑

dart 复制代码
void _tap(int row, int col) {
  if (processing) return;
  setState(() {
    if (selectedRow == null) {
      selectedRow = row;
      selectedCol = col;
    } else {
      bool adjacent = (selectedRow == row && (selectedCol! - col).abs() == 1) ||
                     (selectedCol == col && (selectedRow! - row).abs() == 1);
      if (adjacent) {
        _swap(selectedRow!, selectedCol!, row, col);

这个方法处理玩家的点击操作。

processing检查

dart 复制代码
if (processing) return;

如果正在处理消除和下落,禁止玩家操作。这个锁很重要,防止玩家在动画过程中触发新的操作。

相邻判断

dart 复制代码
bool adjacent = (selectedRow == row && (selectedCol! - col).abs() == 1) ||
               (selectedCol == col && (selectedRow! - row).abs() == 1);

同一行且列差1,或同一列且行差1,就是相邻。

.abs()是取绝对值,因为不知道哪个在前哪个在后。列差可能是1或-1,取绝对值后都是1。

只有相邻的方块才能交换,这是消消乐的基本规则。

交换方块

dart 复制代码
void _swap(int r1, int c1, int r2, int c2) {
  int temp = board[r1][c1];
  board[r1][c1] = board[r2][c2];
  board[r2][c2] = temp;
}

经典的交换算法,用临时变量保存一个值,然后交换两个位置的值。

无效交换回退

dart 复制代码
Timer(const Duration(milliseconds: 200), () {
  if (!_hasMatches()) {
    _swap(selectedRow!, selectedCol!, row, col);
  } else {
    _processMatches();
  }

交换后检查是否形成三连:

  • 没有:交换回去,这次交换无效
  • 有:处理消除

200毫秒延迟让玩家看到交换效果。如果立即回退,玩家可能都没看清发生了什么。

这个设计让游戏更有策略性,玩家需要思考哪些交换能形成三连,而不是随便乱换。

方块渲染

dart 复制代码
AnimatedContainer(
  duration: const Duration(milliseconds: 200),
  decoration: BoxDecoration(
    color: value >= 0 ? colors[value] : Colors.grey[200],
    borderRadius: BorderRadius.circular(8),
    border: selected ? Border.all(color: Colors.white, width: 3) : null,
    boxShadow: [BoxShadow(...)],
  ),
),

这段代码定义了方块的外观,使用AnimatedContainer实现平滑的动画效果。

颜色

dart 复制代码
color: value >= 0 ? colors[value] : Colors.grey[200],

有值显示对应颜色,-1显示灰色(空位,实际上会被填充)。

value是方块的颜色索引,colors是颜色列表。通过索引获取对应的颜色,这样添加新颜色只需要修改colors列表。

选中高亮

dart 复制代码
border: selected ? Border.all(color: Colors.white, width: 3) : null,

选中的方块有白色边框,3像素宽。白色在彩色方块上很醒目,玩家一眼就能看到选中了哪个。

圆角

dart 复制代码
borderRadius: BorderRadius.circular(8),

8像素的圆角让方块看起来更柔和,不那么生硬。圆角是现代UI设计的常见元素。

阴影

dart 复制代码
boxShadow: [BoxShadow(...)],

阴影让方块有立体感,看起来像是浮在棋盘上面。

动画

dart 复制代码
AnimatedContainer(
  duration: const Duration(milliseconds: 200),

颜色变化有200毫秒动画,消除和填充更平滑。AnimatedContainer会自动对它的属性变化添加动画,不需要手动管理动画控制器。

200毫秒是一个适中的时间,足够让玩家看到变化,又不会让游戏节奏变慢。

颜色定义

dart 复制代码
final List<Color> colors = [Colors.red, Colors.blue, Colors.green, Colors.yellow, Colors.purple, Colors.orange];

6种颜色,用索引0-5表示。

颜色的选择很重要,需要足够区分度,让玩家一眼就能分辨。红、蓝、绿、黄、紫、橙是经典的消消乐配色,色相差异大,不容易混淆。

6种颜色是一个平衡的数量。太少了游戏太简单,太多了难以区分。

棋盘初始化

dart 复制代码
void _initGame() {
  final random = Random();
  board = List.generate(size, (_) => 
    List.generate(size, (_) => random.nextInt(colors.length))
  );
  // 确保初始棋盘没有三连
  while (_hasMatches()) {
    _processMatches();
    _dropAndFill();
  }
  score = 0;
  selectedRow = selectedCol = null;
  processing = false;
}

初始化棋盘:

  1. 用随机颜色填充所有格子
  2. 如果有三连,消除并填充,直到没有三连
  3. 重置得分和状态

确保初始棋盘没有三连很重要,否则游戏一开始就会自动消除,玩家还没操作就得分了,体验不好。

游戏流程

完整的游戏流程是:

  1. 玩家点击第一个方块,选中
  2. 玩家点击第二个方块
  3. 如果相邻,交换两个方块
  4. 检查是否形成三连
  5. 如果没有,交换回去
  6. 如果有,消除三连,下落填充
  7. 检查是否有新的三连(连锁)
  8. 重复6-7直到没有三连
  9. 回到步骤1

这个流程循环往复,直到玩家不想玩了。消消乐通常没有"胜利"的概念,目标是尽可能高的得分。

小结

这篇讲了消消乐的下落填充,核心知识点:

  • toRemove数组: 标记要消除的方块,先标记后消除
  • -1表示空: 消除后的方块值为-1,统一表示空位
  • 逐列处理: 每列独立下落,符合物理规律
  • 收集非空: 从下往上收集保留的方块,保持相对顺序
  • 随机填充: 不够的用新方块填充,从顶部"掉下来"
  • 连锁消除: while循环处理连锁,直到没有新的三连
  • 无效回退: 不形成三连就交换回去,增加策略性
  • AnimatedContainer: 颜色变化动画,视觉效果更好
  • processing锁: 防止动画过程中的误操作

下落填充是消消乐的核心机制,让游戏有连锁反应的乐趣。一次好的交换可能触发多次消除,这种"意外之喜"是消消乐吸引人的地方。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
Filotimo_4 小时前
JWT的概念
java·开发语言·python
min1811234564 小时前
软件升级全流程步骤详解
android·java·服务器
黎雁·泠崖4 小时前
Java字符串系列总结篇|核心知识点速记手册
java·开发语言
kirk_wang5 小时前
Flutter艺术探索-BLoC模式实战:业务逻辑组件化设计
flutter·移动开发·flutter教程·移动开发教程
彩妙不是菜喵5 小时前
STL精讲:string类
开发语言·c++
一起养小猫5 小时前
LeetCode100天Day16-跳跃游戏II与H指数
算法·游戏
小屁猪qAq5 小时前
创建型之单例模式
开发语言·c++·单例模式
郝学胜-神的一滴5 小时前
深入解析以太网帧与ARP协议:网络通信的基石
服务器·开发语言·网络·程序人生
鸣弦artha5 小时前
Flutter框架跨平台鸿蒙开发——Container组件基础使用
flutter·华为·harmonyos