Flutter & games: exploring Bonfire
The new toolkit “Flutter Casual Gmes Toolkit” had been announced with Flutter 3.0. It does not pretend to be an universal solution for any issue, but we see how was made the first “official” step toward games development.
There are other instruments and the most powerful of them is Flame framework. And on top of the Flame is built “Bonfire” — a game development framework. It looks the easiest framework for developing RPG games at least.
In this article I want to share my experience with Bonfire and show its good and dark sides… I hope my article would be helpful if you still not sure about framework to use in you first game project.
First look
The Bonfire’s author has made a lot of documentation, added a numerous of well-done examples and also recorded some video tutorials (unfortunately not in English).
Let’s summarize the framework key functionality in the short list:
- Ready control layer: keyboard and virtual gamepad support.
- Entities “Player”, “Enemy”, “Follower” are ready to use.
- Entity for creating “flying attack objects” such as bullets. Melee attack mechanics.
- Mixins and functions to implement NPC’s movement towards the player to attack him.
- Pathfinding for player’s character to the point on the screen you had clicked by mouse.
- Tiled map editor support, animated tiles, collisions with tiles, reading tiles properties. Placing tiles on different levels of height.
- Mapping objects defined in Tiled into classes in Dart to implement custom behavior.
- Lighting, full screen color effects.
- Smooth camera moving.
- User interface, dialogs.
- Mini map.
- …maybe something else I missed or was implemented while I preparing this article.
The toolkit looks very cool, right? Really, the framework allows us to build most of the game just in Tiled, to set up all required parameters using object’s properties, to read them later in you code and implement missing game logic in custom classes by inheriting predefined entities.
After setting up a project everything is really clean and evident: we have a game object, objects have callbacks to receive damage, to handle collisions with other objects, functions to be called on movement and so on. You have everything, just go to implement your idea! And I tried to do so…
Our experimental game
I decided not to do yet another RPG game: at first because I didn’t have any an inspirational idea; at second because with Bonfire it would be too easy, you will do about 80% of work in Tiled. It is not what you need when you want to explore the framework, it’s abilities and bounds.
Do you remember an old game, “Battle City 1990”? On the dark background with buildings made of bricks your tank protects “eagle” from other tanks and bricks are destroyable. If at that “ancient” times people was able to create such game, it should be very easy to implement the same with all power of modern toolkits! Isn’t it? Really?..
1. Controller
With the framework we have bundled class to configure game controller — virtual joystick for mobile devices and WASD for desktop. But in my cause movement should be allowed only in 4th directions. In Bonfire movement is available in every direction, so we need to cut unnecessary functionality. Here I haven’t any another solution but just to copy-paste and rewrite contents of whole mixin “MovementByDirection”. It is not good practice, but still “ok” — just one complete reimplemented entity, not a problem! Let’s go forward…
2. Ready entity Enemy
Here we facing the similar situation: we need to bound NPC for 4-directional movement. At this situation we needn’t to copy-paste anything, but to implement desired behavior you should be familiar with Bonfire’s function call order: when you should use “update()”, how it differs from “onMove()” and what is “moveLeft”, “moveRight” and so on. It is not a problem to learn internal framework structure, deep class/mixins hierarchy, etc, but it is not something you could call “easy” or “intuitive”. Here is the place where Bonfire began to show you its complexity.
3. Ready entities for bullets, damage taking and receiving
Here is the first place we meet a conflict between functionality implemented in Bonfire and functionality required to implement game mechanics.
In Bonfire a bullet is implemented through class “FlyingAttackObject”. It explodes at moment on collision with any game component (of cause if it has any information about collisions). Damage is received only by objects of “Attackable” type. Looks useful. Let’s compare with “Battletank” original game mechanics:
- There are tanks, which receive damage. Standard Bonfire’s mechanics should work nice here.
- There are destructible bricks, and I have no idea how to use Bonfire’s mechanics here without modification, because bricks are not disappearing after first hit — only a part of brick should be deleted.
- Finally, we have water on a map. Water tiles should be collidable because tanks can’t swim, but a bullet should fly freely above water.
How should we solve this problem? I decided to use the following approach:
- There is everything OK with tanks, let’s keep standard mechanics.
- For walls — the first idea was to build the map by small tiles, but it means a lot of crazy handwork because the smallest tile size is 4px. Also, it would be ineffective to process so many tile objects by engine. Then I chose another approach: when a bullet hits a wall, we calculate bullet’s flight vector and shrinks the tile’s size in this direction for half of tile’s full size. If the size is zero the tile should be removed. The solution works fine but all standard mechanics from “onCollision()” function should be rewritten from scratch.
- Water. Here we need to rewrite “onCollision()” function of Bullet class again to ignore collision with water.
This does not look too complex, but one difficulty calls another one: Bonfire does not see any difference between tiles of different types. To make the engine to see that water is water and brick is brick we should do some additional work in its core… I’ll describe it at part 7.
4 and 5. Enemy’s movement towards player. Pathfinding.
At this point all framework’s “out of the box” solutions become useless. Enemy’s movement is possible only in 4th directions. Also, there are a lot of obstacles that need to be quite accurately bypassed in order to get somewhere purposefully.
Bundled pathfinding has surprised, too. Firstly, it is implemented only for Player. For any another game object you should implement everything yourself. Bonfire’s author used third-party package https://pub.dev/packages/a_star_algorithm and it is simple enough to use it directly, without additional framework interface. But then we face with other problem: if the map has complex relief and distance between points is long the algorithm slows down whole game and we see low FPS. All operations are single-threaded, so the more tanks are on the display it will be the slower the game will be. That’s why I finally stopped on simple algorithm of random movement and changing direction on corners, plus firing when player is on the line of sight. It is not exactly what I want to do, but still more clever behavior than in original game.
6. Tiled support, different height levels for game objects.
I’ve already mentioned the Tiled maps support. Now let’s describe what I mean about object’s height. Sometimes one kind of objects should be rendered above another. For example, imagine a dungeon from an RPG game and a torch or cornice there. These objects are higher than player stays on the floor, so it should be drawn above the player when he moves under them.
In the Flame we have a special parameter to control this: “priority” property of the game object. It is integer, the higher it is, the higher corresponding object is placed. It was surprisingly that Bonfire does not allow you to use this functionality! The only way you could do to influence on object’s placement height by to specifying “above” as a tile type of name in Tiled editor. With “above” this tile will be rendered above all the objects in the game. I also found a new type “dynamicAbove” in the last commits, but it seems there is no way to control the “priority” directly.
Bonfire takes a full control over “priority” because it is oriented on games, where camera is placed at 45 or 70 degrees. It does not simple watching vertically from top to bottom. Thus, one object potentially could overlap another one just only because it is lower on Y axis. Maybe the author will extend the framework to allow the uss of multiple height levels, but right now all available levels are hardcoded in the “LayerPriority” class and the only way to add your own layer is to edit the framework’s code.
Why did I even start to explore this? In the original game background is just a simple dark color and after destruction walls simply disappear. I decided to add a ground texture instead of dark background and add ash texture in place of destructed wall. Thus, ash should be placed upper than ground but lower than any other game objects. Unfortunately, I didn’t have any tool to achieve this goal and finally I forked the framework and added a desired logic directly into it.
It’s not good news when you expected just simple using of ready “out-of-the-box” solution.
7. Mapping objects and tiles from map into custom classes in Dart
This was next part required to be rewritten to achieve my goals.
I was surprised the Bonfire does not allow to see differences between tiles for framework’s user. This feature is available only for Tiled “objects”, but it’s very different entity then “tile”. Tiled “object” have no sprite or animation, it only could be positioned and resized on the map. Simplest description is than “objects” are just squares on map with individual names. So, my expectations that it is possible to create separate class to handle “brick” tiles in the Bonfire were not met. In fact, Bonfire’s logic is as follows:
- All objects, intended to be interactive in game, should be created in Tiled as “object” entity. Objects can be transformed into custom class while map loading, but you should manually do sprite or animation loading.
- All environment objects like walls, floor, non-interactive decorations should be created as tiles. But you can specify that some tiles should be collideable though.
You can see this in documentation and tutorials. First time it was the cause of my bewilderment, why does the author not map tiles directly in desired class, but as you can see this functionality hasn’t been implemented yet.
In my cause this functionality was critical because:
- I need to know the tile type while processing bullet’s game logic: to differ water’s tiles from brick’s tiles.
- I need to control every brick tile to shrink its size in case of hitting by a bullet.
- Finally, I want to implement additional mechanics: allow player to hide in the forest, becoming invisible for computer enemy.
So, if stay limited to Bonfire’s build-in functionality, we probably should draw additional layer of objects above tiles layer. This looks much more difficult handwork then exploring and modifying framework’s source code.
Finally, I implemented additional builders — “tileBuilder” and “decorationBuilder”. Along with built-in “objectBuilder” it becomes possible to create every required class for any Tiled object. Here is my code example:
And a bulk on knowledge about internal Bonfire’s structure was an additional bonus… The bonus I wouldn’t like to achieve to…
The biggest disappointment.
At last, I had all necessary tools to implement a game logic — thanks to forking and deep modification of the framework.
And then I faced another surprise: the performance was around 20–30 FPS on desktop and just 12–15 FPS on mobile!
I began to explore the problem disabling every game logic I previously write, but FPS always was around 30. After that I tried to remove layers with collisions and decorative tiles. It had better effect, but still not 60 FPS. Finally, I disabled all graphics, players tank stands on absolutely black screen with tiny square of bricks around game field. Well, with such configuration 60 FPS was achieved. But only till you try to move or to fire or to spawn an enemy.
“Maybe I forked anything wrong?” — I had thought and began to build and launch Bonfire’s demo apps. And — surprise-surprise! — it also works only at 30 FPS!!!
At this point I finished my attempts do to anything with framework or with the game written on this framework. Before abandon everything I look again into source code of Bonfire and compared it internal logic with Flame. And now I think I can show a list of reasons why Bonfire is so slow (and, I’m sure’ will be slow in future without any radical architecture changes):
Background operations
Nothing happens in the Flame in background unless you write your own logic in “update”. Only collisions would be calculated if you enabled it. That’s all.
In the Bonfire at least 3 background tasks are launched permanently:
- A loop through all game objects and determining not visible ones. Objects not in viewport would not be processed. I sure it is dubious solution: if we want to avoid unnecessary render — this is work the graphical engine should handle, and I believe it will do that much faster and more effective.
- Another loop through all game objects to calculate every object’s “priority”. As I said at part 6, Bonfire calculates this parameter automatically to implement “angled camera” view. But implementation looks not optimized: for example, there would be additional layers with objects never overlaps each other. Or, for example, do not recalculate if positions did not change.
- Collision detection. The author tried to optimize this by passing objects not currently in viewport. With this behavior you can easily find NPCs stuck inside textures or even out of the game field, because NPC can travel through walls while it is not visible on screen.
All these operations generate constant background load on you game even if no line of custom game logic was written. The amount of load is just determined by number of objects you draw in the Tiled editor.
Background operations intervals (but all operations are in single thread):
Tile’s rendering
Flame does not create any additional entities while rendering tiles. Tiles are not game objects and does not processed by game loop events. Flame uses SpriteBatch (https://pub.dev/documentation/flame/latest/sprite/SpriteBatch-class.html) to optimize map rendering. It might be more optimized I think, but anyway it still faster than Bonfire’s approach.
Bonfire treats each tile as game object. For every tile Bonfire do calculations, does the tile visible or not? Every tile potentially could contain collisions so every tile is also checked for this. And finally, the “priority” parameter calculated for every tile too. It is rather expensive.
Collision handling.
Flame divides all collision objects on two categories:
- Passive objects, like walls. Such objects simple spay on its place and does not collide each other.
- Active objects, like player, bullets or enemies. Such objects actively move, it can collide between each other and also with passive objects.
Such approach allows to reduce computation time for passive objects. I’m sure passive objects is a main component of most of typical game maps.
In Bonfire case there is no difference between object type. Every object is equal, every is calculated. The system would check and report you every time about collision ow two static walls — the information you newer need to (most probably). Additional overhead is Bonfire’s movement system. On each “update()” call for moving object Bonfire virtually move the object to its position at next tick and do additional collision calculations. As result, the system become slower the more moving objects you add.
Do not forget that everything is calculated on single thread, no additional “isolates” used here — so resuorces are very limited.
Loading animations.
This is probably most harmless problem, but anyway. In Bonfire most of animations always created from scratch: you pass the file name, framework cuts image and makes separate frames from it… Only image itself is cached but it would be useful to cache prepared frames too. I think, this approach potentially can lead to short “freezes” in moments when a new animation should be created.
Resume.
My own decision is not to use Bonfire at all. Firstly, I thought it might be useful if you fork it and rewrite its internal logic for your game purposes. But such performance bottlenecks are very disappointing.
Nonetheless, there are still a lot of useful things you could take from Bonfire to your project. A joystick component for example. Or fullscreen animated color filter. But you should be careful and always analyze what you going to copy.
Does Bonfire have a chance to overcome these problems? I think there is no evolutional way. Too many code already written, there is a lot of dependencies between different parts of system and very radical changes required to make engine more performant and flexible. Even if the author decides to rework everything, it already will be completely another framework.
Finally, do Bonfire really have its application area? I believe, you could use it if you project is not complex: small map, low number of objects on it, easy game logic. If you know that 90% of work could be made in Tiled — Bonfire is definitely your choice!
Would the Flame itself be more performant? Definitely yes! But do not expect that if you create thousands of objects the engine will process them easily. This mistake will lead you to Bonfire’s performance problems.
If you really want to make you Flutter game faster, even with Flame you need to develop special optimizations, suitable for you game mechanics. And I already have a number of nice recipes and strategies that really works! I looking forward to share it in my future article.
If somebody interested, here is the source csode of the project on which I experimented with Bonfire: https://github.com/ASGAlex/battletank_game