© Frank Zammetti 2019
Frank ZammettiPractical Flutterhttps://doi.org/10.1007/978-1-4842-4972-7_9

9. FlutterHero: A Flutter Game

Frank Zammetti1 
(1)
Pottstown, PA, USA
 

All work and no play makes Jack a dull boy (and a murderer at a snowy resort lodge, as we learned in The Shining).

With luck, you’ll avoid that fate, but the point stands: if you don’t stop to smell the roses every now and again, you tend not to have as good a life as you should. This is true for mobile development and Flutter too! (You didn’t think I would be able to pull this back to relevance, did you?)

Throughout this book, you’ve seen Flutter through the lens of writing actual useful code and applications. But nothing says that’s all you can do with Flutter. No, you can do something more frivolous, something more fun, something like, say, write a game!

Games are excellent projects for any developer to undertake because they touch on so many different disciplines in programming, from graphics and sound to AI, data structures, algorithmic efficiency, and so on. In my position as a lead architect, I’m sometimes asked by developers how they can sharpen their skills. My answer is always the same: write a game! I don’t believe any other project provides the diversity and level of creativity that games do and, therefore, opportunity for learning.

Plus, by their very nature, games are fun to write!

In this chapter, you’ll use Flutter to write a game. The benefit in terms of this book is that you’ll get to see a few new Flutter facilities and see others in ways you maybe haven’t before. In the end, you’ll learn while, hopefully, having fun!

So, let’s kick things off by figuring out what kind of game to make and coming up with what every great game needs: a story!

The Story So Far

The inhabitants of Gorgona 6 are a cosmic contradiction: a technologically advanced civilization that is simultaneously kinda backward intellectually! For example, they visited their own moon before figuring out that they should put wheels on luggage and, more importantly for the purposes here, they can build fast, sleek spaceships, but they are wimpy ships that can’t survive much of anything! Just a bump into a space fish is enough to do them in (and being a peaceful people, the Gorgonians never develop weapons of any sort).

Yes, I said space fish! But I digress.

This situation is problematic for them because their star system has a vermin problem: it’s lousy with spaceborne critters and dangers! They have the gargantuan space fish of the third moon of Valtrax, the naturally occurring sentient machine beings of protoplanet 10101110, space aliens (but who doesn’t have space aliens in their solar system, amiright?), and your basic rogue asteroids tumbling about. These things gum up the works of the shipping lanes and pleasure cruise trails (though how anyone can derive pleasure from a cruise where your piece of garbage ship could be destroyed at any moment by the slightest impact is yet another contradiction embodied by the Gorgonians).

Fortunately, there is a solution to these problems: on the outskirts of the solar system is a massive crystal of unknown origin that emits a special type of energy that kills the space vermin, at least for a little while. The Gorgonians have figured out how to collect this energy, little by little. So, they send out ships that are essentially space tankers (but being Gorgonian ships, they at least look cool!) to collect the energy and return it to the homeworld.

Your job, as one of the brave – some might say hero even – pilots of the “crystal tanker fleet,” is to make your way through the space vermin to extract energy from the crystal and then bring it home. When you collect enough energy, the vermin are destroyed, and you’re a Gorgonian hero!

At least for a little while.

You get some points or something for doing this, of course – let’s call ‘em space credits – with which you can maintain your hallucinogenic Gorgonian lizard-licking habit.

And that, friends, is how you conceive a simple game that you can code up with Flutter. I mean, I’ll say up front that if you’re expecting Apex Legends, Halo, or Red Dead Redemption levels of gameplay, then you’re going to be sorely disappointed. This ain’t gonna be no AAA title, and it’s not a game you’ll want to be repeatedly playing (probably – hey, you could wind up loving it I suppose!). But it’ll be a good learning experience, which of course is the goal here.

So, with the story in place, let’s get to work because those vermin aren’t going to destroy themselves (well, probably – they could be as stupid as the Gorgonians I suppose).

The Basic Layout

So, what does this game look like? Well, if you happened to have ever played an old 8-bit game with, say, a frog that hops across lanes of traffic of various types to get to a goal on the other side, well, this game may or may not be conceptually like that. To be more precise, Figure 9-1 shows what it looks like.
../images/480328_1_En_9_Chapter/480328_1_En_9_Fig1_HTML.jpg
Figure 9-1

Maybe I should have called it Space Frogger instead?

Your ship starts at the bottom of the screen, near the homeworld. You control the ship by placing your finger anywhere on the screen, which becomes the “anchor” point, or zero position, of a virtual joystick. Now, just move your finger in any of the eight compass directions, and your ship moves in that direction. Your goal is to move through the lanes of vermin (asteroids, aliens, sentient machines, and space fish, starting from the bottom). When you reach the top, you touch the crystal, and the energy bar at the top fills. Then, you return through the vermin, touch your homeworld, and the energy is transferred, at which point all the vermin explode, you get some points, and the vermin come back so you can do it all over again. Of course, you explode if you touch anything but the crystal or your homeworld.

As I said, it’s not exactly a complex, top-tier game, but it is a game, and it is made with Flutter, so, mission accomplished (unlike all those Gorgonian ship captains lost in the line of duty – we, the Flutter coders of Earth, salute you!)

Directory Structure and Component Source Files

Let’s begin by talking about the directory structure and, more importantly, some of the files it contains. Figure 9-2 shows you the pertinent details. It’s an entirely standard Flutter application structure as you’ve come to know and, I hope, love. In the assets directory , you’ll find a bunch of images and some audio files. For the images, the names should give away what they each are, but the numbers require some explanation.
../images/480328_1_En_9_Chapter/480328_1_En_9_Fig2_HTML.jpg
Figure 9-2

The application’s directory layout and constituent source/asset files

As you’ll see soon, each of the images represents part of an object in the game which will be used by a common class called, not surprisingly, GameObject . For example, there is a player GameObject for the player’s ship that uses images player-0.png and player-1.png, and a crystal GameObject for the crystal. This common class includes logic for animating these objects. An animation in this context is just like the old trick when you were a kid where you take a notebook; draw a series of “frames,” maybe a stick figure doing jumping jacks; and then flip the pages to produce the animation. Here, each frame of the animation is denoted by the number in the filename. So, for the crystal, there are four frames of animation (four pages in your notebook, so to speak): crystal-0.png, crystal-1.png, crystal-2.png, and crystal-3.png, and the GameObject class knows how to “flip the pages,” if you will.

Note

The planet isn’t animated. Hence there’s only a single frame. But it gets wrapped in a GameObject too. So, as you’ll see later, this requires the filename to use the same numbering scheme, hence planet-0.png so that the GameObject class can still work with it.

The MP3 files are audio for various events, the names again hopefully being self-explanatory, but if not, they will be clear when we see them used.

Beyond that, you’ve got a small handful of source files in lib aside from the required main.dart file , and we’ll get to each of those in turn, though like the assets, the names should give you a good clue what they are.

Before any of that though, let’s talk about the pubspec.yaml a bit.

Configuration: pubspec.yaml

The pubspec.yaml file is probably like 99% the same as all the others you’ve seen, save for one new element:
name: flutter_hero
description: FlutterHero
version: 1.0.0+1
environment:
  sdk: ">=2.1.0 <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  audioplayers: 0.11.0
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true
  assets:
    - assets/

This is a game, and most games have audio, so we should probably have some audio too! At the time of this writing, Flutter, perhaps surprisingly, doesn’t have a good API for audio, at least not in the way you need for a game in terms of just being able to play arbitrary sound files included in the project any time you want (and sometimes simultaneously). So, we’ll need a plugin for that. Fortunately, there are a few choices, but perhaps the most popular is the audioplayers plugin ( https://pub.dartlang.org/packages/audioplayers ). This is a fork of an earlier plugin named audioplayer (yes, the new one just has an added s on the end!) that extends the functionality of the old one. This plugin allows us to play audio files stored remotely on the Internet, locally from the user’s device or, critically for us, as assets in our project. With this plugin, you can play files, control their playback (pause, stop, seek to a specific location in the audio), loop the audio if you like (good for background music), and listen for events during playback so you could, for example, show a progress bar if you wanted to.

For FlutterHero, we won’t need most of that! We’ll just need to be able to play our sounds when specific events occur. We’ll look at the API for this plugin when we hit the first sound usage, but as you’ll see, it’s quite minimal.

Aside from that dependency, the assets directory is referenced in the catch-all way you’ve seen previously, so all of our assets are available, image and audio alike. I considered splitting them into assets/images and assets/audio, but that would require a tad more work in terms of getting audioplayers to be able to find them, and given the relatively small number of assets required, I decided to just dump them all in the single assets directory.

The GameObject Class

Now, we start to get to some code! Usually, I would begin with main.dart, but in this case, I want to talk about that GameObject class I mentioned earlier first. Some of what you’ll see here might not immediately be clear, but it will quickly become so when you see this class and its subclasses used.

Speaking of subclasses, that’s right, GameObject is a parent class to two others, as Figure 9-3 shows.
../images/480328_1_En_9_Chapter/480328_1_En_9_Fig3_HTML.jpg
Figure 9-3

The hierarchy of classes: GameObject and its children

The basic idea here is simple Object-Oriented Programming: the GameObject class has data and functionality common to all objects in the game (which we call game object, as opposed to the GameObject class that is the code implementation of that general concept), and then the subclasses extend that data and functionality as required. So, for example, every game object (player, crystal, vermin, and planet) needs data like
  • The width and height of the screen

  • The base filename of their images (“planet-∗.png” or “crystal-∗.png,” for example, where ∗ will be the frame numbers)

  • The width and height of the object

  • The x and y location of the object

  • The total number of frames in the animation cycle; how many game frames to skip between animation frame cycle changes (I know that’s a little confusing, but don’t worry, it won’t be for long!); what the current frame of the animation cycle is; a counter for determining when it’s time to flip to the next animation frame; a function that will be called when the animation cycle completes; and of course all the animation frame images

  • Whether the object is visible or not

Also, every game object has some common functionality:
  • A constructor for setting it up

  • A method to animate it

  • A method to draw it on the screen

But, the Enemy subclass , which will represent the fish, robots, aliens, and asteroids, has some additional needs:
  • How fast they move across the screen

  • What direction they’re moving (left or right)

The Player class obviously represents the player’s ship, and it too has some specific needs beyond those supplied by GameObject:
  • How fast it moves

  • Whether it’s moving left or right (horizontal movement) and/or up or down (vertical movement)

  • How much of the crystal’s energy is onboard

  • How many degrees (in radians) it’s rotated (allows us to have a single image that can be in any orientation depending on the direction of travel) plus some data tables that save us from doing math repeatedly, to boost performance a little (it’s always good to think about performance in games!)

  • A method to be used any time the ship’s orientation changes (based on the direction of movement) so that it can be rotated appropriately

So, now that you have a high-level idea of what these classes are about, let’s get to the actual code of GameObject:
class GameObject {
  double screenWidth = 0.0;
  double screenHeight = 0.0;
It starts off as an ordinary class, not extending from anything else, and we have the first two data properties, namely, the width and height of the screen. As you’ll see later, Flutter provides an API to get this information, and it is retrieved during application startup and is handed to any instance of GameObject during instantiation. This just avoids having to call a potentially expensive API over and over again, but this information is needed by the game objects in various ways, so doing it in one place and giving it to the instances works well.
String baseFilename = "";
The baseFilename is the portion of the filename of the images for this game object that doesn’t change. Put more simply, it’s the name of the object, be it fish, player, planet, or whatever else.
int width = 0;
int height = 0;
The width and height of the object are naturally needed too. I’m sure it would have been possible to find a Flutter API to get this information from the loaded images, but it’s just simpler to provide it in the code when it’s not something that’s going to change ever.
double x = 0.0;
double y = 0.0;
The horizontal (x) and vertical (y) location of the game object on the screen is clearly something every game object will need, so that’s here too.
int numFrames = 0;
int frameSkip = 0;
int currentFrame = 0;
int frameCount = 0;
List frames = [ ];
Function animationCallback;
These six properties are all related to animation. The numFrames property is how many total frames there are. The frameSkip property determines how many ticks of the main game loop must elapse before the next animation frame is shown (we’ll talk about the main game loop later, but as a brief preview, it’s something that will happen 60 times a second, executing our game logic, including animating each game object, at the same interval). The currentFrame property is simply which animation frame is currently showing. The frameCount property gets incremented with every tick of the main game loop, and when it hits the value of frameSkip, the value of currentFrame is incremented. The frames property is a list of the Flutter image assets for the game object, one per animation frame. Finally, the animationCallback property is an (optional) reference to a function that will be called any time the animation cycle end. You’ll see why that’s needed, along with all of these, very soon, but for now, let’s press on.
bool visible = true;

The vermin and player need to be hideable at certain times, and the visible property determines when a game object is visible or not.

With the properties out of the way, we next come to the constructor:
GameObject(double inScreenWidth, double inScreenHeight,
  String inBaseFilename, int inWidth, int inHeight,
  int inNumFrames, int inFrameSkip,
  Function inAnimationCallback) {
  screenWidth = inScreenWidth;
  screenHeight = inScreenHeight;
  baseFilename = inBaseFilename;
  width = inWidth;
  height = inHeight;
  numFrames = inNumFrames;
  frameSkip = inFrameSkip;
  animationCallback = inAnimationCallback;
  for (int i = 0; i < inNumFrames; i++) {
    frames.add(Image.asset("assets/$baseFilename-$i.png"));
  }
}

Pretty straightforward, right? All the incoming arguments get stored in the appropriate properties, and then we need to load the animation frames. Here, you can see how each is an Image widget, loaded with its asset() constructor and using the baseFilename to construct the filename for each frame to load. This again is primarily done for performance: only loading the frames once is a good idea. Flutter may be smart enough to cache them if we were to load the same image twice, but it’s much better not to assume that and instead architect our app to ensure it – it also makes the animation code easier to write in my opinion.

Speaking of animation code:
void animate() {
  frameCount = frameCount + 1;
  if (frameCount > frameSkip) {
    frameCount = 0;
    currentFrame = currentFrame + 1;
    if (currentFrame == numFrames) {
      currentFrame = 0;
      if (animationCallback != null) { animationCallback(); }
    }
  }
}

The logic is simple: every time this is called – which you’ll see later is once per main game loop tick, which means 60 times a second – the frameCount is bumped up. When that value reaches the frameSkip value, then we increment to the next frame. When we reach the end of the frames, we reset it back to the first frame and, if one is supplied, call the animationCallback.

In addition to animating, a GameObject needs to know how to draw itself. As everything is a widget in Flutter, as you well know by now, the goal here is to get the proper widget for inclusion in a widget tree returned by some build() method somewhere (I’m being vague because we obviously haven’t gotten to that yet, but we will!). The draw() method accomplishes this:
Widget draw() {
  return visible ?
  Positioned(left : x, top : y, child : frames[currentFrame])
    : Positioned(child : Container());
}

Not to jump the gun, but we’ll be using a Stack widget as the parent to all our game objects. That’s because within a Stack, you can have Positioned widgets which can be positioned absolutely inside the Stack. If the Stack covers the entire screen then, effectively, we’ve got ourselves a canvas perfectly suitable for game development because we can control the precise location of everything on the screen down to the pixel level. That’s exactly what we’re going to do, so draw() needs to return a Positioned widget that wraps the Image widget associated with the current animation frame of the object. In addition, an object can be hidden. How you do this is something you may find a little weird about Flutter. To hide a widget, regardless of what it is, you simply don’t include it in the widget true! There’s no hidden:true or something like that on widgets as seen in many other frameworks, no hide() method to call. However, as you’ll see later, returning null from this method, as would probably be your first thought, wouldn’t work because it would break the widget tree it’ll eventually be a part of. So instead, an empty Container is returned when this game object isn’t visible. That accomplishes the goal, even if in a somewhat weird way (I know it seemed a little weird to me at first at least!)

Extending from GameObject: The Enemy Class

With the basic GameObject coded, we can now look at the two subclasses, beginning with Enemy . The primary thing that makes an Enemy different from a plain GameObject is that an Enemy can move.
class Enemy extends GameObject {
  int speed = 0;
  int moveDirection = 0;

The movement of an enemy is simple though: it just moves either left or right and at a given speed (where speed here means how many pixels it moves per main game loop tick). So, that’s what the speed and moveDirection properties denote. The value of moveDirection will be 0 for left or 1 for right.

Then, we have a constructor:
Enemy(double inScreenWidth, double inScreenHeight,
  String inBaseFilename, int inWidth, int inHeight,
  int inNumFrames, int inFrameSkip, int inMoveDirection,
  int inSpeed) :
    super(inScreenWidth, inScreenHeight, inBaseFilename,
    inWidth, inHeight, inNumFrames, inFrameSkip, null) {
  speed = inSpeed;
  moveDirection = inMoveDirection;
}

Now, since this class extends GameObject, it means that it supports all the same properties as GameObject, so those need to be set too. That’s where the super() call comes into play. As you can see, the signature of the Enemy constructor includes everything the GameObject constructor does plus the items specific to Enemy, so first the super() call sets the properties common to GameObject, then the code inside the Enemy constructor sets the additional properties specified to Enemy.

And with all that data set, we can implement the move() method:
void move() {
  if (moveDirection == 1) {
    x = x + speed;
    if (x > screenWidth + width) { x = -width.toDouble(); }
  } else {
    x = x - speed;
    if (x < -width) { x = screenWidth + width.toDouble(); }
  }
}

You can now see why the width and height of the screen are needed: that’s how we know when a given enemy has moved off the screen when it’s moving in either direction. Then, it helps us set its new location. So, to go through this conceptually: a given enemy fish is moving right across the screen (the first if branch). When it’s beyond the right edge of the screen (the second if branch), its x location is set to a negative value, which puts it on the left of the screen. Then it continues moving as before and does this again. For left movement (the else branch), the same is done, but in reverse. That’s all there is to the enemy movements, very simple.

Extending from GameObject: The Player Class

The other class that extends from GameObject is for the player:
class Player extends GameObject {
  int speed = 0;
  int moveHorizontal = 0;
  int moveVertical = 0;
Like the vermin, the player obviously can move, so we need to know how fast it can move (speed) and in what direction it’s moving (moveHorizontal and moveVertical). Unlike the enemy vermin, the player can move up, down, left, and right, plus the four combinations of each. Hence, we need two variables to track which way it’s moving instead of just one like for the enemies. But the player can also be not moving, So, each of these has three possible values instead of only two for the enemies: 0 for either means no movement in that direction while for moveHorizontal –1 means left and 1 means right while for moveVertical –1 means up and 1 means down.
double energy = 0.0;
The player can also, at any moment in time, have some of the crystal’s energy onboard. So, we need a variable to track that too.
Map anglesToRadiansConversionTable = {
  "angle45" : 0.7853981633974483,
  "angle90" : 1.5707963267948966,
  "angle135" : 2.3387411976724017,
  "angle180" : 3.141592653589793,
  "angle225" : 3.9269908169872414,
  "angle270" : 4.71238898038469,
  "angle315" : 5.497787143782138
};
double radians = 0.0;

One of the things that makes game development somewhat unique is that you are almost always looking for little tricks to optimize things, to save some memory or cycles here or there. In this case, there are two tricks to be played with the player’s ship. First, the ship should always be pointed in the direction of movement (or remain in whatever position it was going when it last stopped). So, that would mean that we need eight different images: one each for when moving up, down, left, right, up/left, up/right, down/left, and down/right. But, since the ship is animated, and assuming all use two frames, that would mean we need 16 different images! That seems inefficient. So instead, as you saw earlier when we looked at the assets, there’s only two, one for each frame. In order to provide the eight different orientations, those two images will be rotated in real time to the correct orientation. Flutter provides several means to rotate an image, but we’ll get to how to actually rotate the image shortly. Before that, the second trick comes in, and that’s to avoid some calculations. As you’ll see, to rotate the ship, we’ll need to tell Flutter how much to rotate it, and that’s provided in radians. But, from our perspective, we really want to rotate it some number of degrees. So, we could, of course, do a degrees-to-radians calculation every time we need to rotate, but avoiding that calculation saves us some cycles, so that’s what we’ll do! The simplest approach is to just precalculate the radians for each angular degree measure we want to rotate by and store those values in a map for easy lookup, and that’s precisely what the anglesToRadiansConversionTable property is for. The actual number of radians rotated is something we’ll need to keep track of too, and that’s where the radians property comes in. You’ll see both in use very soon.

The constructor comes next:
Player(double inScreenWidth, double nScreenHeight,
  String inBaseFilename, int inWidth, int inHeight,
  int inNumFrames, int inFrameSkip, int inSpeed,
) : super(inScreenWidth, inScreenHeight, inBaseFilename,
  inWidth, inHeight, inNumFrames, inFrameSkip, null) {
  speed = inSpeed;
}

Since Player extends from GameObject, we need to call the GameObject constructor first, and then set the speed, which is the only value specific to the Player that needs to be set during construction. Note the null as the last argument to the GameObject constructor – that’s the animation callback, which isn’t needed for the player, hence passing null.

Now, GameObject provides a draw() method , but for the player, the act of drawing itself is a little different, so we need to override that method:
@override
Widget draw() {
  return visible ?
  Positioned(left : x, top : y, child : Transform.rotate(
    angle : radians, child : frames[currentFrame]))
  : Positioned(child : Container());
}
The difference here, of course, is the rotation discussed earlier. For that, we wrap the Image widget in a Transform widget , which is one Flutter provides to apply a transformation to a child before it’s painted. While using Transform itself requires you to provide a transformation matrix, which can be complicated and math-intense depending on what you’re trying to achieve, this class helpfully provides a handful of constructors for the most common transformations. These include Transform.scale() for scaling the child, or making it bigger or smaller in other words; Transform.translate() for translating the child, or shifting it in other words; and most importantly for us now, Transform.rotate() to rotate the child around its axis. As you can see, this constructor requires the angle of rotation in radians, so here you can see that radians property being used. How that value gets set is done in the orientationChanged() method , which you’ll learn later is called from the code that handles user input:
void orientationChanged() {
  radians = 0.0;
  if (moveHorizontal == 1 && moveVertical == -1) {
    radians = anglesToRadiansConversionTable["angle45"];
  } else if (moveHorizontal == 1 && moveVertical == 0) {
    radians = anglesToRadiansConversionTable["angle90"];
  } else if (moveHorizontal == 1 && moveVertical == 1) {
    radians = anglesToRadiansConversionTable["angle135"];
  } else if (moveHorizontal == 0 && moveVertical == 1) {
    radians = anglesToRadiansConversionTable["angle180"];
  } else if (moveHorizontal == -1 && moveVertical == 1) {
    radians = anglesToRadiansConversionTable["angle225"];
  } else if (moveHorizontal == -1 && moveVertical == 0) {
    radians = anglesToRadiansConversionTable["angle270"];
  } else if (moveHorizontal == -1 && moveVertical == -1) {
    radians = anglesToRadiansConversionTable["angle315"];
  }
}

A check is performed for each of the four cardinal directions, plus the four combinations, to determine which way the player is moving. Once that’s determined, a lookup into anglesToRadiansConversionTable is done and the resultant radians stored in the radians property. It’s not fancy code, but it gets the job done nicely and, again, all while avoiding a potentially costly mathematical operation here.

Tip

It wouldn’t be all that costly in practice, but again, in games, it’s always better to be thinking about optimizations as you code. This is true generally in all kinds of programming, but more so in games where every cycle factors into the main game loop, as we’ll discuss soon. Of course, you have to avoid taking this exercise too far and stretch into premature optimization territory, which is something you should always try to avoid – but a precalculated lookup table like this is quite common.

The final method is for moving the player:
void move() {
  if (x > 0 && moveHorizontal == -1) { x = x - speed; }
  if (x < (screenWidth - width) && moveHorizontal == 1) {
    x = x + speed;
  }
  if (y > 40 && moveVertical == -1) { y = y - speed; }
  if (y < (screenHeight - height - 10) && moveVertical == 1) {
    y = y + speed;
  }
}

This will be called once per main game loop tick. We do horizontal movement direction, and then vertical movement, separately. Recall that there are eight possible directions the player can be moving. The four cardinal directions are handled obviously, but the other four that represents the combinations are also handled by virtue of horizontal and vertical movement being handled separately. Since it’s possible for one of the if statements dealing with x to fire while one of those dealing with y also fires, that yields a combination of vertical and horizontal movement. Of course, we must ensure that the player doesn’t go off the screen too, which is what the bounds checks in each of the if statements do for us. These consider the side of the player as well as the space for the score and energy bar as well.

Where It All Starts: main.dart

As always, our app starts off in the main.dart source file:
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "InputController.dart" as InputController;
import "GameCore.dart";
void main() => runApp(FlutterHero());
class FlutterHero extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setEnabledSystemUIOverlays(
      [SystemUiOverlay.bottom]
    );
    return MaterialApp(
      title : "FlutterHero", home : GameScreen()
    );
  }
}
class GameScreen extends StatefulWidget {
  @override
  GameScreenState createState() => new GameScreenState();
}

The services.dart module is something new. This module provides us some device service access for things like interacting with the clipboard, generating haptic feedback (device shaking), playing system sounds, and selecting text, just to name a few. Something else it lets us do is control the “chrome” that is visible around our app, that is, the system status bar at the top and, on Android at least, the row of soft input buttons at the bottom. If you jump down to the top-level FlutterHero class here, in its build() method, you’ll see a call to SystemChrome.setEnabledSystemUIOverlays(). This is provided by the services module, and this method allows us to pass it an array of chrome identifiers to enable. Here, I specifically enable the SystemUiOverlay.bottom element , which are the soft navigation buttons in Android. Since that’s all that’s in the array, the status bar at the top will be hidden, providing our game a (nearly) full-screen experience.

Of course, before that, you’ll notice that we’re building a stateful widget in GameScreen, which is the home screen defined in the MaterialApp of the FlutterHero class, a pattern you’ve seen before, and the necessary main() method is defined above that.

As you should expect, given that GameScreen is a stateful widget, there will be an associated State class, and there is: GameScreenState :
class GameScreenState extends State with
  TickerProviderStateMixin {
  @override
  Widget build(BuildContext inContext) {
    if (gameLoopController == null) {
      firstTimeInitialization(inContext, this);
    }

For this app, we’re not going to use scoped_model and instead just use the basic state facilities that Flutter provides. But, in addition to the State stuff, this class has something new: the TickerProviderStateMixin . We’ll get to what that’s all about in the next section, but just as a preview I’ll tell you that it has to do with the main game loop that will tick by 60 times a second throughout the lifetime of the game.

The build() method begins with a check of the gameLoopController, which you’re going to see is part of the GameCore.dart file. Ignoring for the moment what that is, if it’s null, then a call to firstTimeInitialization() is made, passing it the BuildContext and also a reference to the GameScreenState class itself using this. That function too will be examined in the next section, but I’m sure you can guess from the name that it performs some tasks that happen the first time the build() method executes (remember that build() will fire many times throughout this game). The issue here is that there are some tasks that must be done to set up the game that can only occur when we have that BuildContext. So, those tasks must be done in the build() method. But, they must only happen once, hence why that check of gameLoopController is done.

But like I said, we’ll get to all of that in the next section!

Continuing in the build() method , we begin to build our widget tree:
List<Widget> stackChildren = [
  Positioned(left : 0, top : 0,
    child : Container(width : screenWidth,
      height : screenHeight,
      decoration : BoxDecoration(image : DecorationImage(
        image : AssetImage("assets/background.png"),
        fit : BoxFit.cover
      ))
    )
  ),
Interestingly, in all previous build() methods you’ve seen, you almost immediately have seen a return statement to return a widget, and all the child widgets were defined “inline” with that widget. Here though, we’re constructing a list first. The situation here is that there is some logic that has to occur while building the widget tree, some loops and such, none of which could be done in a single monolithic inline widget tree like we normally do. Because the widget we ultimately want to return is a Stack (well, a Stack inside a GestureDetector inside a Scaffold, if you want to be pedantic) and because a Stack takes a list as its children, we can do all of the logic and looping outside the widget definition, build the list separately, and then just reference it in the final Stack definition. That’s what’s happening here. The first element in the list is a Positioned wrapping a Container that uses a BoxDecoration to show the background image. The fit specified as BoxFit.cover ensures that the background fills the screen regardless of its physical dimensions. The width and height of the Container are the values of the screenWidth and screenHeight variables. As you’ll find out in the next section (or, well, now I guess, as it happens!) those values are queried during first-time initialization and retrieved from the operating system so that anywhere that information is needed, it will be available without having to do that query many times.
Positioned(left : 4, top : 2,
  child : Text('Score: ${score.toString().padLeft(4, "0")}',
    style : TextStyle(color : Colors.white,
      fontSize : 18, fontWeight : FontWeight.w900)
  )
),

After the background is another Positioned wrapping a Text this time. That’s our score display. As you can see, this widget is placed at x/y location 4/2 as per the left and top properties. That’s the whole point of using a Stack: we can absolutely position these elements as we see fit. The Stack will by default fill its parent, which happens to be the screen, so we have absolute positioning capability across the entire screen. Handy for a game, don’t you think?!

After that comes another Positioned, this time with a LinearProgressIndicator inside for the energy bar:
Positioned(left : 120, top : 2, width : screenWidth - 124, height : 22,
  child : LinearProgressIndicator(
    value : player.energy, backgroundColor : Colors.white,
    valueColor : AlwaysStoppedAnimation(Colors.red)
  )
),
crystal.draw()
];

The valueColor property is important here because by default, a progress indicator in Flutter, whether linear or circular, wants to animate. It will spin if it’s circular, or fill up if it’s linear. But, that’s not what we want. We want a bar that fills up little by little when the ship is in contact with the crystal and empties little by little when in contact with the planet to indicate energy filling or draining to and from the ship, all under the control of our code, not what Flutter wants to do with it (waah, this is my game, do it my way, Flutter!). So, rather than specifying just a single color to indicate what color to make the filled portion of the indicator, we instead use an instance of the AlwaysStoppedAnimation widget . This is a special widget that the progress indicator classes know how to deal with that provides an indicator that isn’t constantly animating, precisely as we need! Of course, what color the filled portion should be is still important information, so it’s passed to the AlwaysStoppedAnimation constructor. Note too that the width of the Positioned that the LinearProgressIndicator is in is set dynamically using the width of the screen minus the space the score takes up. This way, the energy bar fills whatever space is left after the score.

The crystal is also added here, which is the last element in the list that’s added during its declaration (inline, if you will).

After that, we have to add our enemy vermin:
for (int i = 0; i < 3; i++) {
  stackChildren.add(fish[i].draw());
  stackChildren.add(robots[i].draw());
  stackChildren.add(aliens[i].draw());
  stackChildren.add(asteroids[i].draw());
}

There’s three of each, so it’s a simple loop – but this loop is the primary reason we needed to build this list in the first place! Trying to do it all inline would have meant having to write out 12 enemy references here since we couldn’t use a loop construct.

Next, the planet and player are added:
stackChildren.add(planet.draw());
stackChildren.add(player.draw());

It’s important to realize that with a Stack, there is a z-axis at play. That means that elements added later in the list will appear on top of those added before. So, we have to ensure that, to the extent it matters in this game, we add things in the proper order. So, the player must be added after the planet, for example, so that the ship isn’t occluded by the planet when the player flies near it. The ship should remain visible, on top of the planet. Hence it needs to be higher in the z-order and so has to be added after the planet.

Now, although they won’t be visible except at specific times, we need to add any explosions needed to the list next:
for (int i = 0; i < explosions.length; i++) {
  stackChildren.add(explosions[i].draw());
}

Recall that if any given game object isn’t currently visible, its draw() method will return an empty Container. Explosions are when that’s most obvious because while most of the game objects are nearly always visible, explosions are obviously transient in nature. So, this loop may be drawing a bunch of empty Container widgets most of the time, but that’s fine, that’s just the way visibility is controlled in Flutter.

Finally, the widget to return is constructed:
return Scaffold(body : GestureDetector(
  onPanStart : InputController.onPanStart,
  onPanUpdate : InputController.onPanUpdate,
  onPanEnd : InputController.onPanEnd,
  child : Stack(children : stackChildren)
));

The body of the Scaffold returned isn’t the Stack directly, as you might have anticipated. We’re going to need to work some method for the player to control his ship in, and given that most modern mobile devices are touchscreen-oriented, it makes sense that touch will be our control mechanism. So, we need a widget to recognize touch events, and that’s exactly what the GestureDetector widget is for.

This widget recognizes all sorts of various gestures, taps and such, and one such gesture is a pan. This is simply the user putting their finger down and then moving it around. If you were developing a web site where users use a mouse most of the time, then you would recognize events like mouseDown, mouseMove, and MouseUp. But, we don’t have those here, even though conceptually those are what we need. But, three pan events conceptually mimic those (taking the player’s finger to be basically what the mouse pointer is): onPanStart for mouseDown, onPanUpdate for mouseMove, and onPanEnd for mouseUp. The code that handles those events represents the functionality that the InputController class encapsulates, but we’ll get to that later. Before that though, you can see that the Stack is the child of the GestureDetector, which means that gestures anywhere on the screen will be handled because recall that the Stack takes up the whole screen (it fills its parent by default, as does its parent GestureDetector). Finally, as previously mentioned, the children of the Stack references the list that was built up before here.

Remember that everything we just talked about is inside the build() method of the top-level widget, and remember that we’re dealing with a stateful widget. That means that any time state changes, build() will be called again and the screen re-rendered. That’s the key to making this all work as a game. Next, we need to talk about that main game loop I’ve mentioned several times, as well as the core logic that makes up the game, all of which ties into this build() method because ultimately, all of this logic mutates state and triggers this build() method being executed over and over again to move all our game objects.

The Main Game Loop and Core Game Logic

The core logic of the game is contained within the GameCore.dart file, and it begins, as always, with import:

Kicking It Off

import "dart:math";
import "package:flutter/material.dart";
import "package:audioplayers/audio_cache.dart";
import "InputController.dart" as InputController;
import "GameObject.dart";
import "Enemy.dart";
import "Player.dart";

The math package is necessary because we’re going to need to generate some random numbers, and that functionality is included there. The audio_cache.dart module is part of the audioplayers plugin and is the interface we’ll use to load and play the sound assets, as you’ll see. The others are the various source files for FlutterHero as needed.

Then, we have a whole bunch of variables:
  • State state – This is a reference to the State class.

  • Random random = new Random() – The Random class allows us to… you guessed it… generate random numbers! I instantiate it once here because while we’ll need it multiple times, there’s no sense having more than one instance.

  • int score = 0 – The current score of the game.

  • double screenWidth – The width of the screen.

  • double screenHeight – The height of the screen.

  • AnimationController gameLoopController – We’ll talk about this in a moment!

  • Animation gameLoopAnimation – This goes along with gameLoopController.

  • GameObject crystal – The one and only crystal game object.

  • List fish – A list of fish enemy vermin game objects.

  • List robots – A list of robots enemy vermin game objects.

  • List aliens – A list of aliens enemy vermin game objects.

  • List asteroids – A list of asteroids enemy vermin game objects.

  • Player player – The one and only player game object.

  • GameObject planet – The one and only planet game object.

  • List explosions = [ ] – A list of explosions (which are GameObject) instances (this is empty when there are no explosions currently on screen).

  • AudioCache audioCache – A cache of audio assets that can be played (more on this later).

With the variables out of the way, we can now get to the code that uses them.

First Time Initialization

The first bit of code is the firstTimeInitialization() function that you saw called from the build() method of the main widget, remember? It’s the call that was made if and only if the gameLoopController variable was null. Well, here it is finally:
void firstTimeInitialization(BuildContext inContext,
  dynamic inState) {
  state = inState;

The code in this module will need some access to the GameScreenState object since it contains the state for the main widget, so a reference to it is passed in and that reference stored in the state variable.

Next, it’s time to deal with audio:
audioCache = new AudioCache();
audioCache.loadAll([ "delivery.mp3", "explosion.mp3",
  "fill.mp3", "thrust.mp3" ]);

The audioplayers plugin has a couple of different ways to deal with audio, one of which is the AudioCache class . This is used to preload audio and play it efficiently, something that’s important in a game. This is also, a little oddly in my mind, necessary to be able to play sounds that are assets in our app. So, weird or not, we instantiate the class and then call its loadAll() method , passing it a list of audio filenames to load, after which we’re ready to play those sounds any time we want, as you’ll see later.

We then need to get the dimensions of the screen:
screenWidth = MediaQuery.of(inContext).size.width;
screenHeight = MediaQuery.of(inContext).size.height;

The MediaQuery class is provided by the material.dart library and which allows us to retrieve information about a given piece of media, the screen for example. Calling its of() method for the incoming BuildContext gets us a MediaQueryData object about the given context, which we can then drill down into to get the screen width and height.

Next, it’s time to create some game object!
crystal = GameObject(screenWidth, screenHeight, "crystal",
  32, 30, 4, 6, null);
planet = GameObject(screenWidth, screenHeight, "planet",
  64, 64, 1, 0, null);
player = Player(screenWidth, screenHeight, "player",
  40, 34, 2, 6, 2);
fish = [
  Enemy(screenWidth,screenHeight, "fish", 48, 48, 2, 6, 1, 4),
  Enemy(screenWidth,screenHeight, "fish", 48, 48, 2, 6, 1, 4),
  Enemy(screenWidth,screenHeight, "fish", 48, 48, 2, 6, 1, 4)
];

The crystal and planet are plain old GameObject instances, while the player and the enemy vermin are Player and Enemy instances correspondingly. The robots, aliens, and asteroids are created the same way the fish are, so no point in showing those I figured. Note that they had to be created here because we need the screenWidth and screenHeight to have been queried already, hence why this couldn’t have been done as part of the declaration of these variables, or even in a constructor, both of which would seem like natural choices otherwise.

Flutter Animation in Brief

Flutter provides rich animation support in various ways, but it ultimately comes down to a few key classes, even if you don’t use them explicitly (in the case, for example, of widgets that do their own animation – they’re using these classes under the hood). You firstly need a Ticker object, then you need an Animation object, and finally, you need an AnimationController object.

A Ticker object is one that sends a signal at a regular interval that is typically 60 times a second. Every time this object ticks, some callback functions are executed to perform animation-related things.

The Animation object is concerned with generating a number on each tick. This number is part of a sequence between two defined values over a defined period of time and can be generated in a simple linear fashion or via complex curves.

The AnimationController is an object that controls an animation. It can start, stop, and pause an animation. It can also reverse the animation (and remember here that “animation” means nothing but the generation of the next value in the sequence – none of this thus far has any knowledge of what’s on the screen).

An AnimationController gets bound to a Ticker, which most typically is bound to a State object in your app. So, every time the Ticker ticks, the AnimationController is sent a signal. It then sends a signal to the Animation object which then generates a new value. Then, your code hooks into lifecycle events on the Animation and does whatever is necessary to draw the animated elements on the screen. It’s ultimately your code (or the code in a Flutter widget you’re using) that is responsible for actually putting object on the screen and moving them (or otherwise altering them because remember that animation is a generic concept here and doesn’t have to mean movement – we might be animating the size of an object, for example).

So, imagine you have a Ticker ticking off 60 times a second. Also, imagine that an Animation is spitting out a linear set of numbers between 0 and 500 under the control of an AnimationController. Finally, imagine that you hook into the lifecycle of the Animation so that every time a number is generated, you update the X location of one of our enemies on the screen. This will, of course, trigger the build() method to fire again, thus updating the screen. Suddenly, you’ve got a moving object on the screen. In other words, you’ve got animation!

That’s the core concept, so now let’s look at the actual code that puts this theory into practice:
gameLoopController = AnimationController(vsync : inState, duration : Duration(milliseconds : 1000));
gameLoopAnimation = Tween(begin : 0, end : 17).animate(
  CurvedAnimation(parent : gameLoopController,
    curve : Curves.linear)
);
gameLoopAnimation.addStatusListener((inStatus) {
  if (inStatus == AnimationStatus.completed) {
    gameLoopController.reset();
    gameLoopController.forward();
  }
});
gameLoopAnimation.addListener(gameLoop);

First, an AnimationController is instantiated. The sync property associates a Ticker with it, and in this case, it’s our GameScreenState object. If you look back on that code, you’ll see that it extends that class with the TickerProviderStateMixin. That turns it into a Ticker! We also tell the AnimationController how long we want the values animated for, and it’s one second in this case (1,000 milliseconds).

Next, we have to create an Animation object and associate it with the AnimationController . There are a few subclasses we could choose from, and here I’ve used perhaps the simplest: Tween. This allows us to define a sequence from a begin to an end value, which is 0 to 17 here.

Why those values? Well, the goal here is to create what’s called a main game loop. That’s a fancy way of saying we want some function, our main game loop function, to execute once per frame (drawing frame that is). But, how long does each execution take? Well, what we do here is divide the total time by the number of ticks. That means 1,000 divided by 60. That comes out to 16.666667. Round that up to 17 and that’s the range. To put this all succinctly: we want the main game loop function to execute once every 17 milliseconds, which means it will execute 60 times per second (roughly). That’s what this Animation does: it spits out a number between 0 and 17 (linearly, because of the curve property being set to Curves.linear, a standard curve Flutter provides) over the course of 1 second, once every 17 milliseconds.

Now, with all that doing what we want, we have to hook into the lifecycle to do our work. That comes in two places. First, you should recognize that after one second, that animation would be complete. The sequence of values would be exhausted. Obviously, we need it to happen again, and again, and again. So, we set a listener function any time the status of the Animation changes. This function will be called in a couple of different situations, when the animation starts and finishes being two of them. We only care about when it finishes, so we look at the status passed in, and when it’s completed, then we call the reset() and forward() methods on the AnimationController. This does exactly as the name implies: resets all the values to their starting points and starts the Animation sequence again in the forward direction (counting from 0 to 17 again over the course of one second).

We then need to be informed every time a number is generated so that we can call the main game loop function. The addListener() method on the Animation instance does precisely that.

With the main game loop hooked up and ready to go, we just need to reset all game state variables:
resetGame(true);
We’re going to look at this next, so let’s skip discussing it for now. After that is:
InputController.init(player);
The InputController object is responsible for handling user input, but that too is something we’ll look at later, so for now, we’ll skip it too and find that there’s only one more line after that in this function:
gameLoopController.forward();

That effectively starts the game loop, which means our game is now running. Woo-hoo! Things are moving on the screen!

Resetting Game State

When things start up, and after the player explodes or the energy is delivered to the planet, the game needs to be reset. For that, we have the resetGame() function :
void resetGame(bool inResetEnemies) {
  player.energy = 0.0;
  player.x = (screenWidth / 2) - (player.width / 2);
  player.y = screenHeight - player.height - 24;
  player.moveHorizontal = 0;
  player.moveVertical = 0;
  player.orientationChanged();
First, we clear the energy from the ship and reposition the ship to the center of the screen and a few pixels away from the bottom of the screen. Then, we have to make sure the player isn’t currently moving, and also reset its orientation so that it’s facing up via the call to orientationChanged().
crystal.y = 34.0;
randomlyPositionObject(crystal);

After the player, the crystal gets reset. Note that after the first call to this function, there’s no point in setting the y property since it doesn’t change, but there’s no harm either, so it’s done to avoid any logic. The x property is set by the randomlyPositionObject() function, which we’ll look at later, but the name tells you exactly what it does!

The planet is done next in basically the same way:
planet.y = screenHeight - planet.height - 10;
randomlyPositionObject(planet);

The y property needs to consider the height of the planet though so that it doesn’t hang off the bottom of the screen (10 pixels is just an arbitrary value, but it’s one I chose so that the starting position of the ship looks roughly centered on the vertical axis of the planet).

Next comes the enemies (maybe):
if (inResetEnemies) {
  List xValsFish = [ 70.0, 192.0, 312.0 ];
  List xValsRobots = [ 64.0, 192.0, 320.0 ];
  List xValsAliens = [ 44.0, 192.0, 340.0 ];
  List xValsAsteroids = [ 24.0, 192.0, 360.0 ];
  for (int i = 0; i < 3; i++ ) {
    fish[i].x = xValsFish[i];
    robots[i].x = xValsRobots[i];
    aliens[i].x = xValsAliens[i];
    asteroids[i].x = xValsAsteroids[i];
    fish[i].y = 110.0;
    robots[i].y = fish[i].y + 120;
    aliens[i].y = robots[i].y + 130;
    asteroids[i].y = aliens[i].y + 140;
    fish[i].visible = true;
    robots[i].visible = true;
    aliens[i].visible = true;
    asteroids[i].visible = true;
  }
}

When resetGame() is initially called from firstTimeInitialization(), true is passed to it. This causes the preceding block to execute. When the player explodes, false is passed to skip this setup since there’s no point in resetting their positions (and when the energy is delivered to the planet, true is again passed).

The reset logic here is simple: we have four lists, one for each type of enemy, that contains the x location values for each enemy. Rather than calculate these dynamically, I felt it was simpler to “magic number” them. Importantly, this made it easy to introduce some variation without a lot of code: the spacing is mixed up a bit across the enemies to avoid any repeating gaps that the player can too easily get through, improving the challenge of the game. For the y location values, I build from the previous row of enemies such that the rows get a little closer together as you get closer to the top. This again is done to make it just a little harder the further up the screen you move. We also need to ensure that all the enemies are visible at this point because after they all explode when energy is delivered to the planet, they will be hidden as the explosions occur (you’ll see this later), so they have to be shown again, so the game resets appropriately.

Only two small tasks remain:
explosions = [ ];
player.visible = true;

You’ll see how explosions are dealt with later, but at this point, it’s enough to know that the list of them, if any, needs to be cleared, and there’s no easier way than setting explosions to an empty list. Finally, the player is made visible again so that if they just exploded, they’re back to life and ready to try again.

The Main Game Loop

Now, finally, we come to the main game loop function, the function that is called 60 times a second, every 17 milliseconds, as a result of the animation setup code you explored earlier:
void gameLoop() {
  crystal.animate();

The first thing done is to request the crystal to animate itself. As you know from looking at the GameObject code, this just means cycling through the animation frames which, for the crystal, just makes it cycle through some colors.

Next, we have to animate and move the vermin:
for (int i = 0; i < 3; i++) {
  fish[i].move();
  fish[i].animate();
  robots[i].move();
  robots[i].animate();
  aliens[i].move();
  aliens[i].animate();
  asteroids[i].move();
  asteroids[i].animate();
}

Each vermin, three of each type, get a chance to animate and then move. Keeping the logic for these functions in the GameObject and Enemy classes should make sense to you now: it keeps us from having to write a lot of ultimately repetitive code to do it all here.

Then, it’s time for the player to move and animate:
player.move();
player.animate();

Note that for the most part, the exact order of all these calls doesn’t really matter much. If we called player.animate() before player.move() that would be fine, and if we did the player before the vermin, it too wouldn’t make much of a difference.

Now, we get to some good old-fashioned demolitions:
for (int i = 0; i < explosions.length; i++) {
  explosions[i].animate();
}

There might be no explosions on the screen at this time, or there might be one (if the player hit an enemy), or there may be twelve (if they just delivered the energy to the planet and now all the vermin are exploding). So, it’s a simple loop where each iteration gives any explosions in the explosions list a chance to animate.

So far, this has been very straightforward and just producing updates to the location and appearance of our various game objects. But, that’s not all there is to it of course: we also must have some logic to make this an actual game, and that comes next:
if (collision(crystal)) {
  transferEnergy(true);
} else if (collision(planet)) {
  transferEnergy(false);
} else {
  if (player.energy > 0 && player.energy < 1) {
    player.energy = 0;
  }
}

The first part of that logic is to see I the player is in contact (“collided with”) the crystal or the planet. The collision() function implements that check, but we’ll look at that next. For now, know that it simply returns true if the player and the game object passed in have collided, false otherwise. If they are touching the crystal, then we need to transfer energy to the ship (or to the planet from the ship in the case of the planet), which there is a function for, not surprisingly called transferEnergy() (which we’ll look at shortly). Passing true to it tells that the collision was with the crystal, while false means the planet, as you can see in the else if branch.

The else branch covers a “cheat” condition: if the player has energy, but the ship isn’t 100% full of it, then dump it all out. Without this, the player would be able to grab just a tiny bit of energy, but then return it to the planet and be given full credit for the delivery. That would be terrible for the economics of the Gorgonian solar system (and sociologically would probably lead to wars between the ship captains, and since we’ve already established their ships are weak that would probably be a short war – but I digress), so we’ll just put a stop to it right here and now! Since this situation can only arise if they are not in contact with either the crystal or planet, the else branch is the right place for that logic.

Next, we need to check for collisions with the vermin:
for (int i = 0; i < 3; i++) {
  if (collision(fish[i]) || collision(robots[i]) ||
    collision(aliens[i]) || collision(asteroids[i])) {
    audioCache.play("explosion.mp3");
    player.visible = false;
    GameObject explosion = GameObject(screenWidth,
      screenHeight, "explosion", 50, 50, 5, 4,
      () { resetGame(false); }
    );
    explosion.x = player.x;
    explosion.y = player.y;
    explosions.add(explosion);
    score = score - 50;
    if (score < 0) { score = 0; }
  }
}

Obviously, we need to check each enemy, hence the loop. To avoid nested loops, I check one of each enemy with each iteration of the loop. If any collision occurs, then first we play the explosion sound. The audioCache that we set up earlier provides a play() method for this, and all you do is give it the name of the file to play (sans assets/ prefix, it should be noted, since audioplayers assumes that’s where the files are by default). Piece of cake! Next, the player needs to be hidden. That’s because an explosion is going to be shown in its place, which is exactly what we do next: instantiate a GameObject for the explosion. It gets positioned right where the player is (err, was!) and then that GameObject gets added to the explosions list (which, you’ll recall from earlier in this function, means it will be animated beginning with the next frame). The effect of all of that is an exploding ship! A few points are deducted from the player’s score for the mishap (which we must ensure doesn’t go negative) and we’re done.

Only one task remains, a single line of code, but it is absolutely critical:
state.setState(() {});

Without this, to put it succinctly: nothing happens! Without updating state, Flutter doesn’t know anything has changed and so build() won’t fire again and the screen won’t update. Kind of an important step, don’t you think?!

Next, let’s look at that collision() function and see what it’s all about.

Checking for Collisions

Most video games require the ability to detect when two objects collide with each other. Here, we need to know when the ship hits any of the vermin. There are several ways to do this, each having their pluses and minuses. One simple approach is called bounding boxes. This method simply checks the four bounds of the objects, and if the corner of one object is within the bounds of the other, then a collision has occurred.

As illustrated in the example in Figure 9-4, each game object has a square/rectangular area around it called its bounding box. This box defines the boundaries of the area the object occupies. Note in the diagram how the upper-left corner of object 2’s bounding box is within the bounding box of object 1. This represents a collision. You can detect a collision by running through a series of simple tests comparing the bounds of each object. If any of the conditions are untrue, then a collision cannot possibly have occurred. For instance, if the bottom of object 1 is above the top of object 2, then there’s no way a collision could have happened. In fact, since you’re dealing with a square or rectangular object, you have only four conditions to check, any one of which being false precludes the possibility of a collision.
../images/480328_1_En_9_Chapter/480328_1_En_9_Fig4_HTML.jpg
Figure 9-4

The basic idea behind bounding boxes

This algorithm does not yield perfect results though. For example, you will sometimes see the ship “hitting” an object when it clearly did not really touch. Other times, they may appear just barely to collide, but it won’t register as a collision. The rotation of the ship also plays a role in this because this simple version of the algorithm can’t handle the altered geometry of the ship from something that isn’t (roughly) square and aligned perfectly vertically or horizontally. This could be fixed with a more complex version of the algorithm, or with pixel-level detection, meaning checking each pixel of one object against all the pixels in the other (or at least the pixels on their edges). However, the bounding boxes approach shown here gives an approximation that yields "good enough" results in many cases – the game isn’t unplayable even with this margin of error – so all is right with the world.

With all that explained, let’s see that collision() function that was referenced in the previous section:
bool collision(GameObject inObject) {
  if (!player.visible || !inObject.visible) { return false; }
  num left1 = player.x;
  num right1 = left1 + player.width;
  num top1 = player.y;
  num bottom1 = top1 + player.height;
  num left2 = inObject.x;
  num right2 = left2 + inObject.width;
  num top2 = inObject.y;
  num bottom2 = top2 + inObject.height;
  if (bottom1 < top2) { return false; }
  if (top1 > bottom2) { return false; }
  if (right1 < left2) { return false; }
  return left1 <= right2;

If the player isn’t visible, or if the object we’re checking for collision with isn’t visible, then there’s no need to check for a collision because game objects are only ever not visible when they’re exploding. After that, we calculate the values to be compared, which means the coordinates of the top, bottom, left, and right bounds of the player and the target object. Finally, it’s just the four simple checks described to tell you if a collision has occurred.

Randomly Positioning an Object

After the player picks up all the energy from the crystal, or when they have transferred all the energy back to the planet, or when the game resets, the crystal and planet get randomly positioned via a call to randomlyPositionObject() :
void randomlyPositionObject(GameObject inObject) {
  inObject.x = (random.nextInt(
    screenWidth.toInt() - inObject.width)).toDouble();
  if (collision(inObject)) {
    randomlyPositionObject(inObject);
  }
}

The Random object created during startup is used by calling its nextInt() method . The value we want must be in the range zero to the width of the screen minus the width of the object, so that it’s always on the screen and not hanging off the left or right edge. Only the horizontal position of the object is random, so the resultant random value is set to its x property. The other consideration is that the object can’t be in the same place as the player. So, we call collision() to check for that condition and if a collision occurs then randomlyPositionObject() is recursively called until a non-collision position is selected.

Transferring Energy

When the ship “collides” with the crystal or planet, energy must be transferred to or from the ship. The transferEnergy() function is called for that purpose:
void transferEnergy(bool inTouchingCrystal) {
  if (inTouchingCrystal && player.energy < 1) {

If the caller indicates that the crystal is being touched, then we have to ensure that the ship isn’t already full. The values run from 0 to 1 because that’s what the LinearProgressIndicator widget wants for its value range. However, I found that if a check isn’t done to be sure the value never goes over one then the bar “bounces” at the end a little bit, which looks bad.

When the first contact occurs, we need to play the appropriate sound:
    if (player.energy == 0) { audioCache.play("fill.mp3"); }

At the first touch, the energy would be zero, of course, that’s why this condition is checked.

After that, it’s a simple matter of incrementing the energy and capping it at one:
    player.energy = player.energy + .01;
    if (player.energy >= 1) {
      player.energy = 1;
      randomlyPositionObject(crystal);
    }

Also, when the ship is full, the crystal gets randomly repositioned so that the ship is no longer sucking energy (not that it would anyway with the conditions checked for here, but this way it visually isn’t there to be siphoned from).

The else if branch comes next and that’s for contact with the planet:
  } else if (player.energy > 0) {

This only has an effect when the ship has energy onboard of course, so we check for that.

Then, similar to the first contact with the crystal, we want to play a different sound upon the first contact with the planet, so:
    if (player.energy >= 1) {
      audioCache.play("delivery.mp3");
    }
And, as with the crystal, the energy on the ship is adjusted:
    player.energy = player.energy - .01;

Of course, with the energy being adjusted and state being set, the bar will fill or unfill as appropriate, just like we want.

Now, there’s some other logic we need to implement when depositing energy to the planet, and that’s when all the energy is delivered:
    if (player.energy <= 0) {
      player.energy = 0;
      audioCache.play("explosion.mp3");
      score = score + 100;
      for (int i = 0; i < 3; i++) {
        Function callback;
        if (i == 0) {
          callback = () { resetGame(true); };
        }
        fish[i].visible = false;
        GameObject explosion = GameObject(screenWidth,
          screenHeight, "explosion", 50, 50, 5, 4, callback);
        explosion.x = fish[i].x;
        explosion.y = fish[i].y;
        explosions.add(explosion);
        robots[i].visible = false;
      }
    }

Here, we ensure that the energy is at zero, not below, and the explosion sound is played, and the player’s score bumped up. That’s because it’s time to blow up the vermin! The loop executes and for each vermin, it’s hidden, and an explosion shown in its place. Note that the animation callback you saw earlier when looking at the GameObject class now is used: the first (and only the first) vermin has this callback hooked up so that when the animation cycles completes, we can reset the game, including resetting the position of the enemies.

Note

The code you see here for the fish is repeated for the robots, aliens, and asteroids, so I saved a little space by not showing them here.

The result of all of this is shown in Figure 9-5: beautiful vermin carnage!
../images/480328_1_En_9_Chapter/480328_1_En_9_Fig5_HTML.jpg
Figure 9-5

Boom! We all fall down!

Of course, they come right back, so it’s a short-lived victory for our hero ☹

Taking Control: InputController.dart

The final bit of code we need to look at is the InputController class, the one you saw get hooked up to the GestureDetector’s event properties earlier. It implements all the player motion controls and begins thusly:
import "package:flutter/material.dart";
import "Player.dart";
double touchAnchorX;
double touchAnchorY;
int moveSensitivity = 20;

After what I would think are obvious imports, we have three variables. The way the control scheme works is that the player places their finger any where on the screen and that becomes the “anchor point.” Picture a video game joystick in your mind. The center position represents this anchor point. Now, any time the player moves their finger, the new position relative to that anchor point represents movement in that direction. If their finger is, say, 20 pixels above the anchor point, then they want to move the ship up. If they lift their finger and put it somewhere else, then we have a new anchor point. They can, in a sense, create a “virtual joystick” anywhere on the screen that is convenient for them. So, we need two variables to record the X and Y location of the anchor point. We also need to know how many pixels away from the anchor point will register a move, a “sensitivity” setting if you will, and after some experimenting, I landed on 20 being a pretty good value: not too touchy but not too difficult to register a move either.

We also need a reference to the player, which should seem obvious:
Player player;
And, that reference it stored when the init() method here is called from the firstTimeInitialization() method :
void init(Player inPlayer) { player = inPlayer; }
Now, you’ll recall from earlier that we need to handle three events from the GestureDetector: onPanStart (when the player places their finger down), onPanUpdate (when they drag their finger) and onPanEnd (when they lift their finger). First up is onPanStart() for handling the onPanStart event:
void onPanStart(DragStartDetails inDetails) {
  touchAnchorX = inDetails.globalPosition.dx;
  touchAnchorY = inDetails.globalPosition.dy;
  player.moveHorizontal = 0;
  player.moveVertical = 0;
}

The task here is simple: record the new anchor point and ensure the player isn’t moving. The object passed into this method is a DragStartDetails object that contains a few pieces of information, most critically to us being globalPosition.dx and globalPosition.dy for the horizontal (x) and vertical (y) position of the drag event.

Next is the onPanUpdate() function , which is where the majority of the work that this class does is found:
void onPanUpdate(DragUpdateDetails inDetails) {
  if (inDetails.globalPosition.dx <
    touchAnchorX – moveSensitivity) {
    player.moveHorizontal = -1;
    player.orientationChanged();
  } else if (inDetails.globalPosition.dx >
    touchAnchorX + moveSensitivity) {
    player.moveHorizontal = 1;
    player.orientationChanged();
  } else {
    player.moveHorizontal = 0;
    player.orientationChanged();
  }
  if (inDetails.globalPosition.dy <
    touchAnchorY – moveSensitivity) {
    player.moveVertical = -1;
    player.orientationChanged();
  } else if (inDetails.globalPosition.dy >
    touchAnchorY + moveSensitivity) {
    player.moveVertical = 1;
    player.orientationChanged();
  } else {
    player.moveVertical = 0;
    player.orientationChanged();
  }
}

It may look like a lot of code, but it’s straightforward: if the horizontal location of the drag update event, as indicated by the DragUpdateDetails object’s globalPosition.dx property, is more than 20 pixels to the left of the anchor point, then the player’s moveHorizontal value is -1, and the call to player.orientationChanged() results in the proper rotation being applied. Similarly, if the event happened more than 20 pixels to the right, then the player is moving right (moveHorizontal gets a value of one). If neither of those conditions applies, then there is no horizontal movement (moveHorizontal is set to zero). Then, the same logic is applied for the vertical position but using the inDetails.globalPosition.dy property. The result is the movement control mechanics described, the virtual joystick, so to speak.

Finally, we just have to handle the onPanEnd event in the onPanEnd() handler function:
void onPanEnd(dynamic inDetails) {
  player.moveHorizontal = 0;
  player.moveVertical = 0;
}

All we need to do here is stop any movement that may be occurring, and we have a fully controllable player and a fully playable game thanks to Flutter!

Summary

That’s it! You made it! Three complete Flutter apps, the final one a game! In this chapter, you learned about some new things such as the Positioned widget, Random number generation, handling pan input events, AnimationController, Tween and Animation for performing – after a fashion – animation, and audio. You also, if it was something you hadn’t seen before, learned a little bit about how to architect a game.

It is my sincerest hope that you’ve enjoyed Practical Flutter and that you’ve learned a lot from it. The goal was never to make you an absolute expert in all things Flutter – it’s way too big of a thing for a single book to pull that off! But, if I’ve done my job even close to properly, then you now have a solid foundation on which to build your Flutter knowledge, and with luck, I’ve provided you the necessary building blocks to start creating your own Flutter apps.

So, get to it, start twiddling some bits, go forth and create greatness thanks to Flutter and, in some small part I hope, this book!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.228.35