1. 简介
了解如何使用 Flutter 和 Flame 构建平台游戏!在受《涂鸦跳跃》启发的 Doodle Dash 游戏中,您可以扮演 Dash(Flutter 吉祥物)或她最好的朋友 Sparky(Firebase 吉祥物),并尝试通过在平台上跳跃来达到尽可能高的高度。
学习内容
- 如何在 Flutter 中构建跨平台游戏。
- 如何创建可重用的游戏组件,并将其作为 Flame 游戏循环的一部分进行渲染和更新。
- 如何通过游戏物理学知识为角色(称为精灵)设计可控动作和动画。
- 如何添加和管理碰撞检测。
- 如何添加键盘和触控输入作为游戏控件。
前提条件
此 Codelab 假定您具备一定程度的 Flutter 经验。如果没有,您可以通过创建您的第一个 Flutter 应用 Codelab 来学习一些基础知识。
您将构建的内容
此 Codelab 将指导您构建一个名为 Doodle Dash 的游戏:这是一个以 Flutter 吉祥物 Dash 或 Firebase 吉祥物 Sparky 为角色的平台游戏(此 Codelab 的其余部分将以 Dash 为目标,但这些步骤同样适用于 Sparky)。您的游戏将具有以下特点:
- 一个可以水平和垂直移动的精灵
- 随机生成的平台
- 将精灵向下拉的重力效应
- 游戏菜单
- 游戏内控制,如暂停和重新游戏
- 能够保存得分
游戏内容
在 Doodle Dash 中,玩家可以控制 Dash 左右移动,在平台上跳跃。此外,玩家还可以在游戏过程中使用战力提升道具来短暂提升其能力。玩家选择初始难度级别(1 到 5),然后点击 Start 即可开始游戏。
级别
游戏中有 5 个级别。每个级别(级别 1 之后)都会解锁新功能。
- 级别 1(默认):此级别生成
NormalPlatform
和SpringBoard
平台。创建后,任何平台都有 20% 的几率成为移动平台。 - 级别 2(得分 >= 20):添加仅支持一次跳跃的
BrokenPlatform
。 - 级别 3(得分 >= 40):解锁
NooglerHat
战力提升道具。此特殊平台持续 5 秒,并将 Dash 的跳跃能力提高到其正常水平的 2.5 倍。在这 5 秒内,她还会戴上一顶很酷的 Google 员工帽。 - 4 级(得分 >=80):解锁
Rocket
战力提升道具。此特殊平台具有火箭飞船的形态,可让 Dash 处于无敌状态。它还可以将 Dash 的跳跃能力提高到其正常水平的 3.5 倍。 - 级别 5(得分 >= 100):解锁
Enemy
平台。如果 Dash 与敌人相撞,游戏就会自动结束。
按级别划分的平台类型
级别 1(默认)
|
|
级别 2(得分 >= 20) | 级别 3(得分 >= 40) | 级别 4(得分 >= 80) | 级别 5(得分 >= 100) |
|
|
|
|
游戏失败
以下两种方式会导致游戏失败:
- Dash 掉落到屏幕底部下方。
- Dash 与敌人相撞(游戏达到级别 5 时会生成敌人)。
战力提升道具
战力提升道具可增强角色的游戏能力,例如增加其跳跃速度,或让其对敌人处于“无敌”状态,或两者兼而有之。Doodle Dash 有两种战力提升道具。一次只能激活一种战力提升道具。
- Google 员工帽战力提升道具可以将 Dash 的跳跃高度提高至其正常水平的 2.5 倍。另外,Dash 在战力提升期间会戴着一顶 Google 员工帽。
- 火箭飞船战力提升道具可以让 Dash 在面对敌人平台时处于无敌状态(与敌人碰撞没有影响),并将其跳跃高度提高到正常水平的 3.5 倍。Dash 会乘坐火箭飞行,直到重力克服其速度,让其降落在平台上。
2. 获取 Codelab 起始代码
从 GitHub 下载项目的初始版本:
- 在命令行中,将 GitHub 代码库克隆到
flutter-codelabs
目录:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
此 Codelab 的代码位于 flutter-codelabs/flame-building-doodle-dash
目录中。该目录包含此 Codelab 中每个步骤的完整项目代码。
导入起始应用
- 将
flutter-codelabs/flame-building-doodle-dash/step_02
目录导入您的首选 IDE。
安装软件包:
- 所有必需的软件包(例如 Flame)都已添加到项目
pubspec.yaml
文件中。如果您的 IDE 未自动安装依赖项,请打开命令行终端并从 Flutter 项目的根目录运行以下命令,以检索项目依赖项:
flutter pub get
设置您的 Flutter 开发环境
要完成此 Codelab,您需要具备以下条件:
3. 浏览代码
接下来,浏览下代码。
查看 lib/game/doodle_dash.dart
文件,其中包含扩展 FlameGame
的 DoodleDash 游戏。您向 FlameGame
实例注册组件。该实例是 Flame 中最基本的组件(类似于 Flutter Scaffold
),其作用是在游戏过程中渲染和更新所有已注册的组件。可以将其视为游戏的中枢神经系统。
什么是组件?Flutter 应用由 Widgets
组成。同样,FlameGame
则是由 Components
组成。组件即构成游戏的所有构建块。(与 Flutter widget 相类似,组件也可以有子组件。)精灵角色、游戏背景、负责生成新游戏组件(例如敌人)的对象都是组件。事实上,FlameGame
本身就是一个 Component
;Flame 将其称为 Flame 组件系统。
组件继承自抽象 Component
类。实现 Component
的抽象方法以创建 FlameGame
类的机制。例如,您经常会看到整个 DoodleDash 中实现了以下方法:
onLoad
:异步初始化组件(类似于 Flutter 的initState
方法)update
:在游戏循环的每个 tick 中更新组件(类似于 Flutter 的build
方法)
此外,add
方法还会向 Flame 引擎注册组件。
例如,lib/game/world.dart
文件包含 World
类,该类扩展 ParallaxComponent
以渲染游戏背景。该类接受图片资源列表,并按层对这些图片资源进行渲染,确保每个层以不同的速度移动,以增强真实感。DoodleDash
类包含一个 ParallaxComponent
实例,并通过 DoodleDash onLoad
方法将其添加到游戏中:
lib/game/world.dart
class World extends ParallaxComponent<DoodleDash> {
@override
Future<void> onLoad() async {
parallax = await gameRef.loadParallax(
[
ParallaxImageData('game/background/06_Background_Solid.png'),
ParallaxImageData('game/background/05_Background_Small_Stars.png'),
ParallaxImageData('game/background/04_Background_Big_Stars.png'),
ParallaxImageData('game/background/02_Background_Orbs.png'),
ParallaxImageData('game/background/03_Background_Block_Shapes.png'),
ParallaxImageData('game/background/01_Background_Squiggles.png'),
],
fill: LayerFill.width,
repeat: ImageRepeat.repeat,
baseVelocity: Vector2(0, -5),
velocityMultiplierDelta: Vector2(0, 1.2),
);
}
}
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
...
final World _world = World();
...
@override
Future<void> onLoad() async {
await add(_world);
...
}
}
状态管理
lib/game/managers
目录包含三个为 Doodle Dash 处理状态管理的文件:game_manager.dart
、object_manager.dart
和 level_manager.dart
。
GameManager
类(位于 game_manager.dart
中)跟踪整体游戏状态和记分。
ObjectManager
类(位于 object_manager.dart
中)管理生成和移除平台的位置及时间。您将在稍后为此类添加内容。
最后,LevelManager
类(位于 level_manager.dart
中)管理游戏的难度级别以及玩家升级时的所有相关游戏配置。游戏提供五个难度级别 — 玩家在达到其中一个得分里程碑时进入下一级别。当每次进入下一级别后,游戏难度都会增加,并且 Dash 必须跳跃更远的距离。在整个游戏过程中,重力始终是恒定的。因此,跳跃速度会逐渐增加,以便 Dash 能够达到更远的距离。
每当通过一个平台,玩家的得分就会增加。当玩家达到特定的点数阈值时,游戏会进入下一级别并解锁新的特殊平台,以增加游戏的趣味性和挑战性。
4. 在游戏中添加玩家
此步骤会在游戏中添加角色(在本例中为 Dash)。玩家控制角色,并且所有逻辑均包含在 Player
类中(位于 player.dart
文件中)。Player
类扩展 Flame 的 SpriteGroupComponent
类,该类包含一些抽象方法,您可以覆盖这些方法来实现自定义逻辑。这包括加载资源和精灵、确定玩家位置(水平和垂直位置)、配置碰撞检测以及接受用户输入。
加载资源
Dash 具有不同的精灵形态,代表不同版本的角色和战力提升。例如,以下图标显示了 Dash 和 Sparky 分别面向中心、左侧和右侧。
Flame 的 SpriteGroupComponent
允许您使用 sprites
属性管理多种精灵状态,正如 _loadCharacterSprites
方法中所示。
在 Player
类中,将以下代码行添加到 onLoad
方法以加载精灵资源,并将 Player
的精灵状态设置为面向前方:
lib/game/sprites/player.dart
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadCharacterSprites(); // Add this line
current = PlayerState.center; // Add this line
}
下面分析 _loadCharacterSprites
中用于加载精灵和资源的代码。可以直接在 onLoad
方法中实现此代码,但将其放在单独的方法中有助于组织源代码,并增加其可读性。此方法将映射分配给将每个角色状态与加载的精灵资源进行配对的 sprites
属性,如下所示:
lib/game/sprites/player.dart
Future<void> _loadCharacterSprites() async {
final left = await gameRef.loadSprite('game/${character.name}_left.png');
final right = await gameRef.loadSprite('game/${character.name}_right.png');
final center =
await gameRef.loadSprite('game/${character.name}_center.png');
final rocket = await gameRef.loadSprite('game/rocket_4.png');
final nooglerCenter =
await gameRef.loadSprite('game/${character.name}_hat_center.png');
final nooglerLeft =
await gameRef.loadSprite('game/${character.name}_hat_left.png');
final nooglerRight =
await gameRef.loadSprite('game/${character.name}_hat_right.png');
sprites = <PlayerState, Sprite>{
PlayerState.left: left,
PlayerState.right: right,
PlayerState.center: center,
PlayerState.rocket: rocket,
PlayerState.nooglerCenter: nooglerCenter,
PlayerState.nooglerLeft: nooglerLeft,
PlayerState.nooglerRight: nooglerRight,
};
}
更新玩家组件
Flame 会在事件循环的每个 tick(或帧)中调用一次组件的 update
方法,以重新绘制每个发生变化的游戏组件(类似于 Flutter 的 build
方法)。接下来,在 Player
类的 update
方法中添加逻辑,以确定角色在屏幕上的位置。
在 Player
的 update
方法中,添加以下代码,以计算角色的当前速度和位置:
lib/game/sprites/player.dart
void update(double dt) {
// Add lines from here...
if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;
_velocity.x = _hAxisInput * jumpSpeed; // ... to here.
final double dashHorizontalCenter = size.x / 2;
if (position.x < dashHorizontalCenter) { // Add lines from here...
position.x = gameRef.size.x - (dashHorizontalCenter);
}
if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
position.x = dashHorizontalCenter;
} // ... to here.
// Core gameplay: Add gravity
position += _velocity * dt; // Add this line
super.update(dt);
}
在移动玩家之前,update
方法会检查以确保游戏未处于无法移动玩家的非游戏状态,例如初始状态(游戏首次加载时)或游戏结束状态。
如果游戏处于可游戏状态,则使用 new_position = current_position + (velocity * time-elapsed-since-last-game-loop-tick)
等式计算 Dash 的位置,如以下代码所示:
position += _velocity * dt
在构建 Doodle Dash 时,另一个重要因素是确保包含无限的边界。这样一来,Dash 可以跳出屏幕的左侧边缘并从右侧边缘重新进入,反之亦然。
其实现方法是检查 Dash 的位置是否超出了屏幕的左侧边缘或右侧边缘,如果超出某一侧边缘,则会将 Dash 重新放置到另一侧边缘。
重要事件
在初始设计中,Doodle Dash 适用于在网页和桌面设备上运行,因此该游戏需要支持键盘输入,以便玩家可以控制角色的移动。onKeyEvent
方法允许 Player
组件识别箭头键按下操作,以确定 Dash 应面向左侧移动,还是面向右侧移动。
Dash 向左侧移动时将面向左侧 | Dash 向右侧移动时将面向右侧 |
接下来,实现 Dash 的水平移动能力(如 _hAxisInput
变量中所定义)。您还将让 Dash 面向其移动的方向。
修改 Player
类的 moveLeft
和 moveRight
方法,以定义 Dash 的当前方向:
lib/game/sprites/player.dart
void moveLeft() {
_hAxisInput = 0;
current = PlayerState.left; // Add this line
_hAxisInput += movingLeftInput; // Add this line
}
void moveRight() {
_hAxisInput = 0;
current = PlayerState.right; // Add this line
_hAxisInput += movingRightInput; // Add this line
}
修改 Player
类的 onKeyEvent
方法,以便在按下左箭头键或右箭头键时分别调用 moveLeft
或 moveRight
方法:
lib/game/sprites/player.dart
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
_hAxisInput = 0;
// Add lines from here...
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
moveLeft();
}
if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
moveRight();
} // ... to here.
// During development, it's useful to "cheat"
if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
// jump();
}
return true;
}
现在,Player
类已经可以正常运行了,并且可供 Doodle Dash 游戏使用。
在 DoodleDash 文件中,导入 sprites.dart
,这会让 Player
类可供使用:
lib/game/doodle_dash.dart
import 'sprites/sprites.dart'; // Add this line
在 DoodleDash
类中创建一个 Player
实例:
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
DoodleDash({super.children});
final World _world = World();
LevelManager levelManager = LevelManager();
GameManager gameManager = GameManager();
int screenBufferSpace = 300;
ObjectManager objectManager = ObjectManager();
late Player player; // Add this line
...
}
接下来,根据玩家选择的难度级别初始化和配置 Player
跳跃速度,并将 Player
组件添加到 FlameGame
。在 setCharacter
方法中填入以下代码:
lib/game/doodle_dash.dart
void setCharacter() {
player = Player( // Add lines from here...
character: gameManager.character,
jumpSpeed: levelManager.startingJumpSpeed,
);
add(player); // ... to here.
}
调用 initializeGameStart
开始位置的 setCharacter
方法。
lib/game/doodle_dash.dart
void initializeGameStart() {
setCharacter(); // Add this line
...
}
此外,在 initializeGameStart
中,对玩家调用 resetPosition
,以便在每次游戏开始时将玩家的角色移回起始位置。
lib/game/doodle_dash.dart
void initializeGameStart() {
...
levelManager.reset();
player.resetPosition(); // Add this line
objectManager = ObjectManager(
minVerticalDistanceToNextPlatform: levelManager.minDistance,
maxVerticalDistanceToNextPlatform: levelManager.maxDistance);
...
}
运行应用。开始游戏,Dash 出现在屏幕上!
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
5. 添加平台
此步骤会添加一些平台(供 Dash 着落和跳开),以及用于确定 Dash 跳跃是否成功的碰撞检测逻辑。
首先,查看 Platform
抽象类:
lib/game/sprites/platform.dart
abstract class Platform<T> extends SpriteGroupComponent<T>
with HasGameRef<DoodleDash>, CollisionCallbacks {
final hitbox = RectangleHitbox();
bool isMoving = false;
Platform({
super.position,
}) : super(
size: Vector2.all(100),
priority: 2,
);
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
}
}
什么是碰撞框?
Doodle Dash 中引入的每一个平台组件均扩展了 Platform<T>
抽象类,该抽象类是一个带有碰撞框 (hitbox) 的 SpriteComponent
。碰撞框可允许精灵组件检测其是否与其他具有碰撞框的对象发生了碰撞。Flame 支持多种碰撞框形状,例如矩形、圆形和多边形。例如,Doodle Dash 为平台使用矩形碰撞框,并为 Dash 使用圆形碰撞框。Flame 负责处理用于计算碰撞的数学运算。
Platform
类为所有子类型添加了一个碰撞框和碰撞回调。
添加标准平台
Platform
类用于为游戏添加平台。普通平台的形态为 4 个随机选择的视觉元素之一:显示器、手机、终端或笔记本电脑。视觉元素的选择不会影响平台的行为。
|
通过添加 NormalPlatformState
枚举和 NormalPlatform
类来添加标准静态平台:
lib/game/sprites/platform.dart
enum NormalPlatformState { only } // Add lines from here...
class NormalPlatform extends Platform<NormalPlatformState> {
NormalPlatform({super.position});
final Map<String, Vector2> spriteOptions = {
'platform_monitor': Vector2(115, 84),
'platform_phone_center': Vector2(100, 55),
'platform_terminal': Vector2(110, 83),
'platform_laptop': Vector2(100, 63),
};
@override
Future<void>? onLoad() async {
var randSpriteIndex = Random().nextInt(spriteOptions.length);
String randSprite = spriteOptions.keys.elementAt(randSpriteIndex);
sprites = {
NormalPlatformState.only: await gameRef.loadSprite('game/$randSprite.png')
};
current = NormalPlatformState.only;
size = spriteOptions[randSprite]!;
await super.onLoad();
}
} // ... to here.
接下来,生成供角色进行交互的平台。
ObjectManager
扩展了 Flame 的 Component
类,并在整个游戏中生成 Platform
对象。在 ObjectManager
的 update
和 onMount
方法中实现生成平台的功能。
在 ObjectManager
类中创建一个名为 _semiRandomPlatform
的新方法,以便生成平台。您稍后将更新此方法以返回不同类型的平台,但目前,仅返回一个 NormalPlatform
:
lib/game/managers/object_manager.dart
Platform _semiRandomPlatform(Vector2 position) { // Add lines from here...
return NormalPlatform(position: position);
} // ... to here.
重写 ObjectManager
的 update
方法,这次使用 _semiRandomPlatform
方法生成一个平台并将其添加到游戏中:
lib/game/managers/object_manager.dart
@override // Add lines from here...
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
// Losing the game: Add call to _maybeAddEnemy()
// Powerups: Add call to _maybeAddPowerup();
}
super.update(dt);
} // ... to here.
在 ObjectManager
的 onMount
方法中执行同样的操作。最终目的就是,当游戏第一次运行时,_semiRandomPlatform
方法会生成一个起始平台并将其添加到游戏中。
添加 onMount
方法,代码如下:
lib/game/managers/object_manager.dart
@override // Add lines from here...
void onMount() {
super.onMount();
var currentX = (gameRef.size.x.floor() / 2).toDouble() - 50;
var currentY =
gameRef.size.y - (_rand.nextInt(gameRef.size.y.floor()) / 3) - 50;
for (var i = 0; i < 9; i++) {
if (i != 0) {
currentX = _generateNextX(100);
currentY = _generateNextY();
}
_platforms.add(
_semiRandomPlatform(
Vector2(
currentX,
currentY,
),
),
);
add(_platforms[i]);
}
} // ... to here.
例如,如以下代码所示,configure
方法让 Doodle Dash 游戏能够重新配置平台之间的最小和最大距离,并在难度级别增加时启用特殊平台:
lib/game/managers/object_manager.dart
void configure(int nextLevel, Difficulty config) {
minVerticalDistanceToNextPlatform = gameRef.levelManager.minDistance;
maxVerticalDistanceToNextPlatform = gameRef.levelManager.maxDistance;
for (int i = 1; i <= nextLevel; i++) {
enableLevelSpecialty(i);
}
}
DoodleDash
实例(位于 initializeGameStart
方法中)会创建一个已初始化的 ObjectManager
,系统会根据难度级别对其进行配置,并将其添加到 Flame 游戏中:
lib/game/doodle_dash.dart
void initializeGameStart() {
gameManager.reset();
if (children.contains(objectManager)) objectManager.removeFromParent();
levelManager.reset();
player.resetPosition();
objectManager = ObjectManager(
minVerticalDistanceToNextPlatform: levelManager.minDistance,
maxVerticalDistanceToNextPlatform: levelManager.maxDistance);
add(objectManager);
objectManager.configure(levelManager.level, levelManager.difficulty);
}
ObjectManager
再次出现在 checkLevelUp
方法中。当玩家进入下一级别时,ObjectManager
会根据难度级别重新配置其平台生成参数。
lib/game/doodle_dash.dart
void checkLevelUp() {
if (levelManager.shouldLevelUp(gameManager.score.value)) {
levelManager.increaseLevel();
objectManager.configure(levelManager.level, levelManager.difficulty);
}
}
热重载 (如果在网页上进行测试,则重新启动)以启用更改。(保存文件,然后使用 IDE 中的按钮,或者从命令行输入 r
以热重载。)开始游戏,屏幕上会显示 Dash 和一些平台:
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
6. 核心游戏内容
现在,您已经实现了单独的 Player
和 Platform
widget。接下来,您可以开始将所有内容整合在一起。此步骤将实现核心功能、碰撞检测和相机移动。
重力
为了增加游戏的真实感,Dash 会受到重力的作用。重力会在 Dash 跳跃时向其向下拉。在当前的 Doodle Dash 版本中,重力保持为恒定的正值,始终将 Dash 向下拉。不过,您在以后可以选择改变重力来实现其他效果。
在 Player
类中,添加一个值为 9 的 _gravity
属性:
lib/game/sprites/player.dart
class Player extends SpriteGroupComponent<PlayerState>
with HasGameRef<DoodleDash>, KeyboardHandler, CollisionCallbacks {
...
Character character;
double jumpSpeed;
final double _gravity = 9; // Add this line
@override
Future<void> onLoad() async {
...
}
...
}
修改 Player
的 update
方法以添加 _gravity
变量,以影响 Dash 的垂直速度:
lib/game/sprites/player.dart
void update(double dt) {
if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;
_velocity.x = _hAxisInput * jumpSpeed;
final double dashHorizontalCenter = size.x / 2;
if (position.x < dashHorizontalCenter) {
position.x = gameRef.size.x - (dashHorizontalCenter);
}
if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
position.x = dashHorizontalCenter;
}
_velocity.y += _gravity; // Add this line
position += _velocity * dt;
super.update(dt);
}
碰撞检测
Flame 支持开箱即用的碰撞检测。如需在您的 Flame 游戏中启用碰撞检测,请添加 HasCollisionDetection
mixin。如果查看 DoodleDash
类,您会看到其中已经添加了此 mixin:
lib/game/doodle_dash.dart
class DoodleDash extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
...
}
接下来,使用 CollisionCallbacks
mixin 将碰撞检测添加到各个游戏组件。此 mixin 将为组件赋予对 onCollision
回调的访问权限。两个带有碰撞框的对象发生碰撞会触发 onCollision
回调,并传递到其相撞对象的引用,以便于实现对象反应方式的逻辑。
回顾上一步,Platform
抽象类已经具有 CollisionCallbacks
mixin 和一个碰撞框。Player
类也有 CollisionCallbacks
mixin,因此您只需将 CircleHitbox
添加到 Player
类。实际上,Dash 的碰撞框只是一个圆形,因为 Dash 要比矩形更圆。
在 Player
类中,导入 sprites.dart
以便其能够访问各种 Platform
类:
lib/game/sprites/player.dart
import 'sprites.dart';
在 Player
类的 onLoad
方法中添加一个 CircleHitbox
:
lib/game/sprites/player.dart
@override
Future<void> onLoad() async {
await super.onLoad();
await add(CircleHitbox()); // Add this line
await _loadCharacterSprites();
current = PlayerState.center;
}
Dash 需要一个跳跃方法,以便在与平台发生碰撞时完成跳跃。
添加一个 jump
方法,该方法接受一个可选的 specialJumpSpeed
值:
lib/game/sprites/player.dart
void jump({double? specialJumpSpeed}) {
_velocity.y = specialJumpSpeed != null ? -specialJumpSpeed : -jumpSpeed;
}
添加以下代码,覆盖 Player
的 onCollision
方法:
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
}
}
}
每当 Dash 下落并与 NormalPlatform
的顶部发生碰撞时,此回调都将调用 Dash 的 jump
方法。isMovingDown && isCollidingVertically
语句将确保 Dash 在平台中向上移动,而不会触发跳跃。
相机移动
当 Dash 在游戏中向上移动时,相机应保持跟随,而当 Dash 下落时,相机应保持静止。
在 Flame 中,如果“世界”大于屏幕,使用相机的 worldBounds
添加边界,告知 Flame 应当显示世界的哪一部分。为了让相机在保持水平固定的同时向上移动,根据玩家的位置调整每次更新的顶部和底部世界边界,但保持左右边界不变。
在 DoodleDash
类中,将以下代码添加到 update
方法中,确保相机在游戏过程中始终跟随 Dash:
lib/game/doodle_dash.dart
@override
void update(double dt) {
super.update(dt);
if (gameManager.isIntro) {
overlays.add('mainMenuOverlay');
return;
}
if (gameManager.isPlaying) {
checkLevelUp();
// Add lines from here...
final Rect worldBounds = Rect.fromLTRB(
0,
camera.position.y - screenBufferSpace,
camera.gameSize.x,
camera.position.y + _world.size.y,
);
camera.worldBounds = worldBounds;
if (player.isMovingDown) {
camera.worldBounds = worldBounds;
}
var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
if (!player.isMovingDown && isInTopHalfOfScreen) {
camera.followComponent(player);
} // ... to here.
}
}
接下来,每当重新开始游戏时,系统都必须将 Player
位置和相机边界重置为起始点。
在 initializeGameStart
方法中添加以下代码:
lib/game/doodle_dash.dart
void initializeGameStart() {
...
levelManager.reset();
// Add the lines from here...
player.reset();
camera.worldBounds = Rect.fromLTRB(
0,
-_world.size.y,
camera.gameSize.x,
_world.size.y +
screenBufferSpace,
);
camera.followComponent(player);
// ... to here.
player.resetPosition();
...
}
进入下一级别时增加跳跃速度
核心游戏内容中的最后一项就是要求 Dash 的跳跃速度随着难度级别的增加而增加,并且各平台之间的距离会越来越远。
添加对 setJumpSpeed
方法的调用,并提供与当前级别相关联的跳跃速度:
lib/game/doodle_dash.dart
void checkLevelUp() {
if (levelManager.shouldLevelUp(gameManager.score.value)) {
levelManager.increaseLevel();
objectManager.configure(levelManager.level, levelManager.difficulty);
player.setJumpSpeed(levelManager.jumpSpeed); // Add this line
}
}
热重载 (或者,如果在网页环境中,则重新启动)以启用更改。(保存文件,然后使用 IDE 中的按钮,或者从命令行输入 r
以热重载。):
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
7. 有关平台的更多操作
现在,ObjectManager
会生成可供 Dash 跳跃的平台。接下来,您可以再为其提供一些更加刺激的特殊平台。
继续添加 BrokenPlatform
和 SpringBoard
类。顾名思义,BrokenPlatform
会在一次跳跃后破碎,而 SpringBoard
则会提供一个蹦床,让 Dash 能够跳得更高更快。
|
|
与 Player
类一样,所有这些平台类都依赖 enums
来表示其当前状态。
lib/game/sprites/platform.dart
enum BrokenPlatformState { cracked, broken }
平台的 current
状态发生变化也会改变游戏中的精灵形态。在 sprites
属性上定义 State
枚举与图像资源之间的映射,以关联分配给每个状态的精灵。
添加 BrokenPlatformState
枚举和 BrokenPlatform
类:
lib/game/sprites/platform.dart
enum BrokenPlatformState { cracked, broken } // Add lines from here...
class BrokenPlatform extends Platform<BrokenPlatformState> {
BrokenPlatform({super.position});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprites = <BrokenPlatformState, Sprite>{
BrokenPlatformState.cracked:
await gameRef.loadSprite('game/platform_cracked_monitor.png'),
BrokenPlatformState.broken:
await gameRef.loadSprite('game/platform_monitor_broken.png'),
};
current = BrokenPlatformState.cracked;
size = Vector2(115, 84);
}
void breakPlatform() {
current = BrokenPlatformState.broken;
}
} // ... to here.
添加 SpringState
枚举和 SpringBoard
类:
lib/game/sprites/platform.dart
enum SpringState { down, up } // Add lines from here...
class SpringBoard extends Platform<SpringState> {
SpringBoard({
super.position,
});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprites = <SpringState, Sprite>{
SpringState.down:
await gameRef.loadSprite('game/platform_trampoline_down.png'),
SpringState.up:
await gameRef.loadSprite('game/platform_trampoline_up.png'),
};
current = SpringState.up;
size = Vector2(100, 45);
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isCollidingVertically) {
current = SpringState.down;
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
current = SpringState.up;
}
} // ... to here.
接下来,在 ObjectManager
中启用特殊平台。为凸显特殊性,您并不希望在游戏中始终都能看到特殊平台,因此为它们设置有条件的生成几率:SpringBoard
的几率为 15%,BrokenPlatform
的几率为 10%。
在 ObjectManager
的 _semiRandomPlatform
方法内部,在返回 NormalPlatform
的语句前面,添加以下代码,以根据条件返回一个特殊平台:
lib/game/managers/object_manager.dart
Platform _semiRandomPlatform(Vector2 position) {
if (specialPlatforms['spring'] == true && // Add lines from here...
probGen.generateWithProbability(15)) {
return SpringBoard(position: position);
}
if (specialPlatforms['broken'] == true &&
probGen.generateWithProbability(10)) {
return BrokenPlatform(position: position);
} // ... to here.
return NormalPlatform(position: position);
}
在一定程度上,游戏乐趣在于升级时解锁新的挑战和功能。
您希望从级别 1 开始就启用跳板,但当 Dash 达到级别 2 之后,游戏就会解锁 BrokenPlatform
,以增加游戏的难度。
在 ObjectManager
类中,修改 enableLevelSpecialty
方法(当前为桩),在其中添加一个 switch
语句,为级别 1 启用 SpringBoard
平台,并为级别 2 启用 BrokenPlatform
平台:
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) { // Add lines from here...
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
} // ... to here.
}
接下来,让平台能够在水平方向上左右移动。在 Platform
抽象类中**,**添加以下 _move
方法:
lib/game/sprites/platform.dart
void _move(double dt) {
if (!isMoving) return;
final double gameWidth = gameRef.size.x;
if (position.x <= 0) {
direction = 1;
} else if (position.x >= gameWidth - size.x) {
direction = -1;
}
_velocity.x = direction * speed;
position += _velocity * dt;
}
当处于移动状态的平台到达游戏画面的边缘时,它会改变为向相反的方向移动。计算平台位置的方法与计算 Dash 位置的方法相同。_direction
与平台 speed
相乘即可得出速度。然后,将速度乘以 time-elapsed
,并将所得距离与平台的当前 position
相加。
覆盖 Platform
类的 update
方法以调用 _move
方法:
lib/game/sprites/platform.dart
@override
void update(double dt) {
_move(dt);
super.update(dt);
}
若要触发处于移动状态的 Platform
,请在 onLoad
方法中,以 20% 的几率随机将 isMoving
布尔值设置为 true
。
lib/game/sprites/platform.dart
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
final int rand = Random().nextInt(100); // Add this line
if (rand > 80) isMoving = true; // Add this line
}
最后,在 Player
中,修改 Player
类的 onCollision
方法,以识别与 Springboard
或 BrokenPlatform
的碰撞。请注意,SpringBoard
会以 2 倍的速度调用 jump
,而 BrokenPlatform
仅在其状态为 .cracked
时才会调用 jump
,在已经完成跳跃的 .broken
状态下则不会进行调用:
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) { // Add lines from here...
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
} // ... to here.
}
}
重新启动应用。开始游戏即可看到处于移动状态的平台、SpringBoard
和 BrokenPlatform
!
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
8. 游戏失败
此步骤将为 Doodle Dash 游戏添加失败条件。以下两种方式会导致玩家失败:
- Dash 错过一个平台,并掉到屏幕底部以下。
- Dash 与
Enemy
平台发生碰撞。
在实现任一“游戏结束”条件之前,您需要添加将 DoodleDash 游戏状态设置为 gameOver
的逻辑。
在 DoodleDash
类中**,**添加一个 onLose
方法,以便在游戏结束时调用。该方法会设置游戏状态,从屏幕中移除玩家,并激活**Game Over**菜单/叠加层。
lib/game/sprites/doodle_dash.dart
void onLose() { // Add lines from here...
gameManager.state = GameState.gameOver;
player.removeFromParent();
overlays.add('gameOverOverlay');
} // ... to here.
Game Over 菜单:
在 DoodleDash
的 update
方法顶部,添加以下代码,以便在游戏状态为 GameOver
时停止游戏更新:
lib/game/sprites/doodle_dash.dart
@override
void update(double dt) {
super.update(dt);
if (gameManager.isGameOver) { // Add lines from here...
return;
} // ... to here.
...
}
此外,在 update
方法中,当玩家掉落到屏幕底部以下时调用 onLose
。
lib/game/sprites/doodle_dash.dart
@override
void update(double dt) {
...
if (gameManager.isPlaying) {
checkLevelUp();
final Rect worldBounds = Rect.fromLTRB(
0,
camera.position.y - screenBufferSpace,
camera.gameSize.x,
camera.position.y + _world.size.y,
);
camera.worldBounds = worldBounds;
if (player.isMovingDown) {
camera.worldBounds = worldBounds;
}
var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
if (!player.isMovingDown && isInTopHalfOfScreen) {
camera.followComponent(player);
}
// Add lines from here...
if (player.position.y >
camera.position.y +
_world.size.y +
player.size.y +
screenBufferSpace) {
onLose();
} // ... to here.
}
}
敌人具有各种不同的形状和大小;在 Doodle Dash 中,敌人的形态为垃圾桶或错误文件夹图标。玩家应避免与任一敌人发生碰撞,因为这会导致游戏立即结束。
|
添加 EnemyPlatformState
枚举和 EnemyPlatform
类以创建敌人平台类型:
lib/game/sprites/platform.dart
enum EnemyPlatformState { only } // Add lines from here...
class EnemyPlatform extends Platform<EnemyPlatformState> {
EnemyPlatform({super.position});
@override
Future<void>? onLoad() async {
var randBool = Random().nextBool();
var enemySprite = randBool ? 'enemy_trash_can' : 'enemy_error';
sprites = <EnemyPlatformState, Sprite>{
EnemyPlatformState.only:
await gameRef.loadSprite('game/$enemySprite.png'),
};
current = EnemyPlatformState.only;
return super.onLoad();
}
} // ... to here.
EnemyPlatform
类扩展了 Platform
超类型。ObjectManager
会生成和管理敌人平台,就像生成和管理所有其他平台一样。
在 ObjectManager
中,添加以下代码以生成和管理敌人平台:
lib/game/managers/object_manager.dart
final List<EnemyPlatform> _enemies = []; // Add lines from here...
void _maybeAddEnemy() {
if (specialPlatforms['enemy'] != true) {
return;
}
if (probGen.generateWithProbability(20)) {
var enemy = EnemyPlatform(
position: Vector2(_generateNextX(100), _generateNextY()),
);
add(enemy);
_enemies.add(enemy);
_cleanupEnemies();
}
}
void _cleanupEnemies() {
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
while (_enemies.isNotEmpty && _enemies.first.position.y > screenBottom) {
remove(_enemies.first);
_enemies.removeAt(0);
}
} // ... to here.
ObjectManager
保存敌人对象列表 _enemies
。_maybeAddEnemy
以 20% 的几率生成敌人,并将对象添加到敌人列表中。_cleanupEnemies()
方法移除不再可见的过时 EnemyPlatform
对象。
在 ObjectManager
的 update
方法中调用 _maybeAddEnemy()
以生成敌人平台:
lib/game/managers/object_manager.dart
@override
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
_maybeAddEnemy(); // Add this line
}
super.update(dt);
}
在 Player
的 onCollision
方法中添加内容,以检查是否与 EnemyPlatform
发生碰撞。如果发生碰撞,则调用 onLose()
方法。
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is EnemyPlatform) { // Add lines from here...
gameRef.onLose();
return;
} // ... to here.
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) {
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
}
}
}
最后,修改 ObjectManager
的 enableLevelSpecialty
方法,在 switch
语句中添加级别 5:
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) {
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
case 5: // Add lines from here...
enableSpecialty('enemy');
break; // ... to here.
}
}
您已经增加了游戏的挑战性。接下来,请热重载 以启用更改。(保存文件,然后使用 IDE 中的按钮,或者从命令行输入 r
以热重载。):
请注意那些破碎的文件夹敌人。它们很狡猾。它们与背景融为一体!
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
9. 战力提升道具
此步骤将添加一些增强的游戏功能,以便在整个游戏过程中提升 Dash 的战力。Doodle Dash 有两种战力提升道具:Google 员工帽和火箭飞船。您可以将这些战力提升道具视为另一种特殊平台。当 Dash 在游戏中跳跃时,如果碰撞并拥有 Google 员工帽或火箭飞船道具,其速度会加快。
|
|
当玩家得分 >= 40 后,即在级别 3 后,游戏会生成 Google 员工帽。当与 Google 员工帽发生碰撞时,Dash 会戴上 Google 员工帽,其速度会提升到正常水平的 2.5 倍。这将持续 5 秒。
当玩家得分 >= 80 后,即在级别 4 后,游戏会生成火箭。当 Dash 与火箭发生碰撞时,其精灵将变为火箭飞船的形态,并且其速度会提升到正常水平的 3.5 倍,直到其降落在某个平台上。另一项效果是,当拥有火箭道具时,Dash 面对敌人将处于无敌状态。
NooglerHat 和 Rocket 精灵扩展了 PowerUp
抽象类。与 Platform
抽象类一样,PowerUp
抽象类(如下所示)也包括大小调整和碰撞框。
lib/game/sprites/powerup.dart
abstract class PowerUp extends SpriteComponent
with HasGameRef<DoodleDash>, CollisionCallbacks {
final hitbox = RectangleHitbox();
double get jumpSpeedMultiplier;
PowerUp({
super.position,
}) : super(
size: Vector2.all(50),
priority: 2,
);
@override
Future<void>? onLoad() async {
await super.onLoad();
await add(hitbox);
}
}
创建一个扩展 PowerUp
抽象类的 Rocket
类。当与火箭发生碰撞时,Dash 的速度将提升到正常水平的 3.5 倍。
lib/game/sprites/powerup.dart
class Rocket extends PowerUp { // Add lines from here...
@override
double get jumpSpeedMultiplier => 3.5;
Rocket({
super.position,
});
@override
Future<void>? onLoad() async {
await super.onLoad();
sprite = await gameRef.loadSprite('game/rocket_1.png');
size = Vector2(50, 70);
}
} // ... to here.
创建一个扩展 PowerUp
抽象类的 NooglerHat
类。当与 NooglerHat
发生碰撞时,Dash 的速度会提升到正常水平的 2.5 倍。加速状态将持续 5 秒。
lib/game/sprites/powerup.dart
class NooglerHat extends PowerUp { // Add lines from here...
@override
double get jumpSpeedMultiplier => 2.5;
NooglerHat({
super.position,
});
final int activeLengthInMS = 5000;
@override
Future<void>? onLoad() async {
await super.onLoad();
sprite = await gameRef.loadSprite('game/noogler_hat.png');
size = Vector2(75, 50);
}
} // ... to here.
现在,您已经实现了 NooglerHat
和 Rocket
战力提升道具,更新 ObjectManager
以在游戏中生成这些道具。
修改 ObjectManger
类以添加一个列表来保存生成的战力提升道具,并添加 _maybePowerup
和 _cleanupPowerups
这两个新方法,分别用于生成和移除新的战力提升道具平台。
lib/game/managers/object_manager.dart
final List<PowerUp> _powerups = []; // Add lines from here...
void _maybeAddPowerup() {
if (specialPlatforms['noogler'] == true &&
probGen.generateWithProbability(20)) {
var nooglerHat = NooglerHat(
position: Vector2(_generateNextX(75), _generateNextY()),
);
add(nooglerHat);
_powerups.add(nooglerHat);
} else if (specialPlatforms['rocket'] == true &&
probGen.generateWithProbability(15)) {
var rocket = Rocket(
position: Vector2(_generateNextX(50), _generateNextY()),
);
add(rocket);
_powerups.add(rocket);
}
_cleanupPowerups();
}
void _cleanupPowerups() {
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
while (_powerups.isNotEmpty && _powerups.first.position.y > screenBottom) {
if (_powerups.first.parent != null) {
remove(_powerups.first);
}
_powerups.removeAt(0);
}
} // ... to here.
_maybeAddPowerup
方法会以 20% 的几率生成 Google 员工帽,或以 15% 的几率生成火箭。系统会调用 _cleanupPowerups
方法以移除屏幕底部边界下方的战力提升道具。
修改 ObjectManager
update
方法以在游戏循环的每个 tick 调用 _maybePowerup
。
lib/game/managers/object_manager.dart
@override
void update(double dt) {
final topOfLowestPlatform =
_platforms.first.position.y + _tallestPlatformHeight;
final screenBottom = gameRef.player.position.y +
(gameRef.size.x / 2) +
gameRef.screenBufferSpace;
if (topOfLowestPlatform > screenBottom) {
var newPlatY = _generateNextY();
var newPlatX = _generateNextX(100);
final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
add(nextPlat);
_platforms.add(nextPlat);
gameRef.gameManager.increaseScore();
_cleanupPlatforms();
_maybeAddEnemy();
_maybeAddPowerup(); // Add this line
}
super.update(dt);
}
修改 enableLevelSpecialty
方法,在 switch 语句中添加两种新情况:一种是在级别 3 时启用 NooglerHat
,另一种是在级别 4 时启用 Rocket
:
lib/game/managers/object_manager.dart
void enableLevelSpecialty(int level) {
switch (level) {
case 1:
enableSpecialty('spring');
break;
case 2:
enableSpecialty('broken');
break;
case 3: // Add lines from here...
enableSpecialty('noogler');
break;
case 4:
enableSpecialty('rocket');
break; // ... to here.
case 5:
enableSpecialty('enemy');
break;
}
}
将以下布尔值 getter 添加到 Player
类。如果拥有处于活动状态的战力提升道具,Dash 会呈现为各种不同的状态。这些 getter 可以更加轻松地检查哪个战力提升道具处于活动状态。
lib/game/sprites/player.dart
bool get hasPowerup => // Add lines from here...
current == PlayerState.rocket ||
current == PlayerState.nooglerLeft ||
current == PlayerState.nooglerRight ||
current == PlayerState.nooglerCenter;
bool get isInvincible => current == PlayerState.rocket;
bool get isWearingHat =>
current == PlayerState.nooglerLeft ||
current == PlayerState.nooglerRight ||
current == PlayerState.nooglerCenter; // ... to here.
修改 Player
的 onCollision
方法,以使用 NooglerHat
或 Rocket
对碰撞做出响应。此代码还将确保 Dash 仅在未拥有战力提升道具的情况下才会激活新的战力提升道具。
lib/game/sprites/player.dart
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is EnemyPlatform && !isInvincible) {
gameRef.onLose();
return;
}
bool isCollidingVertically =
(intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;
if (isMovingDown && isCollidingVertically) {
current = PlayerState.center;
if (other is NormalPlatform) {
jump();
return;
} else if (other is SpringBoard) {
jump(specialJumpSpeed: jumpSpeed * 2);
return;
} else if (other is BrokenPlatform &&
other.current == BrokenPlatformState.cracked) {
jump();
other.breakPlatform();
return;
}
}
if (!hasPowerup && other is Rocket) { // Add lines from here...
current = PlayerState.rocket;
other.removeFromParent();
jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
return;
} else if (!hasPowerup && other is NooglerHat) {
if (current == PlayerState.center) current = PlayerState.nooglerCenter;
if (current == PlayerState.left) current = PlayerState.nooglerLeft;
if (current == PlayerState.right) current = PlayerState.nooglerRight;
other.removeFromParent();
_removePowerupAfterTime(other.activeLengthInMS);
jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
return;
} // ... to here.
}
如果 Dash 与火箭发生碰撞,则 PlayerState
会更改为 Rocket
,并让 Dash 能够以 3.5 倍的 jumpSpeedMultiplier
进行跳跃。
如果 Dash 与 Google 员工帽发生碰撞,根据当前的 PlayerState
方向(.center
、.left
或 .right
),PlayerState
会更改为相应的Google 员工帽 PlayerState
,同时 Dash 会戴上 Google 员工帽,其速度将提升到 jumpSpeedMultiplier
的 2.5 倍。_removePowerupAfterTime
方法会在 5 秒后移除战力提升道具,并将 PlayerState
从战力提升状态更改回 center
。
调用 other.removeFromParent
会从屏幕上移除 Google 员工帽或火箭精灵平台,以反映 Dash 已获得战力提升道具。
修改 Player 类的 moveLeft
和 moveRight
方法以考虑 NooglerHat
精灵。您不需要考虑 Rocket
战力提升道具,因为无论行进方向如何,该精灵都面向相同的方向。
lib/game/sprites/player.dart
void moveLeft() {
_hAxisInput = 0;
if (isWearingHat) { // Add lines from here...
current = PlayerState.nooglerLeft;
} else if (!hasPowerup) { // ... to here.
current = PlayerState.left;
} // Add this line
_hAxisInput += movingLeftInput;
}
void moveRight() {
_hAxisInput = 0;
if (isWearingHat) { // Add lines from here...
current = PlayerState.nooglerRight;
} else if (!hasPowerup) { //... to here.
current = PlayerState.right;
} // Add this line
_hAxisInput += movingRightInput;
}
在拥有 Rocket
战力提升道具时,Dash 面对敌人将处于无敌状态,因此在这段时间内不会结束游戏。
修改 onCollision
回调,以便在因与 EnemyPlatform
发生碰撞而触发游戏结束之前检查 Dahs 是否处于 isInvincible
状态:
lib/game/sprites/player.dart
if (other is EnemyPlatform && !isInvincible) { // Modify this line
gameRef.onLose();
return;
}
重新启动应用并体验游戏,以查看战力提升道具的实际效果。
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
10. 叠加层
可以将 Flame 游戏封装在一个 widget 中,以便轻松将其与 Flutter 应用中的其他 widget 相集成。您还可以将 Flutter widget 显示为 Flame 游戏之上的叠加层。这可以方便地管理不依赖于游戏循环的非游戏组件,例如菜单、暂停屏幕、按钮和滑块。
在游戏中的得分显示以及 Doodle Dash 中的所有菜单都是常规 Flutter widget,而不是 Flame 组件。所有 widget 的代码均位于 lib/game/widgets
,例如 Game Over 菜单只是一个包含其他 widget(例如 Text
和 ElevatedButton
)的列,如以下代码所示:
lib/game/widgets/game_over_overlay.dart
class GameOverOverlay extends StatelessWidget {
const GameOverOverlay(this.game, {super.key});
final Game game;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: Center(
child: Padding(
padding: const EdgeInsets.all(48.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Game Over',
style: Theme.of(context).textTheme.displayMedium!.copyWith(),
),
const WhiteSpace(height: 50),
ScoreDisplay(
game: game,
isLight: true,
),
const WhiteSpace(
height: 50,
),
ElevatedButton(
onPressed: () {
(game as DoodleDash).resetGame();
},
style: ButtonStyle(
minimumSize: MaterialStateProperty.all(
const Size(200, 75),
),
textStyle: MaterialStateProperty.all(
Theme.of(context).textTheme.titleLarge),
),
child: const Text('Play Again'),
),
],
),
),
),
);
}
}
如需在 Flame 游戏中将 widget 用作叠加层,请在 GameWidget
上定义一个 overlayBuilderMap
属性,其中 key
表示叠加层(作为 String
),并通过 widget 函数的 value
返回一个 widget,如以下代码所示:
lib/main.dart
GameWidget(
game: game,
overlayBuilderMap: <String, Widget Function(BuildContext, Game)>{
'gameOverlay': (context, game) => GameOverlay(game),
'mainMenuOverlay': (context, game) => MainMenuOverlay(game),
'gameOverOverlay': (context, game) => GameOverOverlay(game),
},
)
添加后,即可在游戏中的任何位置使用叠加层。使用 overlays.add
显示叠加层,并使用 overlays.remove
隐藏叠加层,如以下代码所示:
lib/game/doodle_dash.dart
void resetGame() {
startGame();
overlays.remove('gameOverOverlay');
}
void onLose() {
gameManager.state = GameState.gameOver;
player.removeFromParent();
overlays.add('gameOverOverlay');
}
11. 移动平台支持
Doodle Dash 基于 Flutter 和 Flame 构建,因此已经可以在 Flutter 支持的各种平台上运行。但是,到目前为止,Doodle Dash 仅支持基于键盘的输入。对于没有键盘的设备(如手机),可以轻松地将屏幕触控按钮添加到叠加层。
将布尔值状态变量添加到 GameOverlay
,后者用于确定游戏是否在移动平台上运行:
lib/game/widgets/game_overlay.dart
class GameOverlayState extends State<GameOverlay> {
bool isPaused = false;
// Add this line
final bool isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
@override
Widget build(BuildContext context) {
...
}
}
现在,当游戏在移动设备上运行时,叠加层中会显示左右方向按钮。与第 4 步中的“按键事件”逻辑相类似,点击左方向按钮会将 Dash 向左移动。点击右方向按钮会将 Dash 向右移动。
在 GameOverlay
的 build
方法中,添加一个 isMobile
部分,该部分遵循与第 4 步相同的行为:点击左方向按钮会调用 moveLeft
,点击右方向按钮会调用 moveRight
。释放任一按钮会调用 resetDirection
,并导致 Dash 在水平方向上待定。
lib/game/widgets/game_overlay.dart
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned(... child: ScoreDisplay(...)),
Positioned(... child: ElevatedButton(...)),
if (isMobile) // Add lines from here...
Positioned(
bottom: MediaQuery.of(context).size.height / 4,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 24),
child: GestureDetector(
onTapDown: (details) {
(widget.game as DoodleDash).player.moveLeft();
},
onTapUp: (details) {
(widget.game as DoodleDash).player.resetDirection();
},
child: Material(
color: Colors.transparent,
elevation: 3.0,
shadowColor: Theme.of(context).colorScheme.background,
child: const Icon(Icons.arrow_left, size: 64),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 24),
child: GestureDetector(
onTapDown: (details) {
(widget.game as DoodleDash).player.moveRight();
},
onTapUp: (details) {
(widget.game as DoodleDash).player.resetDirection();
},
child: Material(
color: Colors.transparent,
elevation: 3.0,
shadowColor: Theme.of(context).colorScheme.background,
child: const Icon(Icons.arrow_right, size: 64),
),
),
),
],
),
),
), // ... to here.
if (isPaused)
...
],
),
);
}
大功告成!现在,Doodle Dash 应用会自动检测其运行的平台类型,并相应地切换其输入。
在 iOS 或 Android 上运行应用以查看方向按钮的实际效果。
有问题?
如果您的应用运行不正常,请检查是否存在拼写错误。如果需要,请使用以下链接中提供的代码恢复到正常状态。
12. 后续步骤
恭喜!
您已完成此 Codelab 课程,并学习了如何使用 Flame 游戏引擎在 Flutter 中创建游戏。
所学内容:
- 如何使用 Flame 软件包创建平台游戏,包括:
- 添加角色
- 添加多种平台类型
- 实现碰撞检测
- 添加重力组件
- 定义相机移动
- 创建敌人
- 创建战力提升道具
- 如何检测游戏运行所在的平台以及…
- 如何使用该信息在键盘与触控输入控件之间切换
资源
我们希望您已经了解了关于如何在 Flutter 中创建游戏的更多知识!
您可能还会发现以下资源很有帮助,甚至启发灵感:
- Flame 文档和 pub.dev 上的 Flame 软件包
- Flame 游戏引擎基础知识,YouTube 视频,作者:Lukas Klingsbo
- “Simple Platformer”,Flame + Flutter 游戏系列,DevKage 出品
- “Dino Run”,Flutter 游戏开发系列,DevKage 出品
- “Spacescape”,游戏开发系列,DevKage 出品
- Flutter 游戏
- Flutter 的休闲游戏工具包页面和休闲游戏工具包的相应快速入门模板(休闲游戏工具包旨在支持移动广告和游戏内应用购买,而未使用 Flame 引擎。)
- 在 Flutter 中构建您自己的游戏,休闲游戏工具包视频
- Flutter Puzzle Hack 页面(2022 年 1 月举办的竞赛)和相关获奖者视频