这是我的 4 部分系列的最后一部分,我在其中学习如何使用 Flame 引擎构建简单的 Platformer 游戏。我们已经知道如何添加我命名为 The Boy 的动画玩家角色,如何使用 Tiled 编辑器创建可滚动的游戏关卡,以及如何在碰撞检测的帮助下添加重力和跳跃(第 1、2、3部分)。
在这一部分中,我们将添加可以由角色收集的金币、显示玩家拥有多少金币的 HUD 以及收集所有金币后显示的胜利屏幕。
我们需要告诉游戏在哪里产生硬币(或与此相关的任何其他游戏对象)。您可能已经猜到了,我们将使用 Tiled 编辑器添加另一个对象层,其方式与我们添加平台的方式类似,但有两点不同:
通过平台,我们将精灵图像烘焙到关卡数据中。这种方法不太适合硬币,因为我们要移除对象,一旦玩家收集了它。所以我们将只添加生成点,以了解硬币应该出现在游戏中的什么位置,渲染将使用 Flame 组件完成。
对于平台,我们使用任意大小的矩形,但对于硬币,我们将添加 1 个方块大小的生成点。但是,如果您想一个接一个地添加一行硬币(嗨,马里奥),您可以通过修改游戏代码并在添加硬币对象时考虑生成点的大小来轻松实现。但出于本系列的目的,我们假设硬币生成点为 1x1。
在 Tiled 编辑器中打开我们的关卡并创建一个名为 Coins 的新对象层。然后使用矩形工具在地图上添加几个生成点,以便我们使用游戏引擎添加硬币组件。我的水平现在看起来像这样:
我应该补充一点,我们的游戏相当简单,我们知道这些空矩形会在游戏运行时变成硬币。但是如果我们要添加更多的对象类型,就很难区分它们了。幸运的是,Tiled 有一个名为“Insert Tile”的工具,它可以为每个对象添加视觉提示,但这些图像不会在游戏中渲染。
好的,保存关卡并返回到 IDE。让我们将Coin
类添加到/objects/
文件夹中。
class Coin extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { late final SpriteAnimation spinAnimation; late final SpriteAnimation collectAnimation; Coin(Vector2 position) : super(position: position, size: Vector2.all(48)); @override Future<void> onLoad() async { spinAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(16), stepTime: 0.12, ), ); collectAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.range( start: 4, end: 7, amount: 8, textureSize: Vector2.all(16), stepTimes: List.filled(4, 0.12), loop: false ), ); animation = spinAnimation; final hitbox = RectangleHitbox() ..collisionType = CollisionType.passive; add(hitbox); return super.onLoad(); } }
我们有 2 个不同的动画用于旋转和收集,还有一个RectangleHitbox
,用于稍后检查与玩家的碰撞。
接下来,返回game.dart
并修改spawnObjects
方法来生成我们的硬币:
final coins = tileMap.getLayer<ObjectGroup>("Coins"); for (final coin in coins!.objects) { add(Coin(Vector2(coin.x, coin.y))); }
运行游戏并查看添加的硬币:
返回coin.dart
添加collect
方法:
void collect() { animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
调用此方法时,我们会将旋转动画切换为收集动画,完成后我们将从游戏中移除此组件。
然后,转到theboy.dart
类并重写onCollisionStart
方法:
@override void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) { if (other is Coin) { other.collect(); } super.onCollisionStart(intersectionPoints, other); }
我们使用onCollisionStart
而不是onCollision
原因是我们希望碰撞回调只触发一次。
硬币现在会在与男孩碰撞时消失。让我们添加用户界面来跟踪收集的硬币数量。
HUD,或平视显示器,只是一个状态栏,显示有关游戏的任何信息:生命值、弹药等。我们将为每个收集到的硬币显示一个硬币图标。
为了简单起见,我将把硬币的数量存储在一个变量中,但对于更复杂的接口,可以考虑使用flame_bloc包,它允许您以方便的方式更新和观察游戏状态。
添加一个将包含 HUD 逻辑的新类: lib/hud.dart
class Hud extends PositionComponent with HasGameRef<PlatformerGame> { Hud() { positionType = PositionType.viewport; } void onCoinsNumberUpdated(int total) { final coin = SpriteComponent.fromImage( game.images.fromCache(Assets.HUD), position: Vector2((50 * total).toDouble(), 50), size: Vector2.all(48)); add(coin); } }
这里有两件有趣的事情:
positionType
设置为PositionType.viewport
以将我们的 HUD 固定在屏幕角落。如果我们不这样做,由于摄像机移动,HUD 将随关卡移动。onCoinsNumberUpdated
方法。它使用total
参数来计算下一个硬币图标的偏移量,然后将一个新的硬币精灵添加到计算出的位置。
接下来,返回game.dart
文件并添加新的类变量:
int _coins = 0; // Keeps track of collected coins late final Hud hud; // Reference to the HUD, to update it when the player collects a coin
然后在onLoad
方法底部添加Hud
组件:
hud = Hud(); add(hud);
并添加一个新方法:
void onCoinCollected() { _coins++; hud.onCoinsNumberUpdated(_coins); }
最后,从Coin
的collect
方法调用它:
void collect() { game.onCoinCollected(); animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
太棒了,我们的 HUD 现在会显示我们收集了多少硬币!
我要添加的最后一件事是 Win 屏幕,一旦玩家收集了所有硬币,就会显示该屏幕。
向PlatformerGame
类添加一个新的常量:
late int _totalCoins;
并分配给它我们在关卡中拥有的硬币数量。将此行添加到spawnObjects
方法的底部:
_totalCoins = coins.objects.length;
并将其添加到onCoinCollected
方法的底部。
请注意,您可能需要添加import 'package:flutter/material.dart';
手动。
if (_coins == _totalCoins) { final text = TextComponent( text: 'U WIN!', textRenderer: TextPaint( style: TextStyle( fontSize: 200, fontWeight: FontWeight.bold, color: Colors.white, ), ), anchor: Anchor.center, position: camera.viewport.effectiveSize / 2, )..positionType = PositionType.viewport; add(text); Future.delayed(Duration(milliseconds: 200), () => { pauseEngine() }); }
在这里,我检查硬币计数器是否等于关卡中的硬币数量,如果是,则在游戏屏幕顶部添加一个 Win 标签。然后暂停游戏以停止玩家移动。我还添加了 200 毫秒的延迟,以便在暂停之前呈现标签。
它应该是这样的:
这就是游戏!当然,它现在看起来不像是一款完成的游戏,但根据我所解释的一切,添加更多关卡、敌人或其他收藏品应该相当容易。
这部分总结了这个系列。
Flame 引擎还有很多我没有涉及的功能,包括物理引擎 Forge2D、粒子、效果、游戏菜单、音频等。但是在完成本系列之后,我对引擎的情况有了一定的了解,并且我了解如何构建更复杂的游戏。
Flame 是一种功能强大但易于使用和学习的工具。它是模块化的,允许引入其他很酷的东西,比如 Box2D,并且它得到了积极维护。但 Flame 的最大优势之一是它构建在 Flutter 之上,这意味着它提供了多平台支持,只需做一些额外的工作。然而,作为 Flutter 扩展意味着所有 Flutter 问题也存在于 Flame 中。例如, Flutter 的 antialiasing bug已经开放了好几年没有解决,你可能在我们构建的游戏中也注意到了它。但总的来说,它是构建值得尝试的游戏的绝佳工具。
本教程的完整代码,你可以在我的github中找到
在每个部分的末尾,我将添加一个很棒的创作者列表和我从中学到的资源。