Flutter for OpenHarmony游戏集合App实战之俄罗斯方块消行效果
本文介绍了俄罗斯方块游戏消行功能的实现细节。采用20x10的二维数组存储棋盘数据,从下往上遍历检查填满行,通过删除行并在顶部插入空行实现消行效果。关键点包括:正确处理连续消行时的索引变化、锁定方块与消行的执行顺序、计分规则设计(支持连消加分)以及游戏结束判断逻辑。文章还探讨了视觉效果优化(闪烁动画)和难度递增机制(下落速度随分数提升)的实现思路,为开发者提供了完整的消行功能实现方案和技术要点。
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
俄罗斯方块的核心玩法除了堆叠方块,还有消行。当一行被填满时,这一行会消除,上面的方块落下来。
消行是得分的主要方式,一次消多行分数更高。这篇来聊聊消行的实现。
消行逻辑看起来简单,但有一些细节需要注意:从下往上遍历、删除后索引变化、连续消行的处理等。
棋盘数据结构
static const int rows = 20, cols = 10;
late List<List<Color?>> board;
棋盘是20行10列的二维数组,每个格子存储颜色。
Color?表示可空,null表示空格子,有颜色表示有方块。
用颜色而不是布尔值,是因为需要保留方块的颜色信息,让游戏更好看。
初始化
board = List.generate(rows, (_) => List.filled(cols, null));
全部填null,空棋盘。
List.generate创建rows行,每行用List.filled创建cols个null。
消行逻辑
void _clearLines() {
int cleared = 0;
for (int y = rows - 1; y >= 0; y--) {
if (board[y].every((c) => c != null)) {
board.removeAt(y);
board.insert(0, List.filled(cols, null));
cleared++;
y++;
}
}
score += cleared * 100;
}
这是消行的核心方法,检查并消除所有填满的行。
从下往上遍历
for (int y = rows - 1; y >= 0; y--) {
从最底行(rows-1=19)开始往上检查。
为什么从下往上?因为消除一行后,上面的行会落下来,索引会变化。从下往上可以正确处理连续消行。
如果从上往下,消除一行后,原来的下一行变成了当前行,但y已经增加了,会跳过这一行。
检查是否填满
if (board[y].every((c) => c != null)) {
every方法检查列表的每个元素是否都满足条件。
如果这一行的每个格子都不是null(都有颜色),说明填满了。
every接收一个函数,对每个元素调用,全部返回true才返回true。
删除这一行
board.removeAt(y);
从列表中删除第y行。removeAt会把后面的元素往前移。
顶部插入空行
board.insert(0, List.filled(cols, null));
在顶部(索引0)插入一行空格子。
这样总行数保持不变(还是20行),效果就是上面的行"落下来"了。
重新检查当前行
y++;
删除一行后,原来y+1行变成了y行。
y++抵消循环的y--,下一次循环还是检查当前位置(现在是原来的下一行)。
这样可以处理连续多行都填满的情况。比如同时消除第18、19行,删除19行后,原来的18行变成19行,y++后下一次循环还是检查19行。
计分
cleared++;
...
score += cleared * 100;
记录消了几行,每行100分。
消行时机
void _moveDown() {
if (gameOver) return;
setState(() {
if (_canMove(0, 1)) {
pieceY++;
} else {
_lockPiece();
_clearLines();
_spawnPiece();
}
});
}
方块落到底部后:
_lockPiece(): 把方块固定到棋盘_clearLines(): 检查并消除满行_spawnPiece(): 生成新方块
顺序很重要,先锁定再消行。如果先消行再锁定,当前方块还没写入board,消行检查会不正确。
游戏结束检查
if (gameOver) return;
游戏已经结束,不再处理下落。
能下移
if (_canMove(0, 1)) {
pieceY++;
}
如果能下移,就下移一格。
不能下移
} else {
_lockPiece();
_clearLines();
_spawnPiece();
}
不能下移说明到底了(碰到底部或已有方块),执行锁定、消行、生成新方块的流程。
连消加分
当前实现每行100分,可以改成连消加成:
void _clearLines() {
int cleared = 0;
// ... 消行逻辑 ...
// 连消加成
switch (cleared) {
case 1: score += 100; break;
case 2: score += 300; break; // 1.5倍
case 3: score += 500; break; // 约1.67倍
case 4: score += 800; break; // 2倍,俄罗斯方块叫Tetris
}
}
一次消4行(Tetris)是最高分,鼓励玩家堆高再消。
这是俄罗斯方块的经典计分规则,让游戏更有策略性。玩家需要权衡:是尽快消行保持安全,还是冒险堆高争取一次消4行。
视觉效果
当前实现是瞬间消除,没有动画。
如果想加动画,可以:
void _clearLines() async {
List<int> fullRows = [];
for (int y = 0; y < rows; y++) {
if (board[y].every((c) => c != null)) {
fullRows.add(y);
}
}
if (fullRows.isEmpty) return;
// 闪烁效果
for (int i = 0; i < 3; i++) {
setState(() {
for (int y in fullRows) {
board[y] = List.filled(cols, i % 2 == 0 ? Colors.white : null);
}
});
await Future.delayed(Duration(milliseconds: 100));
}
// 实际消除
for (int y in fullRows.reversed) {
board.removeAt(y);
board.insert(0, List.filled(cols, null));
}
score += fullRows.length * 100;
setState(() {});
}
这个实现会让满行闪烁3次(白色和透明交替),然后再消除。
为什么用reversed
for (int y in fullRows.reversed) {
fullRows是从上到下的顺序(因为遍历是从0开始)。删除时要从下往上删,否则索引会乱。
reversed返回一个反向的Iterable。
但这会让代码复杂很多,当前简化处理,没有动画。
游戏结束判断
void _spawnPiece() {
// ... 生成新方块 ...
if (!_canMove(0, 0)) {
gameOver = true;
timer?.cancel();
}
}
新方块生成后,如果初始位置就不合法(和已有方块重叠),说明堆到顶了,游戏结束。
为什么检查_canMove(0, 0)
(0, 0)表示不移动,只检查当前位置是否合法。如果新方块刚生成就和已有方块重叠,说明没有空间了。
取消定时器
timer?.cancel();
游戏结束后取消定时器,方块不再自动下落。
快速下落
void _drop() {
while (_canMove(0, 1)) pieceY++;
_moveDown();
}
玩家可以按下键快速落到底部。
while循环一直往下移,直到不能移为止。
然后调用_moveDown()触发锁定和消行。
为什么不直接锁定
// 不好的写法
void _drop() {
while (_canMove(0, 1)) pieceY++;
_lockPiece();
_clearLines();
_spawnPiece();
}
这样写也可以,但重复了_moveDown里的逻辑。调用_moveDown更简洁,而且如果以后_moveDown的逻辑变了,_drop也会自动更新。
定时下落
timer = Timer.periodic(const Duration(milliseconds: 500), (_) => _moveDown());
每500毫秒自动下落一格。
难度递增
可以根据分数加快速度:
int speed = 500 - (score ~/ 500) * 50;
speed = speed.clamp(100, 500);
每500分加速50毫秒,最快100毫秒。
clamp(100, 500)确保速度在100-500范围内。
要实现这个功能,需要在消行后重新创建定时器:
void _clearLines() {
// ... 消行逻辑 ...
if (cleared > 0) {
timer?.cancel();
int speed = 500 - (score ~/ 500) * 50;
speed = speed.clamp(100, 500);
timer = Timer.periodic(Duration(milliseconds: speed), (_) => _moveDown());
}
}
分数显示
Padding(padding: const EdgeInsets.all(8), child: Text('分数: $score', style: const TextStyle(fontSize: 20))),
顶部显示当前分数。
控制按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(icon: const Icon(Icons.arrow_back), onPressed: _moveLeft),
IconButton(icon: const Icon(Icons.arrow_downward), onPressed: _drop),
IconButton(icon: const Icon(Icons.rotate_right), onPressed: _rotate),
IconButton(icon: const Icon(Icons.arrow_forward), onPressed: _moveRight),
],
)
四个按钮:左移、快速下落、旋转、右移。
用IconButton和Material Design图标,简洁美观。
游戏结束显示
if (gameOver) Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('游戏结束', style: TextStyle(fontSize: 24, color: Colors.red)),
ElevatedButton(onPressed: () => setState(_initGame), child: const Text('重新开始')),
],
),
)
游戏结束时显示红色文字和重新开始按钮。
小结
这篇讲了俄罗斯方块的消行效果,核心知识点:
- every方法: 检查一行是否全部填满,简洁的函数式写法
- removeAt: 删除指定行,List的内置方法
- insert(0, …): 顶部插入空行,保持总行数不变
- 从下往上遍历: 正确处理连续消行,避免索引混乱
- y++技巧: 删除后重新检查当前位置
- 消行时机: 锁定方块后立即检查
- 连消加成: 一次消多行分数更高,增加策略性
- 游戏结束: 新方块无法放置时结束
消行是俄罗斯方块的核心机制,理解了这个,游戏的主要逻辑就完整了。消行带来的满足感是俄罗斯方块吸引人的重要原因。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)