From Zero to a Multiplatform Flutter Game in a week

Mariano Zorrilla
Jun 5 · 12 min read

Most mobile developers don’t look at games because it looks like a crazy amount of work! Thinking about memory management, sprites manipulation, complex UI/UX, etc and, if you come from an Android background like me… you know how limited it is to build games using native code. But there’s where Flutter can give you a hand!

I started using Flutter for a bit more than a year already and I noticed the huge potential to build apps but, also, to build games. The first game I saw running on pure Dart/Flutter was “Space Blast” and blew my mind

then, I saw the amazing guys from 2dimensions

with their platform “Flare” to build/animate characters, environments, etc overall sprites manipulation which not a familiar task for most mobile developers.

I met at the and I told them the idea to build a fun tap game using their platform and they saw it as totally feasible.

To put everyone into context, the game is “Tap Titans”


Where to start… I got the “idea” (inspired one), now I need a simple example. Background, a number and from every tap, decreasing that value:

GestureDetector(
onTapDown: (TapDownDetails details) => damage(details),
)
_bossDamage = 980;

damage(TapDownDetails details) {
setState(() {
_bossDamage = _bossDamage - 30 <= 0 ? 980 : _bossDamage - 30;
});
}

Then, adding the visual elements and that’s it:

Next step was to add the actual UI/Elements to make it look like a “game”. We need a character, an actual boss to deal damage to it, maybe some power-ups and coins to buy those.

My “hero” uses 2 images, one for the “idle” state and one for the “attack” state. When you tap on the screen, the image switches to create the visual impact:

String hero() {
tap ? "assets/character/attack.png" : "assets/character/idle.png";
}

For the boss, I needed more than one (to make it a bit more enjoyable):

List<Bosses> getBosses() {
list = List<Bosses>();
list.add(Bosses("Lunabi", 450, "assets/boss/boss_one.png"));
list.add(Bosses("ivygrass", 880, "assets/boss/boss_two.png"));
list.add(Bosses("Tombster", 1120, "assets/boss/boss_three.png"));
list.add(Bosses("Glidestone", 2260, "assets/boss/boss_four.png"));
list.add(Bosses("Smocka", 2900, "assets/boss/boss_five.png"));
list.add(Bosses("Clowntorch", 4100, "assets/boss/boss_six.png"));
list.add(Bosses("Marsattack", 5380, "assets/boss/boss_seven.png"));
list.add(Bosses("Unknown", 7000, "assets/boss/boss_eight.png"));
list.add(Bosses("ExArthur", 10000, "assets/boss/boss_nine.png"));
list;
}

Don’t tell me how I came out with those names… coding at 2am makes you do that. Every boss will have a name, a life bar and an actual image.

For the Power-Ups I followed a similar logic like the bosses:

List<PowerUps> getPowerUps() {
list = List<PowerUps>();
list.add(PowerUps("Master Sword", 2.15, , 50));
list.add(PowerUps("Lengendary Sword", 2.45, , 180));
list.add(PowerUps("Keyblade", 3.75, , 300));
list.add(PowerUps("Lightsaber", 4.95, , 520));
list.add(PowerUps("Buster Sword", 6.15, , 1700));
list.add(PowerUps("Soul Edge", 8.65, , 2400));
list;
}

You have the Sword/Weapon name, the multiplier (from base deal damage), if you bought them or not and the total amount of coins necessary to get it.

Then I thought about adding a visual “hitbox” for every time you tap on the screen:

Widget hitBox() {
(tap) {
Positioned(
top: yAxis,
left: xAxis,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: Material(
color: Colors.transparent,
child: StrokeText(
"-${damageUser.toInt().toString()}",
fontSize: 14.0,
fontFamily: "Gameplay",
color: Colors.red,
strokeColor: Colors.black,
strokeWidth: 1.0,
),
),
),
Image.asset(
"assets/elements/hit.png",
fit: BoxFit.fill,
height: 80.0,
width: 80.0,
),
],
),
);
} {
Container();
}
}

This will add the visual element, the position of your finger and a small “hint” with the total amount of damage dealt for every single tap with a Y offset.

Remember the “damage” method? I used the TapDownDetails details parameters to get the actual X and Y position of the finger.

xAxis = details.globalPosition.dx - 40.0;
yAxis = details.globalPosition.dy - 80.0;

I also added a “hide” logic to remove that element when you lift your finger from the screen

GestureDetector(
onTapDown: (TapDownDetails details) => damage(details),
onTapUp: (TapUpDetails details) => hide(),
onTapCancel: () => hide(),
),

The list of “Power-Ups” is pretty much straight forward

ListView.builder(
padding: EdgeInsets.only(bottom: 20.0, left: 10.0, right: 10.0),
itemCount: list.length,
itemBuilder: (context, position) {
PowerUps powerUp = list[position];
int bgColor = !powerUp.bought && coins >= powerUp.coins
? 0xFF808080
: !powerUp.bought ? 0xFF505050 : 0xFF202020;

swordElement(bgColor, powerUp, position);
},
)

I’m adding some visual enhancements for the buttons and the backgrounds and every Sword/Weapon

Widget swordElement(int bgColor, PowerUps powerUp, int position) {
Padding(
padding: EdgeInsets.symmetric(
vertical: 5.0,
),
child: Container(
height: 70,
child: Card(
color: Color(bgColor),
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
powerUp.name,
style: Utils.textStyle(11.0),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0),
child: FancyButton(
size: 20,
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 10.0, bottom: 2, top: 2),
child: Text(
!powerUp.bought ? "BUY" : "BOUGHT",
style:
Utils.textStyle(13.0, color: !powerUp.bought ? Colors.white : Colors.grey),
),
),
Padding(
padding: EdgeInsets.only(left: 8.0, right: !powerUp.bought ? 2.0 : 0.0),
child: Text(
!powerUp.bought ? powerUp.coins.toString() : "",
style: Utils.textStyle(13.0),
),
),
coinVisibility(powerUp.bought),
],
),
color: !powerUp.bought && coins >= powerUp.coins
? Colors.deepPurpleAccent
: Colors.deepPurple,
onPressed: !powerUp.bought && coins >= powerUp.coins ? () => buyPowerUp(position) : ,
),
)
],
),
),
),
);
}

After all this work! I have a truly visual improvement over my first GIF

Adding sounds is always a plus for every single game:

AudioPlayer hitPlayer;
AudioCache hitCache;
hitPlayer = AudioPlayer();
hitCache = AudioCache(fixedPlayer: hitPlayer);
// If the audio was playing, we stop it to init again
hitPlayer.pause();
hitCache.play('audio/sword.mp3');

This logic was used across the game, every time you kill an enemy, you’ll earn coins, a sound for that, every time you buy something, a sound for that as well. BTW, what is a game without background music? You can loop it this way:

AudioCache musicCache;
AudioPlayer instance;
playMusic() {
musicCache = AudioCache(prefix: "audio/");
instance = musicCache.loop("bgmusic.mp3");
}

That will go inside your InitState and will be stopped at your Dispose method. Also, If you noticed, I’m using a custom font to make it more “game” look alike.


GOING MULTI-PLATFORM!!! 🎮😎

I think the biggest benefits from using Flutter is the fact that you can actually use the same code to build apps for Android, iOS, Desktop (Mac, Linux, Windows) and…. 🥁🥁🥁

One step at the time! Let’s go for iOS:

You’ll need a Mac and XCode to build the code in order to run the app. You can use your physical device, but an emulator is more than enough.

If you’re using Android Studio like I do, hit the “RUN” button while selecting the iOS device and that’s it. “Open iOS Simulator” should be an option inside your device list.

If you’re old school, you can run “flutter devices” to check the device ID and then hit:

flutter run -d {device id}
// without the curly braces

Desktop:

This one is a bit more tricky because you need extra steps to make it run. I’m using a Mac but it should be a pretty similar process for every other platform (Linux and Windows)

All I did was to follow this exact (and great) Medium post and it was running with a small change inside my code:

I used the terminal to hit this command:

export ENABLE_FLUTTER_DESKTOP=true

If you follow this step, you can clone the official desktop repo:

git clone https://github.com/google/flutter-desktop-embedding.gitcd example

and grab the “Mac”, “Windows” and “Linux” folders, copy those into your project and that’s it.

Now let’s get back into the code… we need to make a small change

_setTargetPlatformForDesktop() {
TargetPlatform targetPlatform;
(Platform.isMacOS) {
targetPlatform = TargetPlatform.iOS;
} (Platform.isLinux || Platform.isWindows) {
targetPlatform = TargetPlatform.android;
}
(targetPlatform != ) {
debugDefaultTargetPlatformOverride = targetPlatform;
}
}

That method will go inside your main() one:

main() {
_setTargetPlatformForDesktop();
runApp(TapHero());
}

make sure to import these 2 classes

'dart:io' Platform;
'package:flutter/foundation.dart' debugDefaultTargetPlatformOverride;

If you want to lock the phone’s rotation, use this line before your return inside your build method:

SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
// use this import
'package:flutter/services.dart';

after all this work, “macOS” should be visible inside your list of devices. Once you hit run for that device, you’ll get something like this:

Something is missing… oh, yeah! THE WEB 🌎 This one is a bit more tricky, but Ayush did also a Medium post about it:

The web version follows the logic of the Desktop one. You need to clone the Web repo if you want (it has a “hello_world” example):

git clone git@github.com:flutter/flutter_web.git
cd examples/hello_world

make sure you’re using the latest version of Flutter with:

flutter upgrade

and you also need to have webdev active

flutter packages pub global activate webdevflutter packages upgrade

this one will allow you to run and build Web apps. You’ll also need to add the paths for Dart and Webdev to make the magic work!

After all this “hard” work, you’ll need to change your flutter import libraries for “flutter_web”. For example:

'package:flutter_web/material.dart';

then, move your assets inside folder and last, but not least, you need to hit this line over your terminal:

webdev serve

and that’s it! should be your new best friend! Let’s take a look:


Harder, Better, Faster, Stronger

Welcome Screen

No game can be complete without a “Welcome Screen”, this will give your users a small hint of what’s coming.

All I did was reusing the game’s assets: Boss, hero, background and I did a logo for it.

To add extra gaming experience (?) a Blur was applied and some ashes/fire particles in the background as well. If you want the Blur snippet, I’m using this one:

BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 4.0,
sigmaY: 4.0,
),
child: Align(
alignment: Alignment.bottomCenter,
child: Align(
alignment: Alignment.topCenter,
heightFactor: heroYAxis,
child: Image.asset(
heroAsset(),
width: size / 1.5,
height: size / 1.5,
fit: BoxFit.cover,
),
),
),
)

If you want the particles one, I’m using a flutter web version logic for that one which you can give it a try here:

https://flutter.github.io/samples/particle_background

and the source code is right here:

https://github.com/flutter/samples/tree/master/web/particle_background

Adding more gaming logic

I’m my quest to make the game a bit difficult, funnier, interesting… etc I decided to add a time limit (which increases every time you kill a boss) and a button to let everyone know how far you got into the game:

I’m using a regular for the clock from the desired limit in milliseconds until it reaches 0. If you want to get the string for the time, use this snippet:

String timerString {
Duration duration = controller.duration * controller.value;
'${(duration.inMinutes).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}

You will happen when the animation controller reaches the dismissed state (that’s because I’m using the reverse(); function):

controller.addStatusListener((status) {
(status == AnimationStatus.dismissed) {
setState(() {
gameOver = ;
});
}
});

Getting an actual web URL

Do you kids know about Firebase? Pretty much does everything we need for this task!

I thought it was going to be hard, but it was super simple. If you’re familiar with the “hosting” service, you should be good to go in 10min.

Make sure to follow every step from the official documentation:

https://firebase.google.com/docs/hosting/quickstart

and after you do that, you’ll need to build your

webdev build

You’ll see a new folder inside your project called and, from your Firebase init process, you should also have a folder.

Copy the entire folder content inside your one and that’s it! hit:

firebase deploy

Your should be running using the new .web.app domain!

If you want to go ahead and give it a try:

https://gametaphero.web.app 🌎 🎮 🎉

Your follows the same logic as any other website. You can add favicons, search/status bar color (for Chrome mobile), app name, etc.

Gamepad support?! What?! 🎮

Experimental!!! Don’t try this at home… or maybe you will 🤔

So far I manage to make it work using Android + Switch Pro Controller (I’ll do my best to get the PS4 and Xbox Controller + maybe a generic one).

You’ll need to use channels for this task. Starting with the code:

var channel =  MethodChannel(flutterView, "gamepad")channel.setMethodCallHandler call, result when {
call.method == "isGamepadConnected" -> {
val ids = InputDevice.getDeviceIds()
for (id in ids) {
val device = InputDevice.getDevice(id)
val sources = device.sources

if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) {
result.success(true)
}
}
result.success(false)
}
call.method == "getGamePadName" -> {
val gamepadIds = InputDevice.getDeviceIds()
for (id in gamepadIds) {
val device = InputDevice.getDevice(id)
val sources = device.sources

if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) {
result.success(device.name)
}
}
}
else -> result.notImplemented()
}
}

this could be inside your class. I saw other implementations but that’s the one I’m currently working with. Take in mind that this will increase the size of your app, but just a little bit.

You can also implement the interface to detect the connection/disconnection of gamepads:

var inputManager = getSystemService(Context.INPUT_SERVICE) as InputManageroverride fun onResume() {
super.onResume()
inputManager.registerInputDeviceListener(this, null)
}

override fun onPause() {
super.onPause()
inputManager.unregisterInputDeviceListener(this)
}

override fun onInputDeviceRemoved(deviceId: Int) {
channel.invokeMethod("gamepadRemoved", true)
}

override fun onInputDeviceAdded(deviceId: Int) {
channel.invokeMethod("gamepadName", InputDevice.getDevice(deviceId).name)
}

override fun onInputDeviceChanged(deviceId: Int) {
channel.invokeMethod("gamepadName", InputDevice.getDevice(deviceId).name)
}

every time an event happens, Android will send a channel notification to our

MethodChannel _channel = MethodChannel('gamepad');

Future<bool> isGamePadConnected {
bool isConnected = _channel.invokeMethod('isGamepadConnected');
isConnected;
}

Future<String> gamepadName {
String name = _channel.invokeMethod("getGamePadName");
name;
}

Those will handle the request to get the and . If you want to listen for the Android events, inside your method do the following:

_channel.setMethodCallHandler((call) {
(call.method) {
"gamepadName":
setState(() {
_gamepadName = call.arguments;
});
;
"gamepadRemoved":
setState(() {
_gamepadName = "Undefined";
});
;
}
});

Your could be any of the followings:

Final result?! 🤯🤯🤯


Moving forward — at June 4th

This game is far away from over! I’m Open Sourcing the entire code for everyone to see, use, implement, improve, etc. The next steps will be using to animate the characters, background and any other UI elements.

I build this during my free time ! I can’t wait to see what’s coming 🥳🥳🥳

I’ll be using for the CI/CD to get the apps running over and .

Please, keep coming back! I’ll be posting updates with the , Flare implementations, etc

UPDATE Open Source

This project is now Open Source, take a look at the repos:

Check the Mobile + Desktop repo:
Check the Web repo:

AMAZING LINKS

https://twitter.com/geekmz
https://github.com/mkiisoft
https://highside.io/

https://www.2dimensions.com/
https://codemagic.io/start/
https://flutter.dev/web
https://twitter.com/EsFlutter

Flutter Community

Articles and Stories from the Flutter Community

Thanks to Nash

Mariano Zorrilla

Written by

Tech Lead — Engineer Manager | Flutter | Android @ Venmo

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade