ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Flame 引擎实战教程:从零开发 Ember Quest 无限横版平台跳跃游戏

2026/9/16 13:06:12 拓冰建站 浏览量
Flame 引擎实战教程:从零开发 Ember Quest 无限横版平台跳跃游戏 Flame 引擎实战教程从零开发 Ember Quest 无限横版平台跳跃游戏【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame本教程基于 Flame 游戏引擎带领你以 7 个步骤从零编码完成一款名为Ember Quest的无限横版平台跳跃网页游戏玩家控制火焰精灵 Ember 左右移动、跳跃收集星星并躲避水敌人通过随机分段加载构建永远没有尽头的关卡并配有主菜单、HUD 与游戏结束界面。读完本文后你将掌握 Flame 的组件系统、精灵动画、资源缓存、分段关卡生成、碰撞检测、键盘输入、特效系统与 Overlay 菜单等一整套实战能力并能直接运行、改造仓库中完整的示例工程见 doc/tutorials/platformer/app。教程概览Ember Quest 是什么在开始任何游戏项目之前你都需要一个明确的想法并给它一个名字。本教程中Ember 将踏上收集尽可能多GitHub星星的冒险之旅游戏命名为Ember Quest。整条学习路径被拆解为 7 个步骤Preparation规划游戏、准备美术资源step_1Start Coding搭建游戏主类与主角精灵step_2Building the World构建无限分段关卡世界step_3Adding the Remaining Components补齐星星、水敌人与地面块step_4Controlling Movement键盘控制、碰撞与重力step_5Adding the HUD加入生命值与得分显示step_6Adding Menus主菜单与游戏结束界面step_7本教程假设你具备基础的编程概念并了解 Dart建议边读边对照。游戏需求规划在动手编码前先明确游戏应该做到什么这也是本教程的验收清单Ember 可以被控制向左、向右移动并跳跃关卡是无限的因此需要一种随机加载关卡分段segment的机制目标是收集星星并躲避敌人敌人无法被击杀你需要借助平台来避开它们被敌人击中会扣除 1 点生命值Ember 拥有 3 条生命存在坑洞Ember 掉入坑洞将直接游戏结束有主菜单和游戏结束界面允许玩家重新开始。选择无限横版卷轴形态的原因在于它可以充分展示随机关卡加载能力——没有两次游玩是相同的。同时这种结构很容易改造成传统关卡制游戏第 5 步末尾会说明改造思路。第一步准备工作——项目骨架与美术资源从 bare flame game 起步首先需要完成基础环境搭建跟随 bare flame game tutorial 完成必要的初始化步骤。完成后main.dart应该是如下最小可运行形态import package:flame/game.dart; import package:flutter/widgets.dart; void main() { final game FlameGame(); runApp(GameWidget(game: game)); }规划先画草图再写类开始一个新游戏往往会让人感到无从下手。本教程建议先确定目标平台——由于是网页游戏玩家将通过键盘与游戏交互。然后画一张简单的草图不必完美从草图中就能梳理出需要实现的类Player 类Enemy 类Star 类Platform 类Ground 类HUD 类显示生命值与已收集星星所有这些最终都会被收拢进继承自FlameGame的EmberQuestGame中。美术资源与 pubspec 配置每个游戏都需要资源图片、精灵、动画、声音等。Ember 是 Flame 吉祥物已按像素画风格设计因此本游戏定位于像素画风。教程特意将图片逐个单独保存而非合成精灵表目的是演示 Flame 引擎的资源缓存能力只有 Ember 与水敌人因包含多帧动画而使用精灵表。将以下图片保存到项目的assets/images文件夹中block.png、ember.png、ground.png、heart_half.png、heart.png、star.png、water_enemy.png对应成品见 doc/tutorials/platformer/app/assets/images。图片尺寸不一16x16 的块、32x32 的心、64x64 的地面与星星没有关系因为游戏中会按需缩放。此时项目结构为emberquest/ ├─assets/ │ └─images/ │ ├─block.png │ ├─ember.png │ ├─ground.png │ ├─heart_half.png │ ├─heart.png │ ├─star.png │ └─water_enemy.png ├─lib/ │ └─main.dart └─pubspec.yaml仅仅把图片放进assets目录还不够必须告知 Flutter。在pubspec.yaml中追加flutter: assets: - assets/images/第二步开始编码——游戏主类与主角登场资源加载与 Flame 缓存机制为了保持文件职责单一创建lib/ember_quest.dart作为游戏主类。Ember Quest 使用多张独立图片文件出于性能考虑应利用 Flame 内置的缓存系统文件只加载一次之后可无限次访问而不影响性能。images.loadAll()接收完整资源路径列表并将其以路径为键载入缓存import package:flame/game.dart; class EmberQuestGame extends FlameGame { EmberQuestGame(); override Futurevoid onLoad() async { await images.loadAll([ assets/images/block.png, assets/images/ember.png, assets/images/ground.png, assets/images/heart_half.png, assets/images/heart.png, assets/images/star.png, assets/images/water_enemy.png, ]); } }脚手架GameWidget.managed改造main.dart通过GameWidget托管游戏实例。.managed工厂模式由 GameWidget 负责创建与生命周期管理import package:flame/game.dart; import package:flutter/material.dart; import ember_quest.dart; void main() { runApp( const GameWidgetEmberQuestGame.managed( gameFactory: EmberQuestGame.new, ), ); }此时运行会得到空白屏幕——接下来让 Ember 登场。CameraComponent 与 World要在世界中移动使用FlameGame自带的CameraComponent与World所有游戏组件都添加到world由camera跟随玩家。教程中所有坐标均假定视口viewfinder锚点位于左上角因此需要显式设置camera.viewfinder.anchor Anchor.topLeft;Ember 主角SpriteAnimationComponent HasGameRef创建lib/actors/ember.dart。EmberPlayer继承SpriteAnimationComponent可定义动画并定位并混入HasGameRefEmberQuestGame——该 mixin 允许反向访问游戏类使用其中的变量与方法例如gameRef.images.fromCache(...)从缓存取出已加载的精灵表交给动画import package:flame/components.dart; import ../ember_quest.dart; class EmberPlayer extends SpriteAnimationComponent with HasGameRefEmberQuestGame { EmberPlayer({ required super.position, }) : super(size: Vector2.all(64), anchor: Anchor.center); override void onLoad() { animation SpriteAnimation.fromFrameData( gameRef.images.fromCache(assets/images/ember.png), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(16), stepTime: 0.12, ), ); } }关键参数说明size: Vector2.all(64)Ember 在世界中的尺寸为 64x64SpriteAnimationData.sequencedamount: 4表示ember.png中共有 4 帧每帧 16x16故textureSize: Vector2.all(16)stepTime: 0.12每帧持续 0.12 秒可依据游戏手感随意调整。将 Ember 加入世界位置为屏幕左下角附近_ember EmberPlayer( position: Vector2(128, canvasSize.y - 70), ); world.add(_ember);运行后即可看到 Ember 在左下角闪烁播放动画。第三步构建世界——无限分段关卡系统分段Segment设计无限世界的最佳实现方式是创建可以反复重新加载的分段。每个分段是一个 10x10 的网格每个块为 64x64 像素因此 Ember Quest 的高度固定为 640宽度无限。设计约束分段首尾必须有地面块敌人出现前至少要有 3 个地面块含跨段延续因为敌人将在 3 个块的范围内来回巡逻。Segment Manager 与 Block 数据结构创建lib/objects目录先放入三个占位类ground_block.dart、platform_block.dart、star.dart以及在lib/actors中的water_enemy.dart占位类。随后在lib/managers目录创建segment_manager.dart——这是 Ember Quest 的心脏class Block { // gridPosition position is always segment based X,Y. // 0,0 is the bottom left corner. // 10,10 is the upper right corner. final Vector2 gridPosition; final Type blockType; Block(this.gridPosition, this.blockType); }Block把网格坐标 类型描述为一个列表条目网格左下角为(0,0)右上角为(10,10)。多个Block组成一个分段segment0、segment1……再汇总到segments列表。完整实现见 segment_manager.dart其中定义了 5 个分段final segments [ segment0, segment1, segment2, segment3, segment4, ]; final segment0 [ Block(Vector2(0, 0), GroundBlock), Block(Vector2(1, 0), GroundBlock), Block(Vector2(2, 0), GroundBlock), Block(Vector2(3, 0), GroundBlock), Block(Vector2(4, 0), GroundBlock), Block(Vector2(5, 0), GroundBlock), Block(Vector2(5, 1), WaterEnemy), Block(Vector2(5, 3), PlatformBlock), Block(Vector2(6, 0), GroundBlock), Block(Vector2(6, 3), PlatformBlock), Block(Vector2(7, 0), GroundBlock), Block(Vector2(7, 3), PlatformBlock), Block(Vector2(8, 0), GroundBlock), Block(Vector2(8, 3), PlatformBlock), Block(Vector2(9, 0), GroundBlock), ];后续分段segment1 ~ segment4在此基础上变化引入更高层的平台如(1,1)~(1,3)阶梯、高处星星如(7,7)、(6,7)、(8,8)、不同位置的敌人以及更复杂的平台组合。每个分段同样遵循首尾地面块 敌人前至少 3 个地面块的规则。你可以自由发挥设计自己的分段只需遵守上述规则。将分段载入世界在ember_quest.dart中编写loadGameSegments方法给定分段索引与 x 轴偏移量遍历该分段的每个Block按类型创建对应组件。仓库成品版本使用 Dart 的 switch 表达式完成分发见 ember_quest.dartvoid loadGameSegments(int segmentIndex, double xPositionOffset) { for (final block in segments[segmentIndex]) { final component switch (block.blockType) { const (GroundBlock) GroundBlock( gridPosition: block.gridPosition, xOffset: xPositionOffset, ), const (PlatformBlock) PlatformBlock( gridPosition: block.gridPosition, xOffset: xPositionOffset, ), const (Star) Star( gridPosition: block.gridPosition, xOffset: xPositionOffset, ), const (WaterEnemy) WaterEnemy( gridPosition: block.gridPosition, xOffset: xPositionOffset, ), _ throw UnimplementedError(), }; world.add(component); } }initializeGame负责计算需要加载的分段数量并循环载入用屏幕宽度除以 640一个分段 10 块 × 64 像素向上取整由于仅定义了 5 个分段还需将数值限制在0 ~ segments.length范围内防止超宽屏越界void initializeGame({required bool loadHud}) { // Assume that size.x 3200 final segmentsToLoad (size.x / 640).ceil(); segmentsToLoad.clamp(0, segments.length); for (var i 0; i segmentsToLoad; i) { loadGameSegments(i, (640 * i).toDouble()); } _ember EmberPlayer( position: Vector2(128, canvasSize.y - 128), ); world.add(_ember); if (loadHud) { camera.viewport.add(Hud()); } }随后在onLoad中调用initializeGame。此时各对象类会报错马上逐个补齐。PlatformBlock游戏循环、速度与命中盒游戏引擎的秘密在于游戏循环它无限调用所有对象以供更新。update(double dt)就是这个钩子dt表示距上次调用经过的秒数用来计算组件应在屏幕上移动多远。所有组件必须以相同速度移动因此在EmberQuestGame中定义全局变量objectSpeedlate EmberPlayer _ember; double objectSpeed 0.0;PlatformBlock继承SpriteComponent并混入HasGameRefEmberQuestGame完整实现见 platform_block.dartclass PlatformBlock extends SpriteComponent with HasGameRefEmberQuestGame { final Vector2 gridPosition; double xOffset; final Vector2 velocity Vector2.zero(); PlatformBlock({ required this.gridPosition, required this.xOffset, }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft); override void onLoad() { final platformImage gameRef.images.fromCache(assets/images/block.png); sprite Sprite(platformImage); position Vector2( (gridPosition.x * size.x) xOffset, gameRef.size.y - (gridPosition.y * size.y), ); add(RectangleHitbox(collisionType: CollisionType.passive)); } override void update(double dt) { velocity.x gameRef.objectSpeed; position velocity * dt; if (position.x -size.x) removeFromParent(); super.update(dt); } }要点拆解位置换算x 轴 gridPosition.x × 块宽 xOffsety 轴 游戏高度 − gridPosition.y × 块高因为网格 y 从底部向上计数而屏幕坐标 y 向下增长移动velocity初始为零向量每帧用全局objectSpeed更新 x 分量再乘以dt累加到positionFlame 组件内置的定位变量。平台只左右滚动y 分量恒为 0回收当position.x -size.x已完全滚出屏幕左侧时调用removeFromParent()彻底移出游戏保证无限关卡下的轻量化碰撞添加RectangleHitbox矩形命中盒CollisionType.passive表示被动碰撞体供后续章节讲解碰撞时使用。在loadGameSegments的switch中补上PlatformBlock分支参数gridPosition为Vector2xOffset为用于换算 x 轴偏移的double后运行即可看到平台显示。此时背景是黑色看起来像地牢通过覆写backgroundColor()换成蓝天import package:flutter/material.dart; override Color backgroundColor() { return const Color.fromARGB(255, 173, 223, 247); }第四步补齐剩余组件——星星、水敌人与地面块StarSizeEffect 脉冲效果星星与平台块类似区别在于锚点改为center为让缩放效果看起来正确需要把位置再偏移半个图片尺寸并添加一个尺寸脉冲特效。完整实现见 star.dartclass Star extends SpriteComponent with HasGameRefEmberQuestGame { final Vector2 gridPosition; double xOffset; final Vector2 velocity Vector2.zero(); Star({ required this.gridPosition, required this.xOffset, }) : super(size: Vector2.all(64), anchor: Anchor.center); override void onLoad() { final starImage gameRef.images.fromCache(assets/images/star.png); sprite Sprite(starImage); position Vector2( (gridPosition.x * size.x) xOffset (size.x / 2), gameRef.size.y - (gridPosition.y * size.y) - (size.y / 2), ); add(RectangleHitbox(collisionType: CollisionType.passive)); add( SizeEffect.by( Vector2(-24, -24), EffectController( duration: .75, reverseDuration: .5, infinite: true, curve: Curves.easeOut, ), ), ); } override void update(double dt) { velocity.x gameRef.objectSpeed; position velocity * dt; if (position.x -size.x) removeFromParent(); super.update(dt); } }SizeEffect.by(Vector2(-24, -24), ...)将星星在两个方向各缩小 24 像素并通过EffectController持续 0.75 秒、反向 0.5 秒、无限循环、Curves.easeOut缓动实现无限脉冲。关于 SizeEffect 的详细文档见 effects.md。记得在loadGameSegments中补上case Star分支后运行即可看到脉动的星星。WaterEnemy动画 MoveEffect 巡逻水敌人与 Ember 一样是动画组件继承SpriteAnimationComponent动画数据为 2 帧、每帧 16x16、步进 0.7 秒与星星的区别是把SizeEffect换成MoveEffect完整实现见 water_enemy.dartoverride void onLoad() { animation SpriteAnimation.fromFrameData( gameRef.images.fromCache(assets/images/water_enemy.png), SpriteAnimationData.sequenced( amount: 2, textureSize: Vector2.all(16), stepTime: 0.70, ), ); position Vector2( (gridPosition.x * size.x) xOffset, gameRef.size.y - (gridPosition.y * size.y), ); add(RectangleHitbox(collisionType: CollisionType.passive)); add( MoveEffect.by( Vector2(-2 * size.x, 0), EffectController( duration: 3, alternate: true, infinite: true, ), ), ); }MoveEffect.by(Vector2(-2 * size.x, 0), ...)让敌人向左移动 128 像素-2 × 图片宽度持续 3 秒、alternate: true往返、无限循环——这就是在 3 个块范围内来回巡逻的实现。运行后水敌人会显示并来回移动。GroundBlock分段衔接与随机加载地面块是逻辑最复杂的组件因为它要识别块生命周期中的两个关键时机完整实现见 ground_block.dart块被添加时若它是分段中的最后一块需要把它的位置记录到全局变量块被移除时若它是分段中的第一块需要随机决定加载下一个分段。首先在游戏类中加入两个全局变量用于追踪当前最后一块地面的位置与标识late double lastBlockXPosition 0.0; late UniqueKey lastBlockKey;地面块类自身持有一个唯一键final UniqueKey _blockKey UniqueKey();。在onLoad末尾注册最后一块gridPosition.x 9即网格第 10 列if (gridPosition.x 9 position.x gameRef.lastBlockXPosition) { gameRef.lastBlockKey _blockKey; gameRef.lastBlockXPosition position.x size.x; }在update中持续更新位置减去 10 像素制造轻微重叠以抵消dt抖动造成的缝隙防止移动中加载地图时出现缺口并处理滚出屏幕 → 移除 随机加载下一段override void update(double dt) { velocity.x gameRef.objectSpeed; position velocity * dt; if (position.x -size.x) { removeFromParent(); if (gridPosition.x 0) { gameRef.loadGameSegments( Random().nextInt(segments.length), gameRef.lastBlockXPosition, ); } } if (gridPosition.x 9) { if (gameRef.lastBlockKey _blockKey) { gameRef.lastBlockXPosition position.x size.x - 10; } } super.update(dt); }随机加载使用了dart:math的Random().nextInt(segments.length)返回从 0含到segments.length不含的随机整数从而让关卡永无重复。别忘了在loadGameSegments中补上case GroundBlock分支。运行后即可看到地面铺展开来Ember 因Anchor.center而陷在地面中间是预期现象下一步将用重力与碰撞解决。第五步控制移动——键盘、碰撞与重力键盘输入先为游戏类和 Ember 添加相应 mixinlib/ember_quest.dart添加HasKeyboardHandlerComponentslib/actors/ember.dart添加KeyboardHandler随后实现onKeyEvent回调override bool onKeyEvent(KeyEvent event, SetLogicalKeyboardKey keysPressed) { horizontalDirection 0; horizontalDirection (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; horizontalDirection (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; }horizontalDirection是一个归一化方向变量取值限制为 -1、0、1A/左方向键为 -1D/右方向键为 1。随后定义移动参数并在update中应用final Vector2 velocity Vector2.zero(); final double moveSpeed 200; override void update(double dt) { velocity.x horizontalDirection * moveSpeed; position velocity * dt; super.update(dt); }为了让 Ember 朝行进方向转身在update末尾根据horizontalDirection与scale.x判断调用flipHorizontally()水平翻转。碰撞系统强烈建议先阅读 collision_detection.md 了解 Flame 的碰撞机制。需要三步配置游戏类混入HasCollisionDetection让世界启用碰撞检测class EmberQuestGame extends FlameGame with HasCollisionDetection, HasKeyboardHandlerComponents {Ember 混入CollisionCallbacks并在onLoad中添加CircleHitbox()圆形命中盒实现onCollision回调。当与地面/平台碰撞时利用两个交点计算碰撞法线与分离距离将 Ember 推出碰撞体并判断是否站在地面fromAbove Vector2(0, -1)表示从上方的法线方向override void onCollision(ListVector2 intersectionPoints, PositionComponent other) { if (other is GroundBlock || other is PlatformBlock) { if (intersectionPoints.length 2) { final mid (intersectionPoints.elementAt(0) intersectionPoints.elementAt(1)) / 2; final collisionNormal absoluteCenter - mid; final separationDistance (size.x / 2) - collisionNormal.length; collisionNormal.normalize(); if (fromAbove.dot(collisionNormal) 0.9) { isOnGround true; } position collisionNormal.scaled(separationDistance); } } super.onCollision(intersectionPoints, other); }重力与跳跃加入物理相关变量gravity 15、jumpSpeed 600、terminalVelocity 150、hasJumped false。在onKeyEvent中记录空格键hasJumped keysPressed.contains(LogicalKeyboardKey.space);并在update中组装// Apply basic gravity. velocity.y gravity; // Determine if ember has jumped. if (hasJumped) { if (isOnGround) { velocity.y -jumpSpeed; isOnGround false; } hasJumped false; } // Prevent ember from jumping to crazy fast. velocity.y velocity.y.clamp(-jumpSpeed, terminalVelocity); // Adjust ember position. position velocity * dt;clamp(-jumpSpeed, terminalVelocity)防止跳跃过猛上升、也防止下落过快穿透地面。为了让玩家看到重力与碰撞的效果把 Ember 出生点从canvasSize.y - 70改为canvasSize.y - 128运行后 Ember 会先坠落再着地然后就可以跳跃了。与物体碰撞收集星星与受击在onCollision末尾追加if (other is Star) { other.removeFromParent(); gameRef.starsCollected; } if (other is WaterEnemy) { hit(); }碰到星星即移除它并累加计数碰到敌人调用hit()——用OpacityEffect.fadeOut让 Ember 闪烁并通过hitByEnemy标志防止在无敌闪烁期间重复扣血完整实现见 ember.dartbool hitByEnemy false; void hit() { if (!hitByEnemy) { gameRef.health--; hitByEnemy true; } add( OpacityEffect.fadeOut( EffectController( alternate: true, duration: 0.1, repeatCount: 5, ), ) ..onComplete () { hitByEnemy false; }, ); }卷轴滚动Scroll最后限制 Ember 的移动范围并驱动地图滚动。在update末尾追加gameRef.objectSpeed 0; // Prevent ember from going backwards at screen edge. if (position.x - 36 0 horizontalDirection 0) { velocity.x 0; } // Prevent ember from going beyond half screen. if (position.x 64 gameRef.size.x / 2 horizontalDirection 0) { velocity.x 0; gameRef.objectSpeed -moveSpeed; } position velocity * dt; super.update(dt);逻辑是Ember 不能向左越过屏幕边缘当它向右走到屏幕一半时Ember 自身停下而把objectSpeed设为-moveSpeed让所有世界对象向左滚动——这就是Ember 带动整个世界前进的实现同时地面块会在滚出屏幕后触发下一随机分段的加载。改编为传统关卡制在第 3 步构建分段时可以新增一个含门或特殊块的分段每加载 N 个分段后插入该特殊分段当 Ember 触碰到它时重新加载关卡、保留已收集的星星与生命值即可。第六步添加 HUD——生命值与星星计数在游戏类中加入状态变量int starsCollected 0; int health 3;HeartHealthComponentSpriteGroupComponent 换肤创建lib/overlays/heart.dart。HeartHealthComponent是 SpriteGroupComponent用两张心形图实心/半透明表示生命状态并通过heartNumber与全局health比较来切换状态enum HeartState { available, unavailable, } class HeartHealthComponent extends SpriteGroupComponentHeartState with HasGameRefEmberQuestGame { final int heartNumber; HeartHealthComponent({ required this.heartNumber, required super.position, required super.size, super.scale, super.angle, super.anchor, super.priority, }); override Futurevoid onLoad() async { await super.onLoad(); final availableSprite await gameRef.loadSprite( assets/images/heart.png, srcSize: Vector2.all(32), ); final unavailableSprite await gameRef.loadSprite( assets/images/heart_half.png, srcSize: Vector2.all(32), ); sprites { HeartState.available: availableSprite, HeartState.unavailable: unavailableSprite, }; current HeartState.available; } override void update(double dt) { if (gameRef.health heartNumber) { current HeartState.unavailable; } else { current HeartState.available; } super.update(dt); } }Hud组合文本、星星图标与三颗心创建lib/overlays/hud.dart。Hud继承PositionComponentpriority 5保证绘制在游戏世界之上右上角是星星计数TextComponent 星星SpriteComponent左上角按health数量循环生成心形组件完整实现见 hud.dartclass Hud extends PositionComponent with HasGameRefEmberQuestGame { Hud({ super.position, super.size, super.scale, super.angle, super.anchor, super.children, super.priority 5, }); late TextComponent _scoreTextComponent; override Futurevoid onLoad() async { _scoreTextComponent TextComponent( text: ${gameRef.starsCollected}, textRenderer: TextPaint( style: const TextStyle( fontSize: 32, color: Color.fromRGBO(10, 10, 10, 1), ), ), anchor: Anchor.center, position: Vector2(gameRef.size.x - 60, 20), ); add(_scoreTextComponent); final starSprite await gameRef.loadSprite(assets/images/star.png); add( SpriteComponent( sprite: starSprite, position: Vector2(gameRef.size.x - 100, 20), size: Vector2.all(32), anchor: Anchor.center, ), ); for (var i 1; i gameRef.health; i) { final positionX 40 * i; add( HeartHealthComponent( heartNumber: i, position: Vector2(positionX.toDouble(), 20), size: Vector2.all(32), ), ); } } override void update(double dt) { _scoreTextComponent.text ${gameRef.starsCollected}; } }将 HUD 添加到相机视口这样它始终固定在屏幕上不随世界滚动在initializeGame中调用camera.viewport.add(Hud());。数据联动在ember.dart中收集星星时累加gameRef.starsCollected被击中时gameRef.health--。运行后即可看到生命值扣除与星星计数实时刷新。第七步添加菜单——Overlay 系统菜单使用 Flame 内置的 overlay 系统。Overlay 本质上是叠加在游戏之上的 Flutter Widget非常适合 UI 界面。主菜单 MainMenu创建lib/overlays/main_menu.dart一个标准StatelessWidget黑底圆角容器居中展示标题Ember Quest、Play 按钮和操作说明文字。其中唯一与 Flame 相关的调用是点击 Play 时移除该 OverlayonPressed: () { game.overlays.remove(MainMenu); },注意技术上在菜单显示时玩家仍可通过键盘移动 Ember本教程不处理输入屏蔽实现方式有多种。游戏结束界面 GameOver创建lib/overlays/game_over.dart结构与主菜单一致区别在于按钮点击时先调用game.reset()重置游戏再移除 OverlayonPressed: () { game.reset(); game.overlays.remove(GameOver); },重构 initializeGame 与 reset为了让重新开始不重复添加 HUDinitializeGame增加loadHud参数reset()重置计分与生命值后重新初始化世界override Futurevoid onLoad() async { await images.loadAll([ assets/images/block.png, assets/images/ember.png, assets/images/ground.png, assets/images/heart_half.png, assets/images/heart.png, assets/images/star.png, assets/images/water_enemy.png, ]); camera.viewfinder.anchor Anchor.topLeft; initializeGame(loadHud: true); } void reset() { starsCollected 0; health 3; initializeGame(loadHud: false); }注册并显示菜单在main.dart中通过overlayBuilderMap将 Overlay 名字映射到 Widget并用initialActiveOverlays指定开局显示主菜单void main() { runApp( GameWidgetEmberQuestGame.managed( gameFactory: EmberQuestGame.new, overlayBuilderMap: { MainMenu: (_, game) MainMenu(game: game), GameOver: (_, game) GameOver(game: game), }, initialActiveOverlays: const [MainMenu], ), ); }游戏结束判定在各组件的update中加入健康检查Ember 掉入坑洞position.y size.y size.y时把health置 0health 0时各组件自我移除游戏主类在update中弹出 GameOver Overlay// ember.dart if (position.y gameRef.size.y size.y) { gameRef.health 0; } if (gameRef.health 0) { removeFromParent(); } // water_enemy.dart / platform_block.dart / star.dart if (position.x -size.x || gameRef.health 0) { removeFromParent(); } // ground_block.dart if (gameRef.health 0) { removeFromParent(); } // ember_quest.dart override void update(double dt) { if (health 0) { overlays.add(GameOver); } super.update(dt); }运行与探索成品完整的成品工程位于 doc/tutorials/platformer/app其pubspec.yaml已声明全部 7 张资源图片lib/actorsEmber 与水敌人、lib/managers分段管理、lib/objects地面块/平台块/星星、lib/overlaysHUD/主菜单/游戏结束界面分层清晰是学习 Flame 目录组织的最佳参考。进入该目录执行flutter run或按 web 目标flutter run -d chrome即可直接体验成品游戏。回顾整条开发链路你实际上已经系统走通了 Flame 游戏开发的主干技术组件体系FlameGame、SpriteComponent、SpriteAnimationComponent、PositionComponent、SpriteGroupComponent与HasGameRef混入的组合运用资源管线images.loadAll预加载 images.fromCache/loadSprite缓存取用关卡生成以 10x10 网格分段描述世界Random().nextInt驱动无限随机加载objectSpeeddt驱动统一卷轴滚动交互系统HasKeyboardHandlerComponentsKeyboardHandler处理按键HasCollisionDetectionCollisionCallbacksRectangleHitbox/CircleHitbox处理碰撞与简单物理重力、跳跃、终端速度、碰撞法线分离特效与 UISizeEffect、MoveEffect、OpacityEffect与EffectController组合出脉冲、巡逻与受击闪烁GameWidget.overlayBuilderMapoverlays实现主菜单与结算界面。以这份七步教程为骨架你可以继续扩展设计更多分段、加入音频、增加可击杀敌人、将关卡改为传统关卡制甚至移植到移动端触控输入——Flame 的组件化设计让所有这些扩展都有清晰落点。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考