Flutter Flame: simplest optimization techniques
I described some problems of Bonfire in my previous article and argued, why it might be bad choice for you new game. Despite “vanilla Flame” is much more lightweight, it also does not provide enough performance. So now I try to share some my recipes of performance tuning of flame-game and describe reasons why it ever works.
General optimization strategy
Talking about game’s speed we look at FPS firstly. It is first and easiest measurable parameter, most of people looks on it to make decisions. That’s why I want to take a closer look on it’s nature (again!) and tell that in Flutter it is a bit more complex that “rendering speed”. In Flutter and Flame FPS is calculated from “game logic layer” and does not show your graphical adapter’s speed, but the CPU’s performance. So, if we have any problems, we should take a look on our game logic — this literally means “updateTree()” and “renderTree()” functions. If these functions will to calculate game logic too slow, the graphical layer will receive updates rarely and we will see “slideshow” instead smooth animations. Another important thing is that not only your’s game code would be problem-maker. Do not hesitate to look into framework’s source code to find a roots of your problem. It might be very helpful to explore library’s sources to locate you own mistake and fix it.
So, our general approach will be reducing “updateTree” and “renderTree” time. Let’s look in details, what exactly does it mean.
Optimization methods
One of the easiest ways to make you app faster is to reduce its computations and cache results of repeating ones. And one of the sources of such computations is our game map, loaded and rendered through the library “flame_tiled”.
Map rendering optimization
1. Speed up the render of static layers
“flame_tiled” renders tiles using SpriteBatch and Canvas.drawAtlas. This approach is faster than rendering each tile individually but it still not very effective. It still performs repeating computations on every tile: searching for corresponding image rectangle, its position and angle on destination surface, rite all commands into batch. And then on every frame it runs saved batch to generate static map image.
But why not to perform it operation just once? We could create and run a batch while loading a map and just save resulting image into “Image” class.
This can be implemented very easily:
Simple yet effective. If you map is small, most probably you would not notice any changes, but than more the map become than more resources you save.
This solution is very basic but we will use it as a core for more complex optimizations.
2. Speed up rendering of animated tiles
Some tiles are not statical. And most probably a set of animated tiles would have such features:
- The map would have many animated objects of similar type, spread on different map’s zones or grouped. For example, rivers, lakes, sands, lava, torches on walls and so on.
- Every class of such tiles might have its own animation, one for every type.
- Animation’s frames might be played synchronically for objects group, starting at same time and switching frames simultaneously.
If you have exactly same case, and such animated objects are relatively large, it would be worth to spend your time to join separate animations into one big SpriteAnimation component. Why it would be better? Firstly, we draw a big static picture only once instead creating it from small puzzle every time — like in previous optimization example. And at second, it is too expensive to handle a lot of identical SpriteAnimation component if we really need just one. Every new animated tile will do “updateTree()” call and waste our processor’s time to calculate already calculated animation values.
Add new components faster
In Flame every component has “priority” property which allows to determine object’s “z-index”: what to display at top and what at bottom.
Suppose you map contains earth, water and trees. Earth is most bottom, water is upper, then we draw player, and at top of all — trees, to hide players under them.
To achieve this behavior, you should to specify “priority” value for each game entity. And everything should work fine while you have constant number of in-game objects. But suppose you have 20 players or NPCs and every player/NPC firing a lot of bullets. Every bullet is additional object and provides its own “priority”. Every bullet lives just several seconds and then disappears — after timeout or after hitting the target.
What happens inside of engine?
- Every new bullet has own “priority” value.
- Flame figures out that it needs to redraw set of components according to its “priority” value. Flame marks parent component of bullet to be recalculated
- At next “update()” call Flame will work under all child components of marked parent and resort them according to new “priority” values.
It seems to me that the reason of non-effectivity is obvious. When adding new objects directly to FlameGame we force engine to sort all game objects. But recalculating all objects is not necessary, all we need is recalculation of limited set inside layer. Steps to achieve this:
- Create components-layers for each class of objects: ground, water, players, trees, bullets…. Add these components directly into FlameGame. Specify corresponding “priority” for every layer.
- New components of every type should be added to corresponding layer. New component will inherit parent’s priority, you do not need to specify it anymore.
With this approach Flame will not recalculate priority for all components tree. If you add a new bullet, Flame will recalculate only components from bullets layer.
Avoid feature-rich components when you do not need them
Keep in mind that any component except basic “Component” class contains any update or render logic. If you have component with extended functionality, but use only basics — check its source code, it might be worth to downgrade component to simpler one or to reimplement any of its expensive methods.
For example, suppose you need to hide PositionComponent or use it as utility class. Let’s check its renderTree function then. You will see that it contains vary expensive canvas.save(), canvas.transform() and canvas.restore() functions, which will be called anyway, no matter do you have any code in render() or not.
So, I urge to review all your code and additionally review how external libraries work. Unfortunately, Flame is not so optimized that you can to forget everything except your game’s logic.
Useful tools
To make things easier I made a small library: flame_tiled_utils.
How to use it?
At first, load the map as you usually do:
final tiledComponent = await TiledComponent.load(mapFile, Vector2.all(8));
After it, call special class to merge statical layers into single sprite component:
final imageCompiler = ImageBatchCompiler();final ground = await imageCompiler.compileMapLayer(tileMap: tiledComponent.tileMap, layerNames: ['ground']);ground.priority = 10;add(ground);
Done! We just made map rendering mush more effective.
Now let’s optimize animated tiles. Look at this code:
TileProcessor helps you to convert every tile to your custom object, mapping it by “Type” in Tiled editor:
With TileProcessor we can get tile’s sprite, animation and collision area. I’d prefer to see such functionality in Flame itself, but it is disappointingly absent. With TileProcessor we collect animated tiles into AnimationBatchCompiler class. Then we call “compile()” method which make one animated component instead hundreds. Isn’t it a cool?
Benchmarks
I wrote small example app to compare FPS in three modes:
- With flutter build-in API (which do not allow animations)
- Optimized map rendering except animated layer: every tile is displayed as individual component (like Bonfire does)
- Fully optimized variant
To make the difference more noticeable, I made relatively big map, 300x300 tiles, 8px every tile.
Here are results in vanilla mode:
Not fine but still usable. But here we have no animations because flame_tiled does not work with them.
Here is benchmark of second mode:
As we can see, it is totally unusable, because processing individual animation for every tile is very expensive. Unfortunately, it is exactly how Bonfire works with animated maps, so you can not expect fine performance from it unless you map is small enough.
And finally, benchmark of optimized render:
Shall I to comment anything here?
Conclusion
As we can see, not every “ready-made” solution can offer you the best functionality. Flutter is very new in gaming; engines are too young and many aspects needs optimization and even realization from scratch.
Are all available optimization methods listed in this article? Definitely no, it is just beginning. In real game you have a lot of mechanics, graphical effects, game objects, collisions… and just few milliseconds for computations!