通过网盘分享的文件: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,检查新的三连
每次循环都会更新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;
}
初始化棋盘:
- 用随机颜色填充所有格子
- 如果有三连,消除并填充,直到没有三连
- 重置得分和状态
确保初始棋盘没有三连很重要,否则游戏一开始就会自动消除,玩家还没操作就得分了,体验不好。
游戏流程
完整的游戏流程是:
- 玩家点击第一个方块,选中
- 玩家点击第二个方块
- 如果相邻,交换两个方块
- 检查是否形成三连
- 如果没有,交换回去
- 如果有,消除三连,下落填充
- 检查是否有新的三连(连锁)
- 重复6-7直到没有三连
- 回到步骤1
这个流程循环往复,直到玩家不想玩了。消消乐通常没有"胜利"的概念,目标是尽可能高的得分。
小结
这篇讲了消消乐的下落填充,核心知识点:
- toRemove数组: 标记要消除的方块,先标记后消除
- -1表示空: 消除后的方块值为-1,统一表示空位
- 逐列处理: 每列独立下落,符合物理规律
- 收集非空: 从下往上收集保留的方块,保持相对顺序
- 随机填充: 不够的用新方块填充,从顶部"掉下来"
- 连锁消除: while循环处理连锁,直到没有新的三连
- 无效回退: 不形成三连就交换回去,增加策略性
- AnimatedContainer: 颜色变化动画,视觉效果更好
- processing锁: 防止动画过程中的误操作
下落填充是消消乐的核心机制,让游戏有连锁反应的乐趣。一次好的交换可能触发多次消除,这种"意外之喜"是消消乐吸引人的地方。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net