For many years Adobe AIR in conjunction with the Starling framework has been a great way to develop 2D cross platform games for Android and iOs. Flash’s concept of a display list to manage on screen objects and the benefit of utilizing the devices hardware acceleration through Stage3D are fantastic.
Unfortunately Adobe’s plans regarding the future of AIR aren’t too clear. Naturally this scares some developers, making them seek new possibilities. Today there are many options to choose from. One that caught my attention is Google’s Flutter. Although it seems more like an UI framework, you’re also able to make games. Under the hood it utilizes the Skia 2D graphic rendering engine, making it hardware accelerated on mobile platforms too. Sweet!
Flutter applications are written using Google’s own Dart language.
To enhance the functionality of Flutter, you can use packages. The one we’ll be looking into in this tutorial is called flame – a game engine. Well, as the author itself states, it’s minimal and simple so we’ll enhance it in turn.
Prerequisites:
- A PC running Windows 7 64 bit or above
- Visual Studio Code
- Flutter SDK
- Android emulator or a real device
I assume your development enviroment is set up and running.
Unfortunately there is no way to create an empty Flutter project – it’s always based on a simple demo application showcasing a floating button and a textfield.
To create a new project, select View -> Command Palette… and enter Flutter: New Project in the textbox.
Afterwards it will ask you for a name for the project and where to store the project on your harddrive. Give it the name breakout. Flutter just accepts lower case letters and no spaces.
To make use of the flame package, we need to add a dependency inside a file called pubspec.yaml
Simply select this file in Visual Studio Code’s own Explorer panel:
Inside the file find:
1 2 3 |
dependencies: flutter: sdk: flutter |
and add flame: ^0.10.2 right below.
CAUTION: It’s crucial that your newly added dependency lines up with flutter: If it’s shifted e.g. one tab to the right, it’s treated as an attribute of the flutter: tag.
In other words, it should look like this now:
1 2 3 4 |
dependencies: flutter: sdk: flutter flame: ^0.10.2 |
If you save the file using ctrl + s, Visual Studio Code will attempt to download the newly added package. If everything went well it will show:
We aren’t done yet! Since our game uses graphical assets, we need to tell Flutter that it should bundle them with the application.
Find the following section:
1 2 3 4 5 6 7 8 9 10 11 12 |
# The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg |
As the comments say, we could go ahead and list our assets one-by-one now but that’s a bit cumbersome. We’re going to embedd our whole folder by
1 2 3 4 |
# The following section is specific to Flutter. flutter: assets: - assets/images/ |
This will bundle any .jpg, .png or .gif file it can find in the specified folder.
At the moment those folders doesn’t exist yet. So go ahead and inside your projects folder create an assets folder and inside it an images subfolder either using Windows Explorer or directly in Visual Studio Code’s Explorer panel.
Speaking of images, download the following graphic and put it into your freshly created assets/images folder.
It’s a seamless tile we’ll use for our game’s background we’re about to work on in a bit.
Let’s start some coding! In the Explorer panel select the file main.dart
This file contains the top-level main() function which serves as the entry point for any application. There’s a whole lot of code in there yet. It’s all related to Flutter’s sample application. You know what? We don’t need it. Simply select everything and delete it!
Speaking of deletion, there’s a folder which is part of the demo application that needs some deletion otherwise we’ll get compiler errors later on. Delete the folder called test from your project.
Now put this piece of code into main.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flame/flame.dart'; import 'dart:ui'; import 'helpers.dart'; void main() async { await Flame.images.loadAll(['backgroundTile.jpg']).then((images) => debugPrint('Done loading ' + Flame.images.loadedFiles.toString())); DeviceOrientation orientation = DeviceOrientation.portraitUp; var waitForScreenSize = await Dimension(true, orientation).set(); debugPrint('Screen size ' + window.physicalSize.toString()); } |
I guess you might be wondering what we’re doing here.
To be able to use the graphics stored inside the assets folder in our game, we need to make sure it’s loaded into memory. This is done using Flame.images.loadAll. Since there’s no direct way to list the files bundled, we need to provide an array containing the filenames. The preceeding await keyword makes sure that it doesn’t continue executing the next lines of code before the image(s) have successfully loaded.
To understand the rest of the code I need to go far afield. As you know there is a magnitude of different display resolutions out there. To render our game at the target’s screen size in pixels, we need to know the exact size. Unfortunately things aren’t that easy because there is no direct reliable way to get the exact size! The Flutter developers added the function window.physicalSize which returns a Size object that should contain the stage’s size. As soon as you’ve tried to run an application in release mode, you’ll notice that this size might be 0! That’s because window.physicalSize doesn’t get updated with the correct values fast enough. Even if it would it might not be correct because it also depends on the rotation of the device, being either landscape or portrait and if the navigation & status bar is hidden or not.
To summarize, with Flutter’s built-in methods as well as Flame’s it’s impossible to get the accurate size. So I created a little helper class myself that takes care of it.
var waitForScreenSize = await Dimension(true, orientation).set();
The first parameter indicates if the game should run in fullscreen mode while the second locks the device to a specific orientation – portrait in this case. As before, the await keyword prevents the next lines from executing before the helper class signaled that the screen dimensions are available. To benefit from this helper class, you need to download the following file and put it into the lib folder of your project: helpers.dart
If you have an Android device connected to your computer or the Android emulator is running it’s now time to hit F5 to test the project. If everything went well you’ll see the following in Visual Studio Code’s debug console:
A look at your device/emulator will just give you a white screen – nothing too fancy. We’ll change that in a bit. 😉
Right-click the lib folder in the Explorer panel and select New File. Name it breakout.dart
Paste this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import 'package:flame/game.dart'; import 'package:flame/sprite.dart'; import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/painting.dart'; import 'dart:ui' as ui; import 'dart:async'; class BreakOut extends BaseGame { List<Sprite> textures = []; int physicalWidth; int physicalHeight; int layoutWidth = 720; int layoutHeight = 1280; double scale; ui.Image background; BreakOut(); // empty constructor Future<void> init(DeviceOrientation orientation) async { Size viewPort = ui.window.physicalSize; if (orientation == DeviceOrientation.portraitUp || orientation == DeviceOrientation.portraitDown) { double tempWidth = viewPort.width; double tempHeight = viewPort.height; if (tempWidth > tempHeight) { Size correctSize = new Size(tempHeight, tempWidth); viewPort = correctSize; } } physicalWidth = (viewPort.width ~/ ui.window.devicePixelRatio); physicalHeight = (viewPort.height ~/ ui.window.devicePixelRatio); scale = physicalWidth / layoutWidth; } void update(double t) {} } |
This will serve as a template for our actual game! But wait! The constructor is empty! Why this? This has to do with Flutter’s asynchronous nature. For our game we need to do preprocess the graphical assets to match the device’s screen size. This involves waiting for some operations in a ‘synchronous fashion’ using the await keyword. The problem is, in Flutter the constructor can’t be marked as async – which is required. To overcome this issue we move all the initializing and preprocessing in a public async function called init() which returns a Future<void> once ready.
If you take a close look at this function you might be a little surprised. There’s more code to handle the device’s display resolution! Didn’t we do this in the main() function yet? Yes we did! Unfortunately sometimes the device gets confused by the screen orientation and returns the reversed numbers for the width and the height. This code takes care of this.
The next important thing is the variable scale. As I mentioned before there are countless different screen resolutions out there. For simplicity we design our game at a fixed resolution of 720 x 1280 and scale everything on screen appropriately depending on the physical resolution.
Take a look at this to get a better understanding:
If we design our game for a 720×1280 resolution we know that the red dot should be 20×20 pixels for example. On our target device we have a resolution of 960×1600 pixels. This means the scale factor is 1.33333 thus we need to resize our red dot by the factor of 1.33333 resulting in roughly 27×27 pixels.
That’s it for the basics. Let’s finally add our tiled background!
Simply insert the following lines right after: scale = physicalWidth / layoutWidth;
1 |
textures.add(new Sprite('backgroundTile.jpg')); |
This puts a new instance of Flame’s Sprite class holding our background image into the textures array. At the moment it doesn’t make sense to store a single Sprite in an array but we’ll add more later on.
1 2 |
int realWidth=64; // this is the width of a single background tile at a screen resolution of 720 x 1280 ui.Image masterTexture = Flame.images.loadedFiles["backgroundTile.jpg"]; |
This is a reference to the actual image inside memory. We need it to get the actual size of the image.
1 2 |
int desiredWidth = (realWidth * scale).toInt(); double tempScale = masterTexture.width / desiredWidth; |
This calculation gives us the final scale for a single tile on the target device.
Now that we know how big a single tile should be, we need to take this tile and floodfill the whole screen with it! Again this turns out to be not that easy.
To do this, we need to create a temporary canvas. As the name implies this is some sort of artboard which you can draw onto using code. The contents of this canvas is then recorded using Flutter’s PictureRecorder class and finally converted to a standard Image object we can then use to draw the floodfilled background onto the real canvas of our game using the drawImage() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Rect bgRect = Rect.fromLTWH( 0, 0, physicalWidth.toDouble(), physicalHeight.toDouble()); ui.PictureRecorder rec = new ui.PictureRecorder(); Canvas tempCanvas = new Canvas(rec, bgRect); paintImage( canvas: tempCanvas, rect: bgRect, image: Flame.images.loadedFiles["backgroundTile.jpg"], scale: tempScale, repeat: ImageRepeat.repeat, filterQuality: FilterQuality.high); ui.Picture pic = rec.endRecording(); background = await pic.toImage(bgRect.width.toInt(), bgRect.height.toInt()); |
The final step of drawing the background to our canvas is done using the following render function. Add it below the init() function.
1 2 3 |
void render(Canvas canvas) { canvas.drawImage(background, new Offset(0.0, 0.0), new Paint()); } |
Your curious what happens if you hit F5 again? Do it – you’ll be surprised!
Ain’t a damn thing changed – it’s still the white background.
The reason is simple: we didn’t create an instance of our breakout game yet and furthermore we didn’t add it to Flutter’s widget list.
There’s remedy of course. Head back to the main.dart file using the Explorer panel and add the following lines to the end of the main() function:
1 2 3 |
BreakOut breakOutGame = new BreakOut(); await breakOutGame.init(orientation); runApp(breakOutGame.widget); |
If you hit F5 this time you should be pleased by something like this:
You might be asking yourself now why on earth we needed that much lines of code just to display a background image. This could have been much easier by just composing this image directly inside e.g. Photoshop and use this instead of the background tile.
Well our background is resolution independent – it will automatically adjust to any screen size and look the same. If the aspect ratio is different from 16:9 there’ll be no distortion. Great!
That’s it for the first part! We’ll continue with this game in the next part!