Constructing Games

Flame Engine Basics


Learning Objectives

  • You know of the Flame game engine for Flutter and you know of the existence of entity component systems.
  • You know how to create a game with Flame that shows an object that reacts to taps.

Games are often developed with the help of a game engine. A game engine is a software framework that provides the necessary functionality for creating games.

It’s basically the same as using a frameworks like Hono and Svelte for building web applications, but for building games.

As aptly put by the YouTuber Pirate Software, there are three reasons for making a game engine: (1) learning how to make a game engine, (2) needing features that are not possible in any of the available game engines, and (3) — check the short video from PirateSoftware for the third reason.

Unfortunately, in the context of this course, we do not have the time to learn how to make a game engine from scratch. Instead, we will use a game engine called Flame that is available for Flutter.

Flame engine

Flame is a cross-platform game engine that is used for creating games with 2D graphics in Flutter. To use Flame, we need to add the flame package to the pubspec.yaml file of the project.

dependencies:
  flutter:
    sdk: flutter

  flame: 1.20.0
  # other dependencies..

Flame engine comes with a GameWidget that can be used like any other Flutter widget. The constructor of GameWidget is (typically) given an instance of FlameGame. FlameGame extends Flame’s Game, providing plenty of ready-made functionality on top of it.

A minimal example of a Flutter app using Flame engine is shown below. In the example, GameWidget is used as the root widget of the app, and an instance of FlameGame is passed to the game property of GameWidget. When the application is run, a blank screen is displayed.

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

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

Games that use Flame engine can consist of multiple screens. A game could, for example, have a start screen and a game screen. Combining GetX and Flame, this could look as follows.

Run the program to see the output

Now, the application starts with a start screen that has a button to navigate to the game screen. When the button is pressed, the game screen is displayed.

Entity component system

Game engines often come with an entity component system. Entities are objects in the game, components are used to define the characteristics of the entities, and system is the logic that operates on the entities.

In terms of object oriented programming, a component could be seen as a class that defines the properties of an entity, while an entity is an instance of a class. The system would be a class — or a set of classes — that operate on the entities.

Flame has Flame Component System that is somewhat similar to the entity component system. To use components from Flame, we need to import components from the flame package.

import 'package:flame/components.dart';

Components are created by extending Component to define how the components should behave. There are a range of ready-made components available in Flame, such as CircleComponent and RectangleComponent.

Components are added to FlameGame through the constructor, or by using the method add. Added components are added to an instance of a World that is encapsulated by FlameGame. The World is the root component for (practically all of) the components in the game.

In the following example, the FlameGame is created with two components, a CircleComponent and a RectangleComponent. The components are added to the game through the constructor of FlameGame.

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

void main() {
  final components = [
    CircleComponent(
      position: Vector2(
        100,
        100,
      ),
      radius: 50,
    ),
    RectangleComponent(
      position: Vector2(200, 200),
      size: Vector2(100, 100),
    ),
  ];

  runApp(
    GameWidget(
      game: FlameGame(
        children: components,
      ),
    ),
  );
}

When running the example, a circle and a rectangle are shown on the screen. The circle is positioned at (100, 100) and has a radius of 50, while the rectangle is positioned at (200, 200) and has a size of 100x100.

The position and size of the components are expressed in pixels. Positions are expressed using (x, y) -pairs, where the x-axis value comes first.

Note that, in 2D games, positioning of components is typically relative to the left upper corner of the screen. X-axis values grow from left to right, and y-axis values grow from top to bottom. As an example, the position (0, 0) is at the left upper corner of the screen, while the position (200, 100) is 200 pixels to the right and 100 pixels down from the left upper corner of the screen.

Mixins and Components

Mixins are a programming language feature that allows defining code that can be reused in multiple classes. In games and component systems, mixins — given that the used programming language supports them — can be used to define common functionality that is shared between different components.

Flame uses mixins to define common functionality that can be shared between components, but that is not part of the core functionality of the components. As an example, the mixin for listening for tap events is defined in TapCallbacks, which can be taken into use by importing events from flame.

import 'package:flame/events.dart';

In Dart, mixins are used with the with keyword. A class that would extend a RectangleComponent to create a rectangle with the position (100, 300), side length of 50, and that would use the TapCallbacks mixin would look as follows.

class TapBox extends RectangleComponent with TapCallbacks {
  TapBox()
      : super(
          position: Vector2(100, 300),
          size: Vector2(50, 50),
        );
}

Taking the TapCallbacks mixin into use adds behavior to the component that allows the component to react to tap events. The mixin defines a number of methods that can be overridden to react to tap events.

As an example, if we would wish that the rectangle would move 10 pixels to the right whenever it is tapped, we could override the onTapDown method, which is called whenever the component is tapped.

class TapBox extends RectangleComponent with TapCallbacks {
  TapBox()
      : super(
          position: Vector2(100, 300),
          size: Vector2(50, 50),
        );

  @override
  void onTapDown(TapDownEvent event) {
    // do something
  }
}

In the onTapDown method, we could, for example, update the position of the rectangle. As the TapBox inherits RectangleComponent, which inherits PositionComponent, the class has property position that can be changed. As an example, the onTapDown method could be overridden to move the rectangle 10 pixels to the right whenever it is tapped.

  @override
  void onTapDown(TapDownEvent event) {
    position.x = position.x + 10;
  }

Altogether, an application that would show a rectangle that reacts to taps would look as follows.

Run the program to see the output

Custom games

In Flame, custom games can be created by extending FlameGame (or Game). As an example, a custom game called TapGame that has a variable score, a method for incrementing the variable, and that shows an instance of a TapBox (defined above) could be created by extending FlameGame as follows.

class TapGame extends FlameGame {
  var score = 0;

  TapGame() {
    add(
      TapBox(),
    );
  }

  incrementScore() {
    score++;
  }
}

There is another useful mixin called HasGameRef that provides a reference to the game that the component is a part of. If the TapBox would be used within the TapGame, the TapBox could use the HasGameRef mixin to gain a reference to the game, which would allow calling the methods of the game.

class TapBox extends RectangleComponent with HasGameRef<TapGame>, TapCallbacks {
  TapBox()
      : super(
          position: Vector2(100, 300),
          size: Vector2(50, 50),
        );

  @override
  void onTapDown(TapDownEvent event) {
    gameRef.incrementScore();
    position.x = position.x + 10;
  }
}

In the example above, the onTapDown method of the TapBox would increment the score of the game by one whenever the TapBox is tapped. The gameRef is a reference to the game that the TapBox is a part of, and the incrementScore method is a method of the TapGame that increments the score of the game by one.

In essence, whenever the tapbox is tapped, the score of the game would be incremented by one.

Drawing this chapter together, and adding a variant of a start screen and an endscreen, we could create a simple game that has a start screen, a game screen, and an end screen, and where the objective would be to tap the tapbox ten times. This would look as follows.

Run the program to see the output

Get.offAll() ???

Note that above, when navigating from TapGame to ResultScreen, we use Get.offAll(() => ResultScreen(score: score));. The Get.offAll method is used to remove all previous routes from the navigation stack, and to navigate to the new route. If we would not do this, the game would be left in the stack, possibly running in the background and wasting resources.


Loading Exercise...