This is the second of two tutorials detailing how to use Fixel to build Frogger for the web and AIR on Android.
Introduction
Welcome to the second of two tutorials detailing how to build Frogger for the Web and AIR on Android. In the previous part I talked about how to set up Flixel, change states and we built our first level for Frogger. Unfortunately there isn’t much we can do with our game without a player. This tutorial will cover the basics of movement, collision detection, managing game state and deploying our code to Android. There is a lot to cover so let’s get started!
Final Result Preview
Here is a preview of what we will build in this tutorial:
Step 1: Creating The Player
Our player represents the last actor class we need to build. Create a Frog
class and configure it like this:
You will also need to add the following code to our new class:
package com.flashartofwar.frogger.sprites { import com.flashartofwar.frogger.enum.GameStates; import com.flashartofwar.frogger.states.PlayState; import flash.geom.Point; import org.flixel.FlxG; import org.flixel.FlxSprite; public class Frog extends FlxSprite { private var startPosition:Point; private var moveX:int; private var maxMoveX:int; private var maxMoveY:int; private var targetX:Number; private var targetY:Number; private var animationFrames:int = 8; private var moveY:Number; private var state:PlayState; public var isMoving:Boolean; /** * The Frog represents the main player's character. This class contains all of the move, animation, * and some special collision logic for the Frog. * * @param X start X * @param Y start Y */ public function Frog(X:Number, Y:Number) { super(X, Y); // Save the starting position to be used later when restarting startPosition = new Point(X, Y); // Calculate amount of pixels to move each turn moveX = 5; moveY = 5; maxMoveX = moveX * animationFrames; maxMoveY = moveY * animationFrames; // Set frog's target x,y to start position so he can move targetX = X; targetY = Y; // Set up sprite graphics and animations loadGraphic(GameAssets.FrogSpriteImage, true, false, 40, 40); addAnimation("idle" + UP, [0], 0, false); addAnimation("idle" + RIGHT, [2], 0, false); addAnimation("idle" + DOWN, [4], 0, false); addAnimation("idle" + LEFT, [6], 0, false); addAnimation("walk" + UP, [0,1], 15, true); addAnimation("walk" + RIGHT, [2,3], 15, true); addAnimation("walk" + DOWN, [4,5], 15, true); addAnimation("walk" + LEFT, [6,7], 15, true); addAnimation("die", [8, 9, 10, 11], 2, false); // Set facing direction facing = FlxSprite.UP; // Save an instance of the PlayState to help with collision detection and movement state = FlxG.state as PlayState; } /** * This manage what direction the frog is facing. It also alters the bounding box around the sprite. * * @param value */ override public function set facing(value:uint):void { super.facing = value; if (value == UP || value == DOWN) { width = 32; height = 25; offset.x = 4; offset.y = 6; } else { width = 25; height = 32; offset.x = 6; offset.y = 4; } } /** * The main Frog update loop. This handles keyboard movement, collision and flagging id moving. */ override public function update():void { //Default object physics update super.update(); } /** * Simply plays the death animation */ public function death():void { } /** * This resets values of the Frog instance. */ public function restart():void { } /** * This handles moving the Frog in the same direction as any instance it is resting on. * * @param speed the speed in pixels the Frog should move * @param facing the direction the frog will float in */ public function float(speed:int, facing:uint):void { } } }
I am not going to go through all the code since it is commented, but here are a few key points to check out. First we setup all the values for the frog’s movement in the constructor along with its animations. Next we create a setter to handle changing the frog’s orientation when we change directions. Finally we have a few helper methods to manage the update loop, death, restart, and floating.
Now we can add our player to the game level. Open up the PlayState
class and put the following code in between the logs and cars we setup in part one.
// Create Player player = add(new Frog(calculateColumn(6), calculateRow(14) + 6)) as Frog;
You will also need to import the Fog class and add the following property:
private var player:Frog;
It is important to place the player at the right depth level in the game. He needs to be above the logs and turtles yet below the cars so when he gets run over so he doesn’t show up on top of a car. We can now test that our player is on the level by recompiling the game. Here is what you should see:
There isn’t much we can do with the player until we add some keyboard controls, so let’s move onto the next step.
Step 2: Keyboard Controls
In Frogger the player can move left, right, up and down. Each time the player moves, the frog jumps to the next position. In normal tile based games this is easy to set up. We simply figure out the next tile to move to and add to the x or y value until it reaches the new destination. Frogger however adds some complexity to the movement when it comes to the logs and turtles you can float on. Since these objects don’t move according to the grid we need to pay extra attention to how the boarders of our level work.
Let’s get started with basic controls. Open up the Frog
class and add the following block of code to our update()
method before super.update()
:
if (state.gameState == GameStates.PLAYING) { // Test to see if TargetX and Y are equal. If so, Frog is free to move. if (x == targetX && y == targetY) { // Checks to see what key was just pressed and sets the target X or Y to the new position // along with what direction to face if ((FlxG.keys.justPressed("LEFT")) && x > 0) { targetX = x - maxMoveX; facing = LEFT; } else if ((FlxG.keys.justPressed("RIGHT")) && x < FlxG.width - frameWidth) { targetX = x + maxMoveX; facing = RIGHT; } else if ((FlxG.keys.justPressed("UP")) && y > frameHeight) { targetY = y - maxMoveY; facing = UP; } else if ((FlxG.keys.justPressed("DOWN")) && y < 560) { targetY = y + maxMoveY; facing = DOWN; } // See if we are moving if (x != targetX || y != targetY) { //Looks like we are moving so play sound, flag isMoving and add to score. FlxG.play(GameAssets.FroggerHopSound); // Once this flag is set, the frog will not take keyboard input until it has reacged its target isMoving = true; } else { // Nope, we are not moving so flag isMoving and show Idle. isMoving = false; } } // If isMoving is true we are going to update the actual position. if (isMoving == true) { if (facing == LEFT) { x -= moveX; } else if (facing == RIGHT) { x += moveX; } else if (facing == UP) { y -= moveY; } else if (facing == DOWN) { y += moveY; } // Play the walking animation play("walk" + facing); } else { // nothing is happening so go back to idle animation play("idle" + facing); } }
There is a lot of code here so let’s go through it block by block starting with our first conditional. We need a way to know if the game state is set to playing. If you look at the end of the Frog
class’s constructor you will see that we save a reference to the current play state in a local variable. This is a neat trick we can use to help our game objects read game state from the active FlxState. Since we know the frog is always going to be used in the PlayState
it is safe to assume that the correct state of the game is set to PlayState
. Now that we have this FlxState we can check the game’s actual state before doing anything in the FrogClass
.
I just want to take a second to clear up some terminology. The word state has several connotations in our FlxFrogger game. There are FlxStates
with represent the current screen or display of the game and then there is game state. Game state represents the actual activity the game is currently in. Each of the game states can be found in the GameState
class inside of the com.flashartofwar.frogger.enum
package. I will do my best to keep the two uses of state as clear as possible.
So back to our frog. If the game state is set to “playing” then it is safe to detect any movement request for the frog and also update the its animation. You will see later on how we handle death animations and freezing the player based on collisions. Our first block of code determines if the Frog has reached a targetX and targetY position. The second block of code actually handles increasing the frog’s x and y values.
Now we can talk about how to actually move the frog. This is the next conditional within out targetX/Y block. Once a keystroke has been detected and the new targetX/Y values have been set we can immediately validate that we are moving. If so we need to play the frog hop sound and set the frog isMoving
flag to true. If these values have not changes we are not moving. After this is set we can handle movement logic in the last conditional.
Finally we test if isMoving
is true and see what direction we need to move based on which way the frog is facing. The frog can only move in one direction at a time so this makes it very easy to set up. We also call the play()
method to animate the frog. The last thing you should know is how we calculate the moveX
and moveY
vales. If you look in the constructor you will see that we determine that the frog will move X number of pixels over Y number of frames. In this case we want our animation to last 8 frames so in order to move the 40 pixels we need each time, we will move by 5px each frame for 8 frames. This is how we get the smooth hop animation and keep the player from continually pressing the keys and interrupting the animation.
Step 3: Car Collision Detection
Flixel handles all of the collision logic we will need for us. This is a huge time saver and all you have to do is ask the FlxU
class to detect any overlapping sprites. What is great about this method is that you can test FlxSprites or FlxSpriteGroups. If you remember we set up our cars in their own carGroup
which will make it incredibly easy to test if they have collided with the frog. In order to get this started we need to open up the PlayState
and add the following code:
/** * This is the main game loop. It goes through, analyzes the game state and performs collision detection. */ override public function update():void { if (gameState == GameStates.GAME_OVER) { } else if (gameState == GameStates.LEVEL_OVER) { } else if (gameState == GameStates.PLAYING) { // Do collision detections FlxU.overlap(carGroup, player, carCollision); } else if (gameState == GameStates.DEATH_OVER) { } // Update the entire game super.update(); }
This includes a little more of the core game state logic which we will fill out in the next few steps but let’s take a look at the code under the “Do collision detections” comment. As you can see we have a new class called FlxU
which helps manage collision detection in the game as I had mentioned above. This class accepts targetA, targetB and a callback method. The overlap()
method will test all of the children of targetA against the children of targetB then pass the results to your supplied callback method. Now we need to add the carCollision
callback to our class:
/** * This handles collision with a car. * @param target this instance that has collided with the player * @param player a reference to the player */ private function carCollision(target:FlxSprite, player:Frog):void { if (gameState != GameStates.COLLISION) { FlxG.play(GameAssets.FroggerSquashSound); killPlayer(); } }
We are taking a few liberties with this callback since we know that the first param will be a FlxSprite
and that the second one is a Frog
. Normally this is an anonymous and untyped method when it is called once a collision is detected. One thing you will notice is that we test that the gameStates
is set to collision. We do this because when a collision happens the entire game freezes to allow a death animation to play but technically the frog is still colliding with the carGroup. If we didn’t add in this conditional we would be stuck in an infinite loop while the animation tried to play.
Now we just need to add the logic for killing the player. This will go in a killPlayer()
method and here is the code:
/** * This kills the player. Game state is set to collision so everything knows to pause and a life is removed. */ private function killPlayer():void { gameState = GameStates.COLLISION; player.death(); }
Finally we need to fill in the death()
logic in our Frog
class. Add the following code to the death method:
play("die");
Here we are telling the frog sprite to play the die animation we setup in the constructor. Now compile the game and test that the player can collide with any of the cars or truck.
Step 4: Restarting After A Death
Now that we have our first set of collision detection in place along with the death animation, we need a way to restart the game level so the player can continue to try again. In order to do this we need to add a way to notify the game when the death animation is over. Let’s add the following to our Frog update()
method just above where we test if the gameState
is equal to playing:
// Test to see if the frog is dead and at the last death frame if (state.gameState == GameStates.COLLISION && (frame == 11)) { // Flag game state that death animation is over and game can perform a restart state.gameState = GameStates.DEATH_OVER; } else
Notice the trailing else
, that should run into if (state.gameState == GameStates.PLAYING)
so that we are testing for collision then playing state.
Now we need to go back into our PlayState
class and add the following method call to else if (gameState == GameStates.DEATH_OVER)
:
restart();
Now we can add a restart method:
/** * This handles resetting game values when a frog dies, or a level is completed. */ private function restart():void { // Change game state to Playing so animation can continue. gameState = GameStates.PLAYING; player.restart(); }
Last thing we need to do is add this code to the Frog
Class’s restart
method.
isMoving = false; x = startPosition.x; y = startPosition.y; targetX = startPosition.x; targetY = startPosition.y; facing = UP; play("idle" + facing); if (!visible) visible = true;
Now we should be ready to compile and test that all of this works. Here is what you should see, when a car hits the player everything freezes while the death animation plays. When the animation is over everything should restart and the player will show up at the bottom of the screen again. Once you have that set up we are ready to work out the water collision detection.
Step 5: Water Collision Detection
With a solid system in place to handle collision, death animation and restarting it should be really easy to add in the next few steps of collision detection. The water detection is a little different however. Since we always know where the water is, we can test against the player’s y position. If the player’s y value is greater then where the land ends, we can assume the player has collided with the water. We do this to help cut down on the amount of collision detection we would need to do if we tested each open space of water when the frog lands. Let’s add the following to our PlayState
under where we test for a car collision:
// If nothing has collided with the player, test to see if they are out of bounds when in the water zone if (player.y < waterY) { if (!player.isMoving && !playerIsFloating) waterCollision(); if ((player.x > FlxG.width) || (player.x < -TILE_SIZE )) { waterCollision(); } }
You will also need to add two properties, the first lets us know where the water begins on the Y axis and the other is a boolean to let us know if the player is floating. We’ll be using this floating boolean in the next step.
private var waterY:int = TILE_SIZE * 8; private var playerIsFloating:Boolean;
Determining the start coordinate of the water is easy considering everything is part of the grid. Next we need to add our waterCollision()
method:
/** * This is called when the player dies in water. */ private function waterCollision():void { if (gameState != GameStates.COLLISION) { FlxG.play(GameAssets.FroggerPlunkSound); killPlayer(); } }
Compile and test that if we go into the water the player dies.
Next we will look into how to allow the frog to float on the logs and turtles when they are within the water area of the level.
Step 6: Floating Collision Detection
In order to figure out if the Frog can float we need to test for a collision on the logGroup
or the turtleGroup
. Let’s add the following code in our PlayState
class just under where we test for the car collision. Make sure it is above the water collision conditional. This is important because if we test for the player in the water before the logs or turtles we could never handle the floating correctly.
FlxU.overlap(logGroup, player, float); FlxU.overlap(turtleGroup, player, turtleFloat);
Here are the two methods we need to handle a collision:
/** * This handles floating the player sprite with the target it is on. * @param target this is the instance that collided with the player * @param player an instance of the player */ private function float(target:WrappingSprite, player:Frog):void { playerIsFloating = true; if (!(FlxG.keys.LEFT || FlxG.keys.RIGHT)) { player.float(target.speed, target.facing); } } /** * This is called when a player is on a log to indicate the frog needs to float * @param target this is the instance that collided with the player * @param player an instance of the player */ private function turtleFloat(target:TimerSprite, player:Frog):void { // Test to see if the target is active. If it is active the player can float. If not the player // is in the water if (target.isActive) { float(target, player); } else if (!player.isMoving) { waterCollision(); } }
You will also need to import WrappingSprite
and TimerSprite
. Once you have that in place we need to go back into our Frog
class and add the following to our float()
method:
if (isMoving != true) { x += (facing == RIGHT) ? speed : -speed; targetX = x; isMoving = true; }
We just added a lot of code and I commented most of it, but I wanted to talk about this last part right here. This code actually handles moving the frog in the same direction with the same speed as the log or turtle the player is on. We use a few tricks to make sure that when the player attempts to move off the floating object they are not overridden by what they are floating on. A big part of game development is state management and using flags such as isMoving
to help let other parts of the code know what can and can’t be done are a huge help.
Let’s compile the game and check out if the player is able to float on logs.
One thing you may have noticed is that once you land on a log or turtle you will no longer drown. That is because we need to reset the playerIsFloating
flag before we do all of our collision detection. Go back into the PlayState
and add the following just before we start testing for the car collision.
// Reset floating flag for the player. playerIsFloating = false;
So your test block should look like this:
As I have already mentioned, there is a delicate balance of maintaining state and making sure you set and unset these state flags at the right time in order to save yourself a lot of frustration when building your own games. Now you can do a new compile to make sure everything is working correctly and we can move onto the next step.
Step 7: Home Collision Detection
Adding in collision detection for the home bases should be very straight forward. Add the following collision test to the end of the collision detection code in our PlayState
class and above where we test if the player has jumped into the water:
FlxU.overlap(homeBaseGroup, player, baseCollision);
Now we need to create our baseCollision()
method to handle what happens when you land on a home:
/** * This handles collision with a home base. * @param target this instance that has collided with the player * @param player a reference to the player */ private function baseCollision(target:Home, player:Frog):void { // Check to make sure that we have not landed in a occupied base if (target.mode != Home.SUCCESS) { // Increment number of frogs saved safeFrogs ++; // Flag the target as success to show it is occupied now target.success(); } // Test to see if we have all the frogs, if so then level has been completed. If not restart. if (safeFrogs == bases.length) { levelComplete(); } else { restart(); } }
We will also need to add the following property to our class:
private var safeFrogs:int = 0;
Here you can see we are testing to see if the home has already been landed in, next we test if all of the homes have frogs in them and finally we just trigger restart on a successful landing at the home base. It is important to note that in this tutorial we are not testing for the state of the home. Remember there is a bonus fly and an alligator you could land on. Later if you want to add that in you can do it here.
Now we need to add some logic for when a level has been completed:
/** * This is called when a level is completed */ private function levelComplete():void { // Change game state to let system know a level has been completed gameState = GameStates.LEVEL_OVER; // Hide the player since the level is over and wait for the game to restart itself player.visible = false; }
We need to add the following code to the restart()
method above where we reset the game state:
// Test to see if Level is over, if so reset all the bases. if (gameState == GameStates.LEVEL_OVER) resetBases();
Finally we need to add a resetBases()
method to our PlayState
class:
/** * This loops through the bases and makes sure they are set to empty. */ private function resetBases():void { // Loop though bases and empty them for each (var base:Home in bases) { base.empty(); } // Reset safe frogs safeFrogs = 0; }
When all of the bases have been landed on we loop through them and call the empty()
method which will reset the graphic and landed value. Finally, we need to set the frogs that have been saved to zero. Let’s compile the game and test what happens when you land in all the bases. After you land in a home base it should change to a frog icon indicating you have landed there already.
As you can see, when we have saved all the frogs the level freezes because there is no logic to restart the level again. We also need to add some game messaging to let the player know what is going on and to use as a notification system in the game so we can manage when to restart all the animations again. This is what we will add in the next step.
Step 8: Adding Game Messages
A lot happens in the game and one of the best ways to communicate to the player what is going on is by adding in game messaging. This will help inform the player when game pauses to activate a game state such as a level complete or a death. In our PlayState
class, add the following code above where we create our home bases in the create()
method:
// Create game message, this handles game over, time, and start message for player gameMessageGroup = new FlxGroup(); gameMessageGroup.x = (480 * .5) - (150 * .5); gameMessageGroup.y = calculateRow(8) + 5; add(gameMessageGroup); // Black background for message var messageBG:FlxSprite = new FlxSprite(0, 0); messageBG.createGraphic(150, 30, 0xff000000); gameMessageGroup.add(messageBG); // Message text messageText = new FlxText(0, 4, 150, "TIME 99").setFormat(null, 18, 0xffff00000, "center"); gameMessageGroup.visible = false; gameMessageGroup.add(messageText);
You will also need to add the following properties:
private var messageText:FlxText; private var gameMessageGroup:FlxGroup; private var hideGameMessageDelay:int = -1;
You will also need to import the FlxText
class. Now let’s go into our update()
method and add the following code into the gameState == GameStates.LEVEL_OVER
conditional:
if (hideGameMessageDelay == 0) { restart(); } else { hideGameMessageDelay -= FlxG.elapsed; }
The basic idea here is that we use a timer to count down how long a game message should be displayed. When the timer reaches zero we can restart the level. This gives the player some time to rest in between levels. Also we will need to add the next block of code just below where we test the water collision around line 203:
// Manage hiding gameMessage based on timer if (hideGameMessageDelay > 0) { hideGameMessageDelay -= FlxG.elapsed; if (hideGameMessageDelay < 0) hideGameMessageDelay = 0; } else if (hideGameMessageDelay == 0) { hideGameMessageDelay = -1; gameMessageGroup.visible = false; }
Here we are able to manage the visibility of the game message. In our baseCollision()
method we need to add the following code below where we test for target.mode != Home.success
conditional around line 317:
// Regardless if the base was empty or occupied we still display the time it took to get there messageText.text = "TIME " + String(gameTime / FlxG.framerate - timeLeftOver); gameMessageGroup.visible = true; hideGameMessageDelay = 200;
Add the following properties which we will actually use in the next step:
private var gameTime:int = 0; private var timer:int = 0;
Then add this line of code around line 328 inside the conditional just under where we call success()
on the target:
var timeLeftOver:int = Math.round(timer / FlxG.framerate);
This allows us to calculate the total time it has taken to complete a label which we will connect in the next step. Now in resetBases()
add the following code at the end just under where we set safeFrogs
to 0:
// Set message to tell player they can restart messageText.text = "START"; gameMessageGroup.visible = true; hideGameMessageDelay = 200;
Compile the game and you should now see game status messages when you land in a home or restart the level.
Step 9: Game Timer
Now we are ready to add the logic for the game timer. In the PlayState
class’s create()
method add the following code under where we create the bg image:
// Set up main variable properties gameTime = 60 * FlxG.framerate; timer = gameTime;
You will need the following property and constant:
private var timeAlmostOverWarning:int; private const TIMER_BAR_WIDTH:int = 300;
And below the last car added to the carGroup
add the following:
// Create Time text timeTxt = new FlxText(bg.width - 70, LIFE_Y + 18, 60, "TIME").setFormat(null, 14, 0xffff00, "right"); add(timeTxt); // Create timer graphic timerBarBackground = new FlxSprite(timeTxt.x - TIMER_BAR_WIDTH + 5, LIFE_Y + 20); timerBarBackground.createGraphic(TIMER_BAR_WIDTH, 16, 0xff21de00); add(timerBarBackground); timerBar = new FlxSprite(timerBarBackground.x, timerBarBackground.y); timerBar.createGraphic(1, 16, 0xFF000000); timerBar.scrollFactor.x = timerBar.scrollFactor.y = 0; timerBar.origin.x = timerBar.origin.y = 0; timerBar.scale.x = 0; add(timerBar);
You will also need the following properties:
private const LIFE_X:int = 20; private const LIFE_Y:int = 600; private const TIMER_BAR_WIDTH:int = 300; private var timerBarBackground:FlxSprite; private var timeTxt:FlxText; private var timerBar:FlxSprite; private var timeAlmostOverFlag:Boolean = false;
Now let’s add the following code in our update function above where we manage hiding the gameMessage on line 234:
// This checks to see if time has run out. If not we decrease time based on what has elapsed // sine the last update. if (timer == 0 && gameState == GameStates.PLAYING) { timeUp(); } else { timer -= FlxG.elapsed; timerBar.scale.x = TIMER_BAR_WIDTH - Math.round((timer / gameTime * TIMER_BAR_WIDTH)); if (timerBar.scale.x == timeAlmostOverWarning && !timeAlmostOverFlag) { FlxG.play(GameAssets.FroggerTimeSound); timeAlmostOverFlag = true; } }
And this is the method that gets called when time is up:
/** * This is called when time runs out. */ private function timeUp():void { if (gameState != GameStates.COLLISION) { FlxG.play(GameAssets.FroggerSquashSound); killPlayer(); } }
Finally we need to reset the timer when the time runs up, the player lands on a home base or the level restarts. We can do this in our restart()
method just before we call player.restart()
:
timer = gameTime; timeAlmostOverFlag = false;
You can compile the game now to validate all of this works. We just added a lot of code but hopefully the code and comments are straight forward enough that we don’t need to explain it too much.
Step 10: Lives
What game would be complete without lives? In frogger you get 3 lives. Let’s add the following function call in our create()
method under where we set up the timeAlmostOverWarning = TIMER_BAR_WIDTH * .7
on line 69:
createLives(3);
And here are the methods that will manage lives for us:
/** * This loop creates X number of lives. * @param value number of lives to create */ private function createLives(value:int):void { var i:int; for (i = 0; i < value; i++) { addLife(); } } /** * This adds a life sprite to the display and pushes it to teh lifeSprites array. * @param value */ private function addLife():void { var flxLife:FlxSprite = new FlxSprite(LIFE_X * totalLives, LIFE_Y, GameAssets.LivesSprite); add(flxLife); lifeSprites.push(flxLife); } /** * This removes the life sprite from the display and from the lifeSprites array as well. * @param value */ private function removeLife():void { var id:int = totalLives - 1; var sprite:FlxSprite = lifeSprites[id]; sprite.kill(); lifeSprites.splice(id, 1); } /** * A simple getter for Total Lives based on life sprite instances in lifeSprites array. * @return */ private function get totalLives():int { return lifeSprites.length; }
Also make sure you add the following property:
private var lifeSprites:Array = [];
Again these methods are well commented, but the basic idea is that we keep all of our lives in an array. We start off by adding a life to the array based on the value passed in. To remove lives we simply splice 1 out of the array. This is a quick way to handle lives by taking advantage of some native classes such as the Array. Now we just need to set up some logic to remove a life when you die.
Add the following call to our killPlayer()
method above where we set player.death()
:
removeLife();
Now you can test that when you die a life should be removed from the display.
Make sure you don’t go past the total lives or you will get an error. In the next step we will add in some game over logic to prevent this error from happening.
Step 11: Game Over Screen
We can quickly test to see if the player is out of lives and the game is over in our restart()
method. Let’s replace the entire method with the following:
/** * This handles resetting game values when a frog dies, or a level is completed. */ private function restart():void { // Make sure the player still has lives to restart if (totalLives == 0 && gameState != GameStates.GAME_OVER) { gameOver(); } else { // Test to see if Level is over, if so reset all the bases. if (gameState == GameStates.LEVEL_OVER) resetBases(); // Change game state to Playing so animation can continue. gameState = GameStates.PLAYING; timer = gameTime; player.restart(); timeAlmostOverFlag = false; } }
Here you see we are testing to see if the total lives equal zero and the game state is set to game over. If so we can call the game over method. If not, it is business as usual and the level gets restarted. Now we need to add a gameOver()
method:
/** * This is called when a game is over. A message is shown and the game locks down until it is ready to go * back to the start screen */ private function gameOver():void { gameState = GameStates.GAME_OVER; gameMessageGroup.visible = true; messageText.text = "GAME OVER"; hideGameMessageDelay = 100; }
Now, before we can see this in action, we just need to add a few more lines of code to our update()
method to handle removing the game over message and returning the player to the StartState
. Look for where we test for gameState == GameStates.GAME_OVER
and add the following code into the conditional:
if (hideGameMessageDelay == 0) { FlxG.state = new StartState(); } else { hideGameMessageDelay -= FlxG.elapsed; }
Now you can test the game over message by killing the player 3 times. You should see this before getting thrown back to the StartState
.
Now that all of the pieces are in place we can easily add in scoring.
Step 12: Score
We need to add up the score when the player jumps, lands in a base and finishes a level. Flixel makes it easy to remember a score and you can access it at any time by using the FlxG.score
property on the FlxG
singleton. First we need to create a class to store some score values for us. Create a class called ScoreValues
and configure it like this:
Here is the code for the class:
package com.flashartofwar.frogger.enum { /** * These represent the values when scoring happens. */ public class ScoreValues { public static const STEP:uint = 10; public static const REACH_HOME:uint = 50; public static const FINISH_LEVEL:uint = 1000; public static const TIME_BONUS:uint = 10; } }
Now go back into the PlayState
class and add the following above our gameMessageGroup code in the create()
method around line 77:
FlxG.score = 0; var scoreLabel:FlxText = add(new FlxText(0, 30, 100, "Score").setFormat(null, 10, 0xffffff, "right")) as FlxText; scoreTxt = add(new FlxText(0, scoreLabel.height, 100, "").setFormat(null, 14, 0xffe00000, "right")) as FlxText; scoreTxt.text = FlxG.score.toString();
You will need the following property:
private var scoreTxt:FlxText;
Let’s add some scoring to our game in the following places.
After line 407 where we calculate the time left over in the target.mode != Home.Success
conditional:
// Increment the score based on the time left FlxG.score += timeLeftOver * ScoreValues.TIME_BONUS;
Make sure you import the ScoreValues
class. Next we will add the following to our levelComplete()
method:
//Increment the score based on FlxG.score += ScoreValues.FINISH_LEVEL;
Now we need to go into our Frog
class and add the following to our update()
method where we set isMoving
to true on line 141:
// Add to score for moving FlxG.score += ScoreValues.STEP;
Before we can test this we need to update the score in our game loop. Let’s go back into the PlayState
class and add the following to our update()
method on line 281 just before where we test for gameState == GameStates.DEATH_OVER
:
// Update the score text scoreTxt.text = FlxG.score.toString();
Compile the game and make sure the score is working.
Step 13: Building for Mobile
Now that everything is in place we can easily compile and deploy FlxFrogger to an android device which has AIR installed. Instead of going through how to set up the pre-release of AIR and the Android SDK I just want to focus on how to get this project ready to compile and deploy. Before we jump in you should check out this excellent post by Karl Freeman on how to get FDT configured to build Air for Android.
You will need to have AIR 2.5 setup in your Flex SDK directory and a copy of the Android SDK on your computer. If you remember in part one our Ant script is pointed to a Flex SDK so we can compile. We need to setup where the Android SDK is so we can deploy our Air apk to the phone. Open up the build.properties
file and look for the android.sdk
property.
You will need to point this to where you downloaded your Android SDK. Here is what my path looks like:
android.sdk= /Users/jfreeman/Documents/AndroidDev/android-sdk-mac_86-2.2
As you can see I have renamed it to let me know it is the 2.2 build. I like to keep back ups of my SDKs based on the version number so this is a good habit to pick up. Let’s open up the Run External Tools Configuration where we set up our original build target in part 1.
Right-click on our build and select “duplicate”. Rename the build copy to FlxFrogger Android Build
. Now click on it and and let’s change the default target to deploy-to-phone
.
Now you have a new run for compiling and deploying to an Android phone. If your phone is connected and you would like to test that it works, simply run the Ant task and wait for it to transfer over. Remember you need to have Air installed on your phone for it to run.
One last thing. You may have noticed that there is also a deploy-to-emulator
target. You can use this if you want to test with the Android Emulator but be warned that the emulator is incredibly slow. I found it almost unbearable for getting any real sense of how Flash ran on the phone so don’t be surprised if you get under 3-4 fps.
Step 14: Touch Controls
By default Flixel is setup to work with the keyboard but on mobile devices you may only have a touchscreen to work with. Setting up touch controls is very easy, we will create our own touch buttons out of FlxSprites
in the next step. Create a new class called TouchControls
and configure it like this:
package com.flashartofwar.frogger.controls { import flash.events.KeyboardEvent; import org.flixel.FlxG; import org.flixel.FlxGroup; import org.flixel.FlxState; import org.flixel.FlxText; import org.flixel.FlxSprite; public class TouchControls extends FlxGroup { /*private var spriteButtons[0]:FlxSprite; private var spriteButtons[1]:FlxSprite; private var spriteButtons[2]:FlxSprite; private var spriteButtons[3]:FlxSprite;*/ private var spriteButtons:Array; /** * Touch controls are special buttons that allow virtual input for the game on devices without a keyboard. * * @param target Where should the controls be added onto * @param x x position to display the controls * @param y y position to display the controls * @param padding space between each button */ public function TouchControls(target:FlxState, x:int, y:int, padding:int) { this.x = x; this.y = y; var txt:FlxText; spriteButtons = new Array(4); //spriteButtons[0] = new FlxSprite(x, y) spriteButtons[0] = new FlxSprite(0, 0) spriteButtons[0].color =0x999999; spriteButtons[0].createGraphic(100, 100); add(spriteButtons[0]); txt = new FlxText(0, 30, 100, "UP").setFormat(null, 20, 0xffffff, "center"); add(txt); spriteButtons[1] = new FlxSprite(spriteButtons[0].right + padding, 0) spriteButtons[1].color =0x999999; spriteButtons[1].createGraphic(100, 100); add(spriteButtons[1]); txt = new FlxText(spriteButtons[1].x, 30, 100, "DOWN").setFormat(null, 20, 0xffffff, "center"); add(txt); spriteButtons[2] = new FlxSprite(spriteButtons[1].right + padding, 0) spriteButtons[2].color =0x999999; spriteButtons[2].createGraphic(100, 100); add(spriteButtons[2]); txt = new FlxText(spriteButtons[2].x, 30, 100, "LEFT").setFormat(null, 20, 0xffffff, "center"); add(txt); spriteButtons[3] = new FlxSprite(spriteButtons[2].right + padding, 0) spriteButtons[3].color =0x999999; spriteButtons[3].createGraphic(100, 100); add(spriteButtons[3]); txt = new FlxText(spriteButtons[3].x, 30, 100, "RIGHT").setFormat(null, 20, 0xffffff, "center"); add(txt); } public function justPressed(button:Number):Boolean { return FlxG.mouse.justPressed() && spriteButtons[button].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y); } public function justReleased(button:Number):Boolean { return FlxG.mouse.justReleased() && spriteButtons[button].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y); } override public function update():void { if (FlxG.mouse.justPressed()) { if (spriteButtons[0].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y)) { spriteButtons[0].color = 0xff0000; } else if (spriteButtons[1].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y)) { spriteButtons[1].color = 0xff0000; } else if (spriteButtons[2].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y)) { spriteButtons[2].color = 0xff0000; } else if (spriteButtons[3].overlapsPoint(FlxG.mouse.x, FlxG.mouse.y)) { spriteButtons[3].color = 0xff0000; } } else if (FlxG.mouse.justReleased()) { spriteButtons[0].color = 0x999999; spriteButtons[1].color = 0x999999; spriteButtons[2].color = 0x999999; spriteButtons[3].color = 0x999999; } super.update(); //Passing update up to super } } }
Go into the Frog
class and replace the block of code where we test for key presses with the following code:
if ((FlxG.keys.justPressed("LEFT") || (touchControls != null && touchControls.justPressed(2))) && x > 0) { targetX = x - maxMoveX; facing = LEFT; } else if ((FlxG.keys.justPressed("RIGHT") || (touchControls != null && touchControls.justPressed(3))) && x < FlxG.width - frameWidth) { targetX = x + maxMoveX; facing = RIGHT; } else if ((FlxG.keys.justPressed("UP") || (touchControls != null && touchControls.justPressed(0))) && y > frameHeight) { targetY = y - maxMoveY; facing = UP; } else if ((FlxG.keys.justPressed("DOWN") || (touchControls != null && touchControls.justPressed(1))) && y < 560) { targetY = y + maxMoveY; facing = DOWN; }
As you can see, in addition to testing for key presses we will directly check our TouchControls
to see if they have been pressed. You will also need to import the TouchControls
class and add the following property:
public var touchControls:TouchControls;
Now, in order to show touch controls when you are compiling for a mobile device, we are going to use a compiler conditional. Add the following code to the PlayState
class in the create()
method just before where we set the gameState
to playing:
// Mobile specific code goes here /*FDT_IGNORE*/ CONFIG::mobile { /*FDT_IGNORE*/ touchControls = new TouchControls(this, 10, calculateRow(16) + 20, 16); player.touchControls = touchControls; add(touchControls); /*FDT_IGNORE*/ } /*FDT_IGNORE*/
I have added some special comments in here to help keep FDT from throwing an error since it doesn’t understand compiler conditionals yet. Here is what the code looks like without these special FDT ignore comments:
CONFIG::mobile { touchControls = new TouchControls(this, 10, calculateRow(16) + 20, 16); player.touchControls = touchControls; add(touchControls); }
Also make sure you add the following property and import TouchControls
:
private var touchControls : TouchControls;
As you can see a compiler conditional is just like a normal if
statement. Here we are just testing that if the value of CONFIG::mobile
is set to true then we are building for mobile and, if so, it should show the controls. Telling the compiler about this config variable is all handled in our build script so you don’t have to worry about anything. Depending on what type of build target you call, the value is changed when compiling. It couldn’t be easier. This is a great technique to use when you have mobile specific code you need to execute that you wouldn’t want to run in your web based version.
You can test these controls by deploying to the phone, here is what you should see:
If you test in the browser you will not see the controls. Hopefully you can see the power of compiler conditionals and how much of an advantage they will be when deploying content over multiple devices and platforms.
Step 15: Optimizations
Optimizing for mobile is a very time consuming process. Luckily this game runs very well on any Android phone with a 1ghz processor or faster. It also helps that we chose to build a game with a very low frame rate. A few things I have noticed which would help give the impression that the game was actually running faster is to speed up the time it takes to move the frog from tile to tile and also speed up the game time and game over wait delay.
Something else you may want to try is to group objects that need to have collision detection together by row instead of type. So right now all of the cars and trucks are being tested as one large group. Since the frog moves vertically along the grid we could speed up the collision detection by only testing a row at a time. Even though this is a much better approach to handling lots of collision detection, I would be surprised if you gain any extra frames per second. Air on Android executes code surprisingly well, the real bottleneck is in the renderer.
To address the slow Flash player renderer you could downscale the images. We built this game at the full 480 x 800 resolution. If we tweaked this to run at half of the pixel size it would be less overhead for Flash to render and may give us a few extra frames per second. Doing something like this would be incredibly time consuming and may not be worth the extra work. With any type of optimization you should have a set list of devices or computer specs to test against and try to build for the lowest common denominator. When building Flash games for web, desktop and mobile it is always best to start with a mobile version since you will hardly see much change in the desktop and web playback.
Conclusion What’s Next?
Right now you have a full Frogger game engine. You can create a new level, move within the level, score and detect when a level is complete. The next thing you should add on your own are additional levels. The game is set up in a clean way that creating new levels based on this template shouldn’t require much work. You may want to break out the level creation code into it’s own class so you can load up specific levels when you need them.
Also you could add a multiplier to the default speed each object gets when they are created inside of the PlayState
so that each new level tells the game actors to move faster and faster.
Finally you could completely re-skin the game and make it your own. There are a lot of Frogger clones out there and since all of the heavy lifting has been done for you, the sky is the limit. If you want to add onto this project and fill in some of the missing features I invite you to fork the source code on GitHub and let me know what additions you make. I’ll be happy to add them to the base project and give you credit.
If you run into any problems or have questions, leave a comment below and I will do my best to answer them. Thanks for reading 🙂