Let’s create a game using Flutter Flame. Part 1: Basic components and lifecycle

Andrii Khrystian
6 min readJul 6, 2023

--

Introduction: Game development is an exciting area, and for those who are new to it, choosing a game engine can be challenging. However, if you’re already familiar with Flutter, you’re in luck! There’s a fantastic tool called Flame that allows you to develop games using Dart programming language and leverage all the advantages of Flutter. One of the key advantages is the ability to run your game on multiple platforms effortlessly. Sounds cool, right?

Getting Started:

To start using Flame, you’ll need to add it as a dependency in your pubspec.yaml file:

dependencies:
flame: ^1.8.1

Alternatively, you can use the command line:

flutter pub add flame

It’s worth noting that Flame provides core functionality, but there are additional plugins available that you can include in your project based on your specific needs. Some examples include:

  • flame_bloc for implementing the bloc architecture in your game
  • flame_lottie for working with Lottie animations
  • flame_audio for playing audio in your game

The modular nature of Flame allows you to selectively add only the plugins you require for your game project.

Everything is Component

In Flutter, widgets are the fundamental building blocks of the UI. In Flame, the equivalent concept is called “components.” Unlike widgets, components have a more complex lifecycle and are organized in a hierarchical structure. The main component we should know is FlameGame, which represents the game itself and implements the Game mixin.

Coding time

One thing before we start coding our first game is how to connect/bridge standard Flutter application with Flame. Well it’s super easy — for this purpose we would need GameWidget. Okay let’s write some code.

Future<void> main() async {
runApp(
GameWidget(
),
);
}

As you can see it’s not compiling as we need to provide a Game (or FlameGame) object to our widget. And this is a right time for creating our first FlameGame class.

class SquareGame extends FlameGame {}

we don’t need to override anything, let’s keep it empty for now. And finally we add our game object to the GameWidget

Future<void> main() async {
runApp(
GameWidget(
game: SquareGame(),
),
);
}

Well we can already execute it. Let’s see what is the result

As you can see I ran it on Mac and our game is just black window with project name in the top.

Well let’s start to customise it. First we will change the color and add some other Component to the screen.

class SquareGame extends FlameGame {

@override
Color backgroundColor() => Colors.redAccent.shade100;

}

As you can see it’s super easy, we just overriden a backgroundColor property from the Game mixin.

Now we create first Component which will be a simple square.

class SquareGameComponent extends PositionComponent {
static const double speed = 20;
final paint = Paint()..color = Colors.blue;

@override
void render(Canvas canvas) {
super.render(canvas);

canvas.drawRect(size.toRect(), paint);
}
}

as alternative there is component called RectangleComponent, which also could be used instead.

and now we can add this component to the SquareGame:

class SquareGame extends FlameGame {
late SquareGameComponent square;

@override
Color backgroundColor() => Colors.redAccent.shade100;

@override
Future<void> onLoad() async {
square = SquareGameComponent();
square.position = Vector2(0, 100);
square.size = Vector2(100, 100);
add(square);
}
}

As the result we will have something like this:

And here we have to jump back to the theory.

Component lifecycle

As you can see in example we use two functions from the components lifecycle: onLoad and render. Now let’s check the whole lifecycle of Component:

Yellow color represents functions that are executed only once, while other functions can be executed multiple times. Let’s go through the lifecycle functions:

  1. onLoad: This function is responsible for initializing your component. If it's a parent component like FlameGame, it can be used to add/initialize child components or process game resources such as images or animations.
  2. onGameResize: This function is called when the screen is resized. It allows you to handle any adjustments needed in response to the new screen size.
  3. onMount: This function is executed when a component is added or mounted to the game tree. When it comes to the execution queue, the onMount of the parent component is always called first.
  4. onRemove: This function marks the end of the component's lifecycle and is called just before the component is removed from the component tree. It provides an opportunity to perform any necessary cleanup or finalization tasks.
  5. update and render combination: These functions work together to handle the appearance and immutability (position, properties) of the component. The update function is called in each game update cycle to update the component's state, while the render function is responsible for rendering the component's visuals on the canvas.

As you can see, the component lifecycle is straightforward. Let’s put it into practice by adding horizontal movement to our square. To achieve this, we need to override the update function.

class SquareGameComponent extends PositionComponent {
static const double speed = 20;

@override
void render(Canvas canvas) {
super.render(canvas);

final paint = Paint()..color = Colors.blue;
canvas.drawRect(position & size, paint);
}

@override
void update(double dt) {
super.update(dt);

position.x += speed * dt;
}
}

We also added horizontal speed for changing x.position. And here is how it looks:

Looks good. As final step let’s check our lifecycle callbacks. In order to do this I’ll override lifecycle functions for SquareGameCompoent and for SquareGame.

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

class SquareGame extends FlameGame {
late SquareGameComponent square;

@override
Color backgroundColor() => Colors.redAccent.shade100;

@override
Future<void> onLoad() async {
print("Lifecycle: SquareGame onLoad");
square = SquareGameComponent();
square.position = Vector2(0, 100);
square.size = Vector2(100, 100);
add(square);
}

@override
void render(Canvas canvas) {
super.render(canvas);
print("Lifecycle: SquareGame render");
}

@override
void onMount() {
super.onMount();
print("Lifecycle: SquareGame onMount");
}

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
print("Lifecycle: SquareGame onGameResize");
}

@override
void update(double dt) {
super.update(dt);
print("Lifecycle: SquareGame update");
}
}

class SquareGameComponent extends PositionComponent {
static const double speed = 20;

@override
void render(Canvas canvas) {
super.render(canvas);

print("Lifecycle: SquareGameComponent render");

final paint = Paint()..color = Colors.blue;
canvas.drawRect(position & size, paint);
}

@override
void update(double dt) {
super.update(dt);

print("Lifecycle: SquareGameComponent update");
position.x += speed * dt;
}

@override
void onMount() {
super.onMount();
print("Lifecycle: SquareGameComponent onMount");
}

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
print("Lifecycle: SquareGameComponent onGameResize");
}

@override
Future<void> onLoad() async {
super.onLoad();
print("Lifecycle: SquareGameComponent onLoad");
}
}

Let’s check output

flutter: Lifecycle: SquareGame onGameResize
flutter: Lifecycle: SquareGame onLoad

flutter: Lifecycle: SquareGameComponent onLoad
flutter: Lifecycle: SquareGame onMount
flutter: Lifecycle: SquareGame render

flutter: Lifecycle: SquareGameComponent onGameResize
flutter: Lifecycle: SquareGameComponent onMount
flutter: Lifecycle: SquareGameComponent update
flutter: Lifecycle: SquareGame update
flutter: Lifecycle: SquareGameComponent render
flutter: Lifecycle: SquareGame render
flutter: Lifecycle: SquareGameComponent update
flutter: Lifecycle: SquareGame update
flutter: Lifecycle: SquareGameComponent render
flutter: Lifecycle: SquareGame render
flutter: Lifecycle: SquareGameComponent update

As we can observe, the parent lifecycle functions are executed first. After the onLoad of the parent component, the onLoad of the child component is executed. Once the entire initialization process of the child component (SquareGameComponent) is complete, our game enters an endless loop of horizontal movement.

This article might have been more theoretical, but it should be sufficient for the first part. In the next part, we will explore other components and make our game controllable.

Happy coding!

--

--