Flutter + Flame + Tiled = a simple game field prototype for a strategy game

Alexander Shevelev
11 min readNov 29, 2023

--

In this story, I tell you how to create a simple hexagon-based grid prototype for a turn-based strategy game. Our prototype should support scaling, scrolling and tapping to a cell — all of this you need in a real game.

First, let’s look at what we want to get in the end.

I’m gonna use Flutter with the Flame game engine. And I’ll use the Tiled editor to design our game map. But let me first explain to you why I selected…

A little bit of theory

Why Flutter?

Flutter is an open source UI framework from Google for building beautiful multi-platform applications. Today, Flutter is used in more than one million apps all over the world and this quantity is growing dramatically.

Talking about games, tens of thousands of them have been published using Flutter, from simple but fun puzzles to more complex arcade and strategy games.

Flutter is a great choice for game developers. Firstly, it’s open source and free, giving you fine-grained control over your game’s rendering and input processing logic.

Secondly, developing in Flutter is highly productive. Flutter introduced a revolutionary capability called hot-reload that allows developers to see instant UI updates after making code changes, thus making the development process more iterative and efficient.

In addition, Flutter supports multi-platform game development, so you can build your game for mobile platforms (iOS and Android), web, and desktop based on a solid, shared codebase.

And finally, Flutter games load fast and are generally very performant, even on low-end devices or in browsers.

Why Flame?

Flame is a game engine over Flutter, that provides a complete set of solutions for games development. It takes all advantage of the powerful infrastructure provided by Flutter but simplifies the code you need to build your game projects.

Flame is a modular framework, which means that you can only include those parts of it that are directly needed in your game project.

In our particular case, we are going to use the flame_tiled module, which will allow us to work with maps created in Tiled.

Why Tiled?

Tiled is a simple but powerful 2D level editor for games with its own output format, based on XML. Tiled is a great choice for game developers. Not least because it’s open source and free. It supports a lot of game engines, including Flame.

What types of games does Tiled support? A short answer is — all types of 2D games (or pseudo-2D — so called 2.5D or isometric) — from arcade hack-and-slash platformers to turn-based strategies.

Within the scope of this article, I’m not gonna deep dive to Tiled — primarily because the comprehensive guidance for the editor can be found here. In addition to the guidance I recommend you watch these short videos. All this will be enough to get started with your maps.

Some thoughts about hexagonal grids

Before we get into the practical part, let’s talk a little bit about hexagon-based grids.

Many great games use hex grids, especially strategy games, including Age of Wonders, Civilization 5, and Endless Legend. But why?

Why use hexagons? If you need a grid, it makes sense to just use squares. Squares are indeed simple to draw and position, but they have a downside. Take a look at a single square in the grid. Then look at its neighbors.

There are eight neighbors in total. Four can be reached by crossing an edge of the square.

They are the horizontal and vertical neighbors. The other four can be reached by crossing a corner of the square. These are the diagonal neighbors.

What is the distance between the centers of adjacent square cells in the grid? If the edge length is 1, then the answer is 1 for the horizontal and vertical neighbors. But for diagonal neighbors, the answer is √2.

Hexagons vs squares

The differences between the two kinds of neighbors lead to complications. If you use discrete movement, how do you treat diagonal movement? Do you allow it at all? How can you create a more organic look? Different games use different approaches, with different advantages and disadvantages. One approach is to not use a square grid at all, but to use hexagons instead.

Compared to a square, a hexagon has only six neighbors instead of eight. All of these neighbors are edge neighbors. There are no corner neighbors. So there is only one kind of neighbor, which simplifies a lot of things. Of course, a hexagon grid is less straightforward to construct than a square grid, but we can deal with that.

Anatomy of a hexagon

Before we get started, we have to settle on a size for our hexagons.

A “blueprint” of a hexagon

Here is a “blueprint” of a hexagon. Let’s assume that we know the value of W (the width of the hexagonal tile in pixels). To draw the tile properly we need to calculate values H (the total height in pixels) and b.

Let’s do this using trivial trigonometric calculations.

y = 𝛑 / 6 radians.

cos y = a / c ; c = a / cos y [1]

tg y = b / a ; b = a * tg y [2]

a = W / 2 [3]

H = c + 2* b [4]

[1, 2] -> [4]

H = a / cos y + 2 * a * tg y [5]

[3] -> [5]

H = W / 2*cos y + W * tg y

H = W * ( 1 / 2*cos y + tg y)

[3] -> [2]

b = W * tg y / 2

Now we can try to fulfill our calculations for the real values. In the prototype I use a 64 pixels width tile. So, let’s get its H (total height) and b values — we are using these values in the next chapter.

H = 64 * (1 / 2 * 0.86602 + 0.57735) = 73.9 = 74 pixels.

b = 64 * 0.57735 / 2 = 18.4752 = 18 pixels.

Sizes of a hexagonal tile for this story

Based on these values, we can now draw a tile in a graphic editor (my personal choice is Adobe Photoshop — but it’s up to you).

And here’s the practical part

Creating your map in Tiled

Download Tiled from here, install and run it. Select File -> New -> New map from the menu.

In the New Map window set up your map settings, like this one:

Our map settings

So we are going to create a map based on hexagons, 30 tiles to 30 tiles, the size of each tile is 64 by 74 pixels. Click OK and your map template will be created.

Next, we ought to draw a tileset for our map. Tileset, as its name indicates, is a set of images for tiles. To me, the most convenient way to have one graphic file for each tileset. So here is what my tileset looks like.

My tileset for the map

As you can see it contains 8 tiles (the middle-right tile is white — you can’t see it). The size of each tile is 64 to 74 pixels. I drew the tileset in a graphics editor, then saved it in PNG format with an alpha channel and, at last, put it in the same directory as the created map.

Now you can link the tile set to the map. To do this, click on the button “New Tileset” in the “Tilesets” panel. In the “New Tileset” window set up your tileset settings in this way:

A tileset settings

Click “Browse”, next, select the tileset file and click “Ok”.

Now, using tiles from the tileset, draw the map how you like it. Mine looks like this:

My map

Don’t forget to save your map. Now we are ready to coding!

Setup the project for Flutter

Create a new Flutter project and let’s set it up.

First, create two subfolders, in the asset folder: images and tiles . Put a .tmx file of your map to the tiles folder and the tileset file — to the images folder.

The asset resources

Then, you have to edit the .tmx file with your map to make a path to the tileset valid. Open the map file in a text editor and add ../images/ string before the tileset file name.

Now you need to tell the project about our resources and libraries that we are going to use. To do this open the pubspec.yaml and specify all resources and dependencies in the pubspec.yaml, like this:

Loading a map

We are ready to write code now.

First, you need to open main.dart and add an empty template for our prototype.

Pay attention to the interfaces ScaleDetector and TapDetector — we’ll use them in a short time.

Next, load our map in the onLoad() method.

The code is quite simple, but take a look at two things.

First, in line 11, we linked a camera to an anchor. An anchor is the point around which the screen is scaling and rotating. For simplicity, I select the top & left corner as the anchor point.

Then, in line 15, we must specify the size of our tiles. My experiments have shown that we should set the tile height 1 pixel less, otherwise the seams between the tiles are visible. Don’t ask me why.

Dragging

We are ready to make our map draggable by finger. Let’s add three methods to our class:

What should you pay attention to here?

First, in line 9 we check — are a user using one-finger gesture or not? If the check is successful, then we start to process his gestures (multi-finger gestures will be used for scaling).

Second, in line 16, we use the zoomDragFactor variable to make the dragging smoother. You can set a constant value 1.0 to the variable and watch the result — it’s funny.

Scaling

Our next goal is to make our map scale by two finger gestures. Let’s add some bit of code to do it:

The code is quite trivial, so let’s get moving.

Checking borders

Our game field must be within the boundaries of the screen. To make this possible, we must check a zoom level when the scaling ends. As well as the camera position when the dragging ends.

Let’s see the code.

The checking for scaling is trivial — we test that a zoom value is in an interval between the minimum and the maximum zoom factor.

The checking for dragging is more cumbersome, but the idea is simple — we test that the camera area is within the map borders and correct the camera position if they are not.

Tapping detection

Not a single strategy game is completed without detecting which cell a user clicks on. Let’s add this functionality to our application as well. To do this, we will have to write two functions in our code.

We’ll add some code to the first function (onTapUp) in a next chapter, but for now, it only contains a call to the second function, where all the magic happens.

The function in which we find the tapped cell is cumbersome, but simple in nature. In this code, we go through all the cells and calculate the distance from their center to the tap point for each one. The cell for which the distance is minimal is the cell we are looking for.

How fast does the tapping detection work?

As you can see, the algorithm is quite straightforward. You might ask yourself: is it fast? Let’s find out!

To do this, add a new file in the project and name it utils.dart. This file contains a single function — here it is.

This function wraps your call and prints its execution time in a log. Now, we can use the function and check the result.

While running the code on my work phone (Google Pixel 2), in debug mode, I got the following results: about 5 milliseconds for the first call and about 1 millisecond for the next calls. To me, it sounds great — the algorithm works fast enough and can be used in real projects.

Adding a unit to the selected cell

Now, we can check that our tap detection algorithm works fine and detects exactly that cell, which was tapped by a user. The simplest and the funniest way to do it is to add a sprite to the cell.

First, add an image for the sprite into the assets/images folder. I use a png image, 256 to 256 pixels, with an alpha-channel. Here it is:

A source image image for the sprite

Then, add the image to the pubspec.yaml. And finally, add some bit of code to the onTapUp() function.

And Bob’s your uncle! Now, when you tap some cell, you’ll get a German WWI infantry trooper on it.

Show a background image

Our prototype is nearly complete, but the last touch remains. The screen surface under our game field is black, it’s grim and dull. Let’s change it to some background image!

First, add a background image into the assets/images folder. For the prototype, I selected a small (683 to 411 pixels) webp image with cloud sky. Don’t forget to add the image to the pubspec.yaml.

The background image

The easiest way to display it is to create your own custom painter. Let’s add a new file with code to our project — background_painter.dart. The painter itself is straightforward:

All it does is simply draw the image to the entire canvas.

So, we’ve got the painter, and now we are ready to use it in a wrapper around the game widget. To do it create a new code file background.dart and write a simple piece of code:

The wrapper is simple: it reads the background image, displays it through the painter and puts a child widget inside.

Our job is nearly done. Now we need to wrap the game field widget into the background widget. To do this, let’s slightly change the main() function.

All that remains to do is to override backgroundColor() function to make a standard game field background transparent. Like this:

Afterword

Ok, friends. Our prototype is over. But who knows, maybe your game project is about to begin.

Anyway, you can find all the codes here. And happy coding!

--

--

Alexander Shevelev
Alexander Shevelev

Written by Alexander Shevelev

An Android developer from Yandex LLC, Moscow, Russia. That’s all.