James Williams
LinkedInMastodonGithub

Making Games with Flutter and Flame

Like many people who become computer science majors, I wanted to make games as a kid. I even took a couple courses and fell accidentally into writing a book on HTML5 Game Programming. Later at Udacity, I was able to architect a short lived game programming credential using libGDX. And then I promptly did nothing for like five years.

The pandemic sparked an opportunity to revisit that hobby. Taking stock of the content I have, much of the HTML5 stuff is broken or uses libraries that don't exist anymore. The libGDX stuff is likewise not functional. That one is not because of the library itself. libGDX is alive and well but there have been lots of changes in Gradle (build system). It would be quite a bit of work update all the project files. Learning new stuff is more fun than maintenance so I'm learning a new framework, Flame. With the announcement of the Flutter Casual Games Toolkit at I/O this year, the choice seems serendipitous.

What is Flame?

Flame is a 2D game engine built on top of Flutter. Flame has a number of extension packages for things like audio, physics, collision detection, and level editing. Mobile, web, are desktop platforms are supported. It uses a component model where each component manages its own state and respond to events and draw calls.

Flame Components similar to Flutter and Widgets. They are the building blocks of your game.

Hello World

The main entry point for our examples, FlameGame is also a Component.

One of the first things I tried to do was to draw text. I initially feared it would be difficult once you start using different fonts. Sometimes that's the case in game frameworks, the default font is easy but you have to do all this . Fortunately, TextComponents derive much of their format from core Flutter. In the pubspec.yaml of a typical Flutter project, you'd specify the locations and common names for the fonts you intent to use.

Here is a portion of the pubspec.yaml declaring the fonts we'll use in the project.

  fonts:
     - family: Cinzel
       fonts:
         - asset: fonts/Cinzel-Medium.ttf
     - family: Corinthia
       fonts:
         - asset: fonts/Corinthia-Regular.ttf
     - family: Redacted Script
       fonts:
         - asset: fonts/RedactedScript-Regular.ttf
 

The game draws random strings in random locations around the screen using the above fonts.

Drawing various random words

TextComponent takes a text string and a TextPaint textRenderer. WordFactory selects a random word from the list and selects a random color and randomizes which preset font to display.

import 'dart:math';

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

import 'package:flutter/material.dart';

class WordCloud extends FlameGame {
  @override
  Future onLoad() async {
    const WORD_COUNT = 20;
    var factory = WordFactory(this.canvasSize);
    for (var i = 0; i < WORD_COUNT; i++) {
      add(factory.generateWord());
    }
  }
}

class WordFactory {
  Vector2 canvasSize;

  WordFactory(this.canvasSize);

  var rnd = Random();
  static const WORDS = [ /* List of words here */  ];

  TextPaint? generateRandomTextPaint() {
    var color = Color((rnd.nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
    var fontNum = rnd.nextInt(4);
    var fontSize = rnd.nextInt(48).toDouble();
    if (fontSize < 24)
      fontSize = 24;
    switch (fontNum) {
      case 0:
        return TextPaint(style: TextStyle(fontFamily: 'Redacted Script',color: color, fontSize: fontSize));
      case 1:
        return TextPaint(style: TextStyle(fontFamily: 'Cinzel' ,color: color, fontSize: fontSize));
      case 2:
        return TextPaint(style: TextStyle(fontFamily: 'Corinthia', color: color, fontSize: fontSize));
      default:
        return TextPaint(style: TextStyle(color: color, fontSize: fontSize));
    }
  }

  TextComponent generateWord() {
    var word = WORDS[rnd.nextInt(WORDS.length)];
    var x = rnd.nextInt(canvasSize.x.toInt());
    var y = rnd.nextInt(canvasSize.y.toInt());
    return TextComponent(text: word, textRenderer: generateRandomTextPaint())
      ..x = x.toDouble()
      ..y = y.toDouble();
  }
}
 

You can expect to spend as much time in general Flutter documentation as you will searching Flame APIs.

Drawing A Starfield

For the next example, I wrote a small bit of code to draw a bunch of components on screen. There isn't a PointComponent in Flame but a point is really just a very small rectangle. Using the canvas size and a desired density of points, the code creates a list of randomized points and adds them to the component tree.

Starfield

import 'dart:math' as math;

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


class DrawAStarfield extends FlameGame {
  final starPaint = Paint()..color = Colors.white;

  final density = 0.2;
  var stars = [];

  @override
  Future onLoad() async {
    var canvasSize = this.canvasSize.toSize();
    var width = canvasSize.width;
    var height = canvasSize.height;
    var size = 10000 * density;
    var r = math.Random();

    for (int i = 0; i < size; i++) {
      var x = r.nextDouble() * width;
      var y = r.nextDouble() * height;
      stars.add(Vector2(x, y));
    }

    for (int j = 0; j < size; j++) {
      add(RectangleComponent(position: stars[j], size: Vector2(1, 1),
          paint: starPaint));
    }
  }
}
 

Drawing Lines

DrawLines is pretty much the previous snippet but with more paints. First a couple different colors and then a LinearGradient with various colors as the stop points.

Lines using various Paints

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

import 'dart:ui' as ui;

class DrawLines extends FlameGame {
  final white = Paint()..color = Colors.white;
  final magenta = Paint()..color = Color(0xFFFF00FF);
  final red = Paint()..color = Colors.red;
  final blue = Paint()..color = Colors.blue;
  final green = Paint()..color = Colors.green;

  @override
  Future onLoad() async {
    add(
        RectangleComponent(position: Vector2(0, 100), size: Vector2(500, 2), angle: 45,
            paint:white));
    add(
        RectangleComponent(
            position: Vector2(150, 100), size: Vector2(500, 2), angle: 45,
            paint:magenta));

    add(
        RectangleComponent(
            position: Vector2(200, 100), size: Vector2(500, 2), angle: 45,
            paint:green));

    final gradientPaint = Paint()
      ..shader = ui.Gradient.linear(
        Offset(70, 100),
        Offset(500, 20),
        [
          Colors.green.withOpacity(1.0),
          Colors.blue.withOpacity(0.2),
          Colors.yellow.withOpacity(0.8),
          Colors.red.withOpacity(0.1),
          Colors.red.withOpacity(0.9),
        ],
        [
          0.0,
          0.5,
          0.7,
          0.9,
          1.0,
        ],
      );

    add(RectangleComponent(
        position: Vector2(70, 100), size: Vector2(500, 20), angle: 45,
        paint:gradientPaint));
  }
}
 

If you don't already have good Flutter fundamentals, working with Flame will definitely get you up to speed.