2. Flappy Starling
In this chapter, we are going to jump (flap?) right into the development of an actual game. Along the way, you will get a peek at Starling’s basic concepts. If you don’t understand all the steps right away, don’t worry: we will look at the details later. For now, it is more important to get a feeling for the framework, and to enjoy the sense of achievement that comes with finishing a game.
2.1. The Blueprint
Flappy Bird is a phenomenon. Developed over the course of just a few days by Vietnamese artist and programmer Dong Nguyen, it was released in 2013, sharing the fate of countless other games of its kind: nobody noticed it.
That changed in early 2014, however. For reasons that remain a mystery, it suddenly skyrocketed up the charts! At that time, the game was so successful that it earned about USD 50,000 per day from its in-app advertisements. The dream of every game developer come true, right?
At the height of this success, though, Dong pulled the game from the app stores. He said that he had a bad conscience about getting people addicted to his game. It only added to the hype, of course, and since then, countless clones of the game have appeared on the stores.
Well, it’s time we made our own!
2.1.1. Setup and Gameplay
In all honesty: I admire this game. It demonstrates that it’s often the most simple ideas that provide the most fun. Yes, it might be argued that it’s not the most original game. Without question, though, it is definitely challenging and addictive!
If you haven’t played it before, here’s what Flappy Bird is about.
-
A bird is flying through a side-scrolling landscape that could have been taken right out of Super Mario Bros.
-
Each time you touch the screen, the bird flaps up a little bit, fighting against the constant pull of gravity.
-
The player’s job is to make it evade the obstacles that appear every few meters: green pipes that leave only a narrow gap for the bird to pass through.
-
You score a point for every pair of pipes that the bird is passing. When you fail, you restart from the very beginning.
As simple as this sounds, it’s actually damn hard to score just a few points — and that’s what this game is all about: the challenge is to stay focused and concentrated as long as possible, without making an error.
There’s nothing else: no growing difficulty curve, no fancy graphics; it’s just you and the bird. And since each session just takes a few seconds, it’s perfect for a mobile game.
Furthermore, it’s a game about a bird right? That’s reason enough to make this the perfect example for our jump into Starling!
2.2. Starting Point
I set up a repository on GitHub that contains all the source code for this game. There’s a git tag for each step we’re going to take; that way you can follow along easily. First, clone the complete project to your development computer.
git clone https://github.com/Gamua/Flappy-Starling.git
This will copy the game to the directory "Flappy-Starling".
Navigate into this directory and checkout the tag start.
cd Flappy-Starling git checkout start
This will revert the repository to the state we want to use to start off our project. To get an overview about what’s there, take a look at the main folders within the project directory.
- assets
-
The textures and fonts we are going to load at runtime. I prepared all the elements we need to set up the game world, as well as a colorful bitmap font that fits the style of the game.
- lib
-
Contains the
starling.swclibrary the game will use (v2.0.1). I added it directly to the repository so that you can be sure the code will always compile. - src
-
The game’s source code; we will spend most of our time here. Right now, it only contains a few very basic classes, but we will build on that soon.
| We are working on a pure Flash Player project here (not AIR). That simplifies some things; after all, we want to focus completely on the code in this chapter. |
You now have to set up your IDE for this project.
-
If you are using IntelliJ IDEA, you’re in luck: the repository contains suitable project and module files (in the root directory).
-
Open the complete project via or
-
import the module into an existing project via .
-
-
For all other IDEs, please follow the setup procedures shown in the previous chapter.
-
Be sure to create a project for the Flash Player (not AIR), using pure ActionScript 3.
-
Link
lib/starling.swcto the project, or use your local version of Starling. -
The file
FlappyStarling.ascontains the main/startup class.
-
Once everything is set up, I recommend you compile and run the project. If your Flash Player fills up with a light blue color, you have succeeded.
The project already contains all the basic setup code for Starling so that we don’t have to start from zero. Take a look around; it’s really not much code yet! Basically, we’re just setting up Starling (just as we did in the Hello World example) and load the assets.
2.2.1. The Game class
It’s time to look at the root object that is going to host Flappy Starling.
import starling.display.Sprite;
import starling.utils.AssetManager;
public class Game extends Sprite (1)
{
private static var sAssets:AssetManager;
public function Game()
{ }
public function start(assets:AssetManager):void (2)
{
sAssets = assets;
}
public static function get assets():AssetManager (3)
{
return sAssets;
}
}
| 1 | The class extends starling.display.Sprite. I’ll explain that in just a minute. |
| 2 | The start method will be called from the startup-class. It also passes us all our assets. |
| 3 | For convenience, I added a static property that allows us to access the assets from anywhere via a simple call to Game.assets. |
As you can see, there isn’t that much going on yet.
All we are doing at the moment is storing a reference to the AssetManager instance that’s passed to us from the startup class.
|
The AssetManager
Wait, I haven’t introduced you to the AssetManager yet! That’s a very nice helper class that comes with Starling. You can stuff all kinds of game assets into it, for example textures, sounds, fonts, etc. That way, you don’t have to manually load your assets when you need them, but can simply access them via their name. |
At this state, our AssetManager instance already contains all the assets we need. The startup class (FlappyStarling) made those preparations for us.
Time to add some content to our game!
I’m getting bored of this empty blue canvas.
Modify the start method like this:
public function start(assets:AssetManager):void
{
sAssets = assets;
var skyTexture:Texture = assets.getTexture("sky"); (1)
var sky:Image = new Image(skyTexture); (2)
sky.y = stage.stageHeight - skyTexture.height; (3)
addChild(sky); (4)
}
| 1 | Get a reference to the sky texture from the AssetManager. |
| 2 | Create an image with that texture. |
| 3 | Move the image to the very bottom of the stage. |
| 4 | Add the image to the stage. |
When you type that code, be careful when you come to the Texture keyword.
There are several classes named Texture, and your IDE will probably ask you which one you want to import.
When working with Starling, you will usually need to pick a class from the starling package.
Otherwise, the IDE will import the wrong class, and you will run into weird errors.
If you accidentally picked the wrong class, it’s easy to fix: just delete that import statement at the top of the file and try again.
When done right, your IDE will add the following import at the top of the file:
import starling.textures.Texture;
In the source code samples of this book, I’m normally omitting the import statements to save space.
If you run into a compile error even though you typed the code correctly, it’s always a good idea to check your imports.
|
Finally, we have drawn something to the screen! But how did we do that?
-
First, we fetched a reference to the sky texture from the AssetManager. In Starling, a
Texturestores the pixels of an image (similar to theBitmapDataclass in classic Flash). -
Then, we wrapped that texture in an
Imageinstance (thinkBitmapin classic Flash). That’s one of several classes that inherit fromstarling.display.DisplayObject— the building blocks of everything you see on the screen. -
Now, it’s just a matter of moving it to the right position (via the x- and y-coordinates) and adding it to Starling’s display list (via
addChild).
It might be necessary to clarify how we moved the texture to the bottom of the screen.
The x- and y-coordinates control the position of an object relative to its parent.
If we left both coordinates at 0, the image would appear at the top left.
That’s because in Starling, the coordinate system looks like this:
We also accessed the stage object and queried its height.
The Stage describes the area to which everything is rendered; it is the very root of Starling’s display list (and parent of the Game instance we are working with right now).
|
Don’t confuse this stage with the one provided by "classic" Flash. Starling manages its own, completely separate display list. |
2.3. Organizing the Code
It’s nice that there is something on the screen now — but before going on, we need to ponder a little bit about how we’re going to organize all of the code we’re going to write.
Of course, we could put all of our game elements right into the Game class, but that’s going to get messy quickly. Instead, it’s a good idea to split the game up into logical units; each with a very limited responsibility.
After all, we’re not only going to create this landscape with the flying bird, but also a title screen, maybe a leaderboard, etc. But how should we organize all these elements?
2.3.1. Introducing: The Sprite
You have already seen this class before: our Game class is extending Sprite. This class is one of the most important building blocks within Starling. It’s a container that can group together multiple elements.
Here’s a simple example showing a typical use-case of a sprite:
var body:Image = ...;
var tail:Image = ...;
var beak:Image = ...;
var bird:Sprite = new Sprite();
bird.addChild(body);
bird.addChild(tail);
bird.addChild(beak);
addChild(bird);
The bird object now contains body, tail and beak, grouped together in one object.
When you want to move around the bird, you just have to change the x- and y-coordinates of the bird instance; all child elements will move with it.
If you want the bird sprite to be reusable, you could also use the following approach:
public class Bird extends Sprite (1)
{
public function Bird()
{
var body:Image = ...;
var tail:Image = ...;
var beak:Image = ...;
addChild(body); (2)
addChild(tail);
addChild(beak);
}
}
var bird:Bird = new Bird(); (3)
addChild(bird);
| 1 | Create the Bird class so that it extends Sprite. |
| 2 | Just like before, we’re adding some child objects; here, we are doing it in the constructor. |
| 3 | Instantiate and use the bird sprite like this. |
That’s a very common pattern when working with Starling. One might even argue that this is your main task as a developer: to organize your display objects hierarchically by grouping them into sprites (or other containers), and to write the code that glues everything together.
As mentioned above, the Game class is our top-level sprite. It’s going to be the controller that manages what’s going on at the highest level. The other elements will follow the lead; one of them is the World class.
2.3.2. The World class
The World class is going to deal with the actual game mechanics. When the Game tells it to launch the bird, it will do just that; and when the bird crashes into an obstacle, it will notify the Game and will wait for instructions about how to go on.
For starters, create that new class and add the following code:
public class World extends Sprite
{
private var _width:Number;
private var _height:Number;
public function World(width:Number, height:Number)
{
_width = width; (1)
_height = height;
addBackground(); (2)
}
private function addBackground():void
{
var skyTexture:Texture = Game.assets.getTexture("sky"); (3)
var sky:Image = new Image(skyTexture);
sky.y = _height - skyTexture.height;
addChild(sky);
var cloud1:Image = new Image(Game.assets.getTexture("cloud-1"));
cloud1.x = _width * 0.5;
cloud1.y = _height * 0.1;
addChild(cloud1);
var cloud2:Image = new Image(Game.assets.getTexture("cloud-2"));
cloud2.x = _width * 0.1;
cloud2.y = _height * 0.2;
addChild(cloud2);
}
}
| 1 | I added width and height arguments to the constructor.
That way, the Game class can pass in the stage dimensions. |
| 2 | The code that creates the sky texture was moved into a separate method. While I was at it, I also added two clouds to the sky to make it look a little more interesting. |
| 3 | Thanks to the static assets property on the Game class, we can easily access our textures without passing the AssetManager instance around. |
|
Relative Positioning
Right now, we are creating a Flash game with a fixed size of 320x480 pixels. However, I’d like to port the game to mobile, later. Mobile devices have all kinds of different screen sizes, so it’s a good idea to take that into account right away. I’m doing that by working with relative instead of absolute positions.
|
What’s left to do, of course, is to reference this new class from Game.as. Here’s how it should look like now:
public class Game extends Sprite
{
private static var sAssets:AssetManager;
private var _world:World; (1)
public function Game()
{ }
public function start(assets:AssetManager):void
{
sAssets = assets;
_world = new World(stage.stageWidth, stage.stageHeight); (2)
addChild(_world);
}
public static function get assets():AssetManager
{
return sAssets;
}
}
| 1 | Add a new member variable. |
| 2 | Instantiate the new class (passing the stage dimensions along) and add it to the stage. |
Compile and run to check if everything worked out. Except for the two inconspicuous new clouds, it should look just like before.
2.4. Adding the Bird
It’s a game about a bird, so let’s finally add the hero to our game. Our assets contain three textures that illustrate a flapping red bird. That’s all we need for a neat flapping animation.
Just like in classic animation movies, we will display those images in quick succession to create the illusion of movement.
2.4.1. The MovieClip class
To do that, we first create a Vector of Textures that references the frames of our animation.
var birdTextures:Vector.<Texture> = Game.assets.getTextures("bird-");
birdTextures.push(birdTextures[1]);
The textures we are looking for are named bird-1, bird-2, and bird-3.
To get those textures from the AssetManager, the getTextures method comes in handy; it returns all textures with a name that starts with a given string (sorted alphabetically).
The middle frame is supposed to be reused for the upward-movement of the wing, so I added another reference to that texture to the very end of the vector.
To actually display those animation frames, let me introduce you to the MovieClip class. It works very similar to an Image (it extends that class), with the difference that it changes its texture over time.
Let’s try this out in our World class. Make the following modifications:
private var _bird:MovieClip; (1)
public function World(width:Number, height:Number)
{
/* ... */
addBackground();
addBird(); (2)
}
private function addBird():void
{
var birdTextures:Vector.<Texture> = Game.assets.getTextures("bird-");
birdTextures.push(birdTextures[1]);
_bird = new MovieClip(birdTextures); (3)
_bird.pivotX = 46; (4)
_bird.pivotY = 45;
_bird.x = _width / 3; (5)
_bird.y = _height / 2;
addChild(_bird);
}
| 1 | Add a new member variable referencing the bird. |
| 2 | Add a new helper method that creates the bird. |
| 3 | Create the actual MovieClip. |
| 4 | Move the bird’s pivot point to the center of its body (see below). |
| 5 | Move the bird itself to its starting point, a little left of the center of the screen. |
As you can see, the creation of the MovieClip is quite straight-forward; you simply pass the Vector of textures to its constructor.
|
Pivot Points
The code also introduces the concept of pivot points.
Per default, the origin of all display objects is at the top left (just as the stage’s origin is at the top left).
Via the Above, we are moving the pivot point to the center of the bird’s body, which has two advantages:
|
Run the project now to see the bird airborne!
Something seems to be wrong, though: there is no flapping animation, just a static image!
That’s because the MovieClip doesn’t have any information about the passage of time yet; and without that information, it will simply stick to the very first frame.
In this case, we want to have full control over the bird’s animation, so we will update it manually once per frame. Let’s do this by adding the following method to the World class:
public function advanceTime(passedTime:Number):void
{
_bird.advanceTime(passedTime);
}
The advanceTime method moves the playhead forward by the given time.
Thus, we want this method to be called once per frame, and the passedTime parameter needs to contain the time passed since the previous frame.
Sounds like a job for our Game class, right?
Open it up and add the following code:
public function start(assets:AssetManager):void
{
/* ... */
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event, passedTime:Number):void
{
_world.advanceTime(passedTime);
}
This code introduces a concept that hasn’t been mentioned before: events.
We will look into that topic in detail a little later, but that code should be rather intuitive.
We listen to an ENTER_FRAME event, which is a standard event that’s dispatched once per frame to every display object.
As a result, the method onEnterFrame (the event handler) will be called repeatedly.
The event handler also contains an argument called passedTime, which is just what we need in the advanceTime method we just defined on World.
Which means that everything is in place to get that bird flapping! Compile and run the code now to see it in action.
2.5. Scrolling
Right now, the bird is flying in empty space. We will now add some grassland below to give the player some sense of the bird’s height and movement. Let’s start with the following additions to the World class:
private var _ground:Image; (1)
public function World(width:Number, height:Number)
{
/* ... */
addBackground();
addGround(); (2)
addBird();
}
private function addGround():void
{
var tile:Texture = Game.assets.getTexture("ground");
_ground = new Image(tile); (3)
_ground.y = _height - tile.height;
addChild(_ground);
}
| 1 | Add a new member variable referencing the ground. |
| 2 | Add a new helper method that creates the ground. |
| 3 | Create the ground image and move it to the bottom. |
That’s quite straight-forward. The result will look like this:
Oops! That ground tile is way too short for the width of the screen. It occupies just a small area at the very left. However, that texture supports tiling, i.e. when you add the same image over and over, it will fill up seamlessly.
One way to fill up the floor would be to simply create several of those images and put them all next to each other. That’s what you would do in most game engines.
However, there is an even simpler way: Starling’s tileGrid.
That’s a property that is defined on the Image class, and as its name suggests, it was built exactly for the purpose of filling an image with tiles.
It works like this: you assign a rectangle that indicates where a single tile should be placed. Starling will then fill out the rest so that the complete area of the image repeats that pattern.
With that in mind, we modify the code of the addGround method slightly.
_ground = new Image(tile);
_ground.y = _height - tile.height;
_ground.width = _width; (1)
_ground.tileGrid = new Rectangle(0, 0, tile.width, tile.height); (2)
| 1 | Make the image as wide as the stage. |
| 2 | Assign a rectangle that points to the first tile. Starling will add other tiles automatically. |
That’s quite simple, right? Looking at the output, it seems that did the trick!
2.5.1. Animating the Ground
That’s all still a rather static business, though. Instead, we want to create the impression that the camera is following a bird that’s flying to the right. To do that, we will move the ground slowly to the left, just like an endless ribbon.
Had we built the ground from separate images, this would be quite painful.
-
In each frame, move all those images a little to the left.
-
If an image moves out of the visible area, put it to the very right.
-
That way, we have an endless stream of tiles.
You might run into that situation one day — it’s rare that the ground is built from just a single tile.
However, in this situation, with our tileGrid in place, it’s much simpler: we let Starling do the work for us.
Our task:
-
In each frame, move the (virtual)
tileGridrectangle a little to the left. -
That’s it.
Our existing advanceTime method will pull that off.
public class World extends Sprite
{
private static const SCROLL_VELOCITY:Number = 130; (1)
/* ... */
public function advanceTime(passedTime:Number):void
{
advanceGround(passedTime); (2)
_bird.advanceTime(passedTime);
}
private function advanceGround(passedTime:Number):void
{
var distance:Number = SCROLL_VELOCITY * passedTime; (3)
_ground.tileGrid.x -= distance; (4)
_ground.tileGrid = _ground.tileGrid; (5)
}
| 1 | A constant defines the speed of movement: 130 points per second. |
| 2 | The new method advanceGround will do the work. |
| 3 | Since passedTime is given in seconds, a simple multiplication will yield the passed distance. |
| 4 | Move tileGrid.x to the left. |
| 5 | That’s a small API oddity: for the changes to show up, tileGrid has to be re-assigned. |
There really isn’t much to it: all that we are doing is reducing the value of tileGrid.x a little in each frame.
The result is a neat, endless animation of the ground ribbon.
| You’ll notice that the we are not moving the blue background in any way. That’s intentional: by keeping it at a fixed position, we’re creating the illusion that it is very far away. |
2.6. Physics
Flappy Starling ought to be a game, but right now, the level of interactivity is rather … lacking. Thus, the next step should be to make the bird’s flight follow some basic physical rules, and to let the player take control.
We have two choices when it comes to physics:
-
We could do it ourselves: Isaac Newton came up with a couple of formulas; we can plug those into our code.
-
Or we could add a physics library to our project and let it do the heavy lifting.
Which approach should we use?
Make no mistake: it’s actually really hard to create a convincing physics simulation. Things get complicated quickly, especially when multiple objects interact with each other. The math will become very challenging, and before you know it, the calculations will eat up all available CPU time.
Thus, I’d typically recommend using a turn-key physics engine (like Nape). Let it do the work for you while you concentrate on your actual project. Games like Angry Birds rely on the realism only such an engine can deliver.
The physics of Flappy Starling, on the other hand, are extremely simple. We have one flying bird that’s affected by gravity and can flap upwards on touch of the screen. That’s something we can easily do ourselves.
2.6.1. What goes up, must come down
A bird that flies in the air is constantly being pulled down by gravity. What does that mean?
Gravity changes the velocity of an object: push a stone off a cliff and its velocity will rise from "zero" (the moment it loses contact) to … well, "very fast" before it hits the ground. The higher the cliff, the faster it will be on impact. Each instant it falls, it will be faster than the instant before.
So we make a mental note that we need to store our bird’s current velocity somewhere, as well as the acceleration imposed on it by gravity.
The other force acting on the bird is — the player. When he or she touches the screen, the bird is catapulted upwards, i.e. the velocity is set to a fixed value.
Let’s incorporate all that into our World class:
public class World extends Sprite
{
private static const SCROLL_VELOCITY:Number = 130;
private static const FLAP_VELOCITY:Number = -300; (1)
private static const GRAVITY:Number = 800; (2)
private var _bird:MovieClip;
private var _birdVelocity:Number = 0.0; (3)
/* ... */
}
| 1 | The bird’s velocity will be set to that value when the user touches the screen. |
| 2 | The gravity constant stores the acceleration that gravity inflicts on the bird. |
| 3 | This value will store the current velocity of the bird; it will be updated in each frame. |
You might ask why birdVelocity is a scalar, not a vector. That’s because our bird actually only moves along the y-axis (i.e. vertically). There is no horizontal movement — that’s just an illusion we created by moving the ground to the left!
Don’t forget to assign birdVelocity a value (zero, in this case).
AS3 has a very peculiar default value for variables of type Number: NaN (not a number).
So much for language designers not having a sense of humor!
|
The next step is to update birdVelocity each frame, and to move the bird by that velocity.
public function advanceTime(passedTime:Number):void
{
/* ... */
advancePhysics(passedTime); (1)
}
private function advancePhysics(passedTime:Number):void
{
_bird.y += _birdVelocity * passedTime; (2)
_birdVelocity += GRAVITY * passedTime; (3)
}
public function flapBird():void (4)
{
_birdVelocity = FLAP_VELOCITY;
}
| 1 | The new method advancePhysics will contain our tiny physics engine. |
| 2 | Move the bird along the y-axis according to its current velocity. |
| 3 | Add the GRAVITY to the bird’s current velocity. |
| 4 | This method will be called when the player touches the screen. It instantly sets the bird’s velocity to a fixed value. |
That’s actually all our physics code. You probably agree that adding a full-fledged physics library would have been an overkill in this situation!
There’s one more step to do: allowing the player to control the bird, i.e. to call that new flapBird method.
Please open up the Game class and make the following modifications:
public function start(assets:AssetManager):void
{
/* ... */
addEventListener(Event.ENTER_FRAME, onEnterFrame);
stage.addEventListener(TouchEvent.TOUCH, onTouch); (1)
}
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN); (2)
if (touch) _world.flapBird(); (3)
}
| 1 | Another event listener: this one listens for TOUCH events on the stage. |
| 2 | In Starling, touches pass through different phases. We are interested in the first phase only: TouchPhase.BEGAN means that the finger has just begun touching the screen. |
| 3 | If there is such a touch, we let the bird flap upwards. |
Just like we were notified of the passing of time via the ENTER_FRAME event, there is an event that’s dispatched when the user’s finger touches the screen: the TOUCH event.
When such a touch occurs, we let the bird flap upwards.
|
Touch Events vs. Mouse Events
We are working on a Flash project, which means that most users will probably use a mouse to flap the bird. Why aren’t we listening to mouse events, then? In Starling, touch and mouse input handling is unified. When the user clicks on the mouse button, this will be registered just the same as if he had touched the screen at this position. The advantage: you can use the same code to handle both mouse and touch input. This greatly simplifies porting the game to mobile platforms. |
Start the game to check out if everything works. The bird will start to fall downwards immediately, and you can bring it up again by clicking on the screen repeatedly. Finally, this is starting to feel like a real game, right?
What’s a little annoying, though, is that when you’re not careful, the bird falls off the bottom of the screen, and it’s quite an effort to bring it back up again.
So I’d like to fix that before moving on, even though I’m getting a little ahead of myself. What we’re going to do is implement the first part of the collision detection logic, a topic we will soon come back to.
Just like the physics code, this needs to be part of the World class.
private static const BIRD_RADIUS:Number = 18; (1)
public function advanceTime(passedTime:Number):void
{
/* ... */
checkForCollisions(); (2)
}
private function checkForCollisions():void
{
var bottom:Number = _ground.y - BIRD_RADIUS; (3)
if (_bird.y > bottom) (4)
{
_bird.y = bottom;
_birdVelocity = 0;
}
}
| 1 | Add a new constant at the very top: the radius of the bird’s body circle (in points). |
| 2 | Collision detection is handled in a separate method, called every frame. |
| 3 | The bird’s fall will stop a little upwards of the ground image. (Remember: we moved the bird’s pivot point to the center of its body.) |
| 4 | When we hit bottom, the bird’s velocity is set to zero immediately, and we make sure it doesn’t move any further down. |
That makes playing around with our prototype much easier! Granted: when we hit rock bottom, it should actually mean "game over", but the bird just moves on. That’s the topic of the next chapter!
2.7. Game Phases
You might not have noticed it, but our game is actually already in a state that you could call a game. The player has a task to fulfill (keep the bird flying) and he can fail by crashing the bird into the ground.
What’s lacking, though, is some kind of progression; a sequence of "phases" that provide a structure to what’s happening. I’d like to split up the game into three such phases:
| Idle |
The bird just flies horizontally. In the final version, this state will also show the game logo and the top score. |
| Playing |
The actual gameplay phase in which the player has full control over the bird. |
| Crashed |
After a collision, the game will stop for a moment so that the player has a chance to realize his failure. |
The game starts in the "Idle" phase; as soon as the player clicks anywhere on the screen, the game starts (phase: "Playing"). A collision will put the game into the "Crashed" phase and will eventually return the game to the "Idle" phase. This loop goes on indefinitely until the player quits the game altogether.
Since these phases influence mostly what’s happening inside the game world, we will store them in the World class. Open it up and make the following modifications:
public class World extends Sprite
{
/* ... */
public static const PHASE_IDLE:String = "phaseIdle"; (1)
public static const PHASE_PLAYING:String = "phasePlaying";
public static const PHASE_CRASHED:String = "phaseCrashed";
private var _phase:String; (2)
public function World(width:Number, height:Number)
{
_phase = PHASE_IDLE; (3)
/* ... */
}
public function start():void
{
_phase = PHASE_PLAYING; (4)
}
public function reset():void
{
_phase = PHASE_IDLE; (5)
resetBird();
}
private function resetBird():void
{
_bird.x = _width / 3; (6)
_bird.y = _height / 2;
}
/* ... */
public function get phase():String { return _phase; } (6)
}
| 1 | Three String constants define our phases. |
| 2 | The current phase is stored in a member variable. |
| 3 | Initially, we’re in the IDLE phase that just animates the flying bird. |
| 4 | The new start method is to be called when the player is ready to play.
It simply switches the phase to PLAYING. |
| 5 | The reset method will be called when the world should be restored to the initial state. |
| 6 | We also move the bird back to its original position.
This code was pulled out from the addBird method. |
This code provides the basis for switching between IDLE and PLAYING phases.
The phases have no effect on the actual gameplay yet, though.
-
In all phases except
CRASHED, the bird should keep flapping and the ground should keep moving. -
In the
PLAYINGphase (and only in this phase), we want physics and collision detection.
That’s easily achieved by modifying advanceTime (still in the World class):
public function advanceTime(passedTime:Number):void
{
if (_phase == PHASE_IDLE || _phase == PHASE_PLAYING)
{
_bird.advanceTime(passedTime);
advanceGround(passedTime);
}
if (_phase == PHASE_PLAYING)
{
advancePhysics(passedTime);
checkForCollisions();
}
}
What’s still missing is code that actually switches to the CRASHED phase.
That’s supposed to happen in the case of a collision with the ground (or, later, with an obstacle).
Still in the World class, insert the following code:
public static const BIRD_CRASHED:String = "birdCrashed"; (1)
private function checkForCollisions():void
{
var bottom:Number = _ground.y - BIRD_RADIUS;
var collision:Boolean = false;
if (_bird.y > bottom)
{
_bird.y = bottom;
_birdVelocity = 0;
collision = true;
}
if (collision)
{
_phase = PHASE_CRASHED; (2)
dispatchEventWith(BIRD_CRASHED); (3)
}
}
| 1 | We define an event type that will be used to inform listeners about a crash. |
| 2 | In case of a collision, we switch to the CRASHED phase. |
| 3 | Furthermore, we dispatch the BIRD_CRASHED event. |
As promised, this code activates the CRASHED phase if the bird ends up hitting the ground.
However, it’s also the first time we are dispatching an event — which makes this a great opportunity to properly introduce you to Starling’s event system.
2.7.1. Events
I wrote earlier that the Game class should act as a high level controller that manages all the components of the game. Now that a collision has occurred, there is some management to do: for example, we might want to save the score and display a "Game Over" screen. The World class wouldn’t be the correct place for these tasks.
However, in order to do anything about it, Game needs to know that a collision occurred. How can we tell it about the crash?
The natural thing to do would be to call a method on the Game instance; however, we don’t have such a reference within World.
|
Technically, we do have a reference: the parent display object can always be accessed via the (parent as Game).onCollision(); It’s good that (e)books don’t reproduce smell yet — because, boy, this code stinks! |
This is a quite common scenario, actually, and the reason why there are events, at all.
-
Any Starling object can dispatch an event; other objects can listen to those events.
-
We already encountered events of type TOUCH and ENTER_FRAME, but you can define additional types yourself. An event type is simply a unique string.
In this case, the event type is the String stored in the static constant BIRD_CRASHED.
We dispatch an event with this type when we encounter a collision; that’s done via the line
dispatchEventWith(BIRD_CRASHED);
For this to be of any use, somebody must listen to this event, of course.
So that’s how the Game class will become aware that a collision has occurred — it simply listens to the BIRD_CRASHED event.
Open it up and make the following modifications:
public function start(assets:AssetManager):void
{
/* ... */
_world = new World(stage.stageWidth, stage.stageHeight);
_world.addEventListener(World.BIRD_CRASHED, onBirdCollided); (1)
addChild(_world);
/* ... */
}
private function onBirdCollided():void (2)
{
Starling.juggler.delayCall(restart, 1.5); (3)
}
private function restart():void
{
_world.reset();
}
| 1 | We register an event listener on the World instance, listening specifically to the BIRD_CRASHED event. |
| 2 | When such an event is dispatched, the method onBirdCollided will be executed. |
| 3 | This line will call the restart method with a delay of 1.5 seconds. |
That’s not so complicated, right?
Now, the method onBirdCollided acts as event listener; it will be called whenever the event occurs.
| Any number of objects could add an event listener for this event; you’re not limited to just one. |
Let me tell you a little more about the delayCall method you just encountered in the event listener.
When the bird crashes, we don’t want to restart the game right away; instead, we want the player to have a little time to realize what just happened.
For this reason, we are not restarting the game right away; instead, we wait a short moment before calling the restart method.
The delayCall instance on Starling’s Juggler is the preferred way of doing this.
| Don’t worry, I will properly introduce you to the Juggler later. |
2.7.2. Starting up
When the application starts, we are in the IDLE phase.
The actual game session is supposed to start when the user touches the screen.
That’s the final step we need to make in order for our phase-system to work.
Update the onTouch event handler inside the Game class like this:
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN);
if (touch)
{
if (_world.phase == World.PHASE_IDLE) (1)
_world.start();
_world.flapBird();
}
}
| 1 | When we are in IDLE phase, call the start method.
This will switch the game into the PLAYING phase. |
Start up the game now.
If everything went right, the whole thing should feel much more like a game already!
We’re now switching properly between the IDLE, PLAYING, and CRASHED phases, and you’ve got the chance to restart properly after a crash.
Not bad!
2.8. Obstacles
The actual challenge of Flappy Starling is to guide the bird safely through as many obstacles as possible. Until now, those obstacles are nowhere to be seen, though. Time to change that!
Before we start, let’s take a look at what exactly we are trying to achieve. If we zoomed out extensively, the game world would look like this:
In this section, we will work on two tasks:
- Creating obstacles
-
Each obstacle is made up of two images; one trunk on the top (the stalactite, if you will) and one on the bottom (the stalagmite). We will create a new class that represents one such pair of images.
- Moving obstacles
-
Those obstacles must be placed at a random height, and they must move in from the right in a steady flow. When they move out on the left, they should be removed from the display list.
2.8.1. Creating Obstacles
We could treat each obstacle simply as two separate images; on the other hand, the two trunks will always move along together, so it will quickly pay off to group them in a custom class. This will also simplify the collision detection code we need to implement in the next chapter.
Create a new class called Obstacle that extends Sprite.
public class Obstacle extends Sprite
{
private var _radius:Number; (1)
private var _gapHeight:Number; (2)
private var _passed:Boolean; (3)
public function Obstacle(gapHeight:Number)
{
var topTexture:Texture = Game.assets.getTexture("obstacle-top");
var bottomTexture:Texture = Game.assets.getTexture("obstacle-bottom");
_radius = topTexture.width / 2;
_gapHeight = gapHeight;
_passed = false;
// TODO: add trunk images
}
public function get passed():Boolean { return _passed; }
public function set passed(value:Boolean):void { _passed = value; }
}
| 1 | The radius of the semi-circle that terminates each trunk. We need this information for collision detection. |
| 2 | The height of the gap through which the bird ought to fly. |
| 3 | A boolean value that will be set to true once the bird has passed this obstacle. |
The basic setup is pretty straight-forward, obviously.
So let’s deal with the code marked as TODO: adding trunk images.
Basically, we just need to add two images with the textures referenced at the top of the constructor. But how exactly should we best position them within the sprite?
I decided that the following setup would make most sense:
You see that the sprite’s origin is in the center and the trunk images extend upwards and downwards. The bird must be guided through the empty gap, so it’s important that it’s always in the visible area of the stage. With the origin right at the center of the gap, it will be easy to place the obstacle within the World later. The two trunk textures are very long; they will always exceed the stage bounds from this point.
The following code (to be added to the Obstacle constructor) sets up the sprite like that.
var top:Image = new Image(topTexture);
top.pixelSnapping = true;
top.pivotX = _radius;
top.pivotY = topTexture.height - _radius;
top.y = gapHeight / -2;
var bottom:Image = new Image(bottomTexture);
bottom.pixelSnapping = true;
bottom.pivotX = _radius;
bottom.pivotY = _radius;
bottom.y = gapHeight / 2;
addChild(top);
addChild(bottom);
Do you remember the pivotX and pivotY properties we used when we created the bird object?
Here they are again!
This time, we move the pivot points of the top and bottom images to the center of the circular shape that terminates them.
We could have placed the images without changing the pivot point, of course — but I find this setup easier to handle.
|
PixelSnapping
When we move the obstacles around, they might become a little blurry when they are not exactly aligned with the screen’s pixels.
With the |
2.8.2. Moving Obstacles
Now that the basic building block is ready, we can integrate obstacles into the actual gameplay. As a first step, let’s add some constants that we’re going to need later. Add the following definitions somewhere near the top of the World class.
private static const OBSTACLE_DISTANCE:Number = 180; (1)
private static const OBSTACLE_GAP_HEIGHT:Number = 170; (2)
public static const OBSTACLE_PASSED:String = "obstaclePassed"; (3)
| 1 | The distance between two successive obstacles. |
| 2 | The height of the gap between the top and bottom trunks of an obstacle. |
| 3 | The type of the event we will dispatch whenever the bird passes an obstacle. |
The first two of those values actually make up the difficulty of the game. The values I used are educated guesses; later, when we fine-tune the game, we can always modify them until they feel right.
We are also going to need a few additional member variables. Add them to the others we already defined at the top of the class.
private var _obstacles:Sprite; (1)
private var _currentX:Number; (2)
private var _lastObstacleX:Number; (3)
| 1 | All obstacles will be grouped together in this sprite. |
| 2 | The distance the player has covered since the start of the current session. |
| 3 | Whenever we add an obstacle, we store the current x-coordinate in this value. That way, we can easily measure the distance covered since that moment, and we will know when to add the next one. |
With that out of the way, let’s move to some basic setup procedures. For example, the container that will take up the obstacles needs to be initialized.
public function World(width:Number, height:Number)
{
/* ... */
addBackground();
addObstacleSprite(); (1)
addGround();
addBird();
}
private function addObstacleSprite():void
{
_obstacles = new Sprite(); (2)
addChild(_obstacles);
}
| 1 | Add the obstacle container sandwiched between the background and the ground tiles. |
| 2 | The actual container remains empty for now. |
Furthermore, when the game session starts or ends, we need to reset some values:
public function start():void
{
_phase = PHASE_PLAYING;
_currentX = _lastObstacleX = 0; (1)
}
public function reset():void
{
_phase = PHASE_IDLE;
_obstacles.removeChildren(0, -1, true); (2)
resetBird();
}
| 1 | Reset the covered distance and the last obstacle’s position. |
| 2 | Remove and dispose all obstacles that are currently visible. |
Agreed: this was a lot of rather boring stuff. We are done with that, though: time to actually add some obstacles to the screen.
Since the obstacles are constantly moving, that logic must be triggered once per frame.
We already have a method that fits that description, right?
Exactly: I’m talking about advanceTime.
Make the following modifications to the part that deals with PHASE_PLAYING:
if (_phase == PHASE_PLAYING)
{
_currentX += SCROLL_VELOCITY * passedTime; (1)
advanceObstacles(passedTime); (2)
advancePhysics(passedTime);
checkForCollisions();
}
| 1 | Update currentX, which reflects the current the position of our (pseudo) camera. |
| 2 | The advanceObstacles method will contain the logic that deals with the obstacles. |
Most of the task is actually forwarded to the advanceObstacles method.
It needs to handle several things:
-
Move any existing obstacles slightly to the left.
-
Add new obstacles in regular intervals.
-
Remove and dispose any obstacles that left the screen to the left.
Translated to actual AS3 source code, this means:
private function advanceObstacles(passedTime:Number):void
{
if (_currentX >= _lastObstacleX + OBSTACLE_DISTANCE) (1)
{
_lastObstacleX = _currentX;
addObstacle(); (2)
}
var obstacle:Obstacle;
var numObstacles:int = _obstacles.numChildren;
for (var i:int=0; i<numObstacles; ++i) (3)
{
obstacle = _obstacles.getChildAt(i) as Obstacle;
if (obstacle.x < -obstacle.width / 2) (4)
{
obstacle.removeFromParent(true); (5)
i--; numObstacles--; (6)
}
else obstacle.x -= passedTime * SCROLL_VELOCITY; (7)
}
}
| 1 | If the distance to the previous obstacle exceeds the threshold … |
| 2 | … add a new one. We will implement this method soon. |
| 3 | Iterate over all obstacles that are currently part of the display list. |
| 4 | Check if an obstacle has left the screen to the left. |
| 5 | If so, remove it from its parent sprite, and dispose it right away. |
| 6 | Attention: we are modifying the sprite we are currently iterating over! To avoid any errors when the loop continues, we need to fix the loop variables. |
| 7 | All other obstacles simply move a little bit to the left. |
Pay special attention to the part where we modify the obstacles sprite while iterating over it. If we hadn’t fixed the loop variables accordingly, this would have lead to an exception. That’s easy to overlook!
I know it’s been quite a while since we could actually run our application without an error. Bear with me — we are almost there!
All that’s left to do now is implementing the addObstacle method we just called.
What exactly must this method achieve?
-
Create a new Obstacle instance and place it at the far right of the screen.
-
Move it to a random vertical position within the available screen height.
private function addObstacle():void
{
var minY:Number = OBSTACLE_GAP_HEIGHT / 2;
var maxY:Number = _ground.y - OBSTACLE_GAP_HEIGHT / 2;
var obstacle:Obstacle = new Obstacle(OBSTACLE_GAP_HEIGHT); (1)
obstacle.y = minY + Math.random() * (maxY - minY); (2)
obstacle.x = _width + obstacle.width / 2; (3)
_obstacles.addChild(obstacle); (4)
}
| 1 | Create a new Obstacle instance (the class we created a little earlier in this section). |
| 2 | Move the obstacle to a random vertical position, somewhere between minY and maxY.
The Math.random() method helps: it returns a random number between zero and one. |
| 3 | Move it just a bit outside of the screen horizontally (so that it’s not yet visible).
The advanceObstacles method will slowly move it to the left later. |
| 4 | For the obstacle to become visible, we must add it to its parent container. |
Finally, we can compile and start the game again! Do just that and give it a try.
This is starting to make fun, right? Don’t get overly confident if you feel invincible when playing the game, though: that’s probably because we didn’t implement collision detection yet. Oops!
In any case, as soon as you click once to start the game, the obstacles should start moving in from the right. When they move out on the left, they are automatically removed from the display list. Looking good!
|
Verifying Assumptions
Are the obstacles really disposed when they move out? Actually, we wouldn’t see that just from playing the game. If that part of the logic doesn’t work, the number of obstacles will simply grow endlessly, and we will only notice it when performance starts to suffer. In such situations, it’s a good idea to actually verify that your code works alright.
An easy way to do that would be to simply
In debug mode, your IDE should allow to display the "console", where those trace statements will end up. If everything went alright, the printed number should stay very low (with the given resolution, switching between just 2 and 3). |
2.9. Collision Detection
We made a lot of progress in the previous section. The obstacles move correctly along the screen — the bird just doesn’t collide with them yet. Which is actually the whole point of them, right? So let’s implement the collision detection code.
Open up the Obstacle class again; we need to add a new method. For now, just add the following stub:
public function collidesWithBird(
birdX:Number, birdY:Number, birdRadius:Number):Boolean
{
return false;
}
The parameters of this method already tell something about how I’d like it to work.
It receives the current position of the bird (in World coordinates), as well es the radius of its body.
With this information, we can find out if the bird is currently overlapping one of the trunks (return value: true), or if it isn’t (return value: false).
As for the actual implementation, we are going to use a three-step approach.
First, notice that the bird itself is represented by a simple circle; wings and feet are not taken into account. This makes the whole collision test much easier — and a too pedantic collision detection would be quite frustrating for the player, anyway.
The three steps are discussed below.
2.9.1. Step 1: Horizontal Position
The first step: check if the bird overlaps the vertical stripe made up by the obstacle, at all.
// check if bird is completely left or right of the obstacle
if (birdX + birdRadius < x - _radius || birdX - birdRadius > x + _radius)
return false;
Remember, we centered the trunk images horizontally in the coordinate system of the Obstacle class. Furthermore, the bird’s pivot point is exactly at the center of its circular body. This pays off now: it allows us to directly use bird and obstacle coordinates for our checks.
We just subtract (or add) the radius from the current x coordinate of the obstacle to get to the left (or right) bounds.
If the bird is outside this range, there cannot possibly be any collision with this obstacle, so we exit the method right away.
2.9.2. Step 2: Vertical Position
If the method is executed further, we know that the bird is horizontally within the obstacle bounds. This means that if the bird is too far up or down (above or below the semi-circular end pieces), there must be a collision.
var topY:Number = y - _gapHeight / 2;
var bottomY:Number = y + _gapHeight / 2;
// check if bird is within gap
if (birdY < topY || birdY > bottomY)
return true;
On the other hand, if the bird is vertically in-between those two areas, we need to proceed to the final step.
2.9.3. Step 3: Intersecting Circles
Both the bird and the end pieces of the obstacle trunks are circular. The final step is to test if those circles intersect one another. Thankfully, the math to test if two circles intersect is rather simple:
-
Create a vector connecting the two center points.
-
If the length of this vector is smaller than the sum of the two radii, the circles intersect.
A little geometry reminder: to calculate the distance vector between two points A and B, you simply subtract the coordinates of A from B:
To get the length of this vector, good old Pythagoras comes to the rescue:
We already know the center of the bird circle (birdX and birdY) and the positions of the end pieces (x and topY / bottomY).
The remaining code simply needs to insert those values into the formulas from above.
var distX:Number = x - birdX;
var distY:Number;
// top trunk
distY = topY - birdY;
if (Math.sqrt(distX * distX + distY * distY) < _radius + birdRadius)
return true;
// bottom trunk
distY = bottomY - birdY;
if (Math.sqrt(distX * distX + distY * distY) < _radius + birdRadius)
return true;
// bird flies through in-between the circles
return false;
That’s it! Our collision detection method is finished. Since it’s not called from anywhere yet, though, there is no change in gameplay yet.
We already have a method called checkForCollisions in the World class.
Right now, it only checks if the bird hits the ground, but it was always destined for more!
With the collision detection code prepared, it’s just a matter of iterating over our obstacles and testing each of them.
if (_bird.y > bottom)
{
/* ... */
}
else
{
for (var i:int=0; i < _obstacles.numChildren; ++i) (1)
{
var obstacle:Obstacle = _obstacles.getChildAt(i) as Obstacle;
if (obstacle.collidesWithBird(_bird.x, _bird.y, BIRD_RADIUS)) (2)
{
collision = true; (3)
break;
}
}
}
| 1 | Iterate over all obstacles. |
| 2 | Check if the bird collides with this specific obstacle. |
| 3 | The collision variable is checked at the end of the method.
If true, the game will switch into the CRASHED phase. |
Now that was easy, wasn’t it? If you give the game a try now, you can reap the benefits of our hard work. The basic gameplay mechanisms are all in place!
Before we move on, I’d like to take care of one more thing, though.
In the same method (checkForCollisions), please add the following code into the for loop (somewhere after assigning the obstacle variable).
if (!obstacle.passed && _bird.x > obstacle.x)
{
obstacle.passed = true;
dispatchEventWith(OBSTACLE_PASSED, true);
}
The player’s score is going to be measured by the number of obstacles he manages to pass. The Game class is supposed to take care of that — so it needs to be notified whenever we pass an obstacle. That’s best done by dispatching an appropriate event.
In order to not count one obstacle twice, we are also enabling the passed property.
If you wondered all along what that property was for, here is your answer!
2.10. Score Display
Since we talked so much about keeping track of the score, let’s make that the goal of our next task, shall we? Don’t worry: compared to the last two chapters (which were rather challenging), this is going to be really easy. We will add a small score display to the top left of the screen, visible during the course of each game session.
2.10.1. Text in Starling
In Starling, the TextField class is responsible for displaying text. It populates a rectangular area with the letters of a String — quite simple, actually.
Naturally, we first have to decide which font we want to use. Starling supports two types of fonts:
- TrueType Fonts
-
The standard vector-based fonts you are probably familiar with. Supports any fonts that are provided by the operating system or included in the application.
- Bitmap Fonts
-
A texture containing all the glyphs of a font, as well as an accompanying XML file. Needs to be created with a tool like Littera or BMFont.
The advantage of TrueType Fonts is that they are very easy to use; however, changing the displayed text is somewhat expensive. For text that changes often, Bitmap Fonts are better suited. Besides, you can pre-bake them with decorations like outlines or color gradients, which is great for games like Flappy Bird.
If you look into the folder assets/fonts within our project, you’ll see that I already included one such bitmap font called "Brady Bunch".
It is based on a TrueType font available on dafont.com (a great resource if you are looking for fancy fonts).
Just like all the other textures we have been using so far, this font is already loaded by the AssetManager by the code we used as our starting point. We can make use of it without any further preparations.
2.10.2. TextFormat
The TextFormat class is used to set up the exact style you want text to be rendered with. It goes hand in hand with the TextField class.
Open up the Game class (all edits in this section are done in this class) and add the following static definition to the top:
private static var sDefaultTextFormat:TextFormat =
new TextFormat("bradybunch", BitmapFont.NATIVE_SIZE, Color.WHITE);
This TextFormat instance describes the font settings we want to use for the score label.
-
Note the usage of
BitmapFont.NATIVE_SIZE: it will make sure that the bitmap font is rendered in exactly the size that it was created with. This will provide the best possible image quality. -
Any bitmap font that already contains colors must be set up as pure white in the source code. Other colors would tint the text in an undesired way.
Classes named TextField and TextFormat exist in the packages starling.text and flash.text.
When you write that code, make sure that you import the correct ones (those that are coming with Starling).
|
2.10.3. The Score Label
The Game class needs to display the score, but it also needs to keep track of it. That means that we have to add two member variables.
private var _score:int;
private var _scoreLabel:TextField;
Creating the actual TextField is best done in the start method.
Add the following setup code (e.g. right below the code that sets up the World instance).
_scoreLabel = new TextField(180, 80, "", sDefaultTextFormat); (1)
_scoreLabel.visible = false; (2)
addChild(_scoreLabel); (3)
| 1 | Set up the TextField with a size of 180 × 80 points, using the text format we just set up. |
| 2 | The label should become visible only during PHASE_PLAYING. For now, we make it invisible. |
| 3 | You know the drill: only objects that are part of the display list will be rendered. |
We are now in a situation that deserves special attention: there are two member variables that are storing a representation of the current score (an Integer and a TextField). Those two objects must always be in sync: when you change one, you need to change the other.
I know myself: if I don’t take special measures right now, I’m bound to forget changing one of the two member variables some time in the future. To make sure that can’t happen, I recommend adding a property that takes care of any score changes. We can even make it private, since it’s only meant for internal use:
private function get score():int { return _score; }
private function set score(value:int):void
{
_score = value;
_scoreLabel.text = value.toString();
}
| While it might seem unnecessary to make this effort even in a simple game like this, make it a habit to always take such precautions. It’s easy to do, and it will potentially save you a lot of trouble later. |
This lays the groundwork for our score display. All of this was done to be able to keep track of the player’s score — so let’s do just that!
Do you remember? Each time the bird passes an obstacle, the World class dispatches a suitable event. That’s our signal to increment the score.
public function start(assets:AssetManager):void
{
/* ... */
_world.addEventListener(World.OBSTACLE_PASSED, onObstaclePassed); (1)
}
private function onObstaclePassed():void (2)
{
this.score += 1; (3)
}
| 1 | Add the event listener somewhere within the start method. |
| 2 | Create the actual event handler. |
| 3 | It makes use of the brand-new score property. |
We are almost ready. All that’s left to do is making the score label visible when the game starts, and hiding it after an error. This means some small modifications to existing methods.
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN);
if (touch)
{
if (_world.phase == World.PHASE_IDLE)
{
this.score = 0; (1)
_scoreLabel.visible = true; (2)
_world.start();
}
_world.flapBird();
}
}
private function restart():void
{
_scoreLabel.visible = false; (3)
_world.reset();
}
| 1 | When a game session starts, the score must be reset to zero. |
| 2 | The label must become visible during that phase of the game. |
| 3 | At the end of the session, we hide the label. |
Again, we finished a major stepping stone of our game production! Start the game to watch the score display in all its glory.
2.10.4. Saving the Top Score
While the player now sees his current score while playing, the top score is not stored yet. As part of the title screen (which will be created in the next section), I’d like to display the current record. Let’s prepare that before moving on.
What’s special about the top score is that it should be persistent, i.e. it must outlive an application restart. Typically, this means we have to write the respective value into a file on the disk.
The ActionScript API provides a very flexible mechanism that does just that: the SharedObject. The advantage: it works both for AIR applications and within the Flash Player. Furthermore, it’s really easy to use.
Still in the Game class, create a new member variable and instantiate it in the constructor.
private var _sharedObject:SharedObject;
public function Game()
{
_sharedObject = SharedObject.getLocal("flappy-data");
}
A local shared object is identified by a simple string (in this case, flappy-data).
Contrary to what the name suggests, only the current application can access that object.
Behind the scenes, the runtime saves its data in a local file.
You can think of a shared object as a persistent Dictionary object. To access the top score easily, we wrap access to this dictionary in a new property.
private function get topScore():int
{
return int(_sharedObject.data["topScore"]);
}
private function set topScore(value:int):void
{
_sharedObject.data["topScore"] = value;
}
This is actually all we need for our data storage needs.
Of course, we also have to make use of this property somewhere.
The method onBirdCollided is a good candidate for that.
private function onBirdCollided():void
{
if (_score > topScore) (1)
topScore = _score;
Starling.juggler.delayCall(restart, 1.5);
}
| 1 | Update the top score whenever the current score beats it. |
Easy, right?
2.11. Title Screen
Believe it or not, we are through with the actual gameplay logic! The biggest remaining task is adding some kind of title screen.
When we split up the game into different phases, we already prepared one phase for this part of the game: PHASE_IDLE.
It shows the bird flying horizontally on autopilot; the actual game only starts when we touch the screen.
The title screen will simply be an overlay (a custom sprite) that’s displayed during that phase.
The main elements of this overlay are the following:
- Game Title
-
The text "Flappy Starling" displayed at the top, using our fancy bitmap font.
- Top Score
-
A TextField at the bottom showing the current score record.
- Tap Indicator
-
We need to tell the player that he is supposed to tap on the screen to start the game. A suitable image is part of the game’s assets.
2.11.1. Creating the TitleOverlay Class
Create a new class called TitleOverlay. Add the following contents:
public class TitleOverlay extends Sprite
{
private var _topScore:int; (1)
private var _topScoreLabel:TextField;
public function TitleOverlay(width:Number, height:Number)
{
var title:TextField = /* ... */;
var tapIndicator:Image = /* ... */; (2)
_topScoreLabel = /* ... */;
addChild(title);
addChild(tapIndicator); (3)
addChild(_topScoreLabel);
}
(4)
public function get topScore():int { return _topScore; }
public function set topScore(value:int):void
{
_topScore = value;
_topScoreLabel.text = "Current Record: " + value;
}
}
| 1 | The current top score, as well as the label to display it, are stored as member variables. |
| 2 | This is where we will set up the elements of the overlay. |
| 3 | Each element is added to the display list right away. |
| 4 | A property allows us to change the top score from the outside. |
This procedure starts to become second nature, right? You create a custom Sprite and add a number of display objects that make up its contents. You do that over and over when working with Starling.
So let’s create those three elements.
Title
var title:TextField = new TextField(width, 200, "Flappy\nStarling");
title.format.setTo("bradybunch", BitmapFont.NATIVE_SIZE, Color.WHITE);
title.format.leading = -20;
We are using the same bitmap font as before ("Brady Bunch").
Per default, the text is centered within the TextField, so by making it just as wide as the screen (and reasonably high), it will look nicely.
Note that I made use of the leading property to reduce the distance between the two lines.
Tap Indicator
var tapTexture:Texture = Game.assets.getTexture("tap-indicator");
var tapIndicator:Image = new Image(tapTexture);
tapIndicator.x = width / 2;
tapIndicator.y = (height - tapIndicator.height) / 2;
Here, we acquire the texture tap-indicator and use it to create an Image.
Vertically, that image is centered; horizontally, it is positioned slightly right of the center.
Score Label
_topScoreLabel = new TextField(width, 50, "");
_topScoreLabel.format.setTo(BitmapFont.MINI, BitmapFont.NATIVE_SIZE * 2);
_topScoreLabel.y = height * 0.80;
This label uses the font BitmapFont.MINI that comes bundled with Starling.
It won’t win any beauty contests, but it will do for this use-case.
Note that I multiplied its NATIVE_SIZE by two — that’s necessary because it is really a tiny font.
2.11.2. Using the TitleOverlay Class
In the Game class, we create a member variable for our new TitleOverlay and instantiate it inside the start method.
private var _title:TitleOverlay; (1)
public function start(assets:AssetManager):void
{
/* ... */
_title = new TitleOverlay(stage.stageWidth, stage.stageHeight); (2)
addChild(_title);
}
| 1 | For easy access, we store the overlay in a member variable. |
| 2 | It is instantiated with the full size of the stage. |
When you start the game now, the overlay will already be visible. It just doesn’t go away when a game session starts, which is a little annoying.
Fading in and out
During the actual gameplay, the overlay should be hidden.
We could simply set it to visible = false, but that would appear a little crude.
Instead, I’d like to fade it out gracefully.
The opacity of a display object is stored in its alpha property, which is in the range of zero (invisible) to one (opaque).
To fade out a display object, we need to animate this property slowly from one to zero.
Those types of animations are called Tweens in Starling.
The easiest way to create a tween is by calling the method Starling.juggler.tween.
For example, if you wanted to fade out a standard image, here is how you could do it:
Starling.juggler.tween(image, 1.0, { alpha: 0.0 });
-
The first argument of this method decides which object you want to animate.
-
The second argument defines the duration of the animation in seconds.
-
The final argument is a key-value pair listing the properties you want to animate, as well as the values you want to animate them to.
This call will change image.alpha from its current value to zero, over a time period of one second.
|
The Juggler
You are probably wondering what that In Starling, the description of an animation (a Tween) is separate from its execution. The execution is handled by the Juggler: it simply advances a number of animations in each frame. You can create your own jugglers (which has some advantages, as shown in the Animations chapter), but there is also a default juggler on the Starling instance. That’s the one we are using here. We briefly met the juggler already when we used its |
With this knowledge, we can write the methods showTitle and hideTitle.
private function showTitle():void
{
_title.alpha = 0.0; (1)
_title.topScore = topScore; (2)
Starling.juggler.tween(_title, 1.0, { alpha: 1.0 }); (3)
}
private function hideTitle():void
{
Starling.juggler.removeTweens(_title); (4)
Starling.juggler.tween(_title, 0.5, { alpha: 0.0 }); (5)
}
| 1 | We start with an alpha value of zero. |
| 2 | Update the topScore property so it shows the current record. |
| 3 | Fade in the title overlay over the course of one second. |
| 4 | Stop all other animations on the title overlay. |
| 5 | Fade it out over the course of half a second. |
You might wonder what the call to removeTweens is for.
When the player makes an error, the title overlay starts to fade in; this takes one second.
The player might be eager to start a new game session right away — before the overlay finished fading in!
This would make the fade-in and fade-out tweens run simultaneously, which might lead to visual glitches (and is rather pointless).
To make sure this doesn’t happen, we remove all tweens on the title overlay before starting to fade out.
|
Hiding a DisplayObject
Instead of setting the title overlay to That doesn’t matter in "Flappy Starling", but it’s a subtle difference you should be aware of. |
To make sure those animations are actually executed, we need to call our new methods at the right time. The following modifications will do the trick:
private function start(assets:AssetManager):void
{
/* ... */
showTitle(); (1)
}
private function restart():void
{
_scoreLabel.visible = false;
_world.reset();
showTitle(); (2)
}
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN);
if (touch)
{
if (_world.phase == World.PHASE_IDLE)
{
hideTitle(); (3)
/* ... */
}
_world.flapBird();
}
}
| 1 | Fade in the title when the game starts. |
| 2 | Show the title when the game is reset (PHASE_IDLE is starting). |
| 3 | Hide the title when a new game session is starting. |
Time to give the game another try! You will be pleased to see how far we have come.
2.12. Sound
Audio is an important element of every game. Music helps set up a distinctive mood — whether you want to build tension or provide comic relief. Sound effects not only make an environment more believable; they also help the player to quickly grasp what’s happening, even in areas he is not currently paying attention to. In this section, we will add a few short sound effects to Flappy Starling.
If you don’t happen to be an audio artist, you will probably ask where to get the appropriate audio files from. Thankfully, an online search will yield many resources.
-
For free music and sound effects, I can recommend Soundimage.
-
The commercial site Soundsnap offers even more variety.
Another option is to create the sound effects yourself. But wait, don’t dig up your microphone or torture a bird just yet! Actually, I’m thinking about simple synthesized sounds like they were used in old 8-bit gaming consoles. While those might not be a perfect fit for every game, they are super easy to create and, if nothing else, make great placeholders.
There is a fantastic free tool available that does this job: sfxr (or its macOS port cfxr). Its user interface is really simple: even if you don’t know exactly what’s going on (I won’t pretend that I do), you can simply generate random sounds until you find something that fits your purpose. Tweak the parameters to your needs, export the sound — and voilà, you have created a unique, royalty free sound!
After exporting the sound, you need to convert it into MP3 format; that’s the only audio format you can load in Flash and AIR. One option for doing that is the open source audio editor Audacity. You can also use it to adapt the volume or make other changes. It’s a truly powerful tool!
For our purpose, we are going to need three sounds:
| flap.mp3 |
The bird flaps upwards. |
| pass.mp3 |
The bird passes an obstacle. |
| crash.mp3 |
The bird hits an obstacle or the ground. |
Create them with the tools shown above and copy them into the directory assets/sounds within the development folder (you will need to create that directory first).
| If you just want to focus on the code for now, you can download the sounds from the game’s GitHub repository instead. |
2.12.1. Embedding Sounds
Up until now, we didn’t bother where the game’s assets actually come from; we simply used what was available in the repository’s starting point. Now, for the first time, we need to add additional files.
Find the class EmbeddedAssets.
It already contains several Embed statements that make sure that the textures and fonts are baked right into the SWF file during compilation.
The sound files will join them!
Add the following code:
[Embed(source="../assets/sounds/flap.mp3")]
public static const flap:Class;
[Embed(source="../assets/sounds/pass.mp3")]
public static const pass:Class;
[Embed(source="../assets/sounds/crash.mp3")]
public static const crash:Class;
The AssetManager will automatically pick up those new references and will make them available under the names "flap", "pass" and "crash".
|
Embedding the sounds like that is a good approach for an SWF-project (like Flappy Bird). In an AIR application (e.g. an app for iOS or Android), however, you would rather load the textures and sounds directly from disk. This has some advantages we will discuss later in the Asset Management section. |
2.12.2. Starting Playback
All that’s left for us to do is to actually start playback at the appropriate times.
Again, the Game class seems like the appropriate spot for that logic.
The method onBirdCollided will trigger the crash sound.
private function onBirdCollided():void
{
/* ... */
assets.playSound("crash"); (1)
}
| 1 | The AssetManager provides the playSound method, which will start playback right away. |
Passing an obstacle triggers an appropriate event handler, as well — do you remember?
private function onObstaclePassed():void
{
this.score += 1;
assets.playSound("pass"); (1)
}
| 1 | Start the pass sound each time an obstacle is passed. |
The flapping sound can be added to the onTouch event handler.
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN);
if (touch)
{
if (_world.phase == World.PHASE_IDLE)
{
/* ... */
}
if (_world.phase == World.PHASE_PLAYING)
{
_world.flapBird();
assets.playSound("flap"); (1)
}
}
}
| 1 | Start the flap sound each time the bird is flapping its wings. |
Note that we are starting the flap sound only when a touch occurs during PHASE_PLAYING.
Otherwise, you would hear it also when the player touches the screen during the short time when we are waiting for the game to be reset (PHASE_CRASHED).
It doesn’t make much sense in that phase.
Compile and run the game to make a proper sound check!
Believe it or not, this was the last missing piece of the Flappy Starling puzzle. The game is ready to be played and published!
2.13. Deployment
After all this hard work, it’s time to show off what we just achieved! You need to publish Flappy Starling on a web page so that your friends and colleagues can finally admire the new hit game you just created.
2.13.1. Creating the Release SWF
Up until now, we only compiled in debug mode, i.e. we created a non-optimized SWF file including debug information. That’s super useful in the development phase, but not what you want for the actual release.
Whenever you want to test the performance of a game, or if you want to release it to the public, it’s important to make a release build. When using Starling, it makes a massive difference if a project is compiled in debug or release mode. Don’t draw any conclusions about performance if you are not running the code with the optimal settings and in the optimal environment!
| For Web projects (SWF), it’s also important to always run them in the release version of the Flash Player. |
Unfortunately, creating a release build is different in each IDE you use. Make sure to familiarize yourself with the process.
-
For AIR apps, there is typically a special export menu that lets you package a project in release mode.
-
For SWF apps, it’s often enough to simply deactivate the option that controls if debug information is to be included in the output.
| The Starling Wiki contains detailed information about how this is done in each major IDE: Project Setup. |
2.13.2. Embedding the SWF in a Website
In the past, embedding Flash content inside a website could prove surprisingly difficult. Different browsers preferred different HTML elements and attributes; you needed to check if the user has a sufficient version of the Flash plugin installed, etc. Thus, most Flash developers used the JavaScript library SwfObject to work around these issues.
Today, the process has thankfully become a little easier; modern browsers essentially all support the object element to embed Flash content.
Thus, I’m typically favoring that simple approach nowadays.
Let’s start with a basic HTML5 scaffold:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flappy Starling</title>
<meta charset="UTF-8">
</head>
<body>
<!-- TODO -->
</body>
</html>
In the body element, embed the SWF file like this:
<object type="application/x-shockwave-flash" data="flappy-starling.swf"
name="flappy-starling" id="flappy-starling"
width="320" height="480">
<param name="wmode" value="direct"/>
<a href="http://www.adobe.com/go/getflash">
<img src="flappy-starling-alt.png" alt="Get Adobe Flash player"/>
</a>
</object>
-
We are passing the same values for
widthandheightas are defined in the project’s startup class (FlappyStarling.as). -
The
wmodeparameter is important for all Starling content: it basically enables Stage3D support. -
As fallback content, I added an image that links to the Flash Player download page.
Save this file as index.html and store it in the same directory as the SWF file.
You can now copy it to a web server or launch it right from your local disk.
2.13.3. Mobile Version
While all the code we wrote so far was targeting the Flash plugin, it’s actually not much work to adapt the game for mobile devices. All the essential game code stays exactly the same; only the startup phase (when Starling is initialized and the assets are loaded) needs to be changed. Now it pays off that we didn’t hard-code any positions, but placed all objects relative to the stage size!
The main challenge of the mobile version is that most mobile screens have a much higher pixel density than typical desktop screens. For this reason, I prepared high resolution versions of all the game’s textures.
Depending on the device the game is running on, we need to load the correct set of textures. Furthermore, instead of embedding all the assets, the mobile version will load them from the disk. That way, only the assets we really need will end up in memory.
We will look in detail at how all this is done in the Mobile Development chapter. If you can’t wait any longer, though, simply check out the head revision of the Flappy Starling project on GitHub. It contains a new build target for mobile devices (iOS and Android).
2.14. Summary
Pat yourself on the back: you just created a complete game! All things considered, it wasn’t so hard, was it?
Granted, Flappy Starling is not the most complex of games. But even the most extensive games will use the same basic building blocks you just became acquainted with. As long as you keep your code organized well, you will be able to tackle anything you can imagine!
It will definitely help, though, to know more about all the tools Starling has to offer. That’s why we will look at them in detail in the following sections.