Create from Scratch a Away3D Shoot’em’Up Game: Part 1

by Matthew Casperson 9

Today we start an Away3D tutorial series on creating a Shoot’em’Up game. We will cover all the necessary topics to create it from scratch, which will help you to understand and apply these guidelines to other projects. At this first part. we look at getting the Away3D engine up and running by creating a UIComponent  that will contain and initialize the necessary Away3D components.

The steps covered on these series will be:

  • Creating the Away3D framework
  • Creating/Defining a level
  • Adding a Player
  • Collision Detection
  • Sound Effects
  • Adding Enemies
  • Saving Games / Scores
  • Adding particles / Effects

Requirements

View DemoDownload Source Files

Creating the framework

Flash player 10 has brought with it some exciting new performance improvements and 3D effects which are currently being incorporated into a number of Flash 3D engines like Away3D. With the massive popularity of Flash games, and their relatively low cost  to develop, there has never been a better time to learn how to make your own 3D Flash games.

In this series we will look at the process of creating a 3D Shoot’em’up Flash game using the Away3D engine. The majority of this article will focus on initializing and managing the
Away3D, loading resources and creating a framework that we will build on to create the game.

EngineManager.as

package
{
	import away3d.cameras.Camera3D;
	import away3d.containers.View3D;
	import away3d.core.math.Number3D;
	import away3d.core.render.Renderer;
	import away3d.events.*;

	import flash.display.*;
	import flash.events.*;
	import flash.utils.*;

	import mx.collections.*;
	import mx.core.*;

	public class EngineManager extends UIComponent
	{
		public static const version:String = "1.0.0";
		protected static const MEASURED_MIN_WIDTH:int = 25;
		protected static const MEASURED_MIN_HEIGHT:int = 25;
		protected static const MEASURED_WIDTH:int = 100;
		protected static const MEASURED_HEIGHT:int = 100;

		// Away3D view
		internal var view:View3D = null;
		// Away3D camera
		internal var cam:Camera3D = null;
		// a collection of the BaseObjects
		protected var baseObjects:ArrayCollection = new ArrayCollection();
		// a collection where new BaseObjects are placed, to avoid adding items
		// to baseObjects while in the baseObjects collection while it is in a loop
		protected var newBaseObjects:ArrayCollection = new ArrayCollection();
		// a collection where removed BaseObjects are placed, to avoid removing items
		// to baseObjects while in the baseObjects collection while it is in a loop
		protected var removedBaseObjects:ArrayCollection = new ArrayCollection();
		// the last frame time
		protected var lastFrame:Date;
		// the application manager
		protected var applicationManager:ApplicationManager = null;
		// true if we need to shutdown the engine during the next frame and drop back to the main menu
		protected var myNextStateChange:String = "";

		internal function get MyApplicationManager():ApplicationManager
		{
			return applicationManager;
		}

		public function EngineManager()
		{
			super();
			addEventListener(Event.REMOVED_FROM_STAGE, this.shutdown);
			addEventListener(Event.ADDED_TO_STAGE, this.initializeEngine);
		}

		override protected function measure():void
		{
			super.measure();

			// set a bunch of predefined sizes
			this.measuredMinWidth = MEASURED_MIN_WIDTH;
			this.measuredMinHeight = MEASURED_MIN_HEIGHT;
			this.measuredHeight = MEASURED_HEIGHT;
			this.measuredWidth = MEASURED_WIDTH;
		}

		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
		{
			super.updateDisplayList(unscaledWidth, unscaledHeight);

			if (view != null)
			{
				// place the viewport in the middle of the control
				if(this.width / 2 != this.view.x)
					this.view.x = this.width / 2;
				if(this.height / 2 != this.view.y)
					this.view.y = this.height / 2;
			}
		}

		public function set nextStateChange(value:String):void
		{
			myNextStateChange = value;
		}

		protected function initializeEngine(event:Event):void
		{
			if (!view)
			{
				myNextStateChange = "";

				cam = new Camera3D({x:0, y:0, z:-100, lookat:new Number3D(0, 0, 0)});

				view = new View3D({x:stage.width/2, y:stage.height/2, camera:cam});
				view.renderer = Renderer.BASIC;
				view.mouseEnabled = true;
				addChild(view);

				addEventListener(Event.ENTER_FRAME, enterFrame);
				addEventListener(MouseEvent.MOUSE_DOWN, this.mouseDown);
				addEventListener(MouseEvent.MOUSE_UP, this.mouseUp);
				addEventListener(MouseEvent.MOUSE_MOVE, this.mouseMove);
				addEventListener(MouseEvent.CLICK, this.click);

				// set the initial frame time
				lastFrame = new Date();

				// start the application manager
				applicationManager = new ApplicationManager().startupApplicationManager(this);
			}
		}

		protected function shutdown(event:Event):void
		{
			if (view)
			{
				applicationManager.shutdown();

				shutdownAll();

				removeChild(view);

				removeEventListener(Event.ENTER_FRAME, enterFrame);
				removeEventListener(MouseEvent.MOUSE_DOWN, this.mouseDown);
				removeEventListener(MouseEvent.MOUSE_UP, this.mouseUp);
				removeEventListener(MouseEvent.MOUSE_MOVE, this.mouseMove);
				removeEventListener(MouseEvent.CLICK, this.click);

				applicationManager = null;
				view = null;
				cam = null;
				myNextStateChange = "";
			}
		}

		protected function enterFrame(event:Event):void
		{
			if (view != null)
			{
				// Calculate the time since the last frame
				var thisFrame:Date = new Date();
				var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
		    	lastFrame = thisFrame;

		    	// sync the baseObjects collection with any BaseObjects created or removed during the
		    	// render loop
		    	removeDeletedBaseObjects();
		    	insertNewBaseObjects();

		    	// allow each BaseObject to update itself
		    	for each (var baseObject:BaseObject in baseObjects)
		    		if (baseObject.inuse)
		    			baseObject.enterFrame(seconds);		    			

		    	// render the 3D scene
		    	view.render();
	  		}

	  		// set the currentState to MainMenu, which drops us back to the menu
	  		if (myNextStateChange != "")
	  		{
	  			Application.application.currentState = myNextStateChange;
	  			myNextStateChange = "";
	  		}
		}

		public function addBaseObject(baseObject:BaseObject):void
		{
			newBaseObjects.addItem(baseObject);
		}

		public function removeBaseObject(baseObject:BaseObject):void
		{
			removedBaseObjects.addItem(baseObject);
		}

		protected function insertNewBaseObjects():void
		{
			for each (var baseObject:BaseObject in newBaseObjects)
				baseObjects.addItem(baseObject);

			newBaseObjects.removeAll();
		}

		protected function removeDeletedBaseObjects():void
		{
			for each (var removedObject:BaseObject in removedBaseObjects)
			{
				var i:int = 0;
				for (i = 0; i < baseObjects.length; ++i)
				{
					if (baseObjects.getItemAt(i) == removedObject)
					{
						baseObjects.removeItemAt(i);
						break;
					}
				}

			}

			removedBaseObjects.removeAll();
		}

		public function shutdownAll():void
		{
			// don't dispose objects twice
			for each (var baseObject:BaseObject in baseObjects)
			{
				var found:Boolean = false;
				for each (var removedObject:BaseObject in removedBaseObjects)
				{
					if (removedObject == baseObject)
					{
						found = true;
						break;
					}
				}

				if (!found)
					baseObject.shutdown();
			}

			removeDeletedBaseObjects();
		}

		public function click(event:MouseEvent):void
		{
			for each (var gameObject:BaseObject in baseObjects)
				if (gameObject.inuse)
					gameObject.click(event);
		}

		public function mouseDown(event:MouseEvent):void
		{
			for each (var gameObject:BaseObject in baseObjects)
				if (gameObject.inuse)
					gameObject.mouseDown(event);
		}

		public function mouseUp(event:MouseEvent):void
		{
			for each (var gameObject:BaseObject in baseObjects)
				if (gameObject.inuse)
					gameObject.mouseUp(event);
		}

		public function mouseMove(event:MouseEvent):void
		{
			for each (var gameObject:BaseObject in baseObjects)
				if (gameObject.inuse)
					gameObject.mouseMove(event);
		}
	}
}

The EngineManager class is responsible for initializing the Away3D engine, managing the render loop, and distributing events to other classes in the system. EngineManager extends the UIComponent class, which is the base class for all visual Flex objects. By extending the UIComponent the EngineManager can be added and removed from the Flex Application like any other control, like a button or textbox.

There are a number of functions exposed by the UIComponent class that a developer can override. The EngineManager overrides just two: measure and updateDisplayList.

override protected function measure():void
{
	super.measure();

	// set a bunch of predefined sizes
	this.measuredMinWidth = MEASURED_MIN_WIDTH;
	this.measuredMinHeight = MEASURED_MIN_HEIGHT;
	this.measuredHeight = MEASURED_HEIGHT;
	this.measuredWidth = MEASURED_WIDTH;
}

The measure function is used to define the minimum and default size of the control. In this case we have four constant variables that define these dimensions, EASURED_MIN_WIDTH, MEASURED_MIN_HEIGHT, MEASURED_WIDTH and MEASURED_HEIGHT, which are assigned to the UIComponent properties measuredMinWidth, measuredMinHeight,
measuredHeight and measuredWidth respectively.

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
	super.updateDisplayList(unscaledWidth, unscaledHeight);

	if (view != null)
	{
		// place the viewport in the middle of the control
		if(this.width / 2 != this.view.x)
			this.view.x = this.width / 2;
		if(this.height / 2 != this.view.y)
			this.view.y = this.height / 2;
	}
}

The updateDisplayList is called to setup the size and position of the control, and here we use it to centre the Away3D View3D object. The View3D object is the container this is used to display the 3D world.

public function EngineManager()
{
	super();
	addEventListener(Event.REMOVED_FROM_STAGE, this.shutdown);
	addEventListener(Event.ADDED_TO_STAGE, this.initializeEngine);
}

In addition to these two UIComponent functions, the EngineManager will call two more functions to actually initialize and clean up the Away3D engine.

protected function initializeEngine(event:Event):void
{
	if (!view)
	{
		myNextStateChange = "";

		cam = new Camera3D({x:0, y:0, z:-100, lookat:new Number3D(0, 0, 0)});

		view = new View3D({x:stage.width/2, y:stage.height/2, camera:cam});
		view.renderer = Renderer.BASIC;
		view.mouseEnabled = true;
		addChild(view);

		addEventListener(Event.ENTER_FRAME, enterFrame);
		addEventListener(MouseEvent.MOUSE_DOWN, this.mouseDown);
		addEventListener(MouseEvent.MOUSE_UP, this.mouseUp);
		addEventListener(MouseEvent.MOUSE_MOVE, this.mouseMove);
		addEventListener(MouseEvent.CLICK, this.click);

		// set the initial frame time
		lastFrame = new Date();

		// start the application manager
		applicationManager = new ApplicationManager(this).startupApplicationManager();
	}
}

The initializeEngine function is set to be triggered (in the constructor) when the EngineManager is added to the parent control through the Event.ADDED_TO_STAGE event.

if (!view)
{
	...
}

First we make sure that view has not been set already. This is to avoid the possibility of setting up the Away3D engine twice.

myNextStateChange = "";

The myNextStateChange property is set to an empty string, which indicates that there is no pending request for a state change. myNextStateChange is used to manage the state of the game, but this won’t be used until later.

cam = new Camera3D({x:0, y:0, z:-100, lookat:new Number3D(0, 0, 0)});

A Camera3D is setup, and assigned to the cam property. We have set the position of the camera at (0, 0, -100), and then asked it to look back at the origin (0, 0, 0).

view = new View3D({x:stage.width/2, y:stage.height/2, camera:cam});

A View3D is setup, and assigned to the view property.  It is positioned in the middle of the stage, and its camera is set to the camera we created above.

view.renderer = Renderer.BASIC;

Next the renderer is specified. Away3D has a number of renderers. We have used the BASIC one, which is the fastest but least capable of the renderers. You could also use CORRECT_Z_ORDER, which is used to order the triangles in the scene before they are rendered, or INTERSECTING_OBJECTS which is used to split intersecting triangles so they are drawn properly. Our game will have no need for the additional processing done by the CORRECT_Z_ORDER and INTERSECTING_OBJECTS renderers, so the BASIC one will do.

view.mouseEnabled = true;

We set the mouseEnabled property to true. This allows us to watch for mouse events on Away3D objects, and this will be used in later articles.

addChild(view);

We add the view as a child of the EngineManager.

addEventListener(Event.ENTER_FRAME, enterFrame);
addEventListener(MouseEvent.MOUSE_DOWN, this.mouseDown);
addEventListener(MouseEvent.MOUSE_UP, this.mouseUp);
addEventListener(MouseEvent.MOUSE_MOVE, this.mouseMove);
addEventListener(MouseEvent.CLICK, this.click);

There are several events that we want to listen for. The first is the Event.ENTER_FRAME event, which is called once per frame. We assign the enterFrame function to this event. The enterFrame function will serve as our render loop. The other events, MouseEvent.MOUSE_DOWN, MouseEvent.MOUSE_UP, MouseEvent.MOUSE_MOVE, MouseEvent.CLICK, are used
to watch for any interaction the user has made with the mouse.

lastFrame = new Date();

One of the functions that will be performed in the render loop (the enterFrame function) is to let the BaseObjects that make up the final game update themselves every frame. The BaseObject class will be used as the base class for any object in the game, like the player, a bullet or the enemies. Most of the time these BaseObjects will want to update themselves by a fixed amount every second (for example moving across the screen at 10 units per second), and to do this the time between frames needs to be calculated. This is where the lastFrame property comes in. It stores the time the last frame was rendered, so the time between the last frame and the current frame can be worked out. Here we set lastFrame to the currentTime, which insures the initial calculation of the time between the fames (done in enterFrame) has a valid time to work with.

applicationManager = new ApplicationManager().startupApplicationManager(this);

Finally we create and start the ApplicationManager. The ApplicationManager is used to manage the flow of the game itself (i.e. loading levels, populating the levels with enemies etc).

protected function shutdown(event:Event):void
{
	if (view)
	{
		applicationManager.shutdown();

		shutdownAll();

		removeChild(view);

		removeEventListener(Event.ENTER_FRAME, enterFrame);
		removeEventListener(MouseEvent.MOUSE_DOWN, this.mouseDown);
		removeEventListener(MouseEvent.MOUSE_UP, this.mouseUp);
		removeEventListener(MouseEvent.MOUSE_MOVE, this.mouseMove);
		removeEventListener(MouseEvent.CLICK, this.click);

		applicationManager = null;
		view = null;
		cam = null;
		myNextStateChange = "";
	}
}

The shutdown function is set to be triggered (in the constructor) when the EngineManager is removed from the parent control through the Event.REMOVED_FROM_STAGE event. The shutdown function does the reverse of the initializeEngine function. It shuts down the ApplicationManager, shuts down all of the BaseObjects (through the shutdownAll function), removes all of the event listeners, and sets all the properties to null.

protected function enterFrame(event:Event):void
{
	if (view != null)
	{
		// Calculate the time since the last frame
		var thisFrame:Date = new Date();
		var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
		lastFrame = thisFrame;

		// sync the baseObjects collection with any BaseObjects created or removed during the
		// render loop
		removeDeletedBaseObjects();
		insertNewBaseObjects();

		// allow each BaseObject to update itself
		for each (var baseObject:BaseObject in baseObjects)
			if (baseObject.inuse)
				baseObject.enterFrame(seconds);		    			

		// render the 3D scene
		view.render();
	}

	// set the currentState to MainMenu, which drops us back to the menu
	if (myNextStateChange != "")
	{
		Application.application.currentState = myNextStateChange;
		myNextStateChange = "";
	}
}

The enterFrame function is called once per frame, and is where all the BaseObjects update themselves and the current frame is rendered.

var thisFrame:Date = new Date();
var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
lastFrame = thisFrame;

The time since the last frame is calculated, and the time of the current frame is stored in the lastFrame property.

removeDeletedBaseObjects();
insertNewBaseObjects();

Next the collection of BaseObjects (the baseObjects ArrayCollection) is updated, removing those that were removed during the last frame (through the removeDeletedBaseObjects function), and adding those that were created during the last frame (through the insertNewBaseObjects function).

for each (var baseObject:BaseObject in baseObjects)
	if (baseObject.inuse)
		baseObject.enterFrame(seconds);

Next all the BaseObjects having their enterFrame function called. The enterFrame function is where the BaseObjects will update themselves, for example by moving across the screen.

view.render();

Once all the BaseObjects have updated themselves we need to render the frame to the screen.

if (myNextStateChange != "")
{
	Application.application.currentState = myNextStateChange;
	myNextStateChange = "";
}

If there is any pending state change (because myNextStateChange has been set to a non-empty string), the currentState property of the main application object is set. This will be used later to switch between menus, level end screens and final score screens.

public function addBaseObject(baseObject:BaseObject):void
{
	newBaseObjects.addItem(baseObject);
}

When new BaseObjects are started they need to be added to the baseObjects ArrayCollection to allow them to receive updates and events. But you can’t modify baseObjects directly, because it is possible that new BaseObjects are started during a call to their enterFrame function, which in turn is called while looping over the baseObjects collection. Modifying a collection while looping over it is a bad idea, so instead any newly started BaseObjects are added to the interim newBaseObjects collection by the addBaseObject function. These are then added to the main baseObjects collection by the enterFrame function by calling insertNewBaseObjects.

public function removeBaseObject(baseObject:BaseObject):void
{
	removedBaseObjects.addItem(baseObject);
}

Similarly BaseObjects that have been shutdown can not remove themselves from the baseObjects collection directly, but need to be added to an interim removedBaseObjects collection, to be removed from the baseObjects when enterFrame calls the removeDeletedBaseObjects function.

protected function insertNewBaseObjects():void
{
	for each (var baseObject:BaseObject in newBaseObjects)
		baseObjects.addItem(baseObject);

	newBaseObjects.removeAll();
}

protected function removeDeletedBaseObjects():void
{
	for each (var removedObject:BaseObject in removedBaseObjects)
	{
		var i:int = 0;
		for (i = 0; i < baseObjects.length; ++i)
		{
			if (baseObjects.getItemAt(i) == removedObject)
			{
				baseObjects.removeItemAt(i);
				break;
			}
		}

	}

	removedBaseObjects.removeAll();
}

As mentioned above, the insertNewBaseObjects and removeDeletedBaseObjects function merge the interim newBaseObjects and removedBaseObjects collections into the main baseObjects collection.

public function shutdownAll():void
{
	// don't dispose objects twice
	for each (var baseObject:BaseObject in baseObjects)
	{
		var found:Boolean = false;
		for each (var removedObject:BaseObject in removedBaseObjects)
		{
			if (removedObject == baseObject)
			{
				found = true;
				break;
			}
		}

		if (!found)
			baseObject.shutdown();
	}

	removeDeletedBaseObjects();
}

The shutdownAll function is called to shutdown all the existing BaseObjects in one go. We do have to be careful though because some BaseObjects may exist in the baseObjects collection after they have already been shutdown (because removeDeletedBaseObjects has not been called to sync the two collections up), so we do have to add some additional logic to ensure that BaseObjects are not shutdown twice.

public function click(event:MouseEvent):void
{
	for each (var gameObject:BaseObject in baseObjects)
		if (gameObject.inuse)
			gameObject.click(event);
}

public function mouseDown(event:MouseEvent):void
{
	for each (var gameObject:BaseObject in baseObjects)
		if (gameObject.inuse)
			gameObject.mouseDown(event);
}

public function mouseUp(event:MouseEvent):void
{
	for each (var gameObject:BaseObject in baseObjects)
		if (gameObject.inuse)
			gameObject.mouseUp(event);
}

public function mouseMove(event:MouseEvent):void
{
	for each (var gameObject:BaseObject in baseObjects)
		if (gameObject.inuse)
			gameObject.mouseMove(event);
}

The remaining functions, click, mouseDown, mouseUp and mouseMove, all forward mouse events onto the BaseObjects.

Now lets take a look at the baseObject class.

BaseObject.as

package
{
	import flash.events.*;

	internal class BaseObject
	{
		protected var engineManager:EngineManager = null;
		public var inuse:Boolean = false;

		public function BaseObject()
		{

		}

		public function startupBaseObject(engineManager:EngineManager):void
		{
			if (!this.inuse)
			{
				this.engineManager = engineManager;
				this.inuse = true;
				this.engineManager.addBaseObject(this);
			}
		}

		public function shutdown():void
		{
			if (this.inuse)
			{
				this.inuse = false;
				this.engineManager.removeBaseObject(this);
				this.engineManager = null;
			}
		} 

		public function enterFrame(dt:Number):void
		{

		}

		public function click(event:MouseEvent):void
		{

		}

		public function mouseDown(event:MouseEvent):void
		{

		}

		public function mouseUp(event:MouseEvent):void
		{

		}

		public function mouseMove(event:MouseEvent):void
		{

		}

		public function collision(other:BaseObject):void
		{

		}
	}
}

The code here is quite simple. There is a reference back to the EngineManager, and inuse property (which will be used for resource pooling later on), a startupBaseObject and shutdown function that add and remove the BaseObject from the main collection managed by the EngineManager, and a bunch of empty functions which match the events that are distributed by the Enginemanager.

The BaseObject class doesn’t do much on its own: it is designed to be extended by other classes to provide more specific functionality. One of these classes is the MeshObject.

MeshObject.as

package
{
	import away3d.core.base.Object3D;

	internal class MeshObject extends BaseObject
	{
		public var model:Object3D = null;

		public function MeshObject()
		{
			super();
		}

		public function startupMeshObject(engineManager:EngineManager, model:Object3D):MeshObject
		{
			super.startupBaseObject(engineManager);
			this.model = model;
			this.engineManager.view.scene.addChild(model);
			return this;
		}

		public override function shutdown():void
		{
			if (model != null)
				engineManager.view.scene.removeChild(model);
			model = null;
			super.shutdown();
		}
	}
}

The MeshObject extends the BaseObject class, and adds the ability to add a 3D mesh to the scene.

public function startupMeshObject(engineManager:EngineManager, model:Object3D):MeshObject
{
	super.startupBaseObject(engineManager);
	this.model = model;
	engineManager.view.scene.addChild(model);
	return this;
}

The startupMeshObject takes an Object3D, stores a reference to it and adds it to the Away3D scene. We also make a call to the BaseObject startupBaseObject function, which ensures that the MeshObject (and any class the extends the MeshObject) will receive any events.

public override function shutdown():void
{
	if (model != null)
		engineManager.view.scene.removeChild(model);
	model = null;
	super.shutdown();
}

The shutdown function does the reverse, removing the model from the scene, and setting its reference to the model to null. It also calls the BaseObject shutdown function to ensure that the MeshObject no longer receives any event notifications.

ApplicationManager.as

package
{
	import away3d.primitives.Cube;

	import mx.core.Application;

	public class ApplicationManager extends BaseObject
	{
		public function ApplicationManager()
		{
			super();
		}

		public function startupApplicationManager(engineManager:EngineManager):ApplicationManager
		{
			this.startupBaseObject(engineManager);

			return this;
		}

		public function startLevel1():void
		{
			new MeshObject().startupMeshObject(
				engineManager,
				new Cube(
					{material:ResourceManager.yellow_Tex,
					width:30,
					height:30,
					depth:30,
					segmentsH:3,
					segmentsW:3}));
		}
	}
}

The ApplicationManager class also extends the BaseObject class. This is because it will eventually be used to place enemies and items in the level inside the enterFrame function, but for now all the ApplicationManager does is create an Away3D Cube primitive, which is assigned to a new MeshObject, in the startLevel1 function.

Eventually the ApplicationManager will take on a more significant role, being responsible for creating all the BaseObjects that will eventually be present in the final game.

ResourceManager.as

package
{
	import away3d.materials.BitmapMaterial;
	import away3d.materials.TransformBitmapMaterial;
	import away3d.materials.WireColorMaterial;

	import flash.utils.ByteArray;

	public class ResourceManager
	{
		public static const yellow_Tex:WireColorMaterial = new WireColorMaterial(0xFFA500);
	}
}

The ResouceManager is where the textures, models and sound effects that will be used in the game are stored. Using the Embed keyword the ResourceManager will provide a location to store and access these resources, although for this article the only resource that is created is a wireframe texture that is applied to the cube created by the ApplicationManager in the startLevel1 function.

The last file to look at is the MXML file. The MXML file is an XML files that describes the main Flex application.

GettingStarted.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
 xmlns:mx="http://www.adobe.com/2006/mxml"
 layout="absolute"
 xmlns:ns1="*"
 width="600"
 height="400"
 currentState="MainMenu"
 frameRate="100"
 backgroundGradientAlphas="[1.0, 1.0]"
 backgroundGradientColors="[#040522, #162654]">

	<mx:states>
		<mx:State name="MainMenu">
			<mx:AddChild position="lastChild">
				<mx:Button x="10" y="368" label="Start" id="btnStart" click="{Application.application.currentState = 'Game';}"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Image x="281" y="10" source="@Embed(source='../media/frontscreen.png')"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Image x="465" y="265" source="@Embed(source='../media/thetechlabs_logosmall.png')"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Text x="10" y="10" text="Away3D Shoot'em'Up tutorial" color="#FFFFFF" fontSize="16"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Text x="10" y="42" text="Brought to you by The Tech Labs" color="#FFFFFF" fontSize="16"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Text x="10" y="74" text="Written by Matthew Casperson" color="#FFFFFF" fontSize="16"/>
			</mx:AddChild>
		</mx:State>
		<mx:State name="Game">
			<mx:enterState>
				<![CDATA[
					engineManager.MyApplicationManager.startLevel1();
				]]>
			</mx:enterState>
			<mx:exitState>
				<![CDATA[

				]]>
			</mx:exitState>
			<mx:AddChild>
				<ns1:EngineManager x="0" y="0" width="100%" height="100%" id="engineManager"/>
			</mx:AddChild>
			<mx:AddChild position="lastChild">
				<mx:Button x="10" y="368" label="Exit">
					<mx:click>{Application.application.currentState = &quot;MainMenu&quot;;}</mx:click>
				</mx:Button>
			</mx:AddChild>
		</mx:State>
	</mx:states>
</mx:Application>

Included in the MXML file are two states, defined in the <mx:State> tags, themselves defined in the <mx:states> tag. States are used by Flex to add and remove children (like buttons, images etc) from the application object. This is useful because Flex/Flash applications tend to only use a single window. So whereas a standard Windows application might have one
form defined for displaying the details of a product, and a separate form for entering those details, a Flex application would have two states instead of two separate forms. You can also run arbitrary code by adding <mx:enterState> and <mx:enterState> tags in the <mx:state> tag.

The first of the two states is called MainMenu, and in this state a few images, labels and a button are shown. When the button is clicked the currentState property of the application object is modified to the next state: Game.

It’s in the Game state that we add the EngineManager. You can see how, thanks to extending the UIComponent class, the EngineManager can be added like any other control through an MXML tag. We also have some code  in a <mx:enterState>  tag, which is called when the state has been entered. In this case we are asking the ApplicationManager to start the game by calling the startLevel1 function.

We have implemented a lot of underlying code to achieve a fairly simple result, but it is necessary to establish the underlying framework before we start making the actual game itself. In the next article we will start building up the first level.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>