欢迎光临
我们一直在努力

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

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

文章目录

  • Flutter实战:从零实现俄罗斯方块(一)数据结构与核心算法
    • 摘要
    • 前言
    • 一、我想先设计好游戏的数据结构
      • 1.1 七种方块怎么表示?
      • 1.2 棋盘用什么数据结构?
      • 1.3 游戏状态需要记录哪些东西?
    • 二、碰撞检测:最关键的算法
      • 2.1 碰撞检测的基本思路
      • 2.2 边界检测怎么写?
      • 2.3 方块重叠怎么判断?
      • 2.4 完整的碰撞检测函数
    • 三、方块的移动和旋转
      • 3.1 移动很简单,但要注意边界
      • 3.2 旋转算法的数学原理
      • 3.3 旋转后卡住怎么办?
      • 3.4 踢墙算法让旋转更流畅
    • 四、消行和计分系统
      • 4.1 如何检测满行?
      • 4.2 分数怎么计算合理?
      • 4.3 等级和速度的关系
      • 4.4 连击奖励让游戏更有趣
    • 五、游戏循环:让方块动起来
      • 5.1 用Timer实现游戏循环
      • 5.2 速度如何随等级提升
      • 5.3 难度曲线的设计
    • 六、算法性能分析
    • 七、本文小结
    • 参考资料
    • 社区支持

摘要

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

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

前言

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

  • 怎么表示七种不同形状的方块?
  • 怎么检测方块是否会碰撞?
  • 怎么实现方块旋转?
  • 怎么判断并消除满行?
  • 这些问题都涉及到数据结构设计和算法实现。我会按照我的思考顺序,一步步记录下来。

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


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

    1.1 七种方块怎么表示?

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

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

    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种颜色

    颜色映射

    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行的网格,这个很明显也用二维数组。

    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 游戏状态需要记录哪些东西?

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

    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 碰撞检测的基本思路

    我的思路很简单:

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

    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 方块重叠怎么判断?

    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 完整的碰撞检测函数

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

    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 移动很简单,但要注意边界

    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]

    代码实现:

    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:简单粗暴 – 不能旋转就不转

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

    final rotated = _rotatePiece(_currentPiece!);

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

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

    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),定义了每种方块的踢墙偏移表。

    简化版的踢墙实现:

    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 如何检测满行?

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

    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 分数怎么计算合理?

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

    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 等级和速度的关系

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

    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 连击奖励让游戏更有趣

    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实现游戏循环

    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 速度如何随等级提升

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

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

    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个格子)
    • 现代手机每秒可以轻松处理百万次运算

    优化建议:

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

  • 七、本文小结

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

  • 数据结构:用二维数组表示方块和棋盘
  • 碰撞检测:边界+重叠的双重检测
  • 移动旋转:基础移动+矩阵旋转+踢墙优化
  • 消行计分:从下往上扫描+连击奖励
  • 游戏循环:Timer定时触发+动态速度调整
  • 这些都是游戏的底层逻辑,虽然看起来简单,但细节很多。下一篇文章我会讲解如何用CustomPaint把这些数据结构绘制成精美的游戏画面。

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


    参考资料

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

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

    • 社区论坛:开源鸿蒙跨平台开发者社区
    • 技术交流:参与讨论,分享经验

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

    赞(0)
    未经允许不得转载:171主机测评 » Flutter实战:从零实现俄罗斯方块(一)数据结构与核心算法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址