Flash Game Development with Flex and Actionscript - Defining a Level

Flash Game Development with Flex and Actionscript - Defining a Level
Page content

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 example 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 < objectB.time) return -1;

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 incredible 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.

LevelDefinitions.as

package

{

import flash.geom.*;

import flash.utils.Dictionary;

public class LevelDefinitions

{

protected static var instance:LevelDefinitions = null;

protected var levelDefinitions:Dictionary = new Dictionary();

static public function get Instance():LevelDefinitions

{

if ( instance == null )

instance = new LevelDefinitions();

return instance;

}

public function LevelDefinitions()

{

}

public function addLevelDefinition(levelID:int, element:LevelDefinitionElement):void

{

if (levelDefinitions[levelID] == null)

levelDefinitions[levelID] = new Array();

(levelDefinitions[levelID] as Array).push(element);

levelDefinitions[levelID].sort(LevelDefinitionElement.sort);

}

public function getNextLevelDefinitionElements(levelID:int, lastTime:Number):Array

{

var returnArray:Array = new Array();

var nextTime:Number = -1;

if (levelDefinitions[levelID] != null)

{

for each (var levelDefElement:LevelDefinitionElement in levelDefinitions[levelID])

{

if (levelDefElement.time > lastTime && nextTime == -1)

{

returnArray.push(levelDefElement);

nextTime = levelDefElement.time;

}

else if (levelDefElement.time == nextTime)

{

returnArray.push(levelDefElement);

}

else if (levelDefElement.time > nextTime && nextTime != -1)

break;

}

}

return returnArray.length == 0?null:returnArray;

}

public function getNextLevelID(levelID:int):int

{

if (levelDefinitions[levelID + 1] == null) return 0;

return levelID + 1;

}

public function startup():void

{

GameObjectManager.Instance.addCollidingPair( CollisionIdentifiers.PLAYER, CollisionIdentifiers.ENEMY);

GameObjectManager.Instance.addCollidingPair( CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYERWEAPON);

GameObjectManager.Instance.addCollidingPair( CollisionIdentifiers.PLAYER, CollisionIdentifiers.ENEMYWEAPON);

LevelDefinitions.Instance.addLevelDefinition(

1,

new LevelDefinitionElement(

1,

function():void {(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallBluePlaneGraphics,

new Point(100, -ResourceManager. SmallBluePlaneGraphics.bitmap.height),

55);}));

LevelDefinitions.Instance.addLevelDefinition(

1,

new LevelDefinitionElement(

4,

function():void

{

for each (var xPos:int in [150, 350])

{

(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallBluePlaneGraphics,

new Point(xPos, -ResourceManager. SmallBluePlaneGraphics.bitmap.height),

55);

}

}

));

LevelDefinitions.Instance.addLevelDefinition(

1,

new LevelDefinitionElement(

5,

function():void {(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallBluePlaneGraphics,

new Point(500, -ResourceManager. SmallBluePlaneGraphics.bitmap.height),

55);}));

LevelDefinitions.Instance.addLevelDefinition(

2,

new LevelDefinitionElement(

1,

function():void {(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallGreenPlaneGraphics,

new Point(100, -ResourceManager. SmallGreenPlaneGraphics.bitmap.height),

55);}));

LevelDefinitions.Instance.addLevelDefinition(

2,

new LevelDefinitionElement(

3,

function():void

{

for each (var xPos:int in [150, 350])

{

(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallGreenPlaneGraphics,

new Point(xPos, -ResourceManager. SmallGreenPlaneGraphics.bitmap.height),

55);

}

}

));

LevelDefinitions.Instance.addLevelDefinition(

2,

new LevelDefinitionElement(

5,

function():void {(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallGreenPlaneGraphics,

new Point(500, -ResourceManager. SmallGreenPlaneGraphics.bitmap.height),

55);}));

}

public function shutdown():void

{

}

}

}

Apart from the singelton property there is only one other property: levelDefinitions. This is a dictionary, where the key is the level ID (level 1, 2, 3 etc), and the value is an array of LevelDefinitionElements. Remember that a LevelDefinitionElement defines a function to be called after a certain amount of time has passed in a level. So a call to levelDefinitions[1][0].func() would effectively mean that we are calling the function (levelDefinitions[1][0].func()) stored by the earliest level element (levelDefinitions[1][0].func() - remember they are sorted in ascending order by the time property) stored for level 1(levelDefinitions**[1]**[0].func()).

This should become clearer once we look at the startup function. Lets dissect one of the enemy placements.

LevelDefinitions.Instance.addLevelDefinition(

1,

new LevelDefinitionElement(

4,

function():void

{

for each (var xPos:int in [150, 350])

{

(Enemy.pool.ItemFromPool as Enemy).startupBasicEnemy(

ResourceManager.SmallBluePlaneGraphics,

new Point(xPos, -ResourceManager.SmallBluePlaneGraphics.bitmap.height),

55);

}

}

));

We are making a call to addLevelDefinition, which essentially just adds a LevelDefinitionElement to the levelDefinitions dictionary. The first variable we pass is the level ID. We are passing 1, which means that we are adding a placement to the first level.

The second variable we pass in is a LevelDefinitionElement. The first variable in that constructor is the time when this LevelDefinitionElement should be executed. We are passing in 4, which means that this LevelDefinitionElement will be executed 4 seconds into the level.

The second variable we pass to the LevelDefinitionElement constructor is an anonymous function. An anonymous function doesn’t have a name, so you can only call it through a Function object. Otherwise the code is exactly the same as a normal function. As you can see the function we have here creates two new enemy objects.

The end result of this is that after 4 seconds in the first level two new enemies will be created.

Once you wrap you head around using functions as objects it should be clear that this allows us an amazingly simple way to define our level structure. We are not constrained by a simple XML format - anything that can be done with Actionscript can be scripted to occur at a certain point during a level. We have used the code to create a new enemy here, but the possibilities are endless.

(A small change too is that we have moved the definitions of the colliding GameObject pairs into the startup function from the Application object. This is to keep all game “definition” code in one place.)

Of course we need a way of calling these functions at the correct point in the game. The Level class will take care of that. Lets look at that code now.

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 TimeBetweenClouds:Number = 2.5;

protected static const TimeToLevelEnd:Number = 2;

protected var nextDefinitions:Array = null;

protected var levelID:int = 0;

protected var totalTime:Number = 0;

protected var timeToNextLevelElement:Number = 0;

protected var levelElementGraphics:ArrayCollection = new ArrayCollection();

protected var timeToNextCloud:Number = 0;

protected var timeToLevelEnd:Number = 0;

protected var backgroundMusic:SoundChannel = null;

public var levelEnd:Boolean = false;

static public function get Instance():Level

{

if ( instance == null )

instance = new Level();

return instance;

}

public function Level()

{

levelElementGraphics.addItem(ResourceManager. SmallIslandGraphics);

levelElementGraphics.addItem(ResourceManager. BigIslandGraphics);

levelElementGraphics.addItem(ResourceManager. VolcanoIslandGraphics);

}

public function startup(levelID:int):void

{

timeToNextLevelElement = 0;

new Player().startupPlayer();

timeToLevelEnd = TimeToLevelEnd;

levelEnd = false;

backgroundMusic = ResourceManager.Track1FX.play(0, int.MAX_VALUE);

this.totalTime = 0;

this.levelID = levelID;

nextDefinitions = LevelDefinitions.Instance. getNextLevelDefinitionElements(levelID, 0);

}

public function shutdown():void

{

backgroundMusic.stop();

backgroundMusic = null;

}

public function enterFrame(dt:Number):void

{

totalTime += dt;

if (nextDefinitions == null)

{

if (Enemy.pool.NumberOfActiveObjects == 0)

levelEnd = true;

}

else

{

var nextLevelDefTime:Number = (nextDefinitions[0] as LevelDefinitionElement).time;

if (totalTime >= nextLevelDefTime)

{

for each (var levelDefElement:LevelDefinitionElement in nextDefinitions)

levelDefElement.func();

nextDefinitions = LevelDefinitions.Instance.getNextLevelDefinitionElements(levelID, nextLevelDefTime);

}

}

// 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 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);

}

if (levelEnd)

{

timeToLevelEnd -= dt;

var scale:Number = timeToLevelEnd / TimeToLevelEnd;

if (scale < 0) scale = 0;

var transform:SoundTransform = backgroundMusic.soundTransform;

transform.volume = scale;

backgroundMusic.soundTransform = transform;

}

if (timeToLevelEnd <= 0)

Application.application.currentState = “LevelEnd”;

}

}

}

We have removed all the code and properties relating to the random creation of enemies. For now we will leave in the creation of random BackgroundElements, although eventually the entire level will be defined inside the LevelDefinitions class.

There are 3 new properties added: nextDefinitions, levelID and totalTime. The nextDefinitions property contains an array of LevelDefinitionElements which are to be execute next. In the enterFrame function the Level class requests the next batch of LevelDefinitionElements through the LevelDefinitions getNextLevelDefinitionElements function. Then, when the time is right, these LevelDefinitionElements are executed and the next bacth requested. When there are no more LevelDefinitionElements (i.e. nextDefinitions == null) the Level waits for all the Enemies to be destroyed and then ends the level (by setting levelEnd to true).

The levelID property simply defines the current level. It is the same value supplied to the addLevelDefinition function.

The totalTime property represents the total time the current level has been active for. It is used in conjunction with nextDefinitions to execute the next batch of LevelDefinitionElements at the appropriate time.

By defining a level structure as a sequence of functions that get executed at predetermined points in the game we have the ability to do some pretty cool things down the track.

Go back to Flash Game Development with Flex and ActionScript

This post is part of the series: Game programming with Flex

Learn how to create a Flash game using Flex with this step by step series.

  1. Flash Game Development with Flex and Actionscript - Getting Started
  2. States &amp; Double Buffer Rendering in Flex 4.0
  3. Flex &amp; Actionscript Tutorial: Adding Game Actions
  4. Creating an Interactive Animated Background With Flex and Actionscript
  5. Adding Weapons to Your Game: Flex &amp; Actionscript Tutorial
  6. Collision Detection With Actionscript
  7. Adding Bitmap Animations With Flex and Actionscript
  8. Adding Music and Sound FX With Flex and Actionscript
  9. Defining Levels: Game Design With Flex and Actionscript
  10. Actionscript Tutorial: Add a Scrolling Tiled Background