This is the last part of my 4 part Series where I learn how to build a simple Platformer game with Flame engine. We already know how to add an animated player character which I named The Boy, how to create a scrollable game level using the Tiled editor, and how to add gravity and jumping with help of collision detection (parts 1, 2, 3).
In this part, we’re gonna add coins, which could be collected by the character, HUD to display how many coins the player has, and a victory screen, which we’re gonna show once all coins are collected.
We need to tell the game where to spawn coins (or any other game object for that matter). As you may have guessed, we’re gonna use the Tiled editor to add another objects layer, in a similar way as we added Platforms, but with two differences:
With the platforms, we baked sprite images into the level data. This method is not very suitable for coins, because we want to remove the object, once the player collects it. So we will just add spawn points, to know where coins should appear in the game, and rendering will be done using Flame components.
For the platforms, we used rectangles of any size, but for the coins, we gonna add spawn points to be the size of 1 tile. Though, if you want to add a line of coins one after another (hi Mario), you can easily do so by modifying the game code and considering the size of a spawn point when adding coin objects. But for the purposes of this series, we assume that coins spawn points are 1x1.
Open our level in the Tiled editor and create a new object layer called Coins. Then using the Rectangular tool add several spawn points across the map for us to add coin components using the game engine. My level now looks like this:
I should add that our game is rather simple and we know that these empty rectangles will turn into coins in the game runtime. But if we want to add more object types, it will become hard to distinguish them. Luckily, Tiled has a tool for that, called “Insert Tile” which can add visual cues for every object, but these images won’t be rendered in the game.
Alright, save the level and return to the IDE. Let’s add our Coin
class to the /objects/
folder.
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();
}
}
We have 2 different animations for spinning and collecting and a RectangleHitbox
, to check collisions with the player later on.
Next, return to game.dart
and modify spawnObjects
method to spawn our coins:
final coins = tileMap.getLayer<ObjectGroup>("Coins");
for (final coin in coins!.objects) {
add(Coin(Vector2(coin.x, coin.y)));
}
Run the game and see added coins:
Return to coin.dart
and add collect
method:
void collect() {
animation = collectAnimation;
collectAnimation.onComplete = () => {
removeFromParent()
};
}
When this method is called, we gonna switch the spinning animation to the collecting one and once it’s finished we’re gonna remove this component from the game.
Then, go to theboy.dart
class and overrideonCollisionStart
method:
@override
void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is Coin) {
other.collect();
}
super.onCollisionStart(intersectionPoints, other);
}
The reason why we use onCollisionStart
instead of onCollision
is that we want the collision callback to trigger only once.
Coins are now disappearing on collision with The Boy. Let’s add the user interface to track the number of collected coins.
HUD, or heads-up display, is simply a status bar that displays any information about the game: hit points, ammo, etc. We’re gonna display a coin icon for each collected coin.
For the sake of simplicity, I’m going to store the number of the coins in a variable, but for more complex interfaces, consider using a flame_bloc package, which allows you to update and observe the game state in a convenient way.
Add a new class that’s going to contain HUD logic: 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);
}
}
Two interesting things here:
positionType
to PositionType.viewport
to stick our HUD to the screen corner. If we don’t do that, due to the camera movement, HUD will be moving with the level.onCoinsNumberUpdated
will be called every time the player collects the coin. It uses total
param to calculate the offset of the next coin icon, and then adds a new coin sprite to the calculated position.
Next, return to the game.dart
file and add new class variables:
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
Then add the Hud
component at the bottom of onLoad
method:
hud = Hud();
add(hud);
And add a new method:
void onCoinCollected() {
_coins++;
hud.onCoinsNumberUpdated(_coins);
}
Finally, call it from collect
method of Coin
:
void collect() {
game.onCoinCollected();
animation = collectAnimation;
collectAnimation.onComplete = () => {
removeFromParent()
};
}
Brilliant, our HUD now shows how many coins we’ve collected!
The last thing I want to add is the Win screen, which will be displayed once the player collected all coins.
Add a new const to the PlatformerGame
class:
late int _totalCoins;
And assign to it the number of coins we have in the level. Add this line to the bottom of the spawnObjects
method:
_totalCoins = coins.objects.length;
And add this to the bottom of the onCoinCollected
method.
Note that you might need to add import 'package:flutter/material.dart';
manually.
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() });
}
Here, I check if the coins counter equals the number of coins in the level, and if it is add a Win label on top of the game screen. Then pause the game to stop player movement. I also added a 200 ms delay, for the label to be rendered before pausing.
It should look like this:
And that is the game! Of course, it doesn’t look like a finished game now, but with everything I explained, it should be fairly easy to add more levels, enemies, or other collectible items.
This part concludes the series.
The Flame engine has a lot more to offer that I didn’t cover, including the physics engine Forge2D, particles, effects, game menus, audio, etc. But after completing this series I have a grasp of what the engine is like and I have an understanding of how to build more complex games.
Flame is a powerful yet easy-to-use and learn tool. It’s modular, which allows bringing other cool things like Box2D and it’s actively maintained. But one of the greatest advantages of Flame is that it is built on top of Flutter, which means that it provides multiplatform support with a little additional work. However, being a Flutter extension means that all Flutter problems persist in Flame too. For example, Flutter’s antialiasing bug has been open for several years without resolution, and you might notice it in the game we built as well. But overall, it’s a great tool for building games that’s worth trying out.
The complete code for this tutorial, you can find in my github
At the end of each part, I’ll be adding a list of awesome creators and resources I learned from.