把一个小游戏从 Flutter 迁到 OpenHarmony 上跑通最大的感受就是Dart 层的逻辑基本不用动但“碰撞检测”和“游戏结束处理”这两块却需要重新从算法选型到工程落地都过一遍脑子。你可能会觉得碰撞检测不就是算两个矩形重不重叠嘛游戏结束不就是弹个框嘛等真正在 OpenHarmony 真机上跑起来你才会发现坐标偏移、状态重复触发、渲染卡顿、资源没释放这些坑一个接一个。这篇就以我的实战过程为例把碰撞检测算法怎么选、怎么写、怎么优化以及游戏结束的状态怎么管理、UI 怎么呈现、资源怎么清理完整拆开讲一遍。适合正在用 Flutter 做小游戏、或者准备往 OpenHarmony 平台迁移游戏项目的开发者参考。1. 为什么在 OpenHarmony 上做 Flutter 游戏我的方案选型过程1.1 Flutter 与 OpenHarmony 的适配路径不是把代码拷过来那么简单Flutter 能跑在 OpenHarmony 上靠的是 OpenHarmony 社区维护的 Flutter 引擎适配分支。简单理解Flutter 的上层是 Dart 框架底层是 C 实现的引擎引擎负责渲染、事件、平台通道。OpenHarmony 的适配工作就是把引擎的渲染后端、窗口系统、输入事件这些接到 OpenHarmony 的图形栈和事件框架上。实际项目里你不需要关心引擎编译的细节但要清楚自己的工程结构。一个标准的 Flutter for OpenHarmony 工程除了 lib、pubspec.yaml 这些常规目录还要有一个 ohos 目录里面是 OpenHarmony 的应用壳工程最终通过 DevEco Studio 构建成 HAP 包安装到开发板上。这个壳工程会把 Flutter 引擎和你的 Dart 产物打包在一起相当于把 Flutter 应用“装进”OpenHarmony 的应用框架里。我当时是先在普通 Flutter 环境里把游戏逻辑全部写完再用 OpenHarmony 的 flutter_flutter 分支重新拉依赖、配置 ohos 壳工程。这个过程的坑主要是版本对齐Flutter SDK 版本、OpenHarmony SDK 版本、DevEco Studio 版本三者只要有一个不匹配构建期会报各种莫名其妙的错误比如缺少某个 native 符号、找不到 ohos 平台目录之类的。建议先照着官方文档把一个 demo 工程跑通再往里面塞游戏代码。1.2 项目初始化与工程结构拿 Flutter 跑在 OpenHarmony 上的正确姿势初始化 Flutter for OpenHarmony 工程我用的方式和普通 Flutter 项目不太一样。普通项目直接flutter create就行OpenHarmony 项目需要先拉 OpenHarmony-SIG 的 flutter_flutter 分支然后用它提供的工具生成带 ohos 目录的工程骨架。工程结构上我的游戏代码放在lib/下按模块分成game/核心逻辑、pages/页面、widgets/UI 组件、utils/工具函数。ohos/目录是壳工程里面包含entry模块entry/src/main/ets/下有一个入口 Ability它的作用就是加载 Flutter 引擎并显示 Flutter 页面。你可以在entry/src/main/module.json5里配置应用权限、窗口属性这些。有一点要特别提一下Dart 侧的网络请求、文件读写、传感器调用等能力在 OpenHarmony 上不一定都有现成的插件可能需要通过 MethodChannel 调 ArkTS 侧的接口。但游戏场景里碰撞检测和游戏结束处理这种纯 Dart 逻辑基本不涉及平台能力所以你不用担心在游戏主体逻辑里被平台适配卡脖子。1.3 为什么游戏逻辑仍然用 Dart 而不是 ArkTS性能和开发效率的平衡可能有朋友会问既然都上 OpenHarmony 了直接用 ArkTS 写游戏不更“原生”但我的选择是游戏逻辑全部保留在 Dart 侧理由很简单第一性能。游戏里每帧都要跑的碰撞检测、坐标更新、列表遍历Dart 的 AOT 编译后性能和 C 差距不大优化好的情况下完全可以支撑 2D 小游戏。ArkTS 写 UI 和页面交互很顺手但游戏循环、批量对象管理、状态同步这些写起来没有 Flutter 的组件树复用和自定义绘制方便。第二生态。Flutter 游戏相关的包非常多比如 Flame、forge2d、google_fonts、audioplayers虽然有些需要等社区适配 OpenHarmony但大部分纯 Dart 实现可以直接用。这意味着你从 Flutter 迁移到 OpenHarmony主要工作量在工程适配和真机验证而不是重写一遍游戏逻辑。第三开发效率。Flutter 的热重载在 OpenHarmony 真机调试时依然有效改完碰撞参数、调完 UI 布局保存一下就能看到效果这比改完 ArkTS 重新编译 HAP 快太多。对于碰撞检测这种需要反复调参的逻辑热重载的优势是决定性的。2. 碰撞检测算法拆解从数学判断到 Dart 代码落地2.1 先选型AABB、圆形碰撞还是像素级检测碰撞检测算法说白了就是判断两个物体有没有“碰到”。但“碰到”在不同的游戏类型里定义完全不同。我的项目是一个俯视角 2D 躲避游戏玩家操控一个小方块躲避从四面八方飞来的障碍物。这种场景我直接选了 AABB轴对齐包围盒当主力方案圆形碰撞做辅助像素级检测压根没考虑。三种方案的取舍逻辑是这样的AABB 矩形碰撞适合物体是矩形、或者可以用矩形近似的情况。判断条件是四个数值比较性能极高代码极其简单。缺点是不支持旋转物体一个矩形旋转 45 度后AABB 检测的结果会把实际没有接触的角落也算成碰撞产生“假碰撞”。圆形碰撞适合圆形的球、金币、圆形碰撞体。判断条件是两点距离是否小于半径之和代码同样简单。圆形碰撞不受旋转影响这是它比 AABB 强的地方。像素级碰撞精度最高可以做到“必须真正碰到透明边界才算撞”。但实现需要读取每帧图像的所有像素逐像素比对 alpha 通道开销巨大。在小游戏里性能优先的移动设备上跑像素级碰撞很容易掉帧。我的建议是绝大多数 2D 动作游戏、躲避游戏、平台跳跃游戏AABB 加圆形碰撞的混合方案就够用了。像素级碰撞只适合需要对碰撞“抠细节”的特定场景比如音游里点击必须精确落在音符边缘。普通项目别轻易上。2.2 AABB 矩形碰撞四个条件的判断逻辑与代码实现AABB 的判断原理很好理解两个轴对齐的矩形分别向 x 轴和 y 轴投影。如果两个矩形在 x 轴上的投影区间有重叠并且在 y 轴上的投影区间也有重叠那么两个矩形一定相交。从数学上反推更清晰。两个矩形不相交只有四种情况A 完全在 B 左边、A 完全在 B 右边、A 完全在 B 上边、A 完全在 B 下边。只要这四种情况都不成立那么它们一定相交。用 Dart 写出来就是class AABBCollider { final double x; final double y; final double width; final double height; AABBCollider({ required this.x, required this.y, required this.width, required this.height, }); bool intersects(AABBCollider other) { // A 在 B 左边A.x A.width B.x // A 在 B 右边B.x B.width A.x // A 在 B 上边A.y A.height B.y // A 在 B 下边B.y B.height A.y // 四种情况都没发生就是相交 bool noOverlapX x width other.x || other.x other.width x; bool noOverlapY y height other.y || other.y other.height y; return !noOverlapX !noOverlapY; } }这段代码的关键点在于宽高都是正数判断“A.y A.height B.y”是 A 的底边在 B 的顶边之上说明 A 整个在 B 上方。同理other.y other.height y是 B 的底边在 A 的顶边之上说明 B 整个在 A 上方。这两个条件只要有一个成立y 轴方向就没有重叠两个矩形就不可能相交。实际项目里我还踩过另外一个坑宽高可能是负数。如果你用了负 width 或者负 height 的 Rect上面的判断会直接失效。比如 Flutter 的 Rect 类在构造时不会阻止你传负数但我的 AABBCollider 默认要求 width 和 height 都是正数传进来之前先width.abs()处理一下能省掉很多后续排查的麻烦。还有一点AABB 判断的是轴对齐矩形。如果你的游戏里角色或者障碍物做了旋转AABB 就不准了。Flutter 的 Transform.rotate 会把子组件旋转但碰撞体的坐标依然按旋转前的轴对齐矩形算视觉上已经错开实际碰撞却触发了。这种场景要么在碰撞体上手动同步旋转后的 OBB要么直接用圆形碰撞。小游戏我建议直接上圆形简单可靠。2.3 圆形碰撞检测的平方距离优化圆形碰撞的判断思路更直观两个圆心之间的距离如果小于等于两个半径之和就说明碰撞了。但直接用dart:math的sqrt求距离性能上不划算。平方根运算比乘法运算慢很多每帧几百次碰撞检测累积的开销就很明显。优化的思路是不开方直接比较距离平方和半径之和的平方。bool circleIntersects({ required Offset centerA, required double radiusA, required Offset centerB, required double radiusB, }) { final double dx centerA.dx - centerB.dx; final double dy centerA.dy - centerB.dy; final double distanceSquared dx * dx dy * dy; final double radiusSum radiusA radiusB; return distanceSquared radiusSum * radiusSum; }为什么可以这么做因为开平方函数在定义域上是单调递增的sqrt(a) b等价于a b * b前提是 b 非负。所以直接比较平方值结果和用距离比较完全一致但省掉了每次的sqrt调用。圆形碰撞还有一个优势圆没有“朝向”的概念不管物理对象怎么旋转碰撞判断都不会变这在处理玩家的圆形护盾、角色周围的光圈、金币收集判定时尤其方便。我在项目里就把所有“收集物”统一用圆形碰撞处理而墙壁和障碍物用 AABB两种算法各司其职。2.4 避免每帧全量检测空间划分和对象筛选的工程实践碰撞检测最容易踩的性能坑就是“每帧把所有对象两两比对”。假设场景里有 100 个障碍物再加上玩家每帧就是 100 次碰撞检测看起来不多。但如果是 500 个、1000 个复杂度就是 O(n²)帧率会肉眼可见地掉。游戏里对象数量不多时少于 50 个两层 for 循环完全没问题代码还简单。但超过 100 个以后就要考虑空间划分了。我用得最多的是“网格法”把游戏世界切成固定大小的网格每个物体根据它的坐标放进对应的网格里碰撞检测时只检测“同一个网格”以及“相邻八个网格”里的物体。网格法的实现逻辑是这样的先定一个cellSize比如 64 像素。每个物体根据中心坐标(cx, cy)算出它所在的格子索引gx (cx ~/ cellSize).floor()、gy (cy ~/ cellSize).floor()。把物体按格子索引存进一个Map(int, int), ListCollider。碰撞检测时取出当前物体所在格子及周围格子里的物体列表再做两两判断。class SpatialGrid { final double cellSize; final Map(int, int), ListAABBCollider _cells {}; SpatialGrid({required this.cellSize}); void insert(AABBCollider collider) { int gx (collider.x / cellSize).floor(); int gy (collider.y / cellSize).floor(); _cells.putIfAbsent((gx, gy), () []).add(collider); } ListAABBCollider queryNearby(AABBCollider collider) { int gx (collider.x / cellSize).floor(); int gy (collider.y / cellSize).floor(); ListAABBCollider result []; for (int dx -1; dx 1; dx) { for (int dy -1; dy 1; dy) { final key (gx dx, gy dy); if (_cells.containsKey(key)) { result.addAll(_cells[key]!); } } } return result; } }注意网格的cellSize要选得合理。太大一个格子里的物体太多划分效果不明显太小每个物体要查的邻居格子太多反而浪费。经验值是取游戏里最常见物体尺寸的两倍左右比如主角方块是 32 像素cellSize 就设 64。我在实际项目中还发现如果只是“玩家 vs 所有障碍物”其实不需要空间网格直接遍历障碍物列表就够了。只有“所有物体之间都要互相碰撞检测”的玩法比如弹幕游戏里子弹互相引爆才需要网格法。所以别一上来就整复杂的数据结构先用最简单的遍历把功能跑通卡了再优化。3. 游戏结束处理状态流转、UI 呈现与资源清理3.1 游戏结束的触发条件与判定流程游戏结束处理是“碰撞检测”的下游逻辑。碰撞检测告诉你“玩家撞到了障碍物”游戏结束处理则要决定“这次碰撞的后果是什么”。我的项目里游戏结束的触发条件有三种玩家生命值归零每次碰撞判定成功生命值减一减到 0 就触发 GameOver。时间耗尽关卡有倒计时倒计时结束游戏结束。收集目标达成后的特殊结束比如通关条件达成进入胜利结算这本质上也是一种“结束”。这个流程看起来简单但工程上有一个很关键的坑游戏结束不能直接在碰撞检测的回调里处理。原因很简单碰撞检测每帧可能触发多次如果第一次碰撞就把游戏状态置为结束后续同帧的碰撞回调还会继续跑可能又触发一次结束流程造成重复弹窗、重复计分。我的做法是碰撞检测只负责“通知”游戏逻辑层游戏逻辑层内部通过一个状态机判断当前是否已经处于结束状态如果已经结束后续碰撞直接忽略。这样就能保证 GameOver 在整个生命周期里只触发一次。3.2 用 Provider 维护游戏状态机避免 setState 满天飞游戏结束处理涉及多个 UI 组件游戏页面的计分板、碰撞特效、GameOver 弹窗、重开按钮它们都需要知道当前游戏状态。如果每个组件都用setState自行管理状态分散在各处一旦出现“碰撞触发结束”和“倒计时归零触发结束”同时发生你根本不知道谁先谁后。我用的是状态管理方案是 Provider。先定义一个游戏状态枚举再写一个GameStateNotifier统一管理enum GameState { ready, // 待开始 running, // 进行中 paused, // 暂停 gameOver, // 游戏结束 } class GameStateNotifier extends ChangeNotifier { GameState _state GameState.ready; int _score 0; int _lives 3; GameState get state _state; int get score _score; int get lives _lives; void start() { _state GameState.running; _score 0; _lives 3; notifyListeners(); } void takeDamage() { if (_state ! GameState.running) return; _lives--; if (_lives 0) { _state GameState.gameOver; } notifyListeners(); } void reset() { _state GameState.ready; _score 0; _lives 3; notifyListeners(); } }这里的关键设计是takeDamage()内部就做了状态校验只有running状态下才允许扣血避免了碰撞回调里重复扣血的问题。而且只要状态一变所有监听这个 Notifier 的组件都会收到通知UI 自动刷新。把 Provider 挂在 MaterialApp 的上层ChangeNotifierProvider( create: (_) GameStateNotifier(), child: MaterialApp( home: GamePage(), ), )实际用起来你会感受到ProviderChangeNotifier 这套组合在小项目里的优势不是“少写代码”而是“状态变化路径清晰”。你随时可以从 Notifier 里看到当前处于什么状态、数据是多少排查问题非常方便。3.3 游戏结束界面的呈现与交互从 GameOver 到重新开始游戏结束的 UI 我用了全屏 Stack 遮罩的方式。游戏主界面是一个 Stack底层是游戏画布上层根据游戏状态叠加不同的 UI 层ready状态显示“点击开始”按钮。running状态只显示计分板不遮挡游戏画面。gameOver状态显示半透明遮罩、最终得分、最高分、“再来一局”按钮。用 Consumer 监听游戏状态根据状态决定渲染哪一层ConsumerGameStateNotifier( builder: (context, notifier, _) { switch (notifier.state) { case GameState.gameOver: return GameOverOverlay( score: notifier.score, onRestart: () { notifier.reset(); notifier.start(); }, ); default: return SizedBox.shrink(); } }, )这里有一个值得注意的点“再来一局”不是直接调用notifier.start()而是先reset()再start()。因为游戏结束之后场景里的所有障碍物位置、玩家坐标、粒子特效都需要重置。reset 负责把整个游戏世界恢复成初始状态start 再把状态切换回 running。如果你只调 start那些残留的障碍物和特效就会跟着进入新一局游戏体验极差。游戏结束的动画我用了AnimatedOpacity控制遮罩的淡入配合TweenAnimationBuilder让得分数字有一种“跳分”效果。有一点要提醒游戏结束那一刻最好把 Player 的速度、动画、定时器全部冻结否则画面会出现“游戏已经结束但角色还在动”的尴尬场景。我的做法是在 Notifier 进入 gameOver 状态时同步调用游戏循环管理器的 pause 方法把所有动态对象停住。3.4 清理定时器、动画控制器与内存泄漏避坑游戏结束处理最容易忽略的是资源清理。很多小游戏开发者只关注“弹窗是否显示”不关注“定时器是否还在跑”结果就是游戏结束之后障碍物还在移动、音乐还在播放、内存一直在涨。我总结了一套资源清理的规范所有Timer必须保存引用在游戏结束时cancel()。比如倒计时 Timer如果不取消它会在游戏结束后继续跑可能触发第二次 gameOver。所有AnimationController必须在页面销毁时dispose()否则会报A Ticker was active的异常。游戏循环里创建的粒子特效、动态生成的 Widget在重开时要从父列表中移除不然每局残留几千个 Particle 对象几局之后内存就爆了。一个典型的生命周期管理模板Timer? _countdownTimer; AnimationController? _controller; void _startCountdown() { _countdownTimer?.cancel(); _countdownTimer Timer.periodic(Duration(seconds: 1), (timer) { // 倒计时逻辑 }); } void _stopGame() { _countdownTimer?.cancel(); _countdownTimer null; _controller?.stop(); } override void dispose() { _countdownTimer?.cancel(); _controller?.dispose(); super.dispose(); }这个模板的价值在于不管游戏是正常结束还是被意外中断dispose 兜底能保证资源一定会被回收。Flutter 社区里流传的e/flutter unhandled exception这类崩溃日志有相当一部分就是 Ticker 或 Timer 没清理导致的。4. 真机调试与碰撞/结束逻辑的踩坑实录4.1 先搭好真机调试环境确认碰撞逻辑真的跑在目标系统上模拟器上跑碰撞检测和真机上跑完全是两种体验。OpenHarmony 生态的模拟器成熟度不如 Android很多渲染细节、触摸事件行为必须真机验证。我的调试环境就是一块 RK3568 开发板跑 OpenHarmony 标准系统通过 DevEco Studio 安装 HAP 包。调试时最常用的命令是hdc shell类似 Android 的adb shell。看 Flutter 日志可以这样hdc shell hilog | grep flutter如果游戏有异常崩溃这个命令会直接打出 Dart 侧和原生侧的报错。我在调试碰撞检测时还经常用hdc file send把带有调试日志的 HAP 包推到开发板上然后hdc shell aa start启动应用。有一点要强调OpenHarmony 开发板的渲染性能和手机上差别很大特别是一些低端板子GPU 能力弱Shader 编译慢游戏第一次打开时会有明显的卡顿。这个不是碰撞检测代码的问题而是渲染后端的预热问题。解决方法是避免在游戏启动时一次性加载大量复杂资源可以把场景拆成多个部分分帧加载。4.2 高频问题速查碰撞判定错位、GameOver 重复触发、真机白屏我在真机调试中遇到的问题整理成了一张速查表小伙伴遇到类似情况可以直接对号入座现象根因解决办法碰撞判定偏了角色明明没碰到障碍物却提示 GameOver碰撞体用了 AABB但视觉上有旋转/缩放坐标没有同步旋转物体改用圆形碰撞或手动同步旋转后的碰撞体坐标GameOver 弹窗闪现好几次gameOver 状态没有做状态机保护碰撞回调重复执行在扣血/结束函数里先判断_state ! running直接 return游戏结束后角色还在移动结束时没有冻结对象进入 gameOver 时暂停 GameLoop停止所有动画和定时器真机白屏模拟器正常HAP 包未打包 Dart 产物或 libflutter_engine.so 未包含在包内检查有 ohos 目录的构建配置确认 Dart 产物已打包帧率低碰撞越多越卡每帧全量两两碰撞检测引入空间网格只查相邻格子里的物体重开一局后障碍物越来越多重开时没有清空障碍物列表reset 方法里清空所有动态对象列表重新初始化这里最想展开说两点。第一点碰撞判定错位。这个问题在模拟器上几乎发现不了因为模拟器的屏幕坐标和逻辑坐标比例是固定的但真机上的屏幕宽度、安全区、可能出现的系统导航栏、窗口缩放都会影响 Flutter 组件在画布上的实际坐标。如果碰撞体的 x、y 是用组件在 Stack 里的 Positioned 值直接传的忽略了窗口缩放比例那真机上自然就对不上。我的经验是游戏逻辑层统一使用逻辑分辨率坐标系UI 层只负责把逻辑坐标映射到屏幕坐标中间加一层坐标转换千万别让逻辑层直接碰屏幕像素。第二点GameOver 重复触发。除了状态机保护还有一个隐藏坑碰撞检测可能在“同一次碰撞中”多次返回 true因为一个帧周期内两个物体的移动步进不是单步完成的。我见过一个项目玩家撞到障碍物后由于对象还在继续移动下一帧还在互相穿插又触发了一次碰撞回调导致生命值从 3 直接减到 0。正确的做法是单次碰撞事件加个冷却时间比如碰撞后 200ms 内同一对物体不重复触发或者更简单在 takeDamage 里确保每次碰撞事件有唯一标识。4.3 性能优化对象多时碰撞检测卡顿怎么办真机上当场景中的障碍物数量超过 300 个单纯遍历所有障碍物做 AABB 检测帧率下降到可以感受到的级别。此时空间网格是首选优化方案但不是唯一方案。我实际用过的优化策略包括把游戏世界划分成网格只检测玩家所在格子及相邻 8 个格子中的物体。对物体做“休眠/激活”处理屏幕外的障碍物不参与碰撞检测。将碰撞检测频率从每帧一次降到每两帧一次。对于速度不快、体积较小的物体肉眼几乎感知不到差别但 CPU 时间能省下一半。优先检测距离更近的物体先比较 x 轴投影重叠如果不重叠直接短路跳过这样大部分物体在第一个条件就排除了不用再比较 y 轴。这些优化叠加起来效果非常明显。我在开发板上从 300 个障碍物提升到 1000 个障碍物帧率从 20 帧左右提升到稳定 50 帧以上。当然前提还是“够用原则”先确认卡顿的瓶颈确实在碰撞检测而不是在渲染线程。用 DevEco Studio 自带的性能分析工具抓一下各阶段耗时再决定优化方向。5. 我的个人经验与后续扩展5.1 碰撞检测的“够用原则”别上来就上像素级检测每次有朋友拿着自己写的小游戏问我碰撞检测怎么做我的第一建议永远是“先用 AABB”。不是因为 AABB 最先进而是因为它最简单、最快、最好调。游戏开发里碰撞检测不是越精确越好而是越“够用”越好。像素级碰撞听起来很酷但你要考虑三件事一是性能移动端做全屏像素级碰撞除非物体数量极少否则撑不住二是调试像素级碰撞的误差非常难排查你根本不知道是哪一行像素计算错了三是游戏设计大多数游戏根本不需要那么精确的碰撞边界一个稍微圆润一点的矩形碰撞体玩家的体验差异很难感知到。从 AABB 起步遇到旋转物体的需求再上圆形碰撞遇到真正需要精确边缘的场景再考虑像素级。这个路径既稳妥又高效。5.2 状态管理的心得小游戏用 Provider 足够游戏结束处理的状态管理我见过各种方案有直接用 ValueNotifier 的有上 Bloc 的有自己写 EventBus 的。在小项目里我真心觉得 Provider 是最平衡的选择。它的学习成本低、代码量少而且借助 ChangeNotifier天然支持状态变化监听。如果你熟悉组件通信Provider 还有一个好用的点它把跨组件通信变成了状态读取而不是一层层回调传递。以前你需要把“得分变化”通过回调一层层传给计分板现在直接在计分板组件里context.watchGameStateNotifier().score数据变了 UI 自动刷新中间不留任何胶水代码。5.3 后续还能怎么扩展Flame 引擎、粒子特效、音频与触感反馈这个游戏项目跑通之后我其实还留了几个扩展方向。如果接下来想把功能做得更完整可以往这几个方向走第一接入 Flame 引擎。Flame 是 Flutter 社区最成熟的 2D 游戏引擎它内置了精灵、动画、音频、粒子系统甚至自带简单的碰撞检测组件。如果你不想自己维护游戏循环和碰撞体Flame 可以省很多功夫。第二加粒子特效。碰撞瞬间的火花、游戏结束时的散射粒子能让游戏质感提升不少。Flutter 的 CustomPainter 配合 AnimationController 就能实现轻量粒子系统不需要额外依赖库。第三接 OpenHarmony 的平台能力。比如用振动马达做碰撞触感反馈、用音频服务播放背景音乐、用账号服务保存最高分。这些都需要通过 MethodChannel 调 ArkTS 侧接口把 Dart 的逻辑和 OpenHarmony 的能力打通。第四做性能压测。把障碍物数量成倍提高用 DevEco Studio 的性能工具记录 CPU、GPU、内存的占用曲线确认碰撞检测算法在极限情况下是否稳健。这个步骤能帮你提前发现很多上架铺量后才会暴露的问题。我个人在实际操作中的体会是碰撞检测和游戏结束处理单看都是很小的模块但把两者串起来涉及的是游戏循环、状态管理、资源生命周期、真机适配这一整条链路。任何一个环节偷懒都会在真机上以最直观的方式反馈给你——要么角色穿模要么 GameOver 闪两次要么玩几分钟开始掉帧。顺着这篇的路线走一遍把基础逻辑理顺再把真机上的坑填平后面再往平台上扩展功能就从容很多。