Teaching Your Character to Run in Flame

Written by eugene-kleshnin | Published 2023/02/28
Tech Story Tags: flutter | flame | game-development | game-engine | flutter-tutorial | programming | hackernoon-top-story | flame-game-development | hackernoon-tr | hackernoon-ko | hackernoon-de | hackernoon-bn

TLDRThis series of articles is my journey of learning Flame (and Flutter) and building a basic platformer game. I’ll try to make it quite detailed, so it should be useful to anyone who’s just dipping their toes into Flame or game dev in general. In the first part, we’re going to create a new Flame project, load all assets, add a player character, and teach him how to run.via the TL;DR App

I’ve always wanted to make video games. My first ever Android app that helped me to get my first job was a simple game, made with Android views. After that, there were a lot of attempts to create a more elaborate game using a game engine, but all of them failed due to a lack of time or the complexity of a framework. But when I first heard about Flame engine, based on Flutter, I was immediately attracted by its simplicity and cross-platform support, so I decided to try building a game with it.

I wanted to start with something simple, but still challenging, to get a feel of the engine. This series of articles is my journey of learning Flame (and Flutter) and building a basic platformer game. I’ll try to make it quite detailed, so it should be useful to anyone who’s just dipping their toes into Flame or game dev in general.

Over the course of 4 articles, I’m going to build a 2d side-scrolling game, that includes:

  • A character that can run and jump

  • A camera that follows the player

  • Scrolling level map, with ground and platforms

  • Parallax background

  • Coins that the player can collect and HUD that displays the number of coins

  • Win screen

In the first part, we’re going to create a new Flame project, load all assets, add a player character, and teach him how to run.

Project setup

First, let’s create a new project. The official Bare Flame game tutorial does a great job of describing all steps to do that, so just follow it.

One thing to add: when you’re setting up pubspec.yaml file, you can update libraries versions to the latest available, or leave it as is, because the caret sign (^) before a version will ensure your app uses the latest non-breaking version. (caret syntax)

If you followed all steps, your main.dart file should look like this:

import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';

void main() {
  final game = FlameGame();
  runApp(GameWidget(game: game));
}

Assets

Before we continue, we need to prepare assets that will be used for the game. Assets are images, animations, sounds, etc. For purposes of this series, we’ll use only images which are also called sprites in game dev.

The simplest way to build a platformer level is to use tile maps and tile sprites. It means that the level is basically a grid, where each cell indicates what object / ground / platform it represents. Later, when the game is running, information from each cell is mapped to the corresponding tile sprite.

Games graphics built using this technique could be really elaborate or very simple. For example, in Super Mario bros, you see that a lot of elements are repeating. That’s because, for each ground tile in the game grid, there’s only one ground image that represents it. We will follow the same approach, and prepare a single image for each static object we have.

We also want some of the objects, such as the player character and coins to be animated. Animation is usually stored as a series of still images, each representing a single frame. When animation is playing, frames go one after another, creating the illusion of the object moving.

Now the most important question is where to get the assets. Of course, you can draw them yourself, or commission them to an artist. Also, there are a lot of awesome artists who contributed game assets to open-source. I will be using Arcade Platformer Assets pack by GrafxKid.

Typically, image assets come in two forms: sprite sheets and single sprites. The former is a large image, containing all game assets in one. Then game developers specify the exact position of the required sprite, and the game engine cuts it from the sheet. For this game, I will use single sprites (except animations, it’s easier to keep them as one image) because I don’t need all assets provided in the sprite sheet.

Whether you’re creating sprites yourself or getting them from an artist, you might need to slice them to make them more suitable for the game engine. You can use tools created specifically for that purpose (like texture packer) or any graphical editor. I used Adobe Photoshop, because, in this sprite sheet, sprites have unequal space between them, which made it hard for automatical tools to extract images, so I had to do it manually.

You also might want to increase the size of the assets, but if it’s not a vector image, the resulting sprite could become blurry. One workaround I found that works great for pixel art is to use Nearest Neighbour (hard edges) resizing method in Photoshop (or Interpolation set to None in Gimp). But if your asset is more detailed, it probably won’t work.

With explanations out of the way, download the assets I prepared or prepare your own and add them to assets/images folder of your project.

Whenever you add new assets, you need to register them in pubspec.yaml file like this:

flutter:
  assets:
    - assets/images/

And the tip for the future: if you are updating already registered assets you need to restart the game to see the changes.

Now let’s actually load the assets into the game. I like to have all asset names in one place, which works great for a small game, as it’s easier to keep track of everything and modify if needed. So, let’s create a new file in the lib directory: assets.dart

const String THE_BOY = "theboy.png";
const String GROUND = "ground.png";
const String PLATFORM = "platform.png";
const String MIST = "mist.png";
const String CLOUDS = "clouds.png";
const String HILLS = "hills.png";
const String COIN = "coin.png";
const String HUD = "hud.png";

const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD];

And then create another file, which will contain all game logic in the future: game.dart

import 'package:flame/game.dart';
import 'assets.dart' as Assets;

class PlatformerGame extends FlameGame {

  @override
  Future<void> onLoad() async {
    await images.loadAll(Assets.SPRITES);
  }
}

PlatformerGame is the main class that represents our game, it extends FlameGame, the base game class used in the Flame engine. Which in turn extends Component - the basic building block of Flame. Everything in your game, including images, interface or effects are Components. Each Component has an async method onLoad, which is called on component initialization. Usually, all component setup logic goes there.

Finally, we imported our assets.dart file we created earlier and added as Assets to explicitly declare where our assets constants are coming from. And used method images.loadAll to load all assets listed in the SPRITES list to the game images cache.

Then, we need to create our new PlatformerGame from main.dart. Modify the file as follows:

import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';
import 'game.dart';

void main() {
  runApp(
    const GameWidget<PlatformerGame>.controlled(
      gameFactory: PlatformerGame.new,
    ),
  );
}

All preparation is done, and the fun part begins.

Adding player character

Create a new folder lib/actors/ and a new file theboy.dart inside of it. This is gonna be the component that represents the player character: The Boy.

import '../game.dart';
import '../assets.dart' as Assets;
import 'package:flame/components.dart';

class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> {

  TheBoy({
    required super.position, // Position on the screen
  }) : super(
    size: Vector2.all(48),  // Size of the component
    anchor: Anchor.bottomCenter // 
  );

  @override
  Future<void> onLoad() async {
    animation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.sequenced(
        amount: 1, // For now we only need idle animation, so we load only 1 frame
        textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet
        stepTime: 0.12, // Time between frames, since it's a single frame not that important
      ),
    );
  }
}

The class extends SpriteAnimationComponent which is a component used for animated sprites and has a mixin HasGameRef which allows us to reference the game object to load images from the game cache or get global variables later.

In our onLoad method we create a new SpriteAnimation from the THE_BOY sprite sheet we declared in the assets.dart file.

Now let’s add our player to the game! Return to the game.dart file and add following to the bottom of onLoad method:

final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2));
add(theBoy);

If you run the game now, we should be able to meet The Boy!

Player movement

First, we need to add the ability to control The Boy from the keyboard. Let’s add HasKeyboardHandlerComponents mixin to the game.dart file.

class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents

Next, let’s return to theboy.dart and KeyboardHandler mixin:

class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>

Then, add some new class variables to TheBoy component:

final double _moveSpeed = 300; // Max player's move speed

int _horizontalDirection = 0; // Current direction the player is facing
final Vector2 _velocity = Vector2.zero(); // Current player's speed

Finally, let’s override the method onKeyEvent which allows listening for keyboard inputs:

@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> 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;
}

Now _horizontalDirection equals 1 if the player moves to the right, -1 if the player moves to the left, and 0 if the player doesn’t move. However, we cannot yet see it on the screen, because the position of the player isn’t changed yet. Let’s fix that by adding the update method.

Now I need to explain what the game loop is. Basically, it means that the game is being run in an endless loop. In each iteration, the current state is rendered in Component's method render and then a new state is calculated in the method update. The dt parameter in the method’s signature is time in milliseconds since the last state update. With that in mind, add the following to theboy.dart:

@override
void update(double dt) {
    super.update(dt);
    _velocity.x = _horizontalDirection * _moveSpeed;
    position += _velocity * dt;
}

For each game loop cycle, we update horizontal velocity, using the current direction and max speed. Then we change the sprite position with the updated value multiplied by dt .

Why do we need the last part? Well, if you update the position with just velocity, then the sprite will fly away into space. But can we just use the smaller speed value, you may ask? We can, but the way the player moves will be different with different frames per second (FPS) rate. The number of frames (or game loops) per second depends on the game performance and hardware it’s run on. The better the device performance, the higher the FPS, and the faster the player moves. In order to avoid that, we make the speed depend on the time passed from the last frame. That way the sprite will move similarly on any FPS.

Okay, if we run the game now, we should see this:

Awesome, now let’s make the boy turn around when he goes to the left. Add this to the bottom of the update method:

if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) {
      flipHorizontally();
    }

Fairly easy logic: we check if the current direction (the arrow the user is pressing) is different from the direction of the sprite, then we flip the sprite along the horizontal axis.

Now let’s also add running animation. First define two new class variables:

late final SpriteAnimation _runAnimation;
late final SpriteAnimation _idleAnimation;

Then update onLoad like this:

@override
  Future<void> onLoad() async {
    _idleAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.sequenced(
        amount: 1,
        textureSize: Vector2.all(20),
        stepTime: 0.12,
      ),
    );

    _runAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.sequenced(
        amount: 4,
        textureSize: Vector2.all(20),
        stepTime: 0.12,
      ),
    );

    animation = _idleAnimation;
  }

Here we extracted previously added idle animation to the class variable and defined a new run animation variable.

Next, let’s add a new updateAnimation method:

void updateAnimation() {
    if (_horizontalDirection == 0) {
      animation = _idleAnimation;
    } else {
      animation = _runAnimation;
    }
  }

And finally, invoke this method at the bottom of the update method and run the game.

Conclusion

That’s it for the first part. We learned how to set up a Flame game, where to find assets, how to load them into your game, and how to create an awesome animated character and make it move based on keyboard inputs. The code for this part could be found on my github.

In the next article, I’ll cover how to create a game level using Tiled, how to control the Flame camera, and add a parallax background. Stay tuned!

Resources

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


Written by eugene-kleshnin | Mobile Engineer @ Meta
Published by HackerNoon on 2023/02/28