If you’re a mobile app developer, chances are at some point you’ve looked at a really popular game and thought you should give game development a try.
I remember being surprised by the popularity of the simple but highly addictive Flappy Bird and thinking that it really wasn’t a very difficult game to build. It wasn’t nearly as complex as Clash of Clans, Monument Valley, or similar games that obviously required a much larger development team. This was several years before Flutter was released. At the time, I was still experimenting with libGDX on Android!
Over the years, I’ve enjoyed experimenting with developing games with a variety of game engines on different platforms and frameworks.
In this article, we’ll explore different ways to create games with Flutter. We’ll also compare different Flutter game engines. This article will not cover adding the Unity app to Flutter. In that scenario, the game is actually created outside the Flutter framework.
The only prerequisite for this article is a basic understanding of Flutter.
Game engines can be quite diverse in terms of the type and quality of functionality they provide. Some offer a full suite of solutions, while others have a much narrower offering. Still, all game engines need to address or compensate for a game’s dynamic nature, handling of a large number of objects, and limited interaction with the underlying UI components.
Games are generally very dynamic. They may need to account for scrolling backgrounds, bouncing objects, and even button smashing by confused or frustrated users. For good performance, a game needs to update as quickly as possible without requiring a setState(() {})
, such as a callback to render a new frame.
The critical code that makes this possible is the game loop. The game loop runs over and over, allowing a game to update object positions, resize the screen, change the camera location or perspective, and more.
Game loops are present in most, if not all, game engines. This is a key difference from the underlying Flutter framework, in which new frames are drawn after an event occurs rather than having a continuously updated canvas.
Most games have an abundance of visual objects, and some even use fancy particle effects. These objects use a lot of memory, so game engines generally provide methods to load and dispose of them at appropriate places. These techniques are somewhat similar to the initState()
and dispose()
calls of the normal Flutter framework state.
Games don’t generally use the UI components of the underlying framework. For example, you can’t use Flutter widgets to build game objects in most engines. Game engines usually render on a canvas. This allows for a large number of objects to be on the screen at once. It also enables a game to be ported uniformly across platforms, since nearly all operating systems support canvas rendering.
Visual game objects are usually referred to as sprites. These can be static or animated and may be created in a myriad of ways. One example is a sprite sheet. This is essentially one large image file consisting of multiple sprites or versions of a sprite. The individual images may be edited and reordered to give an illusion of movement. The sprites can be PNG images that are rendered onto the base canvas.
Other features (such as audio, gesture detection, and cameras) usually vary much more from one game to the next compared to the basic graphics.
Can you create a game without an engine? Yes. Should you? In most cases, no. Here’s the short answer for why you shouldn’t: absolute math hell.
To clarify further, I believe everyone should try creating a full game (at least once) without any help from an engine. This is hard work. It will essentially involve writing a basic engine from scratch.
It’s understandable to be cautious about attempting a project of this scale, but it can be very instructive to attempt your own implementation. Diving into the elements of a game system can provide invaluable experience.
Flutter can be used to create the basic components of a game. Tickers and timers may be used to create a game loop. You can create a positioning system, build a sprite system, make calculations for collisions, and add your own gesture detection according to element positions. Of course, none of this will be easy. But, it could be a fun exercise.
Game engines have a distinct advantage when it comes to building complex elements or features. Any game that requires advanced features (such as hitboxes, physics, audio support, and camera movement) will be much easier to design using a solid engine rather than trying to code it from scratch.
Flame is a complex, mature game development framework and is currently the most popular Flutter game engine. It supports everything needed to design a basic game, including a game loop, sprites and sprite sheets, collision detection, and audio. Flame also offers several complementary packages for more complex functionality, such as enhanced audio support, physics, SVG support, and Rive integrations.
Flame uses a GameWidget
to add a game to an app:
GameWidget( game: game, loadingBuilder: (context) => const Material( child: Center( child: CircularProgressIndicator(), ), ), errorBuilder: (context, ex) { return Material( child: Text('Error'), ); }, overlayBuilderMap: { 'victory': // Build victory overlay, 'defeat': // Build defeat overlay }, ),
The overlayBuilderMap
argument allows us to neatly define any overlays that may be needed throughout the course of the game, such as a victory, defeat, or pause menu. The loadingBuilder
and errorBuilder
arguments can be used to let users know the game is loading or to provide an error message, as needed.
We can define the game itself, DemoGame
, by extending the FlameGame
class:
class DemoGame extends FlameGame { @override Future<void> onLoad() async { // Load sprites, camera, etc. return super.onLoad(); } }
We define capabilities and functionality with mixins:
class DemoGame extends FlameGame with HasCollidables, HasTappables, HasDraggables { @override Future<void> onLoad() async { // Load sprites, camera, etc. return super.onLoad(); } }
To create game objects, we subclass any component type that has a position
and size
. In this example, we subclass PositionComponent
:
class DemoComponent extends PositionComponent with Hitbox, Collidable { DemoComponent({ required Vector2 position, required Vector2 size, }) : super(position: position, size: size); @override Future<void> onLoad() async { await super.onLoad(); // Initialize sprites, hitboxes } @override void render(Canvas canvas) { super.render(canvas); // Render objects } }
We can also use Flame’s Forge2D package to add Box2D physics to the engine. This package provides the functionality to build more intricate games, featuring objects with more realistic movement.
Personally, I like the code structure of the Flame engine components and the neatly separated game logic. Flame offers several types of game objects, as well as various mixins that supply different kinds of functionality. Flame provides thorough documentation and multiple tutorials on its website and in its README file.
Quill is a lightweight game engine that uses simple components, cleverly referred to as Feather
and Quill
, to render game objects.
To design a game with Quill, we start by initializing a new QuillEngine
and supplying a Feather
containing the game engine loop for initialization, update, and disposal.
void main() async { QuillEngine(Demo())..start(); }
Next, we extend the Feather
class to get the game loop:
class Demo extends Feather { @override void init() { // Initialize } @override void input(Event event) { // Handle input } @override void update(Time time) { // Update objects on new frame } }
We can create a Sprite()
inside the Feather
subclass:
Sprite _demo; _demo = new Sprite() ..initWithColor(const Color(0xFFFFFFFF)) ..setPosition(0.0, 0.0) ..setSize(100.0, 100.0);
Quill is far less complete compared to Flame. There are several missing features, like audio and image caching, that are listed in the engine’s documentation as being slated for a future version. Additionally, Quill’s game objects appear to have less code separation compared to other engines.
SpriteWidget is a toolkit that can be used to create animations and games in Flutter. This package works well with the widget hierarchy making it feel much more Flutter-like (or “Flutter-y”) compared to other game engine solutions.
SpriteWidget can be used to create both sprite nodes and node graphs, making for some really interesting possibilities. For example, the toolkit’s documentation describes creating a car from different sprites and linking wheels to the base car node through offsets. SpriteWidget also contains comprehensive animation techniques, including sequences and grouping multiple animations.
SpriteWidget offers several useful techniques and provides a unique solution for handling many aspects of game development. However, it does not offer a full suite of game development tools and also has not been well maintained. The pub scores at the time of this article reflect the toolkit’s resulting degradation.
I recently created a small demo game engine of my own: Illume. Illume uses Flutter widgets as game objects and adds a simple game loop.
While researching Flutter game development engines for this article, I noticed that most of the solutions rely on the technique of adding sprites to a canvas. This is probably the most rational and permanent solution for a game engine, but I wanted to try to leverage Flutter’s “everything is a widget” philosophy.
I wanted to build an engine that would mesh better with a normal Flutter app, rather than being entirely separated from the main widget UI code. To some extent, SpriteWidget achieves this, but technically it uses wrappers rather than widgets.
To build a game with Illume, we simply use the Illume
widget with an IllumeController
argument, which controls different aspects of the game:
IllumeController gameController = IllumeController(); // Inside build Illume( illumeController: gameController, ),
To define game objects, we extend the GameObject
class. For example, we can use the following code to initialize walls:
class Wall extends GameObject { int initialDistance; Wall( this.initialDistance, ); @override Widget build(BuildContext context) { return Container( color: Colors.green, child: const Text('Demo'), ); } @override void init() { // Init size, alignment, position, etc } @override void onCollision(List<Collision> collisions) { // Called when collisions occur // E.g: illumeController.stopGame(); } @override void onScreenSizeChange(Vector2 size) { // Transform object positions on screen changed } @override void update(Duration delta) { // Update on new frame } }
GameObject
provides access to position, alignment, and basic box-based collision detection for every object, triggering a callback when a collision occurs. The build
method allows us to create an object directly in the Flutter widgets. We can even use Flutter’s default gesture detection widgets.
Illume is not meant to be a replacement for mature game engines. Widgets are heavier than sprites drawn on canvas, so Illume takes more memory to run and currently lacks the functionality for complex game features. Illume does, however, provide an easy solution for building a simple game quickly using widgets.
In this article, we explored how game engines differ from the underlying framework. We also discussed the pros and cons of coding a Flutter game from scratch or using one of the following game engines: Flame, Quill, SpriteWidget, or Illume.
Flame is currently the only fully developed, well-maintained solution available for creating games on Flutter. Other game engines offer creative techniques but have not yet developed to a point where they are viable to use for full-fledged games. I think any of the solutions described in this article are appropriate for smaller games, but I would recommend Flame for development of larger production games. At present, game development in Flutter is still pretty much a one-horse race.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Comparing Flutter game engines"
Very nice walk through!
Flame v1.1 has been released since you wrote the article too so now it is even more stable and we have a much nicer and efficient collision detection system.