Getting Started
With Flash Player and the Flex SDK Adobe has supplied everything you need to start creating your own Flash games. Creating a Flash game using Flex has several advantages like:
- Cross platform compatibility – there is a flash player for every major platform
- Easy deployment – publishing the game is as simple as uploading the swf file
- (Almost) zero installation requirements – an end user only needs a web browser with the Flash player plugin installed
- Free tools - all you need is the free Flex SDK and a text editor
This article series will step you through the process of creating a 2D Flash game using Flex, with the result being a complete game in the style of the old school top down shooters.
To get started you will first need to download the Flex 3 SDK from http://www.adobe.com/products/flex/flexdownloads/. The SDK contains all the tools you will need to compile the source code presented here into a SWF file that can be added to a web page. You will also need a decent text editor. I quite like Textpad, which can be downloaded from http://www.textpad.com/. Finally you will need to download a Flash debug player from http://www.adobe.com/support/flashplayer/downloads.html. The debug player will allow you to open up a SWF file directly without having to create a web page to contain it. With these three tools you are ready to start coding.
The graphics used in the game are courtsey of spritelib, which you can download from http://www.flyingyogi.com/fun/spritelib.html.
Creating the Application
Conceptually Flex splits up an average program into two sections: the GUI and the Actionscript code. The GUI is created in an MXML file, which is an XML file that contains user interface elements nested in tags very similar to HTML. Note that the MXML file can contain Actionscript code inside an mx:Script tag, but the main focus of the MXML file is to define the user interface.
The top level tag of an MXML file is the mx:Application tag. This Application object is the entry point of the Flex application, and is the most logical place to start.
main.mxml Actionscript Source Code
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
width="600"
height="400"
frameRate="100"
creationComplete="CreationComplete()"
enterFrame="EnterFrame(event)">
We start by defining some of the properties of the Application object. These properties can be set through attributes in the mx:Application tag. This should look familiar to anyone who has written HTML.
width and height
Specify the screen size of the program in pixels.
framerate
Specifies a limit on the frames per second. The default is 24, but since we want the game to run as fast as possible it’s best to override this with a much higher number. Note that just setting the framerate to 100 doesn’t guarantee that the frame rate will always be 100 (or even get anywhere near it). This property just sets a ceiling on what the frame rate could be.
creationComplete
Attaches a function to be called when the Application has been created. We use this as the entry point in the program.
enterFrame
Attaches a function to be called every time the screen is redrawn. We will use this to repaint the screen with the next frame of the game.
The mx:Script tag gives us a place to write some Actionscript code. The [CDATA[ ]] tag just means that any special characters inside the mx:Script tag will be interpreted as text rather than XML characters. Inside the mx:Script tag we need to add the two functions which match the values for the creationComplete and enterFrame properties.
Compiling and Running
To compile the program you need to run the command mxmlc main.mxml from the command prompt. You can then open up the resulting main.swf file in the Flash debug player through File->Open.
And the end result? A blank screen that does nothing. Not terribly exciting I’ll admit, but it is a start. We will build off this code in part 2 of the series to start drawing to the screen.
You can play the Flash game at http://flexfighters.sourceforge.net/flexfighters1.html, and download the source code from https://sourceforge.net/project/showfiles.php?group_id=241490&package_id=293860&release_id=631084.
In part one of the series we created the initial Flex application class. In part two we will be adding states and a double buffered rendering process.
States are quite self explanatory: they represent the different states that a program can be in. For example a shopping cart might have one state for browsing the store and another state while looking at the details of a particular item. Our game will also have a number of states which will include the main menu, the game play itself, the end of level summary and maybe a high score screen.
Flex includes native support for states. These states were designed with a transition from one GUI to another in mind, but they have the functionality we need to change between states that don’t necessarily have any GUI components. Modifying the currentState property of the Application will trigger a state change, and by adding the required startup and shutdown code to the functions associated with the enterState and exitState events we can update the internal game state to match.
Double Buffering is a technique used to remove the visual tearing associated with drawing directly to the screen. It gets its name because you use two graphics buffers to draw the final image: one that resides in-memory (the back buffer), and the buffer which is displayed on the screen (front buffer). You can think of the back buffer as a kind of scratch pad which is built up as the individual elements that make up the final scene write to it. Once a frame has been drawn it is copied to the front buffer in one operation. The screen then displays the contents of the front buffer.
So let’s look at how these concepts are implemented in Flex.
main.mxml
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
width="600"
height="400"
frameRate="100"
creationComplete="creationComplete()"
enterFrame="enterFrame(event)"
currentState="MainMenu">
name="Game"
enterState="enterGame(event)"
exitState="exitGame(event)">
The first thing to notice is the addition of the currentState property to the mx:Application element. As mentioned above, the currentState property of the Application object defines the current state of the program. By setting it to MainMenu through this attribute we are saying that the program should start in the MainMenu state.
We have also added a mx:States element. This element defines the states that the program can take through the child mx:State elements. We have defined two states to start with: MainMenu and Game. In the MainMenu state the end user will see the starting screen of the game. The Game state represents the gameplay itself.
Both mx:State elements have a name property. This property is the name of the state, and by changing the currentState property of the Application object to this name we can transition into the state. The Game state also includes two more properties: enterState and exitState. By associating functions with these events we have an opportunity to manually “sync” the internal game logic to this state. As you can see we use the EnterGame function to startup the GameObjectManager (more on that class later) and set the internal flag inGame to true. The inGame flag is used during the rendering loop to allow the game to draw to the screen. The ExitGame function simply sets the inGame flag to false, which allows a GUI to be displayed.
Remember how I mentioned that the states in Flex were designed with GUI transitions in mind? The MainMenu state shows how easy this is. The mx:AddChild element is used to add a GUI element to the state. In this case we use it to add a button that the player can click to get into the game. But as soon as we leave the MainMenu state Flex will automatically remove the button without any additional code or effort.
To allow us to render to the screen we have added a mx:Canvas element. The canvas (or more specifically its graphics property) will serve as the front buffer in the double buffering rendering process. The back buffer exists in the GameObjectManager class. During the enterFrame function we call the GameObjectManager’s enterFrame function, which will allow it to draw the back buffer. Once the frame has been drawn we take the back buffer and draw it onto the canvas using the clear, beginBitmapFill, drawRect and endFill functions of the canvas’s graphics property.
GameObjectManager.as
package
{
import mx.core.*;
import mx.collections.*;
import flash.display.*;
public class GameObjectManager
{
// double buffer
public var backBuffer:BitmapData;
// colour to use to clear backbuffer with
public var clearColor:uint = 0xFF0043AB;
/// static instance
protected static var instance:GameObjectManager = null;
// the last frame time
protected var lastFrame:Date;
static public function get Instance():GameObjectManager
{
if ( instance == null )
instance = new GameObjectManager();
return instance;
}
public function GameObjectManager()
{
if ( instance != null )
throw new Error( "Only one Singleton instance should be instantiated" );
backBuffer = new BitmapData(Application.application.width, Application.application.height, false);
}
public function startup():void
{
lastFrame = new Date();
}
public function shutdown():void
{
}
public function enterFrame():void
{
// Calculate the time since the last frame
var thisFrame:Date = new Date();
var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
lastFrame = thisFrame;
drawObjects();
}
protected function drawObjects():void
{
backBuffer.fillRect(backBuffer.rect, clearColor);
}
}
}
The GameObjectManager object will be responsible for managing the elements that will make up the final game like the enemies, the player and the various background elements. It is also responsible for managing the back buffer to which these elements will draw themselves to. If you recall the front buffer was implemented as a canvas element. This was for convenience as a canvas can be added directly as a child of the Application object. The back buffer is implemented as a BitmapData object, which allows us to quickly and directly manipulate the pixels that make up the final image.
The clearColor property specifies the colour that will be used to wipe the back buffer before the scene is built up. Eventually the entire back buffer will be overwritten by the game elements, which makes this color irrelevant, but for now it is quite important because it will make up a good chunk of the final frame. The value 0xFF0043AB is a dark blue. The first two hex values (those after the 0x) represent the alpha: FF for opaque and 00 for transparent. The next 6 hex values make up the red (00), green (43) and blue (AB) components.
The static instance property is used with the Instance function to implement the Singleton design pattern. Basically we only ever want one GameObjectManager to exist in the program (hence the name), and by referencing the GameObjectManager through this instance property we can be assured that only one GameObjectManager will ever be created. The Singleton design is quite a common programming paradigm, and while ActionScript lacks support for a protected constructor it still useful as a self documentation tool (if you ever see an Instance property, chances are that the object is designed as a Singelton).
The lastFrame property simply stores the time when the last frame was renedered. By keeping a track of this time we can determine how long it has taken between the last frame and this current one, which (will eventually) in turn allows us to update the game elements by this amount. Even though we don’t have any game elements yet the time between frames is calculated in seconds during the enterFrame function. The lastFrame time is reset during a call to startup. This is because the GameObjectmanager is not updated while the program is not in the Game state. If we didn’t reset lastFrame the first frame of the next level would be equal to the time the player spent in the menus inbetween levels. The player could end up jumping halfway across the level during the first frame, which is definitely best avoided.
So, what have we achieved here? By implementing states we have created a menu screen, from which the player can enter the game by clicking a button. The “game” itself is just an implantation of a double buffer, which draws a blue background. However with the states and rendering implemented we can finally get to the fun stuff: drawing to the screen.
When writing a new program there is always a point where you first get to see the fruits of your labour. With the state and rendering “plumbing” now done we can start doing some fun stuff by adding graphics to our game and displaying them on the screen. But before we do let’s take a look at what changes we have to make in the main.mxml file.
main.mxml
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
width="600"
height="400"
frameRate="100"
creationComplete="creationComplete()"
enterFrame="enterFrame(event)"
currentState="MainMenu">
name="Game"
enterState="enterGame(event)"
exitState="exitGame(event)">
There is only one change made to the exitState function which calls the GameObjectManager shutdown function. This will allow the GameObjectManager to clean up its resources when we leave the Game state. Now let’s move onto the changes to the GameObjectManager.
The majority of Flash games tend to be quite simple in nature. They are 5 – 10 minute distractions that are played during lunch, or when the boss isn’t looking. This simplicity is also reflected in the typical control scheme employed by Flash games: mouse input with a single left click. It’s intuitive (someone who will only devote a few minutes of their time to playing the game isn’t going to want to read a help page with a complicated control scheme), and Flash doesn’t let you (easily) work with the right mouse button anyway. Thankfully our top down shooter style of game play lends itself nicely to this simple control scheme. The player will simply move the mouse around the screen to move the players ship and click the left mouse button to fire weapons. But before we can create a game object that represents the players ship we first need a way of detecting where the mouse has been moved to, and when the mouse button has been clicked. Lets look at what changes have to be made to the main.mxml file to accommodate this.
main.mxml
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
width="600"
height="400"
frameRate="100"
creationComplete="creationComplete()"
enterFrame="enterFrame(event)"
click="click(event)"
mouseDown="mouseDown(event)"
mouseUp="mouseUp(event)"
mouseMove="mouseMove(event)"
currentState="MainMenu">
name="Game"
enterState="enterGame(event)"
exitState="exitGame(event)">
We have added functions to listen to 4 new events: mouseMove, click, mouseDown and mouseUp. MouseMove is, as you would expect, called when the mouse has been moved. It allows us to monitor the position of the mouse cursor over the flash player window. Similarly click monitors when the mouse button has been clicked (i.e. pressed and released). When the mouse button is pressed mouseDown is fired, and when it is released mouseUp is fired. We need the ability to monitor the mouseDown and mouseUp events individually (as opposed to the click event which fired after the mouse has been pressed and then released) because eventually we want the player to fire weapons when the mouse button is pressed and stop when it is released.
The four new functions have the same name as the event as their respective event, and simply pass the message along to the GameObjectManager.
GameObjectManager.as
package
{
import flash.display.*;
import flash.events.*;
import mx.collections.*;
import mx.core.*;
public class GameObjectManager
{
// double buffer
public var backBuffer:BitmapData;
// colour to use to clear backbuffer with
public var clearColor:uint = 0xFF0043AB;
// static instance
protected static var instance:GameObjectManager = null;
// the last frame time
protected var lastFrame:Date;
// a collection of the GameObjects
protected var gameObjects:ArrayCollection = new ArrayCollection();
// a collection where new GameObjects are placed, to avoid adding items
// to gameObjects while in the gameObjects collection while it is in a loop
protected var newGameObjects:ArrayCollection = new ArrayCollection();
// a collection where removed GameObjects are placed, to avoid removing items
// to gameObjects while in the gameObjects collection while it is in a loop
protected var removedGameObjects:ArrayCollection = new ArrayCollection();
static public function get Instance():GameObjectManager
{
if ( instance == null )
instance = new GameObjectManager();
return instance;
}
public function GameObjectManager()
{
if ( instance != null )
throw new Error( "Only one Singleton instance should be instantiated" );
backBuffer = new BitmapData(Application.application.width, Application.application.height, false);
}
public function startup():void
{
lastFrame = new Date();
}
public function shutdown():void
{
shutdownAll();
}
public function enterFrame():void
{
// Calculate the time since the last frame
var thisFrame:Date = new Date();
var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
lastFrame = thisFrame;
removeDeletedGameObjects();
insertNewGameObjects();
Level.Instance.enterFrame(seconds);
// now allow objects to update themselves
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse)
gameObject.enterFrame(seconds);
}
drawObjects();
}
public function click(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.click(event);
}
}
public function mouseDown(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseDown(event);
}
}
public function mouseUp(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseUp(event);
}
}
public function mouseMove(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseMove(event);
}
}
protected function drawObjects():void
{
backBuffer.fillRect(backBuffer.rect, clearColor);
// draw the objects
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse)
gameObject.copyToBackBuffer(backBuffer);
}
}
public function addGameObject(gameObject:GameObject):void
{
newGameObjects.addItem(gameObject);
}
public function removeGameObject(gameObject:GameObject):void
{
removedGameObjects.addItem(gameObject);
}
protected function shutdownAll():void
{
// don't dispose objects twice
for each (var gameObject:GameObject in gameObjects)
{
var found:Boolean = false;
for each (var removedObject:GameObject in removedGameObjects)
{
if (removedObject == gameObject)
{
found = true;
break;
}
}
if (!found)
gameObject.shutdown();
}
}
protected function insertNewGameObjects():void
{
for each (var gameObject:GameObject in newGameObjects)
{
for (var i:int = 0; i <>
{
if (gameObjects.getItemAt(i).zOrder > gameObject.zOrder ||
gameObjects.getItemAt(i).zOrder == -1)
break;
}
gameObjects.addItemAt(gameObject, i);
}
newGameObjects.removeAll();
}
protected function removeDeletedGameObjects():void
{
// insert the object acording to it's z position
for each (var removedObject:GameObject in removedGameObjects)
{
var i:int = 0;
for (i = 0; i <>
{
if (gameObjects.getItemAt(i) == removedObject)
{
gameObjects.removeItemAt(i);
break;
}
}
}
removedGameObjects.removeAll();
}
}
}
As you can see GameObjectManager has 4 new functions that reflect the 4 new mouse events in the Application object. The GameObjectManager in turn loops over the gameObjects collection and passes the message to any GameObject that is currently active.
At this point in the series we have implemented enough underlying code to make adding new elements to the game quite easy. With GameObjectManager and GameObject classes handling the work of drawing and updating the game elements and the Level in place to actually create the new elements, there is only a minimal amount of code required to implement new game elements. We will take advantage of this to add some enemy fighters to the game, and give the player some weapons to fight them with. First, let’s take a look at the changes that have been made to the Level class.
Level.as
package
{
import flash.events.*;
import flash.geom.*;
import flash.media.*;
import flash.net.*;
import flash.utils.*;
import mx.collections.ArrayCollection;
import mx.core.*;
public class Level
{
protected static var instance:Level = null;
protected static const TimeBetweenLevelElements:Number = 2;
protected static const TimeBetweenEnemies:Number = 3;
protected static const TimeBetweenClouds:Number = 2.5;
protected var timeToNextLevelElement:Number = 0;
protected var levelElementGraphics:ArrayCollection = new ArrayCollection();
protected var timeToNextEnemy:Number = 0;
protected var enemyElementGraphics:ArrayCollection = new ArrayCollection();
protected var timeToNextCloud:Number = 0;
static public function get Instance():Level
{
if ( instance == null )
instance = new Level();
return instance;
}
public function Level(caller:Function = null )
{
if ( Level.instance != null )
throw new Error( "Only one Singleton instance should be instantiated" );
levelElementGraphics.addItem( ResourceManager.SmallIslandGraphics);
levelElementGraphics.addItem( ResourceManager.BigIslandGraphics);
levelElementGraphics.addItem( ResourceManager.VolcanoIslandGraphics);
enemyElementGraphics.addItem( ResourceManager.SmallBluePlaneGraphics);
enemyElementGraphics.addItem( ResourceManager.SmallGreenPlaneGraphics);
enemyElementGraphics.addItem( ResourceManager.SmallWhitePlaneGraphics);
}
public function startup():void
{
timeToNextLevelElement = 0;
new Player().startupPlayer();
}
public function shutdown():void
{
}
public function enterFrame(dt:Number):void
{
// add a background element
timeToNextLevelElement -= dt;
if (timeToNextLevelElement <= 0)
{
timeToNextLevelElement = TimeBetweenLevelElements;
var graphics:GraphicsResource = levelElementGraphics.getItemAt( MathUtils.randomInteger(0, levelElementGraphics.length)) as GraphicsResource;
var backgroundLevelElement:BackgroundLevelElement = BackgroundLevelElement.pool.ItemFromPool as BackgroundLevelElement;
backgroundLevelElement.startupBackgroundLevelElement(
graphics,
new Point(Math.random() * Application.application.width, -graphics.bitmap.height),
ZOrders.BackgoundZOrder,
50);
}
// add an enemy
timeToNextEnemy -= dt;
if (timeToNextEnemy <= 0)
{
timeToNextEnemy = TimeBetweenEnemies;
var enemygraphics:GraphicsResource = enemyElementGraphics.getItemAt( MathUtils.randomInteger(0, enemyElementGraphics.length)) as GraphicsResource;
var enemy:Enemy = Enemy.pool.ItemFromPool as Enemy;
enemy.startupBasicEnemy(
enemygraphics,
new Point(Math.random() * Application.application.width, -enemygraphics.bitmap.height),
55);
}
// add cloud
timeToNextCloud -= dt;
if (timeToNextCloud <= dt)
{
timeToNextCloud = TimeBetweenClouds;
var cloudBackgroundLevelElement:BackgroundLevelElement = BackgroundLevelElement.pool.ItemFromPool as BackgroundLevelElement;
cloudBackgroundLevelElement. startupBackgroundLevelElement(
ResourceManager.CloudGraphics,
new Point(Math.random() * Application.application.width, -ResourceManager.CloudGraphics.bitmap.height),
ZOrders.CloudsBelowZOrder,
75);
}
}
}
}
The timeToNextEnemy / TimeBetweenEnemies and timeToNextCloud / TimeBetweenClouds pair of properties serve the same function as the timeToNextLevelElement / TimeBetweenLevelElements pair of properties (which was detailed in part 4) except they are used to add enemies and clouds respectively. The enemyElementGraphics property is used for the same purpose as the levelElementGraphics (which was also detailed in part 4) except here is it used to provide GraphicsResource’s when creating new enemies.
We have also added two new blocks of code in the enterFrame function to create the enemies and the clouds. This code is almost exactly the same as the block of code that is used to create the BackgroundLevelElement’s.
The clouds are implemented as a BackgroundLevelElement, except with a slightly faster scroll rate and a higher zOrder. This gives them the appearance of being at a higher altitude.
A new class, Enemy, has been created to represent the enemy fighters. Let’s look at that code now.
Enemy.as
package
{
import flash.geom.Point;
import mx.core.*;
public class Enemy extends GameObject
{
static public var pool:ResourcePool = new ResourcePool(NewEnemy);
protected var logic:Function = null;
protected var speed:Number = 0;
static public function NewEnemy():Enemy
{
return new Enemy();
}
public function Enemy()
{
super();
}
public function startupBasicEnemy(graphics:GraphicsResource, position:Point, speed:Number):void
{
super.startupGameObject(graphics, position, ZOrders.PlayerZOrder);
logic = basicEnemyLogic;
this.speed = speed;
}
override public function shutdown():void
{
super.shutdown();
logic = null;
}
override public function enterFrame(dt:Number):void
{
if (logic != null)
logic(dt);
}
protected function basicEnemyLogic(dt:Number):void
{
if (position.y > Application.application.height + graphics.bitmap.height )
this.shutdown();
position.y += speed * dt;
}
}
}
This code is very similar to the BackgroundLevelElement code. In fact the only difference is that we have separated the enemy logic into its own function called basicEnemyLogic. We save a reference to the basicEnemyLogic function in the logic property (much like we reference the NewEnemy function in the ResourcePool). This may seem redundant, but will later on allow us to implement new types of enemies by creating new logic functions. The startupBasicEnemy function is used to initialise the underlying GameObject and setup point the logic property to the basicEnemyLogic function, which will n turn be called during the render loop (i.e. the enterFrame function).
With those few simple changes we now have enemies created at specific intervals during the game. The next step is to create some weapons so the player can shoot them. To do this we need to create a Weapon class. Let’s look at the code for that.
Weapon.as
package
{
import flash.geom.*;
public class Weapon extends GameObject
{
static public var pool:ResourcePool = new ResourcePool(NewWeapon);
protected var logic:Function = null;
protected var speed:Number = 0;
static public function NewWeapon():Weapon
{
return new Weapon();
}
public function Weapon()
{
super();
}
public function startupBasicWeapon(graphics:GraphicsResource, position:Point, speed:Number):void
{
super.startupGameObject(graphics, position, ZOrders.PlayerZOrder);
logic = basicWeaponLogic;
this.speed = speed;
}
override public function shutdown():void
{
super.shutdown();
logic = null;
}
override public function enterFrame(dt:Number):void
{
if (logic != null)
logic(dt);
}
protected function basicWeaponLogic(dt:Number):void
{
if (position.y < -graphics.bitmap.height)
this.shutdown();
position.y -= speed * dt;
}
}
}
Player.as
package
{
import flash.events.*;
import flash.geom.*;
import mx.core.*;
public class Player extends GameObject
{
protected static const TimeBetweenShots:Number = 0.25;
protected var shooting:Boolean = false;
protected var timeToNextShot:Number = 0;
public function Player()
{
}
public function startupPlayer():void
{
startupGameObject(ResourceManager.BrownPlaneGraphics, new Point(Application.application.width / 2, Application.application.height / 2), ZOrders.PlayerZOrder);
shooting = false;
timeToNextShot = 0;
}
override public function shutdown():void
{
super.shutdown();
}
override public function enterFrame(dt:Number):void
{
super.enterFrame(dt);
timeToNextShot -= dt;
if (timeToNextShot <= 0 && shooting)
{
timeToNextShot = TimeBetweenShots;
var weapon:Weapon = Weapon.pool.ItemFromPool as Weapon;
weapon.startupBasicWeapon(
ResourceManager.TwoBulletsGraphics,
new Point(
position.x + graphics.bitmap.width / 2 - ResourceManager.TwoBulletsGraphics.bitmap.width / 2,
position.y - graphics.bitmap.height + ResourceManager.TwoBulletsGraphics.bitmap.height * 2),
150);
}
}
override public function mouseMove(event:MouseEvent):void
{
// move player to mouse position
position.x = event.stageX;
position.y = event.stageY;
// keep player on the screen
if (position.x <>
position.x = 0;
if (position.x > Application.application.width - graphics.bitmap.width)
position.x = Application.application.width - graphics.bitmap.width;
if (position.y <>
position.y = 0;
if (position.y > Application.application.height - graphics.bitmap.height )
position.y = Application.application.height - graphics.bitmap.height ;
}
override public function mouseDown(event:MouseEvent):void
{
shooting = true;
}
override public function mouseUp(event:MouseEvent):void
{
shooting = false;
}
}
}
We have added the shooting property. When set to true (in the mouseDown function, which is called when the left mouse button is pressed down) the Player will periodically add create new instances of the Weapon class. The mouseUp (which is called when the left mouse button is released) sets shooting to false, and the Player stops creating new Weapons. The timeToNextShot / TimeBetweenShots pair of properties are used in the timing of the creation of the new Weapon objects.
By creating two new classes (Weapon and Enemy), and adding some slight changes to the Level and Player classes we are almost at the point where we have a playable game. You will notice that you can’t actually shoot the enemies though. That requires something that we will add in part 6: collision detection.
Collision detection is the ability to detect when two objects have collided, and then react appropriately. In part 5 we gave the player the ability to fire weapons at the oncoming enemies. The only problem was that these bullets just passed right through the enemies. In part 6 we will add the necessary code to implement collection detection, which will allow us to shoot the enemies down. Collision detection is one of those seemingly simple concepts that can be incredibly difficult to implement. You will find entire books devoted to the topic of testing for intersections between 2D and 3D shapes. Fortunately for us our collision detection will be quite simple. Each object on the screen will have a rectangular area in which collisions will be detected (the "collision area"). For simplicity this area will be the same as the size of the underlying graphic that is used to display the sprite on the screen. Once these rectangles overlap we will detect a collision. In order to get the best results these graphics should cropped as closely to the image that is to be displayed as possible. Below is an example of two images that could be used in the game. Both will display exactly the same way because the space around the plane is transparent. However the top image is more optimized for the collision detection system though because it is closely cropped to the actual image of the plane. The bottom image would appear to collide with other objects before it should because the collision detection system doesn't take any notice of the transparent border, and assumes that the entire area of the image will be used as the collision area. So lets take a look at the changes that are needed in the GameObject class to implement collision detection. In part 6 we introduced collision detection, which allowed us to destroy and crash into the enemies on the screen. But the enemies just disappeared, which was a little unsatasifying. In this article we will add animations, which in turn will allow us to include some nice explosions. If there is one thing Flash is famous for, it's animation. Flex does allow you to use the same vector based animations that have been so popular with Flash, however for our game we will be using good old frame based animations. There are two reasons for this. The first is that frame based animation is easier to create, and more applicable to the style of game we are making. The second is that I am not an artist and have to make do with the free artwork that I can find out on the web, which in this case is frame based :). The image above shows what I mean by frame based animation. It is a series of frames that are displayed in sequence to produce an animation. Have you ever drawn a little animation in the corners of your textbook at school? It's the same concept. To implement animaions we first need to make some changes to the GraphicsResource class. Lets take a loot at that now. package { import flash.display.*; public class GraphicsResource { public var bitmap:BitmapData = null; public var bitmapAlpha:BitmapData = null; public var frames:int = 1; public var fps:Number = 0; public function GraphicsResource(image:DisplayObject, frames:int = 1, fps:Number = 0) { bitmap = createBitmapData(image); bitmapAlpha = createAlphaBitmapData(image); this.frames = frames; this.fps = fps; } protected function createBitmapData(image:DisplayObject):BitmapData { var bitmap:BitmapData = new BitmapData(image.width, image.height); bitmap.draw(image); return bitmap; } protected function createAlphaBitmapData(image:DisplayObject):BitmapData { var bitmap:BitmapData = new BitmapData(image.width, image.height); bitmap.draw(image, null, null, flash.display.BlendMode.ALPHA); return bitmap; } } } The changes here are quite simple. We add two properties: frames and fps. The frames property is a count of how many frames of animation are included in the image held by the GraphicsResource. Using the explosion image above as an example, the frames would be set to 7. The fps property defines the frames per second that the animation will play. The GameObject class does not know about animations. If you initialized a GameObject with the image above, the entire image would be displayed on the screen rather than an animated sequence. To enable animations we need to create a new class: AnimatedGameObject. Lets look at that code now.GraphicsResource.as
In part 7 we added the ability to play animations, which then gave us some nice explosions. At this point we almost have something resembling a game, except for one: sound. In this article we will add some sound and music to the game.
Fortunately for us Flex makes embedding, playing and transforming sounds very easy. The first logical place to start is by adding some sounds to our ResourceManager.
ResourceManager.as
package
{
import flash.display.*;
import mx.core.*;
public final class ResourceManager
{
[Embed(source="../media/brownplane.png")]
public static var BrownPlane:Class;
public static var BrownPlaneGraphics:GraphicsResource = new GraphicsResource(new BrownPlane(), 3, 20);
[Embed(source="../media/smallgreenplane.png")]
public static var SmallGreenPlane:Class;
public static var SmallGreenPlaneGraphics:GraphicsResource = new GraphicsResource(new SmallGreenPlane(), 3, 20);
[Embed(source="../media/smallblueplane.png")]
public static var SmallBluePlane:Class;
public static var SmallBluePlaneGraphics:GraphicsResource = new GraphicsResource(new SmallBluePlane(), 3, 20);
[Embed(source="../media/smallwhiteplane.png")]
public static var SmallWhitePlane:Class;
public static var SmallWhitePlaneGraphics:GraphicsResource = new GraphicsResource(new SmallWhitePlane(), 3, 20);
[Embed(source="../media/bigexplosion.png")]
public static var BigExplosion:Class;
public static var BigExplosionGraphics:GraphicsResource = new GraphicsResource(new BigExplosion(), 7, 20);
[Embed(source="../media/smallisland.png")]
public static var SmallIsland:Class;
public static var SmallIslandGraphics:GraphicsResource = new GraphicsResource(new SmallIsland());
[Embed(source="../media/bigisland.png")]
public static var BigIsland:Class;
public static var BigIslandGraphics:GraphicsResource = new GraphicsResource(new BigIsland());
[Embed(source="../media/volcanoisland.png")]
public static var VolcanoIsland:Class;
public static var VolcanoIslandGraphics:GraphicsResource = new GraphicsResource(new VolcanoIsland());
[Embed(source="../media/twobullets.png")]
public static var TwoBullets:Class;
public static var TwoBulletsGraphics:GraphicsResource = new GraphicsResource(new TwoBullets());
[Embed(source="../media/cloud.png")]
public static var Cloud:Class;
public static var CloudGraphics:GraphicsResource = new GraphicsResource(new Cloud());
[Embed(source="../media/gun1.mp3")]
public static var Gun1Sound:Class;
public static var Gun1FX:SoundAsset = new Gun1Sound() as SoundAsset;
[Embed(source="../media/explosion.mp3")]
public static var ExplosionSound:Class;
public static var ExplosionFX:SoundAsset = new ExplosionSound() as SoundAsset;
[Embed(source="../media/track1.mp3")]
public static var Track1Sound:Class;
public static var Track1FX:SoundAsset = new Track1Sound() as SoundAsset;
}
}
Here we embed the sound files just like we do for our graphics. This packages the sounds in the final SWF file giving us a convenient way to distribute and access them.
Now that we have made the sounds available we need to play them. Using the sound effects here we have an explosion that will be played when the player or an enemy dies, a sound for when the player fires weapons, and some background music to play while in the game.
Lets take a look at the Enemy class to see how to play a sound effect.
To this point our "level" has really just been one endless stream of randomly placed enemies. Obviously this is not ideal as it gives us as the developer no control over how the level plays out. In order for us to provide a structured level design we need a way to define and save that level structure. It sounds like a simple challenge, but we have many options each with their own advantages and disadvantages. My first instinct was to define the level in XML. Actionscript has excellent support for XML, allowing you to declare an XML variable directly in the code. Similarly Actionscript also provides an easy interface for traversing an XML document. The downside to using XML is that you need to supply the code to interpret the XML nodes. For exampe you may have an XML node that defines an enemy placement. To turn this node into an actual object the XML attributes or child nodes need to be parsed, stored and then passed into the function used to actually create the enemy object. While not a difficult task, it is tedious to write. Thankfully Actionscript gives us another possibility. By using a Function object Actionscript can treat any function as an object, which can then be passed around and stored like any other object. What's more we can assign anonymous functions to the Function constructor. We can use this to create an anonymous function that directly creates a new enemy object, which is then stored in a Function object to be called at a certain point during the level. This sounds complicated, but will become clearer with some example code. The first class we need to create to store our level definitions is the LevelDefinitionElement. Lets look at that code now. package { public class LevelDefinitionElement { public var time:Number = 0; public var func:Function = null; public function LevelDefinitionElement(time:Number, func:Function) { this.time = time; this.func = func; } static public function sort(objectA:LevelDefinitionElement, objectB:LevelDefinitionElement):int { if (objectA.time <> if (objectA.time == objectB.time) return 0; return 1; } } } The purpose of this class is to save a Function object that will be called after a certain amount of time has passed in the level. For example you might want to create an enemy fighter that appears 10 seconds into the game. It has two properties: time and func. The time property defines the point during the level when the func Function will be called. The func property contains a function to be executed. This function can do anything from creating a new enemy, creating a new background element, playing a sound effect etc. In fact because we can assign any function to this property we have increadible flexibility over how we define the level structure. The sort function is used to sort the LevelDefinitionElements in an array, with those with a smaller time appearing before those with a larger time. The LevelDefinitions class will serve as a container for the many LevelDefinitionElements that we will eventually need to define a cmplete level structure. Lets look at that code now. In part 9 of the series we added the ability to define a level structure using a series of timed function calls. This works well for placing enemies on the screen, but is not so useful for drawing the background level. In this article we will add the ability to render a predefined tiled background. Tiled backgrounds are made up of a handful of smaller repeated tiles arranged, in this case, in a grid. This approach has a number of advantages, but the biggest is that it reduces the memory requirements of the game. Using one single prerendered background image could conceivably take up several megabytes of memory per level. Of course using a prerendered background would give you the highest level of detail, but all the detail in the world doesn’t matter if the player clicks off the page because they are sick of waiting for the game to load. By contrast a tiled background will take up a fraction of the memory used by a prerendered background, and a good artist can still make some nice looking tiled backgrounds. The first step in making a tiled background are the tiles themselves. I found a nice set of free tiled wilderness graphics from http://lostgarden.com/labels/free%20game%20graphics.html. The site also has another of other tiled resources sets which you may find interesting. The next step is finding a level editor that will allows us to draw the levels through a graphical interface. Of course you could write your own (that’s another article series in itself), but luckily someone has already done the hard work for us. The TaT tile map editor from http://kotisivu.dnainternet.net/ttilli/tilemapeditor/download.htm will do the job nicely. It has a few nice features like layers and XML exporting that we will make use of. And of course we need to add some code to render the tiled background within the game. First we need some way to hold the tiles background data. The TiledBackgroundDefinition class takes care of that for us. Lets look at the Actionscript code for that class now. package { public class TiledBackgroundDefinition { public var tiles:Array = null; public var tileScrollRate:Number = 0; public var tileWidth:int = 0; public var tileHeight:int = 0; } } The tiles property is a multidimensional array that will contain references to the GraphicsResources that will be used to draw the background. When populated the tiles array will contain a dimension for the layers, then the rows and then finally the columns. For example tiles[1][4][5] would point to the GraphicsResource for the sixth column (tiles[1][4][5]) of the fifth row (tiles[1][4][5]) of the second layer (tiles[1][4][5]) – remember that arrays have zero based indexers. The tileWidth and tileHeight properties define the size in pixels of the tiles that make up the level. And tileScrollRate defines the speed at which the tiled level scrolls underneath the player to give the illusion of movement. Now that we have a way to define a tiled background we need a place to store the definitions. The LevelDefinitions class will be used to store these definitions. Lets take a look at the new Actionscript code for the LevelDefinitions class now.LevelDefinitionElement.as
TiledBackgroundDefinition.as
No comments:
Post a Comment