From Zero to a Multiplatform Flutter Game in a week

Mariano Zorrilla
Jun 5 · 12 min read

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

void damage(TapDownDetails details) {
setState(() {
_bossDamage = _bossDamage - 30 <= 0 ? 980 : _bossDamage - 30;
});
}
String hero() {
return tap ? "assets/character/attack.png" : "assets/character/idle.png";
}
static List<Bosses> getBosses() {
var 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"));
return list;
}
static List<PowerUps> getPowerUps() {
var list = List<PowerUps>();
list.add(PowerUps("Master Sword", 2.15, false, 50));
list.add(PowerUps("Lengendary Sword", 2.45, false, 180));
list.add(PowerUps("Keyblade", 3.75, false, 300));
list.add(PowerUps("Lightsaber", 4.95, false, 520));
list.add(PowerUps("Buster Sword", 6.15, false, 1700));
list.add(PowerUps("Soul Edge", 8.65, false, 2400));
return list;
}
Widget hitBox() {
if (tap) {
return Positioned(
top: yAxis,
left: xAxis,
child: Column(
children: <Widget>[
Padding(
padding: const 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,
),
],
),
);
} else {
return Container();
}
}
xAxis = details.globalPosition.dx - 40.0;
yAxis = details.globalPosition.dy - 80.0;
GestureDetector(
onTapDown: (TapDownDetails details) => damage(details),
onTapUp: (TapUpDetails details) => hide(null),
onTapCancel: () => hide(null),
),
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;

return swordElement(bgColor, powerUp, position);
},
)
Widget swordElement(int bgColor, PowerUps powerUp, int position) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 5.0,
),
child: Container(
height: 70,
child: Card(
color: Color(bgColor),
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
powerUp.name,
style: Utils.textStyle(11.0),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0),
child: FancyButton(
size: 20,
child: Row(
children: <Widget>[
Padding(
padding: const 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) : null,
),
)
],
),
),
),
);
}
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');
AudioCache musicCache;
AudioPlayer instance;
void playMusic() async {
musicCache = AudioCache(prefix: "audio/");
instance = await musicCache.loop("bgmusic.mp3");
}

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…. 🥁🥁🥁 WEB!

flutter run -d {device id}
// without the curly braces
export ENABLE_FLUTTER_DESKTOP=true
git clone https://github.com/google/flutter-desktop-embedding.gitcd example
void _setTargetPlatformForDesktop() {
TargetPlatform targetPlatform;
if (Platform.isMacOS) {
targetPlatform = TargetPlatform.iOS;
} else if (Platform.isLinux || Platform.isWindows) {
targetPlatform = TargetPlatform.android;
}
if (targetPlatform != null) {
debugDefaultTargetPlatformOverride = targetPlatform;
}
}
void main() {
_setTargetPlatformForDesktop();
runApp(TapHero());
}
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride;
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
// use this import
import 'package:flutter/services.dart';
git clone git@github.com:flutter/flutter_web.git
cd examples/hello_world
flutter upgrade
flutter packages pub global activate webdevflutter packages upgrade
import 'package:flutter_web/material.dart';
webdev serve

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.

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,
),
),
),
)

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 Share Score button to let everyone know how far you got into the game:

String get timerString {
Duration duration = controller.duration * controller.value;
return '${(duration.inMinutes).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
controller.addStatusListener((status) {
if (status == AnimationStatus.dismissed) {
setState(() {
gameOver = true;
});
}
});

Getting an actual web URL

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

webdev build
firebase deploy

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

Pro Tip 2! Your public/index.html 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 🤔

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()
}
}
}
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)
}
static const MethodChannel _channel = const MethodChannel('gamepad');

static Future<bool> get isGamePadConnected async {
final bool isConnected = await _channel.invokeMethod('isGamepadConnected');
return isConnected;
}

static Future<String> get gamepadName async {
final String name = await _channel.invokeMethod("getGamePadName");
return name;
}
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case "gamepadName":
setState(() {
_gamepadName = call.arguments;
});
break;
case "gamepadRemoved":
setState(() {
_gamepadName = "Undefined";
});
break;
}
});

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 Flare to animate the characters, background and any other UI elements.

UPDATE Open Source

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

AMAZING LINKS

Follow me on Twitter: https://twitter.com/geekmz
Github: https://github.com/mkiisoft
HighSide: https://highside.io/

Flutter Community

Articles and Stories from the Flutter Community

Thanks to Nash.

Mariano Zorrilla

Written by

Head of Mobile Development | Flutter | Android @ HighSide

Flutter Community

Articles and Stories from the Flutter Community