How to Add a HUD to Your Flame Game

Written by hacker7867369 | Published 2023/03/20
Tech Story Tags: game-development | flutter | flame | video-game-development | flame-tutorial | gaming | hackernoon-top-story | flame-game-development

TLDRThis is the last part of my 4 part Series where I learn how to build a simple Platformer game with Flame engine. 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.via the TL;DR App

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.

Adding coins

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:

  1. 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.

  2. 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:

Now let’s make them disappear when the player collects them.

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.

Adding the HUD

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:

  1. We set 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.
  2. Method 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!

Win screen

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.

Summary

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.

Other stories of the series:

The complete code for this tutorial, you can find in my github

Resources

At the end of each part, I’ll be adding a list of awesome creators and resources I learned from.


Written by hacker7867369 | Mobile Engineer @ Meta
Published by HackerNoon on 2023/03/20