2. Start Coding

The Plan

Now that we have the assets loaded and a very rough idea of what classes we will need, we need to start thinking about how we will implement this game and our goals. To do this, let’s break down what the game should do:

  • Ember should be able to be controlled to move left, right, and jump.

  • The level will be infinite, so we need a way to randomly load sections of the level.

  • The objective is to collect stars while avoiding enemies.

  • Enemies cannot be killed as you need to use the platforms to avoid them.

  • If Ember is hit by an enemy, it should reduce Ember’s health by 1.

  • Ember should have 3 lives to lose.

  • There should be pits that if Ember falls into, it is automatically game over.

  • There should be a main menu and a game-over screen that lets the player start over.

Now that this is planned out, I know you are probably as excited as I am to begin and I just want to see Ember on the screen. So let’s do that first.

Note

Why did I choose to make this game an infinite side scrolling platformer?

Well, I wanted to be able to showcase random level loading. No two game plays will be the same. This exact setup can easily be adapted to be a traditional level game. As you make your way through this tutorial, you will see how we could modify the level code to have an end. I will add a note in that section to explain the appropriate mechanics.

Loading Assets

For Ember to be displayed, we will need to load the assets. This can be done in main.dart, but by so doing, we will quickly clutter the file. To keep our game organized, we should create files that have a single focus. So let’s create a file in the lib folder called ember_quest.dart. In that file, we will add:

import 'package:flame/game.dart';

class EmberQuestGame extends FlameGame {
  EmberQuestGame();

  @override
  Future<void> onLoad() async {
    await images.loadAll([
      'block.png',
      'ember.png',
      'ground.png',
      'heart_half.png',
      'heart.png',
      'star.png',
      'water_enemy.png',
    ]);

  }
}

As I mentioned in the assets section, we are using multiple individual image files and for performance reasons, we should leverage Flame’s built-in caching system which will only load the files once, but allow us to access them as many times as needed without an impact to the game. await images.loadAll() takes a list of the file names that are found in assets\images and loads them to cache.

Scaffolding

So now that we have our game file, let’s prepare the main.dart file to receive our newly created FlameGame. Change your entire main.dart file to the following:

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

import 'ember_quest.dart';

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

You can run this file and you should just have a blank screen now. Let’s get Ember loaded!

Ember Time

Keeping your game files organized can always be a challenge. I like to keep things logically organized by how they will be involved in my game. So for Ember, let’s create the following folder, lib/actors and in that folder, create ember.dart. In that file, add the following code:

import 'package:flame/components.dart';

import '../ember_quest.dart';

class EmberPlayer extends SpriteAnimationComponent
    with HasGameRef<EmberQuestGame> {
  EmberPlayer({
    required super.position,
  }) : super(size: Vector2.all(64), anchor: Anchor.center);

  @override
  Future<void> onLoad() async {
    animation = SpriteAnimation.fromFrameData(
      game.images.fromCache('ember.png'),
      SpriteAnimationData.sequenced(
        amount: 4,
        textureSize: Vector2.all(16),
        stepTime: 0.12,
      ),
    );
  }
}

This file uses the HasGameRef mixin which allows us to reach back to ember_quest.dart and leverage any of the variables or methods that are defined in the game class. You can see this in use with the line game.images.fromCache('ember.png'). Earlier, we loaded all the files into cache, so to use that file now, we call fromCache so it can be leveraged by the SpriteAnimation. The EmberPlayer class is extending a SpriteAnimationComponent which allows us to define animation as well as position it accordingly in our game world. When we construct this class, the default size of Vector2.all(64) is defined as the size of Ember in our game world should be 64x64. You may notice that in the animation SpriteAnimationData, the textureSize is defined as Vector2.all(16) or 16x16. This is because the individual frame in our ember.png is 16x16 and there are 4 frames in total. To define the speed of the animation, stepTime is used and set at 0.12 seconds per frame. You can change the stepTime to any length that makes the animation seem correct for your game vision.

Now before you rush to run the game again, we have to add Ember to the game world. To do this, go back to ember_quest.dart and add the following:

import 'package:flame/game.dart';

import 'actors/ember.dart';

class EmberQuestGame extends FlameGame {
  EmberQuestGame();

  late EmberPlayer _ember;

  @override
  Future<void> onLoad() async {
    await images.loadAll([
      'block.png',
      'ember.png',
      'ground.png',
      'heart_half.png',
      'heart.png',
      'star.png',
      'water_enemy.png',
    ]);
    _ember = EmberPlayer(
      position: Vector2(128, canvasSize.y - 70),
    );
    add(_ember);
  }
}

Run your game now and you should now see Ember flickering in the lower left-hand corner.

Building Blocks

Now that we have Ember showing on screen and we know our basic environment is all working correctly, it’s time to create a world for Embers Quest! Proceed on to 3. Building the World!