俄罗斯方块这东西,看起来简单,实际做起来能把 Flutter 的边边角角都摸一遍。再加上要跑在鸿蒙上,这里面的适配问题比想象中多得多。这篇文章就围绕“用 Flutter 做跨平台鸿蒙开发 + 俄罗斯方块游戏”这套组合拳,把从环境搭建、游戏逻辑设计到鸿蒙打包上机的完整过程拆开讲清楚,方便同样想入坑 Flutter 鸿蒙开发的兄弟少走弯路。
这个项目适合谁?两类人。一类是想把 Flutter 的跨平台能力落到鸿蒙设备上验证一下的移动端开发,另一类是刚学 Flutter、想找个完整项目练手的前端或客户端新人。俄罗斯方块这个选题非常聪明,它不像电商 App 那样重度依赖业务逻辑,也不像音视频应用那样纠缠底层 SDK,它的核心是一套清晰的状态机和渲染逻辑,恰好能检验 Flutter 在计算密集型场景下的表现,同时也能暴露出平台适配时的各种细节问题。
1. 为什么是 Flutter 跨平台鸿蒙开发这个组合
1.1 鸿蒙生态需要跨平台方案填坑
鸿蒙系统这几年的发展速度有目共睹,但不得不承认,它的原生应用生态相比 Android 和 iOS 还有明显缺口。大量中小团队不可能专门养一支 HarmonyOS 原生开发队伍,市面上的存量应用也不可能说迁移就迁移。这个时候,跨平台框架的桥接价值就体现出来了:一套 Dart 代码,编译产物能同时覆盖 Android、iOS、Web、Windows,再加上对鸿蒙的适配支持,就能极大摊薄多端维护成本。
鸿蒙目前主推的应用开发语言是 ArkTS,但和 Flutter 的 Dart 相比,ArkTS 的社区积累、三方库成熟度、面试人才储备都还差着一截。与其等生态慢慢长起来,不如先用跨平台方案快速补齐产品矩阵。这也是我们当时选择 Flutter 而不是直接上 ArkTS 做鸿蒙版的核心原因——不是 ArkTS 不行,而是我们的团队技术栈和现有代码资产都沉淀在 Flutter 上。
1.2 跨平台方案横向对比,Flutter 优势在哪
拿几张主流方案放一起看一下:
| 方案 | 跨端覆盖 | UI 渲染方式 | 游戏类项目适配度 | 鸿蒙支持成熟度 |
|---|---|---|---|---|
| Flutter | iOS/Android/Web/桌面/鸿蒙 | 自绘引擎 Skia/Impeller | 高,渲染可控 | 社区活跃,适配方案基本可用 |
| React Native | iOS/Android/Web/鸿蒙(社区) | 桥接原生控件 | 中,频繁原生交互有损耗 | 社区分支存在,维护节奏一般 |
| ArkTS 原生 | 鸿蒙 | 官方自绘 ArkUI | 中,游戏需要配合 XComponent | 官方资料齐全,但仅限鸿蒙 |
Flutter 的自绘渲染机制在游戏类 App 上是个隐藏优势。俄罗斯方块这种高频重绘的场景,如果用 RN 这种桥接原生控件的方案,每次界面刷新都要走 JS 和 Native 的通信通道,频繁操作下帧率很难稳住。而 Flutter 的渲染管线是从底层直接绘制到纹理上的,没有中间商赚差价,做游戏逻辑反而比不少跨平台方案更顺手。
1.3 俄罗斯方块这个选题的价值
说句实话,俄罗斯方块做起来远没有看起来那么轻松。它涉及的核心点很全面:七种基础方块的坐标建模、旋转系统的边界处理、碰撞检测、消行判定、计分与速度反馈、键盘和手势双输入通道,任意一个环节偷懒都会在玩起来的时候暴露短板。把这些逻辑用 Flutter 完整实现一遍,就等于把 Flutter 的自定义绘制、状态管理、焦点控制、Timer 调度这几个核心能力全部练到了。
而且俄罗斯方块天然适合做跨平台适配的“试金石”。游戏界面的结构是一致的,逻辑是纯的,剩下就是看不同平台上渲染、输入、生命周期这些底层差异能不能被 Flutter 屏蔽掉。鸿蒙上跑出来的表现,就是这套方案靠不靠谱的直接证据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与鸿蒙工程的初始化
2.1 Flutter SDK 与鸿蒙 SDK 的版本搭配
鸿蒙目前不直接支持官方渠道下载的 Flutter SDK 打 HAP 包,需要用到社区适配过的 Flutter SDK 或 OpenHarmony 分支版本。以我实操时用到的版本组合为例,Flutter 选择 3.7.12 的 ohos 分支,OpenHarmony SDK 选择 API 9 以上,DevEco Studio 用来管理鸿蒙工程和签名。
环境变量是第一个容易踩坑的地方。常规 Flutter 开发只需要配好 Flutter SDK 路径,鸿蒙开发还要检查下面几个值是否指向正确的目录:
bash复制export DEVECO_SDK_HOME=/path/to/ohos-sdk
export PATH=$DEVECO_SDK_HOME/command-line-tools/bin:$PATH
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
镜像源配置非常重要。国内网络环境下,pub.dev 和 flutter storage 的访问经常抽风,不配镜像的话,flutter pub get 能卡到你怀疑人生。配置好之后可以用 flutter doctor -v 检查 Flutter 环境,再用 ohos -v 检查 OpenHarmony 命令行工具。
2.2 创建工程并生成鸿蒙平台目录
普通 Flutter 工程默认只有 android、ios、web、linux 等平台目录,鸿蒙目录需要手动生成。我的做法是先建一个标准 Flutter 工程,再用工具把鸿蒙的 ohos 目录补上。
bash复制flutter create tetris_game
cd tetris_game
# 如果安装了 flutter_ohos 相关工具,可以执行:
flutter create --platforms=ohos .
如果没有现成工具,也可以直接从社区的空模板工程里拷贝 ohos 目录到工程根目录。关键是要保证目录名是 ohos,同时检查 oh-package.json5 里的包名和工程配置是否匹配。
工程目录初始化完,基本结构长这样:
text复制tetris_game/
├── lib/ # Dart 代码主目录
│ ├── main.dart
│ ├── models/ # 数据模型
│ ├── controllers/ # 游戏控制器
│ └── widgets/ # UI 组件
├── ohos/ # 鸿蒙平台目录
│ ├── entry/
│ │ └── src/main/
│ │ ├── ets/ # ArkTS 入口与 UI 外壳
│ │ ├── resources/ # 资源文件
│ │ └── module.json5 # 模块配置
│ ├── build-profile.json5 # 签名与构建配置
│ └── oh-package.json5 # 鸿蒙依赖配置
├── pubspec.yaml
├── android/
└── ios/
2.3 工程配置里那些容易忽略的细节
鸿蒙工程里 build-profile.json5 和 module.json5 是部署上机的关键。签名调试阶段,很多东西有一个没配置对,装到真机上就会出现安装失败或闪退。
一个比较典型的坑是 module.json5 里的 deviceTypes 字段。默认模板可能只有 phone,如果你要跑在平板上,必须显式加上 tablet。此外,requestPermissions 一般不需要额外配置,但如果后续要做本地存储读写,就需要提前把存储权限声明好。这个权限声明和 Android 的 AndroidManifest 是一个逻辑,只是写法不同。
另外,鸿蒙的 UI 入口壳是用 ArkTS 写的。Flutter 跑在鸿蒙上的原理,简单说就是鸿蒙应用启动时,通过一个 Flutter 引擎容器视图加载 Dart 生成的页面。所以 entry/src/main/ets/entryability/EntryAbility.kt 里你会看到,它本质上是在创建一个 FlutterViewController 之类的容器,里面加载了我们的 Flutter 页面。这个壳一般不用动,但如果你需要自定义启动页或者调整页面方向,就要在这里改。
3. 俄罗斯方块的核心数据结构与算法设计
3.1 七种方块的坐标建模
俄罗斯方块的游戏盘通常是一个 10 列、20 行的二维网格。每个方块类型用一组相对坐标来描述它在 4x4 或 3x3 小方格内的占据位置。
这里我采用了一个比较朴素的建模方式:每种方块只有一个基准坐标数组,旋转时统一做矩阵变换,而不是为每种方块硬编码四个旋转状态。这样代码量小、可维护性高,后续想扩展新方块类型也容易。
dart复制class Tetromino {
final int type; // 0=O, 1=I, 2=T, 3=S, 4=Z, 5=J, 6=L
final int rotation; // 0, 1, 2, 3
final int x, y; // 基准点坐标
Tetromino(this.type, this.rotation, this.x, this.y);
List<Point> get cells {
final raw = _baseShapes[type];
final rotated = _rotate(raw, rotation);
return rotated.map((p) => Point(p.x + x, p.y + y)).toList();
}
}
_baseShapes 是一个二维数组,每个元素都是形如 (row, col) 的相对坐标点。旋转时,把 (row, col) 映射为 (col, 3 - row),这就是顺时针旋转九十度的标准公式。对于 4x4 网格来说,这个变换是严格的,不会因为方块形状不同而需要特判。
这里有个容易踩的坑:O 方块是 2x2 的,旋转后必须保证图形不变,如果统一套 4x4 旋转逻辑也没问题,因为旋转前后坐标算出来是同一个形状,只是位置会漂移。为了体验稳,我在旋转后做了一次边界钳制,保证方块不会因为旋转而横向越界。
3.2 碰撞检测与消行判定的实现思路
碰撞检测的规则很好理解:每个方块格子移动到新位置时,判断它是否超出了游戏盘的左右边界或底部边界,或者是否和已有的固定块重叠。如果满足其中任意一个条件,就判定为碰撞。
放下一步的判定我封装成了一个独立函数:
dart复制bool canMove(Tetromino next) {
for (final cell in next.cells) {
if (cell.x < 0 || cell.x >= 10 || cell.y >= 20) return false;
if (cell.y >= 0 && board[cell.y][cell.x] != 0) return false;
}
return true;
}
消行的逻辑则反过来,从最后一行向上扫描,如果某一行全部被填满,就从棋盘里移除这一行,并在顶部补一行空行。这里我踩过一个细节坑:消行后分数变化和方块下落动画最好是先后分开处理,如果同一个帧里同时触发消行和生成新方块,偶尔会出现新方块瞬间卡进已消行的位置,视觉上很不自然。后来我把消行动画和生成新方块的逻辑用 100 毫秒的间隔拆开了,体验立刻顺滑很多。
3.3 游戏状态机与时间驱动机制
俄罗斯方块本质上是一个“时间驱动”的有限状态机。状态可以划分为:准备中、游戏中、暂停中、游戏结束四个大状态。在每个游戏帧中,判断当前状态的响应逻辑:
dart复制enum GameState { ready, playing, paused, gameOver }
游戏主循环用一个周期性的 Timer 驱动,每一步 tick 里尝试将当前方块向下移动一格,如果移动失败,就把方块固定到棋盘上,然后检查消行、生成下一个方块、更新分数和等级。Timer 的周期不是固定的,随着等级升高,interval 逐渐缩短,这才是俄罗斯方块难度递进感的来源。
Timer 在 Flutter 里有两个选择:Timer.periodic 和 Stopwatch + 手动计算。我在最初版本用了 Timer.periodic,后面发现一个问题:当应用切入后台再回前台时,Timer 的间隔会产生累积误差,导致游戏加速跳步。后来改成基于 Stopwatch 的增量更新模型,每次只处理 elapsed - lastUpdate 这段时间内应走的路程,问题就解决了。这个经验对任何实时类游戏都有参考价值:尽量不要依赖定时器的绝对间隔,而是用相对时间差驱动状态刷新。
4. 用 Flutter 实现俄罗斯方块的 UI 与交互
4.1 游戏盘的渲染:CustomPaint vs 组件堆叠
俄罗斯方块的 UI 渲染,最直接的方案是用 GridView 或 Column + Row 堆出一堆 Container。这种方案在最初开发时看效果清晰,但性能很差,因为每次刷新都要重建几十上百个 widget 节点。
我的做法是直接在 CustomPainter 上绘制整个棋盘。CustomPainter 拿到游戏盘的二维数组状态,用 Canvas 把固定块、当前下落块、网格线一次绘制出来。用 Canvas 的好处是刷新代价低,只重绘变化区域即可,而且在鸿蒙设备上的渲染帧率明显比组件堆叠方案稳。
dart复制class BoardPainter extends CustomPainter {
final List<List<int>> board;
final Tetromino? current;
final Color Function(int)? colorOf;
BoardPainter({required this.board, required this.current, this.colorOf});
@override
void paint(Canvas canvas, Size size) {
final cellW = size.width / cols;
final cellH = size.height / rows;
// 绘制网格线
// 绘制固定块
// 绘制当前下落方块
}
@override
bool shouldRepaint(covariant BoardPainter oldDelegate) {
return oldDelegate.board != board || oldDelegate.current != current;
}
}
在 shouldRepaint 里我做了精细控制:只有当棋盘数据引用变化时才会触发重绘。这要求我们在 setState 时,不要修改原来的 List,而是创建一个新的 List 引用来承载更新后的棋盘。这个习惯和 React 的不可变数据思想是一个道理。
4.2 输入处理:键盘、手势两手都要硬
俄罗斯方块的输入设计是一个很容易被忽略但考验细节的部分。我同时支持了物理键盘和触屏手势两套输入方式。
键盘输入用 Flutter 的焦点机制实现。给主界面包一个 Focus 组件,然后通过监听 onKeyEvent 处理方向键和空格键:
dart复制Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
gameController.moveLeft();
break;
case LogicalKeyboardKey.arrowRight:
gameController.moveRight();
break;
case LogicalKeyboardKey.arrowDown:
gameController.softDrop();
break;
case LogicalKeyboardKey.space:
gameController.hardDrop();
break;
case LogicalKeyboardKey.arrowUp:
gameController.rotate();
break;
}
}
return KeyEventResult.handled;
},
)
触屏输入我用的是 GestureDetector,分别处理左右滑动、下滑和点击。需要注意一个问题:如果同时支持点击和滑动,手势竞技场可能会产生冲突。我的处理是把“点击”定义为硬降,“滑动”定义为左右移动,然后把点击识别的 deadline 调大一点,避免快速滑动被误判成点击。
为了让鸿蒙设备上的触控体验不输 PC,屏幕上还额外放了四个方向按钮做兜底。测试时发现一个细节:按钮的 onPressed 响应在快速点击时偶尔会有丢帧感,我改成了 onTapDown 触发移动,手指按下去立刻响应,延迟直接从几十毫秒降到了接近零。
4.3 计数面板、下一个方块预览与游戏结束处理
除了主棋盘,游戏界面还有几个辅助区域:分数、等级、消除行数以及下一个方块的预览。
分数我用一个公式来计算:每次消除 1 行得 100 分乘以当前等级,消除 2 行得 300 分乘以等级,3 行 600 分,4 行 1000 分。这个分数模式不是标准 Tetris 官方规则,而是我根据自己的手感调整的,鼓励玩家一次消多行而不是频繁单消。
下一个方块预览的实现很简单:在棋盘右上角放一个小型 CustomPaint,绘制下一个方块的相对坐标。绘制时注意不要直接复用主棋盘的颜色映射函数,否则预览格的颜色会和主棋盘混在一起看不清。我给预览独立配了一套半透明配色,效果干净很多。
游戏结束时,我弹了一个全屏遮罩,展示最终分数并给出“重新开始”按钮。这里的交互细节是:游戏结束时必须把 Timer 彻底 dispose,否则再次开始时会有多个 Timer 同时在跑,轻则计时错乱,重则内存泄漏。
5. 鸿蒙平台适配、打包与真机验证
5.1 Flutter 与鸿蒙的底层协作原理
了解 Flutter 在鸿蒙上怎么跑,对排查问题帮助很大。Flutter 应用在鸿蒙上运行,本质上是通过鸿蒙的 XComponent 能力承载一个 Flutter 引擎的渲染内容。Dart 代码负责业务逻辑和 UI 描述,ArkTS 壳负责鸿蒙生命周期和系统能力调用,两者之间通过平台通道通信。
这意味着你在 Dart 里写的 TextField、CustomPaint 最终不是渲染成 ArkUI 的组件,而是 Flutter 自绘渲染引擎直接画到 XComponent 视图上的。所以,理论上大部分纯 UI 逻辑在 Android 和鸿蒙上表现是一致的,不一致的通常是系统能力相关的内容,比如 IAP 支付、相册调用、推送能力等。
我在游戏里没有用到太多系统能力,但为了测试平台通道,我给游戏加了一个“保存最高分”的功能。在 Dart 里通过 MethodChannel 调用原生的本地存储能力,在鸿蒙侧用 ArkTS 的 Preferences 实现。这个通道调通的体验,基本就相当于你掌握了 Flutter 鸿蒙原生混搭的基础姿势。
5.2 打包 HAP 的关键步骤与签名配置
打包成鸿蒙安装包 HAP,常规流程是用 DevEco Studio 打开 ohos 工程,配置好签名后直接 Build。但在命令行环境里也可以完成全部操作。
bash复制# 先构建 Flutter 产物
flutter build hap --debug
# 进入鸿蒙工程目录
cd ohos
# 使用 hvigor 构建 HAP
hvigorw assembleHap --mode module -p product=default
签名这一步是新手最容易卡住的。鸿蒙的签名分 debug 和 release 两种,debug 签名可以一键自动生成,release 签名则需要去应用市场申请证书。调试阶段建议直接用自动签名的 debug 包,不要去折腾 release 证书,省下大量时间。
签名配置在 build-profile.json5 的 signingConfigs 里,你需要确保:
json5复制{
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "your.cer",
"storePassword": "your-store-password",
"keyAlias": "yourAlias",
"keyPassword": "your-key-password",
"profile": "your.p7b"
}
}
配置完签名,用 hdc 工具连接鸿蒙真机,执行 hdc install 安装 HAP 包。这个流程和 Android 的 adb install 几乎一样。如果之前编译过 Flutter 的 Android 包,再切到鸿蒙上会觉得很亲切。
5.3 真机性能验证与界面适配实测
我在鸿蒙手机上安装并运行了近百局游戏,重点观察几个指标:
- 游戏下落过程中是否出现帧率波动
- 硬降和旋转时是否有明显的卡顿
- 屏幕旋转或切入后台再切回时,游戏状态是否丢失
- 不同屏幕宽高比下,棋盘是否变形或溢出
Flutter 的布局是像素级定义的,不处理适配的话,在鸿蒙不同机型上会出现棋盘偏左或偏右的情况。我的处理办法是用 AspectRatio 组件包裹棋盘,固定宽高比为 10:20,然后放在屏幕中央,剩余空间均分给辅助面板。这个方案在手机和平板上都能保持合适的比例。
性能方面,中间遇到过一个诡异问题:游戏跑了一段时间后,帧率会从 60 帧跌到 40 帧以下。排查后发现是 CustomPainter 里创建了过多的临时对象,每次重绘都会 new 出大量 Paint、Path,Dart 的 GC 在弱鸡机型上扛不住。优化方式是复用 Paint 对象,尽量少的创建新实例。这提醒我,Flutter 做游戏类项目时,GC 压力一样是真实存在的性能瓶颈。
6. 开发过程中的高发问题与排查实录
6.1 Flutter 鸿蒙开发的常见报错
这一节把我在开发流程中实际踩过的坑整理成速查表,绝大多数是环境和构建类问题:
| 症状 | 常见原因 | 解决方法 |
|---|---|---|
flutter create --platforms=ohos 失败 |
SDK 分支不支持 ohos 参数 | 从社区模板拷贝 ohos 目录 |
| 编译报错:Flutter engine 版本不匹配 | Flutter SDK 和鸿蒙插件版本不一致 | 统一升级到同一版本序列 |
| 真机闪退,logcat 里有 so 找不到 | HAP 缺 src/main/jniLibs 动态库 | 检查 jniLibs 目录是否存在且完整 |
| 界面中文显示为方块 | 缺少中文字体资源 | 在工程中内置字体文件 |
| 后台切回后游戏 UI 黑屏 | XComponent 重建导致 Flutter 渲染丢失 | 在 EntryAbility 中重写 FlutterView 的恢复逻辑 |
这里有个通用排查思路:先确认是 Dart 层问题还是鸿蒙壳层问题。最简单的方法是用 flutter run -d ohos 这种命令直接跑,如果命令能正常输出日志并热重载,那 90% 的 Dart 层问题是可以在堆栈日志中直接定位的。只有 Dart 层日志一切正常但 UI 黑屏或崩溃时,才需要把注意力放到鸿蒙壳层。
6.2 游戏运行中的性能优化专项
俄罗斯方块的性能压力不来自 GPU,而来自 CPU 的布局和重绘开销。我做了几轮优化,效果最明显的是下面这几招:
第一招,控制重绘范围。最初我每次下落 Tick 都对整个棋盘做 setState,这样会导致整个 CustomPainter 重绘。优化后,我把棋盘区域和辅助面板拆成不同的 State,棋盘用 ValueNotifier 驱动重绘,辅助面板用 ValueNotifier 驱动各自的数据展示。这样每次下落时,只有棋盘重绘,分数面板不会跟着白白重建。
第二招,避免在 build 方法里做耗时的计算。我最初把方块的 cells 计算放在了 build 方法里,虽然逻辑没问题,但每次 rebuild 都会重新计算旋转坐标,累积下来非常费 CPU。后来我把 cells 计算的结果缓存到了状态对象里,只有方块发生旋转或移动时才重新计算。
第三招,小心隐式动画。俄罗斯方块里我没用任何隐式动画,因为每个方块的颜色、位置变化都是瞬间的,加了动画反而会拖慢响应速度。如果你想做下落动画效果,建议用显式的 AnimationController,并且只在需要的时候启动,不要在整个游戏周期里一直跑着一个 AnimationController。
6.3 输入响应不灵敏的坑与解决
游戏过程中如果玩家按方向键,下落中的方块如果不能马上响应,手感就会非常黏。这里我踩过一个典型的坑:键盘事件监听放在 GestureDetector 的内部,导致焦点不在棋盘区域时键盘完全不响应。后来我把 Focus 移到了外层根节点,并设置 autofocus: true,这样整个页面加载后键盘事件就能第一时间捕获。
触屏手势的坑更隐蔽。Windows 上测试时滑动很流畅,但到了鸿蒙平板上,横向滑动偶尔会被系统手势拦截,出现“划不动”的感觉。解决方式是在 GestureDetector 的 dragStartBehavior 和 behavior 上做调整,设置 behavior: HitTestBehavior.opaque,并尽量把手势区域限制在棋盘内部,避开屏幕边缘的系统手势热区。
7. 项目经验沉淀与后续扩展思路
7.1 从项目中沉淀的通用代码组织方式
这个项目虽然规模不大,但代码组织方式值得梳理。我采用了 controller + UI 彻底分离的架构:游戏的逻辑全部放在 GameController 类里,UI 只负责将 controller 的状态渲染出来。这样做的好处是,玩家输入、动画、音频等任何外部事件都只需要调用 controller 的方法,而不需要直接操作 widget 树。测试的时候,我甚至可以在没有 UI 的情况下单测 controller 的核心逻辑,这才是代码可维护性的关键。
Controller 内部有一个状态枚举和一张棋盘数据表,对外暴露移动、旋转、硬降、软降等方法,内部维护 Timer 与状态机。UI 侧则通过 ValueListenableBuilder 或 StreamBuilder 监听 controller 的状态变化。这个模式对所有用 Flutter 开发小游戏的人都有参考意义——它把一个看似复杂的游戏逻辑拆成了清晰、可测的模块。
7.2 游戏扩展方向:音效、本地存储与联机对战
俄罗斯方块做完基础版之后,可以扩展的方向非常明确,而且每一个方向都能踩到 Flutter 和鸿蒙的新知识点。
本地存储最高分,我是用 MethodChannel 调鸿蒙 Preferences 实现的。如果你希望代码更通用,也可以直接用 sqflite 或者 shared_preferences 的鸿蒙适配库。音效方面,用 audioplayers 包可以直接播放资源文件,注意鸿蒙平台的音频格式要提前确认好,我试过 mp3 和 ogg 都没问题。
联机对战是更复杂的扩展方向,但这套游戏逻辑天然适合做成互动模式:双方共用一个棋盘或者轮流竞技,用 WebSocket 同步状态。这个扩展会逼着你处理网络延迟、状态同步、断线重连,对理解分布式状态管理很有帮助。如果你是想用这个项目练手,联机对战绝对是性价比最高的进阶方向。
7.3 几个可以复用的小技巧
项目里有些小技巧,在别的 Flutter 项目里也能直接用。
分数数字变化时,如果想要滚动效果,可以自己做一个简单的 TweenAnimationBuilder,不用引第三方库。游戏结束弹窗用 showGeneralDialog 而非 showDialog,因为前者对弹窗动画和背景遮罩的控制粒度更高,在游戏场景中更灵活。在 main 函数里,初始化游戏前先做一次引擎预热操作,可以减少首帧显示延迟:
dart复制void main() {
WidgetsFlutterBinding.ensureInitialized();
// 预热一次布局计算
runApp(const TetrisGameApp());
}
这个预热操作对于鸿蒙这种刚起步的 Flutter 运行环境尤其有效,能减少冷启动时首次 UI 检查带来的卡顿感。
最后再分享一个我在真机调试阶段总结的经验:每一次改完游戏平衡性参数(比如下落速度、消行分数),不要只在模拟器上测。模拟器和真机的性能差异非常大,你可能在模拟器上觉得流畅的难度曲线,到了真机上就变成“游戏还没看清就已经结束了”。我后来形成了一个固定流程:模拟器调试逻辑,真机验证手感,两边都过了才算是调整完成。这个习惯不仅适用于俄罗斯方块,开发任何 Flutter 跨平台小游戏都通用。
