Flutter实战:从零实现俄罗斯方块(一)数据结构与核心算法

Flutter实战:从零实现俄罗斯方块(一)数据结构与核心算法

文章目录

摘要

这是我用Flutter开发OpenHarmony俄罗斯方块游戏的第一篇文章。我会记录我从零开始设计游戏数据结构和核心算法的完整思考过程。本文重点介绍如何用二维数组表示方块和棋盘、如何实现碰撞检测算法、如何让方块旋转,以及消行和计分系统的实现。所有代码都是实际可运行的,我会详细讲解每个设计决策背后的思路。

关键词:Flutter、OpenHarmony、俄罗斯方块、数据结构、碰撞检测、游戏算法

前言

这是我第一次用Flutter开发游戏,选择俄罗斯方块是因为它看起来简单,但实际上包含了游戏开发的很多核心概念。在开始编码之前,我想先理清楚几个问题:

  1. 怎么表示七种不同形状的方块?
  2. 怎么检测方块是否会碰撞?
  3. 怎么实现方块旋转?
  4. 怎么判断并消除满行?

这些问题都涉及到数据结构设计和算法实现。我会按照我的思考顺序,一步步记录下来。

系列说明:这是Flutter俄罗斯方块游戏开发系列教程的第1篇,本系列共3篇文章,涵盖数据结构、绘制技术、交互控制


一、我想先设计好游戏的数据结构

1.1 七种方块怎么表示?

俄罗斯方块有7种标准形状,我得想个好方法来表示它们。

我想到的方案:用二维数组!0表示空格,1-7表示不同颜色的方块实体。

dart 复制代码
class TetrisGame {
  // 7种标准方块定义
  static const List<List<List<int>>> shapes = [
    // I形 - 青色
    [
      [0, 0, 0, 0],
      [1, 1, 1, 1],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
    ],
    // O形 - 蓝色
    [
      [2, 2],
      [2, 2],
    ],
    // T形 - 橙色
    [
      [0, 3, 0],
      [3, 3, 3],
      [0, 0, 0],
    ],
    // S形 - 黄色
    [
      [0, 4, 4],
      [4, 4, 0],
      [0, 0, 0],
    ],
    // Z形 - 绿色
    [
      [5, 5, 0],
      [0, 5, 5],
      [0, 0, 0],
    ],
    // J形 - 紫色
    [
      [6, 0, 0],
      [6, 6, 6],
      [0, 0, 0],
    ],
    // L形 - 红色
    [
      [0, 0, 7],
      [7, 7, 7],
      [0, 0, 0],
    ],
  ];
}

我觉得这样设计的好处是:

  • 直观:看数组就能想象出方块形状
  • 易操作:移动和旋转都是对数组的操作
  • 颜色编码:数字1-7对应7种颜色

颜色映射

dart 复制代码
Color _getPieceColor(int value) {
  switch (value) {
    case 1: return Colors.cyan;      // I
    case 2: return Colors.blue;      // O
    case 3: return Colors.orange;    // T
    case 4: return Colors.yellow;    // S
    case 5: return Colors.green;     // Z
    case 6: return Colors.purple;    // J
    case 7: return Colors.red;       // L
    default: return Colors.transparent;
  }
}

1.2 棋盘用什么数据结构?

游戏棋盘是10列×20行的网格,这个很明显也用二维数组。

dart 复制代码
class TetrisGame {
  static const int cols = 10;  // 10列
  static const int rows = 20;  // 20行
  final List<List<int>> _board = [];

  void _resetBoard() {
    _board.clear();
    for (int y = 0; y < rows; y++) {
      _board.add(List.filled(cols, 0));  // 全部初始化为0
    }
  }

  List<List<int>> get board => _board;
}

坐标系统_board[y][x]表示坐标(x, y)位置的方块

  • (0, 0)是左上角
  • (9, 19)是右下角
  • 值为0表示空格,1-7表示已固定的方块

1.3 游戏状态需要记录哪些东西?

我想了想,需要记录这些:

dart 复制代码
class TetrisGame {
  // 当前下落的方块
  List<List<int>>? _currentPiece;
  List<List<int>>? get currentPiece => _currentPiece;

  // 下一个方块(预览用)
  List<List<int>>? _nextPiece;

  // 当前方块在棋盘上的位置
  int _currentX = 0;
  int get currentX => _currentX;
  int _currentY = 0;
  int get currentY => _currentY;

  // 游戏状态
  int score = 0;
  int level = 1;
  bool gameOver = false;
  bool paused = false;

  // 游戏循环计时器
  Timer? _timer;
}

二、碰撞检测:最关键的算法

碰撞检测是整个游戏的核心!每次移动或旋转前都要检查。

2.1 碰撞检测的基本思路

我的思路很简单:

  1. 遍历方块的每个格子
  2. 如果是实心格子,计算它在棋盘上的新位置
  3. 检查这个位置是否:
    • 超出棋盘边界
    • 与已有方块重叠

2.2 边界检测怎么写?

dart 复制代码
bool _checkBoundary(int x, int y, List<List<int>> piece) {
  for (int py = 0; py < piece.length; py++) {
    for (int px = 0; px < piece[py].length; px++) {
      if (piece[py][px] != 0) {
        final newX = x + px;
        final newY = y + py;

        // 检查三个边界
        if (newX < 0) return true;        // 左边界
        if (newX >= cols) return true;    // 右边界
        if (newY >= rows) return true;    // 下边界
      }
    }
  }
  return false;
}

注意:上边界(y < 0)不检测,因为方块生成时可能在棋盘上方。

2.3 方块重叠怎么判断?

dart 复制代码
bool _checkOverlap(int x, int y, List<List<int>> piece) {
  for (int py = 0; py < piece.length; py++) {
    for (int px = 0; px < piece[py].length; px++) {
      if (piece[py][px] != 0) {
        final newX = x + px;
        final newY = y + py;

        // 确保在棋盘范围内
        if (newY >= 0 && newY < rows && newX >= 0 && newX < cols) {
          // 检查该位置是否已有方块
          if (_board[newY][newX] != 0) {
            return true;
          }
        }
      }
    }
  }
  return false;
}

2.4 完整的碰撞检测函数

把边界检测和重叠检测合并:

dart 复制代码
bool _checkCollision(int x, int y, List<List<int>> piece) {
  for (int py = 0; py < piece.length; py++) {
    for (int px = 0; px < piece[py].length; px++) {
      if (piece[py][px] != 0) {
        final newX = x + px;
        final newY = y + py;

        // 边界检测
        if (newX < 0 || newX >= cols || newY >= rows) {
          return true;
        }

        // 重叠检测
        if (newY >= 0 && _board[newY][newX] != 0) {
          return true;
        }
      }
    }
  }
  return false;
}

应用场景

  • 左移前检测:_checkCollision(_currentX - 1, _currentY, _currentPiece)
  • 右移前检测:_checkCollision(_currentX + 1, _currentY, _currentPiece)
  • 下落前检测:_checkCollision(_currentX, _currentY + 1, _currentPiece)
  • 旋转前检测:_checkCollision(_currentX, _currentY, rotatedPiece)

三、方块的移动和旋转

3.1 移动很简单,但要注意边界

dart 复制代码
void moveLeft() {
  if (gameOver || paused) return;

  // 先检测能不能左移
  if (!_checkCollision(_currentX - 1, _currentY, _currentPiece!)) {
    _currentX--;
    updateCallback();
  }
}

void moveRight() {
  if (gameOver || paused) return;

  if (!_checkCollision(_currentX + 1, _currentY, _currentPiece!)) {
    _currentX++;
    updateCallback();
  }
}

3.2 旋转算法的数学原理

我记得矩阵旋转的数学公式:对于N×N矩阵,顺时针旋转90度后:

复制代码
新矩阵[x][y] = 原矩阵[N-1-y][x]

代码实现

dart 复制代码
List<List<int>> _rotatePiece(List<List<int>> piece) {
  return List.generate(
    piece[0].length,
    (x) => List.generate(
      piece.length,
      (y) => piece[piece.length - 1 - y][x],
    ),
  );
}

这个函数的意思是:

  • 创建一个新矩阵
  • 新矩阵的宽度 = 原矩阵的高度
  • 新矩阵的每个元素 = 原矩阵对应旋转位置的元素

3.3 旋转后卡住怎么办?

有时候靠墙旋转会卡住,我有两个方案:

方案1:简单粗暴 - 不能旋转就不转

dart 复制代码
void rotate() {
  if (gameOver || paused) return;

  final rotated = _rotatePiece(_currentPiece!);

  if (!_checkCollision(_currentX, _currentY, rotated)) {
    _currentPiece = rotated;
  }
  // 否则什么都不做
}

方案2:踢墙算法 - 尝试微调位置

dart 复制代码
void rotateWithWallKick() {
  if (gameOver || paused) return;

  final rotated = _rotatePiece(_currentPiece!);

  // 先尝试原位置
  if (!_checkCollision(_currentX, _currentY, rotated)) {
    _currentPiece = rotated;
    return;
  }

  // 尝试向左踢
  if (!_checkCollision(_currentX - 1, _currentY, rotated)) {
    _currentX--;
    _currentPiece = rotated;
    return;
  }

  // 尝试向右踢
  if (!_checkCollision(_currentX + 1, _currentY, rotated)) {
    _currentX++;
    _currentPiece = rotated;
    return;
  }
}

3.4 踢墙算法让旋转更流畅

标准俄罗斯方块使用SRS(Super Rotation System),定义了每种方块的踢墙偏移表。

简化版的踢墙实现

dart 复制代码
void rotateWithSRS() {
  final rotated = _rotatePiece(_currentPiece!);

  // 定义尝试的偏移量
  final kicks = [
    [0, 0],    // 原位置
    [-1, 0],   // 左移1格
    [1, 0],    // 右移1格
    [0, -1],   // 上移1格
    [-1, -1],  // 左上移1格
    [1, -1],   // 右上移1格
  ];

  // 依次尝试每个偏移
  for (final kick in kicks) {
    if (!_checkCollision(_currentX + kick[0], _currentY + kick[1], rotated)) {
      _currentX += kick[0];
      _currentY += kick[1];
      _currentPiece = rotated;
      return;
    }
  }
}

四、消行和计分系统

4.1 如何检测满行?

我的思路:从底部往上扫描,发现满行就消除。

dart 复制代码
void _clearLines() {
  int linesCleared = 0;

  // 从下往上检查每一行
  for (int y = rows - 1; y >= 0; y--) {
    bool fullLine = true;

    // 检查该行是否填满
    for (int x = 0; x < cols; x++) {
      if (_board[y][x] == 0) {
        fullLine = false;
        break;
      }
    }

    // 如果该行满了
    if (fullLine) {
      _board.removeAt(y);              // 移除该行
      _board.insert(0, List.filled(cols, 0));  // 顶部插入空行
      linesCleared++;
      y++;  // 重新检查当前行(因为上面的行下移了)
    }
  }

  if (linesCleared > 0) {
    _updateScore(linesCleared);
  }
}

4.2 分数怎么计算合理?

我想了个简单的计分公式:

dart 复制代码
void _updateScore(int linesCleared) {
  // 基础分:行数 × 100 × 当前等级
  int baseScore = linesCleared * 100 * level;

  // 连击奖励
  double bonus = 1.0;
  if (linesCleared == 4) {
    bonus = 2.0;  // 4行连击翻倍
  } else if (linesCleared == 2) {
    bonus = 1.2;  // 2行小奖励
  }

  score += (baseScore * bonus).toInt();

  // 更新等级
  level = (score ~/ 1000) + 1;
}

计分表

消除行数 等级1 等级2 等级3 等级4
1行 100 200 300 400
2行 240 480 720 960
3行 300 600 900 1200
4行 800 1600 2400 3200

4.3 等级和速度的关系

等级越高,下落速度越快:

dart 复制代码
void start() {
  _timer = Timer.periodic(
    Duration(milliseconds: _getDropSpeed()),
    (_) => tick(),
  );
}

int _getDropSpeed() {
  // 基础1000ms,每级减少50ms,最快200ms
  int speed = 1000 - (level - 1) * 50;
  return speed.clamp(200, 1000);
}

速度表

等级 下落间隔 描述
1 1000ms 初学者
5 800ms 入门
10 550ms 中级
15 300ms 高级
20 200ms 专家

4.4 连击奖励让游戏更有趣

dart 复制代码
class TetrisGame {
  int _consecutiveClears = 0;

  void _clearLines() {
    int linesCleared = 0;

    // 检测满行...

    if (linesCleared > 0) {
      _consecutiveClears++;

      // 连击奖励
      double comboBonus = 1.0 + (_consecutiveClears - 1) * 0.5;
      score += (linesCleared * 100 * level * comboBonus).toInt();
    } else {
      _consecutiveClears = 0;  // 重置连击
    }
  }
}

五、游戏循环:让方块动起来

5.1 用Timer实现游戏循环

dart 复制代码
class TetrisGame {
  Timer? _timer;

  void start() {
    _timer = Timer.periodic(
      Duration(milliseconds: _getDropSpeed()),
      (_) => tick(),
    );
  }

  void tick() {
    if (gameOver || paused) return;

    // 尝试向下移动
    if (!_checkCollision(_currentX, _currentY + 1, _currentPiece!)) {
      _currentY++;
    } else {
      _mergePiece();  // 碰到东西就固定
    }

    updateCallback();
  }
}

游戏循环流程

5.2 速度如何随等级提升

dart 复制代码
int _getDropSpeed() {
  // 简单线性递减
  int speed = 1000 - (level - 1) * 50;
  return speed.clamp(200, 1000);
}

但我觉得这样不太合理,因为后期提升太快了。让我改用分段函数:

dart 复制代码
int _getDropSpeed() {
  if (level <= 5) {
    // 前期:每级-60ms
    return 1000 - (level - 1) * 60;
  } else if (level <= 15) {
    // 中期:每级-40ms
    return 800 - (level - 5) * 40;
  } else {
    // 后期:每级-20ms
    return 400 - (level - 15) * 20;
  }
}

5.3 难度曲线的设计

我画个图说明一下难度曲线(省略了,但实际文章可以加图)。


六、算法性能分析

我简单分析了一下算法的时间复杂度:

算法 时间复杂度 说明
碰撞检测 O(h×w) h,w是方块尺寸,最多4×4
消行检测 O(rows×cols) 最坏20×10=200次循环

这个复杂度完全没问题,因为:

  • 方块尺寸很小(最多16个格子)
  • 棋盘也不大(200个格子)
  • 现代手机每秒可以轻松处理百万次运算

优化建议

  1. 一旦发现碰撞立即返回(提前退出)
  2. 缓存方块的边界框,避免重复计算

七、本文小结

这篇文章我记录了俄罗斯方块游戏的核心数据结构和算法设计:

  1. 数据结构:用二维数组表示方块和棋盘
  2. 碰撞检测:边界+重叠的双重检测
  3. 移动旋转:基础移动+矩阵旋转+踢墙优化
  4. 消行计分:从下往上扫描+连击奖励
  5. 游戏循环:Timer定时触发+动态速度调整

这些都是游戏的底层逻辑,虽然看起来简单,但细节很多。下一篇文章我会讲解如何用CustomPaint把这些数据结构绘制成精美的游戏画面。

系列说明:这是Flutter俄罗斯方块游戏开发系列教程的第1篇(共3篇)。下一篇将介绍CustomPaint绘制技术。


参考资料

  1. Dart语言官方文档
  2. Flutter Timer类API
  3. 俄罗斯方块SRS系统详解
  4. 游戏开发算法大全
  5. 开源鸿蒙跨平台社区

社区支持

欢迎加入开源鸿蒙跨平台社区:

如果本文对你有帮助,欢迎点赞、收藏、评论!

相关推荐
ujainu2 小时前
Flutter + OpenHarmony 用户输入框:TextField 与 InputDecoration 在多端表单中的交互设计
flutter·交互·组件
2501_901147832 小时前
四数相加问题的算法优化与工程实现笔记
笔记·算法·面试·职场和发展·哈希算法
●VON2 小时前
Flutter 与 OpenHarmony 应用交互优化实践:从基础列表到 HarmonyOS Design 兼容的待办事项体验
flutter·交互·harmonyos·openharmony·训练营·跨平台开发
●VON2 小时前
无状态 Widget 下的实时排序:Flutter for OpenHarmony 中 TodoList 的排序策略与数据流控制
学习·flutter·架构·交互·openharmony·von
亿秒签到2 小时前
第六届传智杯程序设计国赛B组T4·小苯的字符串染色
数据结构·算法·传智杯
chao1898442 小时前
基于字典缩放的属性散射中心参数提取算法与MATLAB实现
开发语言·算法·matlab
●VON2 小时前
面向 OpenHarmony 的 Flutter 应用实战:TodoList 多条件过滤系统的状态管理与性能优化
学习·flutter·架构·跨平台·von
wqwqweee2 小时前
Flutter for OpenHarmony 看书管理记录App实战:关于我们实现
android·javascript·python·flutter·harmonyos
鸣弦artha2 小时前
Scaffold布局模式综合应用
flutter·华为·harmonyos