Setting Up a Widget-Tree Based Game with Flame and Flutter

One of the advantages of being a developer making your own games is that you have full control over the outcome. You control how a certain element functions or how an enemy character behaves. You can even control the (in-game) world and play around with the physical rules like gravity.

With this kind of freedom though, most beginner developers find it hard to establish a structure when developing games along with the UI that goes with it.

Standard structure

It seems that there’s no standard structure when it comes to game development with Flame and Flutter.

The closest thing to it is simply feeding the main game class’ .widget property directly into runApp. This setup is perfect for simple “tap-tap” games and the hyper-casual games like Langaw.

Here’s an example ./lib/main.js from Shadow Training.

import 'package:flame/flame.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:shadow_training/shadow-training.dart';

void main() async {
  await Flame.util.fullScreen();
  await Flame.util.setOrientation(DeviceOrientation.portraitUp);

  await Flame.images.loadAll([
    'background.png',
    'boxer/dizzy.png',
    'boxer/idle.png',
    'boxer/punch-left.png',
    'boxer/punch-right.png',
    'boxer/punch-up.png',
  ]);

  ShadowTraining game = ShadowTraining();
  runApp(game.widget);

  TapGestureRecognizer tapper = TapGestureRecognizer();
  tapper.onTapDown = game.onTapDown;
  Flame.util.addGestureRecognizer(tapper);
}

If a game has been released for a while, chances are, there are going to be updates. These updates most probably present more features.

As the number of features grows, the game will be needing UI (user-interface) elements.

UI elements help the players connect, understand, and interact with the game. Examples of UI elements include the score display, health bar, settings dialog box, exit button, play button.

In fact, even the simplest game (except maybe the Box Game) needs to have at least a play button or a score display.

This presents a problem with the “standard structure” as this would mean building the UI manually. The developer has to write everything. Rendering managing states and screens, and animation.

Again, as long the game is simple, this can be bearable to manually write.

The moment your game gets more complex than a game that can be described as dead simple, this becomes an easy source of a headache (based on personal experience).

Taking advantage of the widget-tree

Let me introduce Flutter, no wait, we’re already developing in Flutter. When developing games using Flame, it’s easy to forget that we’re still developing apps using Flutter.

Flutter is actually a framework designed for business mobile app development. The type of apps that aren’t really that graphical, just some nice looking buttons, lists, labels, checkboxes, input controls. Maybe some basic animation and transition effects.

One of the things that are very easy to do in Flutter is UI. Exactly those elements we need for our game but too troublesome to write on our own.

Let’s take advantage of this

A Flame game is still rendered inside a widget (Game‘s .widget property). If we put this game widget in a Stack, we can easily build UI elements on top of it.

Here’s the same code from Shadow Training‘s ./lib/main.dart; but this time the game widget is enclosed in a MaterialApp widget (for possible navigation or routing between screens in the future).

import 'package:flame/flame.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:shadow_training/shadow-training-ui.dart';
import 'package:shadow_training/shadow-training.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() async {
  await Flame.util.fullScreen();
  await Flame.util.setOrientation(DeviceOrientation.portraitUp);

  await Flame.images.loadAll([
    'background.png',
    'boxer/dizzy.png',
    'boxer/idle.png',
    'boxer/punch-left.png',
    'boxer/punch-right.png',
    'boxer/punch-up.png',
    'markers.png',
    'perfect-time.png',
  ]);

  SharedPreferences storage = await SharedPreferences.getInstance();
  ShadowTrainingUI gameUI = ShadowTrainingUI();
  ShadowTraining game = ShadowTraining(gameUI.state);
  gameUI.state.storage = storage;
  gameUI.state.game = game;

  runApp(
    MaterialApp(
      title: 'Shadow Training',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: Stack(
          fit: StackFit.expand,
          children: [
            Positioned.fill(
              child: GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTapDown: game.onTapDown,
                child: game.widget,
              ),
            ),
            Positioned.fill(
              child: gameUI,
            ),
          ],
        ),
      ),
      debugShowCheckedModeBanner: false,
    ),
  );
}

The main difference here is instead of passing game.widget directly to runApp, we pass a MaterialApp first. This MaterialApp has a Scaffold as its home property (which is basically the child widget that will be shown when starting the app).

The Scaffold in turn, has a Stack as its body.

This Stack has two Positioned.fill widgets as its children. This is just to make sure that whatever gets assigned as their children will take up the entire screen. One has the game.widget, the other has gameUI which is a ShadowTrainingUI. It’s a class that extends a StatefulWidget.

Performance and drawbacks

As I was developing and testing the game, I checked and kept an eye on the performance every step of the way.

There’s actually a commented out code that prints out the real-time FPS of the game and this is what I’ve been monitoring. The whole time I was close to 60 FPS.

That being said, there were no noticeable performance issues seen during these tests. So having the widget inside a MaterialApp in the widget tree while running the game is fine within a Flame game.

Of course, this can’t all be good. There are some drawbacks like the need to expose the state in the widget so you can pass it on the to the game class. This is important in order to update the state of the UI from the game class.

A two-way communication system between the game and the UI class is important as you will probably need to write code on each side that changes things on the other side.

This can be a tricky situation but can be solved by writing the classes so they have instance variables that hold the reference to the other. Another way is to have a globally accessible singleton class that has references to both the main game class and the UI class.

Conclusion

Based on my experience developing games using Flame and Flutter, I have found out that having a regular widget tree in a game is generally good practice.

Buttons, text, dialog boxes, text input, and other UI elements become significantly easier to create.

There’s no set-back on performance and the only thing tricky to set up is the communication between the game and the UI class.

If you have any questions, please leave a comment below or ask me by joining my Discord server. Shadow Training is an open-source game that can be found on GitHub.