Advertisement

Create an Epic War Game in Flash: Part 1

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

Creating a game is definitely one of the most involved tasks in the world of programming. If you want to write your own game, be prepared to spend a lot of time trying to figure out how to work around a lot of different problems. My mission here is to help make this process a little bit easier.


Also available in this series:

  1. Create an Epic War Game in Flash: Part 1
  2. Create an Epic War Game in Flash: Part 2

In this two-part tutorial we'll be creating a top-down war game from scratch. It will implement many different concepts from building a background based on xml data to breaking colors into separate channels using bitwise operators. The game itself will not be like some MMORPG (it would probably take several volumes of a book to describe the process of complicated game creation). This game will only have a couple of types of player and enemy vehicles to check the movements, collisions and weapons, and two missions just to make sure everything works properly (you'll be able to create more missions anytime you want using the unified engine that we'll make in this tutorial).


The Game

Before we dive into any work, take a look at what the final result will look like, after completing both parts of this tutorial. Use the "View Demo" link above to check it out!


Preparation

Let's begin by taking a quick look at the tools I used to create this game:

  1. FlashDevelop 3.2.2 (you may use Flex Builder or Flash authoring tool instead, but I'd recommend you to write the classes in FlashDevelop because it's much easier). It's absolutely free of charge, and can be downloaded here
  2. Flash IDE (Adobe Flash CS4). I used it only for 3 things: to create movie clips, as a container to hold most of the game assets, and as the compiler.
  3. Adobe Photoshop CS5 or below to edit bitmaps
  4. Google SketchUp 7 to model different vehicles, and Vray to render them (Of course you can use any other software which you're comfortable with)

Now look at the main features we'll be working towards:

  1. Create XML database that will be used to store missions data.
  2. Create the engine that will build game missions based on the information from XML database
  3. Write the camera object that will follow an active player character
  4. Create player and, of course, the enemy that will be able to detect player objects and destroy them.
  5. Make the system of collisions and destructions.
  6. And finally, the weapons! :)

It's just the general list of tasks, there's much more to do in reality, so, let's get started.


Step 1: Setting Up a Project

In this lesson we'll create all of the main classes needed to visualize the game.

At the beginning, we need to set up a project. Open up FlashDevelop and go to Project > New Project.

Choose Actionscript 3 Flash IDE (in case you use Flash to compile everything) project then directory and name for it and click ok.


Now right-click the newly created project file and chose add > New XML File


At this step, let's make something simple. Add just a couple of nodes that will contain the name of the mission and the map source path.

This is how our XML file's body should look for now:

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map.jpg</map>	 
	</mission> 
</missions>

Notice that the name of the mission is an attribute, not a separate node (we'll be using attributes a lot) To make an attribute just type its name (which can be any word) inside any tag (but make sure the tag's name goes before the attribute), put an equals sign and quotes. Everything inside quotes will be considered the attribute.

Ok, since the folder containing the map doesn't exist yet, we need to create it now. Again, right-click on the project file but now go to Add > New Folder. Give this folder a name of "maps" (without quotes) and put a map.jpg file inside it.

Now open up Flash IDE and create new Flash File (Actionscript 3). Save it as WarGame.fla in the same folder with your project.


Right-click the stage and change the document properties like so:



Step 2: Parsing XML

It's time to create an XML loader class. Since we want to keep it all clean, lets put our initialization classes into a separate package. Right-click the project file in FlashDevelop, go to Add > New Folder. Give it a name of "builder" then right-click this folder and choose Add > New Class. Call it "xmlParser"

This is what the structure should be like:


The code for xmlParser.as is:

 
package builder 
{ 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.net.URLLoader; 
	import flash.net.URLRequest; 
 
	public class xmlParser extends Sprite 
	{ 
		private var _request:URLRequest; 
		private var _urlLoader:URLLoader; 
		// missionAssets variable will be accessed from the other classes, that's why I made it public 
		public var missionAssets:XML; 
		 
		public function xmlParser()  
		{ 
			// create a new URLRequest containing the path to the xml file with mission assets 
			_request = new URLRequest("Assets.xml"); 
			// a new URLLoader to load xml. Pass the created URLRequest as the parameter 
			_urlLoader = new URLLoader(ks_request); 
			// and finally, the event listener that will trigger "assignAssets" method after xml's loaded 
			_urlLoader.addEventListener(Event.COMPLETE, assignAssets); 
		} 
		 
		private function assignAssets(e:Event):void  
		{ 
			// now assign loaded data to the  missionAssets variable 
			missionAssets = new XML(e.target.data); 
			dispatchEvent(new MyEvent(MyEvent.MAP_STRUCTURED)); 
		}	 
	} 
}

Now let's examine this line of code in detail:

 
dispatchEvent(new MyEvent(MyEvent.MAP_STRUCTURED));

When the xml is loaded and assigned to a variable, the method assignAssets() dispatches a custom event "MyEvent" (It can do so because the Sprite class also extends EventDispatcher). MyEvent event doesn't exist yet, so it's time to create it.

Right-click the project file in FlashDevelop (just like you did before) and go to Add > New Event. Give it a name of MyEvent.as

You don't have to worry about the complexity of the Event code, FlashDevelop will do it all for you. You will get a code like this:

 
package   
{ 
	import flash.events.Event; 
	 
	public class MyEvent extends Event  
	{ 
		 
		public function MyEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)  
		{  
			super(type, bubbles, cancelable); 
			 
		}  
		 
		public override function clone():Event  
		{  
			return new MyEvent(type, bubbles, cancelable); 
		}  
		 
		public override function toString():String  
		{  
			return formatToString("MyEvent", "type", "bubbles", "cancelable", "eventPhase");  
		} 
		 
	} 
	 
}

All you have to do now is to add a public static constant right before this line:

 
public function MyEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)

This is the final version of the event code:

 
package   
{ 
	import flash.events.Event; 
	 
	public class MyEvent extends Event  
	{ 
		public static const MAP_STRUCTURED:String = "mapIsStructured"; 
		 
		public function MyEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)  
		{  
			super(type, bubbles, cancelable); 
			 
		}  
		 
		public override function clone():Event  
		{  
			return new MyEvent(type, bubbles, cancelable); 
		}  
		 
		public override function toString():String  
		{  
			return formatToString("MyEvent", "type", "bubbles", "cancelable", "eventPhase");  
		}	 
	}	 
}

What is the word "static" for? Static variables or constants do not have to be instantiated, which means they are created right away, without the keyword "new" and can be accessed from other classes directly. Sometimes it's very handy, and this event is not the only case we'll be using static constants or variables in our game.


Step 3: MapBuilder Class

At this point we have all means to load a game map source, but there's no class to build map. Let's fix it now. Right-click the builder folder and add a new class "MapBuilder.as". FlashDevelop creates a new class but does not extend it. You'll have to do it manually. This class won't need a time line so it will extend Sprite. Once you write "extends Sprite" and hit enter, FlashDevelop will automatically import the Sprite class to our library path.

 
import flash.display.Sprite;

Create a public (class level) variable with the data type of xmlParser.
Here's the code for the class at this point:

 
package builder  
{ 
	import flash.display.Sprite; 
 
	public class MapBuilder extends Sprite 
	{ 
		 
		public var xmlData:xmlParser; 
		 
		public function MapBuilder() 
		{ 
			xmlData = new xmlParser(); 
			xmlData.addEventListener(MyEvent.MAP_STRUCTURED, readyToBuildMap); 
		} 
	} 
}

Look at the event listener. Remember, MyEvent.MAP_STRUCTURED is dispatched by xmlParser class when the XML file is loaded and assigned to a variable. In this class we listen for that event and when it occurs, trigger readyToBuildMap() method.

Important note: Only the objects that dispatch a custom event can listen for that event! This means that only instances that can listen for the MAP_STRUCTURED event in this case are the instances with xmlParser data type.

The method itself isn't created yet but there's a good trick in FlashDevelop. Click on the method's name "readyToBuildMap" and hit Ctrl + Shift + 1.
FlashDevelop will show you this hint:


Select "Generate MyEvent handler" and it will create the method for you (of course you can create it all manually in case you're not using FlashDevelop).



Step 4: Testing MapBuilder

A good programming practice is to write everything in small pieces. So all we will do now is add a little trace statement inside this method, just to make sure everything works ok.

 
private function readyToBuildMap(e:MyEvent):void  
{ 
	trace("I can build a map now!"); 
}

Finally open up the WarGame.fla file, and for the document class name type in builder.MapBuilder (remember this is the name of our package).


Now hit Ctrl + Enter on PC (Command + RETURN on mac) to test it.

Wow! It works.


If you get the same result, you did everything correctly. If not, go thru all the steps once again more thoroughly.


Step 5: More Methods and Variables for the MapBuilder

Open up MapBuilder class in FlashDevelop and add new variables:

 
public var mission:uint = 0; 
private var _bmd:BitmapData;

The first one is a very important variable. It will let our MapBuilder know what map source and assets positions to use in a particular mission. It is equal to 0 now because XMLList is like Array. It counts from 0, not 1. So the first mission will have to use 0 node index.

The second one is a BitmapData object that will be used to copy our map data into.

Since the map is supposed to be tile-based, let's add two more variables to hold the numbers of rows and columns.

 
private var _rows:int; 
private var _cols:int;

The data type for them is integer because we will use only full value tiles (no halves).


Step 6: What Kind of Tiles to Use?

Talking about tile engine construction, we cannot leave behind the tiles themselves. Many tutorials that you can find on the Internet work with tiles based on sprites or movie clips using addChild() method every time they need to add another tile. This approach has a lot of disadvantages.

And the biggest of them is a huge resource consumption. Really, try to add 300 (200x200) MovieClip tiles to the screen and see what happens. This will terribly slow your system down (I tried it myself and Flash Player even froze a couple of times).

Another disadvantage is their "floating position". If you add e.g. 100 MovieClip tiles to the display list and try to move some character over them, you will see how they vibrate. This bug doesn't look very attractive, believe me.

So, the experience of many Flash developers shows that the best way to build tiled map is to use a single BitmapData object, then, after the map is built into it convert it to Bitmap and add to the display list.


Step 7: Bitmap Creation

Let's get started with map creation. First of all, we need a "map.jpg" the path to which we used in our xml file before. To create map.jpg you will need any seamless texture. In this case it will be grass. The texture size for this project must be 200x200 px. So open it up in Photoshop and go to Image > Image size. If it's bigger or smaller, make it 200x200.


Save the tile to our project's map folder as map.jpg.


Step 8: Enumerated Tiles for Test

Actually, one tile will be enough to use with this tile engine. But as we need to test it, we'll create at least 2 more tiles in one background and number them. In Photoshop, double-click the word Background in the layers palette to unlock it.


Then go to Image – Canvas Size. And make the canvas width 200%, like so:


Pay attention to the Anchor. Once it's done, go to View – New Guide and add 2 vertical guides for 200 and 400 pixels.


Now Alt-Click the tile and simply drag it to the right. Repeat it once again to get this:


Now, choose Type Tool (T on your keyboard) and put numbers 1 to 3 over each tile.


Save it as map2.jpg to the same folder as map.jpg.


Step 9: Update XML

Since we have our tiles ready. It's time to update the XML file. Open up Assets.xml in FlashDevelop and add node containing 2 attributes for rows and columns to it.

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map2.jpg</map>	 
		<tiles rows="4" columns="4">1,1,2,2,  2,2,2,1,  3,2,2,1, 1,1,1,1</tiles> 
	</mission> 
</missions>

This will let the engine know how many tile to add vertically and horizontally. (Note: I've changes map.jpg to map2.jpg for now. Pay special attention to this.)

I've added types of tiles for each row (1,1,2,2, 2,2,2,1, 3,2,2,1, 1,1,1,1)
The numbers are supposed to represent the tiles from map2.jpg.


Step 10: More Methods and Variables for MapBuilder.as

Let's get back to MapBuilder.as. I've modified it's code now. New variables and methods were commented so you can understand what they're supposed to do:

 
package builder  
{ 
	import flash.display.BitmapData; 
	import flash.display.Sprite; 
 
	public class MapBuilder extends Sprite 
	{ 
		 
		public var xmlData:xmlParser; 
		public var mission:uint = 0; 
		private var _bmd:BitmapData; 
		private var _rows:int; 
		private var _cols:int; 
		private var _tileArray:Array; // An array that will be used to keep the numbers of tiles taken from _levelDesign  
		private var _levelDesign:XMLList; // An XMLList that will contain the path to the tiles node in Assets.xml file 
		 
		 
		public function MapBuilder() 
		{ 
			_tileArray = []; // initialize array of tiles 
			initializeMap();  
		} 
		public function initializeMap():void // a public method which I used to place stuff from the constructor. You can actually do it all in the constructor, but I think it looks cleaner this way. 
		{ 
			xmlData = new xmlParser(); 
			xmlData.addEventListener(MyEvent.MAP_STRUCTURED, readyToBuildMap); 
		} 
		 
		private function readyToBuildMap(e:MyEvent):void  
		{ 
			// once the xml with mission data is loaded and MyEvent.MAP_STRUCTURED event is truggered. Load map2.jpg 
			loadBitmap(); 
		} 
		 
		private function loadBitmap():void 
		{ 
			// assign the path to the tiles node to our _levelDesign variable 
			//missionAssets is a public variable from xmlParser.as, and [mission] is the number of current mission 
			// which tells the program what node to use for mission construction. 
			_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
			trace(_levelDesign); 
		} 
	} 
}

If you trace _levelDesign at this point, you'll get the next output:

 
1,1,2,2,  2,2,2,1,  3,2,2,1, 1,1,1,1

This signals that everything's just fine. Hit Ctrl + Enter (or F6 in FlashDevelop) to check it out.

Now it's time to initialize out _rows and _cols variable. Modify the loadBitmap() method like this:

 
private function loadBitmap():void 
{ 
	_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
	_rows = _levelDesign.@rows; 
	_cols = _levelDesign.@columns; 
}

Notice, that we can now use _levelDesign variable instead of typing the whole path again. And pay special attention to the "at sign" @. It's used because rows and columns are the attributes of the tiles node, not the stand-alone nodes. You can trace _cols and _rows if you want, and you'll get 4 for each one. But we won't do it here.


Step 11: Loading Map Image

To load something like an image you will need a Loader object. So, add the loader variable to your MapBuilder:

 
private var _loader:Loader;

Now go down to loadBitmap() method again and initialize the _loader. Basically, you will also have to add one more function at this step. What function? Let's look at it now to see what we're getting into.

 
private function loadBitmap():void 
{ 
	_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
	_rows = _levelDesign.@rows; 
	_cols = _levelDesign.@columns; 
	_loader = new Loader(); // new instance of _loader 
	// Note: you can only add event listener to the contentLoaderInfo of the Loader object and not to the loader itself! 
	_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete); // don't forget to create a handler function for "onLoadComplete"  
	_loader.load(new URLRequest(xmlData.missionAssets.mission[mission].map)); // and finally load the map using a path to it from Assets.xml 
			 
} 
		 
private function onLoadComplete(e:Event):void  
{ 
	trace("map loaded!"); 
}

onLoadComplete is an event handler for the event that will be triggered after our _loader loads the map. Just put a trace statement into it for now and hit Ctrl + ENTER to test it.
If you get "map loaded!" string in Flash's output panel – everything works correctly. This means that you can now use map2.jpg to actually build the game's background from its tiles.


Step 12: Globals Class

Globals is a special class which is not obligatory but it will definitely make the process of game creation a little bit easier. What is it used for?

Well, it's used as a centralized storage container for some data that is supposed to be used many times by many other classes. For example, the Globals.as class may store some constants that convert angles from degrees to radians and vice versa, or to keep some dimensions like the width and height of the tile. That's what we'll use it for. Right click the project file in FlashDevelop and go to Add > New Class. Name it Globals. And all we will do with this class is add three constants.

Here's the body of it:

 
package   
{ 
 
	public class Globals extends Object 
	{ 
		public static const DEG_RAD:Number = Math.PI / 180; // degrees to radians 
		public static const RAD_DEG:Number = 180 / Math.PI; // radians to degrees 
		public static const TILE:Number = 200; // tile's width and height 
		 
		public function Globals()  
		{ 
			// the constructor does not have to do anything at all. The class will never be instantiated 
		}	 
	} 
}

That's it for the class. You may close it now.


Step 13: Main BitmapData Object

In order to create a map using our palette we'll need a BitmapData object to copy the palette's pixels to. The BitmapData object must be the size of the map we wanna create. So, let's go back to the MapBuilder class and declare two more variables.

 
private var _bitmapData:BitmapData;  
public var _bmdContainer:Bitmap;

The first one is the BitmapData object we need, and the second (note: it's declared as public) is a Bitmap to hold our BitmapData since the BitmapData object cannot be added to the display list itself.

Now go down to the loadBitmap() method and instantiate _bitmapData and _bmdContainer

 
private function loadBitmap():void 
{ 
	_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
	_rows = _levelDesign.@rows; 
	_cols = _levelDesign.@columns; 
	_loader = new Loader();  
		 
	_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete); 
	_loader.load(new URLRequest(xmlData.missionAssets.mission[mission].map));  
			 
	_bitmapData = new BitmapData(Globals.TILE * _cols, Globals.TILE* _rows, true, 0xFFFFFF); 
	_bmdContainer = new Bitmap(_bitmapData); 
}

The parameters for BitmapData object are: width, height, transparent, and fillColor.

The width of the BitmapData equals the number of columns multiplied by tile width which we've put into our Globals class. The same with height. It will be transparent so you may choose any fill color. Then, pass this _bitmapData to the _bmdContainer's constructor.


Step 14: Using Palette

Declare three more private variables in MapBuilder class:

 
private var _palette:Bitmap; 
private var _tileWidth:Number; 
private var _tileHeight:Number;

And go down to the onLoadComplete() method. Delete the trace("map loaded!"); statement.

 
private function onLoadComplete(e:Event):void  
{ 
	// first of all, remove the event listener for Event.COMPLETE, since it's not needed anymore 
	_loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onLoadComplete); 
	_palette = Bitmap(_loader.content); // assign the loader's content to the _palette variable 
	// the next to variables will be needed in the copyPixels() method a little bit later 
	_tileWidth = Globals.TILE; 
	_tileHeight = Globals.TILE; 
 
	tileMap(); 
}

Let's break down this line:

 
_palette = Bitmap(_loader.content);

Actually, the program knows that the loader's content is bitmap now, but we want to make sure it's bitmap in any case, that's why the _loader content is passed to the Bitmap's constructor. It's like you're telling the program "Yes, it's bitmap, so treat it the way you treat bitmaps".

If you didn't understand what bitmap it is, it's our map2.jpg.


Step 15: Populate _tileArray with Tiles

As you can see, onLoadComplete() method calls tileMap() method which is not created yet.
Lets fix it now. Create this method under onLoadComplete()

 
private function tileMap():void 
{ 
	// the next line converts XMLList _levelDesign to string and creates _tileArray from it 
	// the split(",") method in this case searches thru XMLList and puts everything it finds  	 
	// into the array's 0 index intil it reaches comma, then it goes to the 1st index, 2nd and so on 
	_tileArray = String(_levelDesign).split(","); 
	trace(_tileArray); 
}

Let's test it now. If you get this output:

 
1,1,2,2,  2,2,2,1,  3,2,2,1, 1,1,1,1

...it works alright.

Check Milestone

It looks exactly like the output we had before but actually it's not an XMLList anymore, it's an array of tiles that can be used to tile map.


Step 16: Tiling the Map

At this point we need six more variables in our MapBuilder:

 
private var _tileCoordX:Number = 0; 
private var _tileCoordY:Number = 0; 
private var _tileRectX:Number = 0; 
private var _tileRectY:Number = 0; 
private var tileRow:int; 
private var tileHorCoord:int;

The first two are the coordinates in the _bitmapData object to which each tile is supposed to be copied. Right now they both equal 0 and it means the the first tile will be copied into the top left corner of the _bitmapData. These coordinates will change after each tile is copied (you'll see that a bit later)

_tileRectX and _tileRectY are the coordinates of the top-left corner of the rectangle which copies pixels from the palette.
These coordinates vary depending on the type of tile in the _tileArray.

If you look at the palette from Step 8, it has three types of tiles. So if tile type is 1, _tileRectX and _tileRectY will both be 0 but if the type is 3, _tileRectX will be 400.

The width and height of this rectangle equal to width and height of the tile (Globals.TILE)

tileRow is a variable that holds the row's number into which the last tile was copied (You'll understand it later).

And tileHorCoord holds the horizontal coordinate of this tile.

Now let's update tileMap() method:

 
private function tileMap():void 
{ 
	_tileArray = String(_levelDesign).split(","); 
	 
	 
	if (_tileArray.length == _rows * _cols) // This if statement checks if there's enough tiles in XML to tile the map 
	{ 
		// create a tile for each element in _tileArray 
		for each (var tile:int in _tileArray) 
		{	 
 
			// checks what row the tile is placed to 
			tileRow = Math.ceil(tile / (_palette.width / Globals.TILE));  
			// checkss the horizontal coordinate of the tile to make sure  
			// it does not exceed the row's width - tile width (number of tiles multiplied by tile width)  
			tileHorCoord = int((tile * Globals.TILE) - Globals.TILE); 
			 
			_tileCoordX = tileHorCoord - (_palette.width * (tileRow - 1)); 
			_tileCoordY = (tileRow * Globals.TILE) - Globals.TILE; 
			 
			 
			// if the row is tiled switch to the next one 
			if (_tileRectX >= _cols * Globals.TILE) 
			{ 
				_tileRectX = 0; 
				_tileRectY += Globals.TILE; 
			} 
			// copying pixels to the main bitmapData object from our palette's bitmapData.  
			_bitmapData.copyPixels(_palette.bitmapData, new Rectangle(_tileCoordX, _tileCoordY, Globals.TILE, Globals.TILE), new Point(_tileRectX, _tileRectY)); 
			 
			// adds 200 to the X coordinate of each tile after the previous is copied to _bitmapData. 
			_tileRectX += Globals.TILE;	 
		} 
		// Checks if all tiles are placed to their positions and if they are, add _bmdContainer to the display list 
		if (_tileRectY + Globals.TILE >= Globals.TILE * _rows && _tileRectX == Globals.TILE * _cols) 
		{ 
			addChild(_bmdContainer); 
		} 
	} 
}

And finally, let's overview the whole MapBuilder class:

 
package builder  
{ 
	import flash.display.Bitmap; 
	import flash.display.BitmapData; 
	import flash.display.Loader; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	import flash.net.URLRequest; 
 
	public class MapBuilder extends Sprite 
	{ 
		 
		public var xmlData:xmlParser; 
		public var mission:uint = 0; 
		private var _bmd:BitmapData; 
		private var _rows:int; 
		private var _cols:int; 
		private var _tileArray:Array;  
		private var _levelDesign:XMLList;  
		private var _loader:Loader; 
		private var _bitmapData:BitmapData;  
		public var _bmdContainer:Bitmap; 
		private var _palette:Bitmap; 
		private var _tileWidth:Number; 
		private var _tileHeight:Number; 
		private var _tileCoordX:Number = 0; 
		private var _tileCoordY:Number = 0; 
		private var _tileRectX:Number = 0; 
		private var _tileRectY:Number = 0; 
		private var tileRow:int; 
		private var tileHorCoord:int; 
		 
		 
		public function MapBuilder() 
		{ 
			_tileArray = [];  
			initializeMap();  
		} 
		public function initializeMap():void  
		{ 
			xmlData = new xmlParser(); 
			xmlData.addEventListener(MyEvent.MAP_STRUCTURED, readyToBuildMap); 
		} 
		 
		private function readyToBuildMap(e:MyEvent):void  
		{ 
			loadBitmap(); 
		} 
		 
		private function loadBitmap():void 
		{ 
			_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
			_rows = _levelDesign.@rows; 
			_cols = _levelDesign.@columns; 
			_loader = new Loader();  
			_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete); 
			_loader.load(new URLRequest(xmlData.missionAssets.mission[mission].map));  
			_bitmapData = new BitmapData(Globals.TILE * _cols, Globals.TILE * _rows, true, 0xFFFFFF); 
			_bmdContainer = new Bitmap(_bitmapData); 
		} 
		 
		private function onLoadComplete(e:Event):void  
		{ 
			_loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onLoadComplete); 
			_palette = Bitmap(_loader.content);  
			_tileWidth = Globals.TILE; 
			_tileHeight = Globals.TILE; 
			 
			tileMap(); 
		} 
		 
		private function tileMap():void 
		{ 
			_tileArray = String(_levelDesign).split(","); 
 
			if (_tileArray.length == _rows * _cols)  
			{ 
				for each (var tile:int in _tileArray) 
				{	 
					tileRow = Math.ceil(tile / (_palette.width / Globals.TILE));  
					tileHorCoord = int((tile * Globals.TILE) - Globals.TILE);	 
					_tileCoordX = tileHorCoord - (_palette.width * (tileRow - 1)); 
					_tileCoordY = (tileRow * Globals.TILE) - Globals.TILE; 
					 
					if (_tileRectX >= _cols * Globals.TILE) 
					{ 
						_tileRectX = 0; 
						_tileRectY += Globals.TILE; 
					} 
					_bitmapData.copyPixels(_palette.bitmapData, new Rectangle(_tileCoordX, _tileCoordY, Globals.TILE, Globals.TILE), new Point(_tileRectX, _tileRectY)); 
					_tileRectX += Globals.TILE;	 
				} 
				if (_tileRectY + Globals.TILE >= Globals.TILE * _rows && _tileRectX == Globals.TILE * _cols) 
				{ 
					addChild(_bmdContainer); 
				} 
			} 
		} 
	} 
}

If you test it now, you'll get this output:


Compare these numbers to the ones in Assets XML (1,1,2,2, 2,2,2,1, 3,2,2,1, 1,1,1,1) and if they are the same for each row and column, the engine works! Praise yourself :)


Step 17: First Map Design

The design with numbers doesn't look very attractive but at least we know everything works. You may now delete map2.jpg file and change the "path to map" in Assets.xml back to map.jpg. And don't forget to change tile types to 1 here:

 
<tiles rows="4" columns="4">1,1,1,1,  1,1,1,1,  1,1,1,1,   1,1,1,1</tiles>

...because map.jpg has only one tile by now. Open it up in Photoshop again and unlock the background layer like you did before (Step 8). Go to Image > Canvas Size and increase its width by 200 percent and height by 400;


Now Alt–click the tile and drag it until you fill the whole area. Well, generally speaking, do the same work you did in Step 8 but for the bigger area.

At this step you can make each tile unique. I added road with a couple of turns, crossroads and a small pond. You can do whatever you like and make this map as big as you want. As it's not a graphics tutorial, I won't explain the way I made everything up in details, I'll show you just a final result.

We'll need 2 versions of the same map. The first one is the actual map we'll use in our game:


...and the second is the numerated map, in order to know what tiles to use and where.


Let's try and design our first map. First of all, decide how many rows and tiles you want. If you're following along, we'll make the map 5 x 6. Not very big but it will definitely need more room the the stage can give it. That's what we need.

Open up Assets.xml and type this:

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map.jpg</map>	 
		<tiles rows="6" columns="5">5,2,2,2,2, 	 5,3,15,15,15, 	 9,4,15,15,15,	 15,4,15,15,15,		15,4,15,15,15,		15,4,15,15,15</tiles> 
	</mission> 
</missions>

You can put tabs after each row to make it look cleaner. XML ignores white spaces.

Check Milestone


Step 18: MAP_BUILT Custom Event

The MapBuilder class is almost ready, except for two things. Right now WarGame.fla calls its constructor method and does not check if the map is already built or not. To fix this, open up MyEvent.as and add one more constant to it:

 
public static const MAP_BUILT:String = "mapIsBuilt";

Now, since we have another custom event, we can add it to the MapBuilder class. Open up MapBuilder.as, find this line:

 
addChild(_bmdContainer);

And put this line:

 
dispatchEvent(new MyEvent(MyEvent.MAP_BUILT));

right below it.

What is it for?
This event is like a permission that lets the program add another stuff only after the map is ready. Just to make sure that nothing is under the map when it's added to the display list. It wouldn't be so good if some tank or jeep moves underground :)


Step 19: Last Big Stroke for MapBuilder

You can now build a map using MapBuilder engine but, imagine, if you want to build a map that's much bigger than the visible part of the screen. You just wouldn't know what coordinates what tile is at. So you will definitely need some debugging grid that will define borders between tiles, show their coordinates and types. It may sound a little scary but actually, can be fixed pretty easily.

Let's do it now.

Add three more variables to the MapBuilder:

 
private var debugTileType:int; // will show the type of tile that debugger draws grid for 
private var tileNumber:int; // will let our debugger know what tile to draw grid for.  
private var _container:Sprite; // we'll need it to put our map into

It's time to initialize these variables.
Find this code:

 
for each (var tile:int in _tileArray) 
{

and put this:

 
debugTileType = tile;

right below it.

Now, put this line:

 
tileNumber += 1;

under this:

 
_tileCoordY = (tileRow * Globals.TILE) – Globals.TILE;

Initialize _container in the constructor and add it to the screen:

 
public function MapBuilder() 
{ 
	_tileArray = []; 
	_container = new Sprite(); 
	addChild(_container); 
	initializeMap();  
}

find this line once again:

 
addChild(_bmdContainer);

and change it to:

 
_container.addChild(_bmdContainer);

It's needed to visualize the debugging grid. If you don't do this, your grid will be placed under the map and you just won't see it, even though it will exist. Doing it, you're placing the container to the stage before anything else, so the grid will be added above this anyway.

Now create a private function setDebugger().
Here's the body of it:

 
private function setDebugger():void 
{ 
					 
	var sprite:Sprite = new Sprite(); // container for debugging tile  
	sprite.graphics.lineStyle(1, 0xFFFFFF, 0.12); // debugging tile stroke 
	sprite.graphics.beginFill(0xFFFFFF, 0); // a transparent fill 
	sprite.graphics.drawRect(_tileRectX, _tileRectY, Globals.TILE, Globals.TILE);  
	addChild(sprite); 
	 
	// text fields for each parameter		 
	var debugNumber:* = addChild(new TextField()); 
	debugNumber.text = String("tile type: " + debugTileType); 
	debugNumber.selectable = false; 
	debugNumber.textColor = 0xFFFF00; 
	debugNumber.x = _tileRectX + 10; 
	debugNumber.y = _tileRectY + 10; 
			 
	var rowAndColNumber:* = addChild(new TextField()); 
	rowAndColNumber.text = String("tile number: " + tileNumber); 
	rowAndColNumber.selectable = false; 
	rowAndColNumber.textColor = 0xFFFFFF; 
	rowAndColNumber.x = _tileRectX + 10; 
	rowAndColNumber.y = _tileRectY + 30; 
			 
	var xCoor:* = addChild(new TextField()); 
	xCoor.text = String("x: " + _tileRectX + "   y:" + _tileRectY); 
	xCoor.selectable = false; 
	xCoor.textColor = 0xCFCFCF; 
	xCoor.x = _tileRectX + 100; 
	xCoor.y = _tileRectY + 10; 
}

That's it. You just have to call this method.
Find this line:

 
_bitmapData.copyPixels(_palette.bitmapData, new Rectangle(_tileCoordX, _tileCoordY, Globals.TILE, Globals.TILE), new Point(_tileRectX, _tileRectY));

and put this right below it:

 
setDebugger();

Ough, you may probably be a little confused now. So let's overview the whole class just to make sure you did everything right:

 
package builder  
{ 
	import flash.display.Bitmap; 
	import flash.display.BitmapData; 
	import flash.display.Loader; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	import flash.net.URLRequest; 
	import flash.text.TextField; 
 
	public class MapBuilder extends Sprite 
	{ 
		 
		public var xmlData:xmlParser; 
		public var mission:uint = 0; 
		private var _bmd:BitmapData; 
		private var _rows:int; 
		private var _cols:int; 
		private var _tileArray:Array;  
		private var _levelDesign:XMLList;  
		private var _loader:Loader; 
		private var _bitmapData:BitmapData;  
		public var _bmdContainer:Bitmap; 
		private var _palette:Bitmap; 
		private var _tileWidth:Number; 
		private var _tileHeight:Number; 
		private var _tileCoordX:Number = 0; 
		private var _tileCoordY:Number = 0; 
		private var _tileRectX:Number = 0; 
		private var _tileRectY:Number = 0; 
		private var tileRow:int; 
		private var tileHorCoord:int; 
		private var debugTileType:int; 
		private var tileNumber:int; 
		private var _container:Sprite; 
		 
		 
		public function MapBuilder() 
		{ 
			_tileArray = []; 
			_container = new Sprite(); 
			addChild(_container); 
			initializeMap();  
		} 
		public function initializeMap():void  
		{ 
			xmlData = new xmlParser(); 
			xmlData.addEventListener(MyEvent.MAP_STRUCTURED, readyToBuildMap); 
		} 
		 
		private function readyToBuildMap(e:MyEvent):void  
		{ 
			loadBitmap(); 
		} 
		 
		private function loadBitmap():void 
		{ 
			_levelDesign = xmlData.missionAssets.mission[mission].tiles; 
			_rows = _levelDesign.@rows; 
			_cols = _levelDesign.@columns; 
			_loader = new Loader();  
			_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete); 
			_loader.load(new URLRequest(xmlData.missionAssets.mission[mission].map));  
			_bitmapData = new BitmapData(Globals.TILE * _cols, Globals.TILE * _rows, true, 0xFFFFFF); 
			_bmdContainer = new Bitmap(_bitmapData); 
		} 
		 
		private function onLoadComplete(e:Event):void  
		{ 
			_loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onLoadComplete); 
			_palette = Bitmap(_loader.content);  
			_tileWidth = Globals.TILE; 
			_tileHeight = Globals.TILE; 
			 
			tileMap(); 
		} 
		 
		private function tileMap():void 
		{ 
			_tileArray = String(_levelDesign).split(","); 
 
			if (_tileArray.length == _rows * _cols)  
			{ 
				for each (var tile:int in _tileArray) 
				{	 
					debugTileType = tile; 
					 
					tileRow = Math.ceil(tile / (_palette.width / Globals.TILE));  
					tileHorCoord = int((tile * Globals.TILE) - Globals.TILE);	 
					_tileCoordX = tileHorCoord - (_palette.width * (tileRow - 1)); 
					_tileCoordY = (tileRow * Globals.TILE) - Globals.TILE; 
					 
					tileNumber += 1; 
					 
					if (_tileRectX >= _cols * Globals.TILE) 
					{ 
						_tileRectX = 0; 
						_tileRectY += Globals.TILE; 
					} 
				 
					_bitmapData.copyPixels(_palette.bitmapData, new Rectangle(_tileCoordX, _tileCoordY, Globals.TILE, Globals.TILE), new Point(_tileRectX, _tileRectY)); 
					 
					setDebugger(); 
					 
					_tileRectX += Globals.TILE;	 
				} 
				if (_tileRectY + Globals.TILE >= Globals.TILE * _rows && _tileRectX == Globals.TILE * _cols) 
				{ 
					_container.addChild(_bmdContainer); 
				} 
			} 
		} 
		private function setDebugger():void 
		{ 
					 
			var sprite:Sprite = new Sprite(); 
			sprite.graphics.lineStyle(1, 0xFFFFFF, 0.12); 
			sprite.graphics.beginFill(0xFFFFFF, 0); 
			sprite.graphics.drawRect(_tileRectX, _tileRectY, Globals.TILE, Globals.TILE); 
			addChild(sprite); 
			 
			var debugNumber:* = addChild(new TextField()); 
			debugNumber.text = String("tile type: " + debugTileType); 
			debugNumber.selectable = false; 
			debugNumber.textColor = 0xFFFF00; 
			debugNumber.x = _tileRectX + 10; 
			debugNumber.y = _tileRectY + 10; 
			 
			var rowAndColNumber:* = addChild(new TextField()); 
			rowAndColNumber.text = String("tile number: " + tileNumber); 
			rowAndColNumber.selectable = false; 
			rowAndColNumber.textColor = 0xFFFFFF; 
			rowAndColNumber.x = _tileRectX + 10; 
			rowAndColNumber.y = _tileRectY + 30; 
			 
			var xCoor:* = addChild(new TextField()); 
			xCoor.text = String("x: " + _tileRectX + "   y:" + _tileRectY); 
			xCoor.selectable = false; 
			xCoor.textColor = 0xCFCFCF; 
			xCoor.x = _tileRectX + 100; 
			xCoor.y = _tileRectY + 10; 
		} 
	} 
}

You can turn the debugging grid on and off anytime, just commenting and uncommenting this line:

 
setDebugger();

Click to Check Milestone


Step 20: Introducing Camera

The camera object in the game has nothing to do with a real camera. Basically it's just a container that will hold all of the game objects.

Right-click the project's file in FlashDevelop and add a new Class. Call it Camera and just put this code inside:

 
package   
{ 
	import flash.display.DisplayObjectContainer; 
	import flash.display.Sprite; 
	import flash.geom.Point; 
 
	public class Camera extends Sprite 
	{ 
		 
		public function Camera(parent:DisplayObjectContainer, location:Point)  
		{ 
			this.x = location.x; 
			this.y = location.y; 
			parent.addChild(this); 
		} 
		 
	} 
 
}

Now look at the constructor. Its first parameter is a parent that camera will be added to. Since we don't know what type of parent will use yet, its data type is DisplayObjectContainer. Because Sprites, MovieClips and even the Stage itself are all DisplayObjectContainers.

So, in order to add camera to the display list we will only need to pass a parent object to its constructor later. The second parameter is the location of the top left corner of the Camera object. This is more than enough for the camera. You'll see how to use it a little bit later.

Make sure you have the same files structure at this point.



Step 21: Game Structure Class Creation

Right-click project's file again and add another class. Name it GameStructure.

 
package   
{ 
	import flash.display.Sprite; 
	 
	public class GameStructure extends Sprite 
	{ 
		 
		public function GameStructure()  
		{ 
			 
		} 
	} 
}

Now open up WarGame.fla and change its document class from builder.MapBuilder to GameStructure.

If you test the game now it will not show anything at all. Just a blank screen. From now on we have to initialize the map construction from inside the GameStructure class.

 
package   
{ 
	import builder.MapBuilder; 
	import flash.display.Sprite; 
	import flash.geom.Point; 
	 
	public class GameStructure extends Sprite 
	{ 
		 
		public var map:MapBuilder;  
		public var camera:Camera; 
		 
		public function GameStructure()  
		{ 
			camera = new Camera(stage, new Point(0,0)); // create a camera. Remember the first parameter is parent, in this case – stage. The second parameter is location, in this case top left corner of the stage 
			map = new MapBuilder(); // new instance of map builder class 
			map.addEventListener(MyEvent.MAP_BUILT, addMap); // remember we dispatchedThis event in our MapBuilder class after the _container was added 
		} 
		 
		private function addMap(e:MyEvent):void  
		{ 
			// once the MAP_BUILT event is triggered, add map to the screen 
			camera.addChildAt(map, 0); 
		} 
	} 
}

If you test it now, you'll get the exact same result as in Step 19 but in this case MapBuilder was not instantiated by WarGame.fla file.


Step 22: Keys to Control Camera

In addMap() method, add two event listeners:

 
stage.addEventListener(KeyboardEvent.KEY_DOWN, buttonControls); 
stage.addEventListener(KeyboardEvent.KEY_UP, releaseKey);

And once you do it, create event handlers for each listener:

 
private function releaseKey(e:KeyboardEvent):void  
{ 
			 
}	 
private function buttonControls(e:KeyboardEvent):void  
{ 
			 
}

Before you can add any code to these methods you need to declare four boolean variables for each arrow key:

 
private var _upPressed:Boolean = false; 
private var _downPressed:Boolean = false; 
private var _leftPressed:Boolean = false; 
private var _rightPressed:Boolean = false;

They will be true or false according to the key state to which they're tied.


Step 23: Populate releaseKey() and buttonControls() Methods

Go to releaseKey() method and modify it like so:

 
private function releaseKey(e:KeyboardEvent):void  
{ 
	switch (e.keyCode) 
	{ 
		case Keyboard.UP: 
		_upPressed = false; 
		break; 
		case Keyboard.DOWN: 
		_downPressed = false; 
		break; 
		case Keyboard.RIGHT: 
		_rightPressed = false; 
		break; 
		case Keyboard.LEFT: 
		_leftPressed = false; 
		break; 
	} 
}

This method is called every time any key is released. The switch statement checks the key code of the released key and according to this reaches one of the blocks.

The buttonControls() method will do more job that this one. But for now it will only need to keep track of these four variables and ENTER_FRAME listener.

On to buttonControls(). Here's the code for buttonControls() method:

 
private function buttonControls(e:KeyboardEvent):void  
{ 
	if (e.keyCode == Keyboard.LEFT) 
	{ 
		_leftPressed = true; 
		if (camera.hasEventListener(Event.ENTER_FRAME)) 
		{ 
			 
		} 
		else  
		{ 
			camera.addEventListener(Event.ENTER_FRAME, moveMap); 
		} 
	} 
	if (e.keyCode == Keyboard.RIGHT) 
	{ 
		_rightPressed = true; 
		if (camera.hasEventListener(Event.ENTER_FRAME)) 
		{ 
			 
		} 
		else  
		{ 
			camera.addEventListener(Event.ENTER_FRAME, moveMap); 
		} 
	} 
	if (e.keyCode == Keyboard.UP) 
	{ 
		_upPressed = true; 
		if (camera.hasEventListener(Event.ENTER_FRAME)) 
		{ 
			 
		} 
		else  
		{ 
			camera.addEventListener(Event.ENTER_FRAME, moveMap); 
		} 
	} 
	if (e.keyCode == Keyboard.DOWN) 
	{ 
		_downPressed = true; 
		if (camera.hasEventListener(Event.ENTER_FRAME)) 
		{ 
			 
		} 
		else  
		{ 
			camera.addEventListener(Event.ENTER_FRAME, moveMap); 
		} 
	} 
}

To understand what it does, let's look at one of the "if" statements:

 
if (e.keyCode == Keyboard.LEFT) 
{ 
	_leftPressed = true; 
	if (camera.hasEventListener(Event.ENTER_FRAME)) 
	{ 
					 
	} 
	else  
	{ 
		camera.addEventListener(Event.ENTER_FRAME, moveMap); 
	} 
}

When one of the arrow keys is pressed, this method is called. Then it checks what arrow key exactly was pressed. For example it was left arrow key. It turns leftPressed variable to true and then check if the camera has an event listener for Event.ENTER_FRAME. And in case the camera already has event listener it does nothing. Else it adds the event listeners to the camera which calls moveMap() method (which doesn't exist yet but we're about to create it).

What is it for?

It's pretty simple; the more ENTER_FRAME listeners you have in a program, the more calculations your computer has to make every frame, the slower your system works. And this construction makes sure that camera has only one ENTER_FRAME listener in any case.
The other blocks do exactly the same job but for other arrows.


Step 24: Moving The Map With Keyboard

The only thing left to make our map move is moveMap() method. Let's create one.

 
private function moveMap(e:Event):void  
{ 
	// turns the map's local coordinates (in Camera object) to global (Stages coordinates) 
	var mapPosition:Point = map.localToGlobal(new Point(stage.x, stage.y)); 
	// the number of pixels at which the map will move every frame 
	var moveSpeed:int = 15; 
	 
	// the next conditional statements check if the map brinks exceed the stage's limits and doesn't let them do so 
	if (_leftPressed && mapPosition.x < 0 - moveSpeed ) 
	{ 
		camera.x += moveSpeed 
	} 
	else if (_rightPressed && mapPosition.x + map.width > stage.stageWidth + moveSpeed) 
	{ 
		camera.x -= moveSpeed; 
	} 
	if (_upPressed && mapPosition.y < 0 - moveSpeed) 
	{ 
		camera.y += moveSpeed; 
	} 
	else if (_downPressed && mapPosition.y + map.height > stage.stageHeight + moveSpeed) 
	{ 
		camera.y -= moveSpeed; 
	} 
	 
	 
	if (!_upPressed && !_downPressed && !_leftPressed && !_rightPressed) 
	{ 
		camera.removeEventListener(Event.ENTER_FRAME, moveMap); 
	} 
}

You can test it now, and you'll probably think there's a bug with this method, because doesn't let the map get back exactly to 0 x-position when you move it to the right. But I can assure you, there's no bug, you will understand why when we add come characters to the game, but for now, let's leave it as is.

And the last part of the method:

 
if (!_upPressed && !_downPressed && !_leftPressed && !_rightPressed) 
{ 
	camera.removeEventListener(Event.ENTER_FRAME, moveMap); 
}

Checks if no key is pressed and if so, removes ENTER_FRAME listener from camera.

Overview the whole class in case you have some problems with it:

 
package   
{ 
	import builder.MapBuilder; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.events.KeyboardEvent; 
	import flash.geom.Point; 
	import flash.ui.Keyboard; 
	 
	public class GameStructure extends Sprite 
	{ 
		 
		public var map:MapBuilder; 
		public var camera:Camera; 
		private var _upPressed:Boolean = false; 
		private var _downPressed:Boolean = false; 
		private var _leftPressed:Boolean = false; 
		private var _rightPressed:Boolean = false; 
		 
		public function GameStructure()  
		{ 
			camera = new Camera(stage, new Point(0,0)); 
			map = new MapBuilder(); 
			map.addEventListener(MyEvent.MAP_BUILT, addMap); 
		} 
		 
		private function addMap(e:MyEvent):void  
		{ 
			camera.addChildAt(map, 0); 
			stage.addEventListener(KeyboardEvent.KEY_DOWN, buttonControls); 
			stage.addEventListener(KeyboardEvent.KEY_UP, releaseKey); 
		} 
		 
		private function releaseKey(e:KeyboardEvent):void  
		{ 
			switch (e.keyCode) 
			{ 
				case Keyboard.UP: 
				_upPressed = false; 
				break; 
				case Keyboard.DOWN: 
				_downPressed = false; 
				break; 
				case Keyboard.RIGHT: 
				_rightPressed = false; 
				break; 
				case Keyboard.LEFT: 
				_leftPressed = false; 
				break; 
			} 
		} 
		 
		private function buttonControls(e:KeyboardEvent):void  
		{ 
			if (e.keyCode == Keyboard.LEFT) 
			{ 
				_leftPressed = true; 
				if (camera.hasEventListener(Event.ENTER_FRAME)) 
				{ 
					 
				} 
				else  
				{ 
					camera.addEventListener(Event.ENTER_FRAME, moveMap); 
				} 
			} 
			if (e.keyCode == Keyboard.RIGHT) 
			{ 
				_rightPressed = true; 
				if (camera.hasEventListener(Event.ENTER_FRAME)) 
				{ 
					 
				} 
				else  
				{ 
					camera.addEventListener(Event.ENTER_FRAME, moveMap); 
				} 
			} 
			if (e.keyCode == Keyboard.UP) 
			{ 
				_upPressed = true; 
				if (camera.hasEventListener(Event.ENTER_FRAME)) 
				{ 
					 
				} 
				else  
				{ 
					camera.addEventListener(Event.ENTER_FRAME, moveMap); 
				} 
			} 
			if (e.keyCode == Keyboard.DOWN) 
			{ 
				_downPressed = true; 
				if (camera.hasEventListener(Event.ENTER_FRAME)) 
				{ 
					 
				} 
				else  
				{ 
					camera.addEventListener(Event.ENTER_FRAME, moveMap); 
				} 
			} 
		} 
		 
		private function moveMap(e:Event):void  
		{ 
			var mapPosition:Point = map.localToGlobal(new Point(stage.x, stage.y)); 
			var moveSpeed:int = 15; 
			 
			if (_leftPressed && mapPosition.x < 0 - moveSpeed ) 
			{ 
				camera.x += moveSpeed 
			} 
			else if (_rightPressed && mapPosition.x + map.width > stage.stageWidth + moveSpeed) 
			{ 
				camera.x -= moveSpeed; 
			} 
			if (_upPressed && mapPosition.y < 0 - moveSpeed) 
			{ 
				camera.y += moveSpeed; 
			} 
			else if (_downPressed && mapPosition.y + map.height > stage.stageHeight + moveSpeed) 
			{ 
				camera.y -= moveSpeed; 
			} 
			 
			 
			if (!_upPressed && !_downPressed && !_leftPressed && !_rightPressed) 
			{ 
				camera.removeEventListener(Event.ENTER_FRAME, moveMap); 
			} 
		} 
	} 
}

Click to Check Milestone


Step 25: Additional Assets: Trees and Houses

To add some trees and houses to the game we need another BitmapData object and Bitmap to store it. So, let's create two new variables in our GameStructure class.

 
private var treesBMD:BitmapData; 
private var treesBM:Bitmap;

And, of course add these objects to our Assets.xml.

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map.jpg</map>	 
		<tiles rows="6" columns="5">5,2,2,2,2, 	 5,3,15,15,15, 	 9,4,15,15,15,	 15,4,15,15,15,		15,4,15,15,15,		15,4,15,15,15</tiles> 
		<staticObjects> 
			<object name="trees" positionX="200" positionY="200">trees</object> 
		</staticObjects> 
	</mission> 
</missions>

I added only one tree for now, just for a test.


Step 26: Create Assets Palette in Photoshop

Open up Photoshop and create new file 600 x 200 pixels, and transparent background.


Now go to View – New Guide. And add vertical guide at 200 px. Do the same for 400 px.
You'll get three imaginary cells, to which you can add different assets.


I've modeled a house roof in Google SketchUp and found a fir tree in SketchUp free Warehouse
You can add any graphics you like. Press Ctrl+Shift+Alt+S or just go to "File > Save For Web and Devices" and save it as statics.png (png24) to the same folder where you have map.jpg.


Step 27: Loading Assets

Open Assets.xml and right under this node:

maps/map.jpg

add one more node: maps/statics.png

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map.jpg</map>	 
		<assets>maps/statics.png</assets> 
		<tiles rows="6" columns="5">5,2,2,2,2, 	 5,3,15,15,15, 	 9,4,15,15,15,	 15,4,15,15,15,		15,4,15,15,15,		15,4,15,15,15</tiles> 
		<staticObjects> 
			<object name="trees" positionX="200" positionY="200">trees</object> 
		</staticObjects> 
	</mission> 
</missions>

Add one more variable to GameStructure class:

 
private var _assets:xmlParser;

This variable needs to be instantiated in addMap() method. I've modified it and removed camera.addChildAt(map, 0); line. We'll add it again later in another method.

 
private function addMap(e:MyEvent):void  
		{ 
			_assets = new xmlParser(); 
			// Remember this event is dispatched by xmlParser() when XML's loaded 
			_assets.addEventListener(MyEvent.MAP_STRUCTURED, setEnvironment); 
			 
			stage.addEventListener(KeyboardEvent.KEY_DOWN, buttonControls); 
			stage.addEventListener(KeyboardEvent.KEY_UP, releaseKey); 
		}

Don't forget to create event handler setEnvironment():

 
private function setEnvironment(e:MyEvent):void  
		{ 
			trace("set environment"); 
		}

If you test it now and get blank stage and "set environment" string in the Output panel, everything's alright.

Let's instantiate treesBMD and treesBM variables:

 
private function setEnvironment(e:MyEvent):void  
		{ 
			 
			treesBMD = new BitmapData(map.width, map.height, true, 0xFFFFFF); 
			treesBM = new Bitmap(treesBMD); 
		}

The width and height of treesBMD must correspond to the width and height of the map. It must be transparent; fillColor isn't important, choose whatever you want or leave default.

Update setEnvironment() once again:

 
private function setEnvironment(e:MyEvent):void  
{ 
			 
	treesBMD = new BitmapData(map.width, map.height, true, 0xFFFFFF); 
	treesBM = new Bitmap(treesBMD); 
	var loader:Loader = new Loader(); 
	loader.contentLoaderInfo.addEventListener(Event.COMPLETE, assetsLoaded); 
	loader.load(new URLRequest(_assets.missionAssets.mission[map.mission].assets)); 
}

Notice, I've created new Loader object to load assets.png.

The URLRequest is a path to assets.png taken from Assets.xml. We can do it this way because _assets is an instance of xmlParser and so has access to its public variable missionAssets.

[map.mission] is a node index that is equal to the current mission in MapBuilder class.

Now create assetsLoaded() event handler:

 
private function assetsLoaded(e:Event):void  
{ 
	e.target.removeEventListener(Event.COMPLETE, assetsLoaded); 
	trace("assets loaded"); 
}

Step 28: Parsing Path to Assets and Placing Them

At this point, we can add a block of code that will copy trees and houses to our treesBMD.
For this purpose we need to update assetsLoaded() method.

 
 
		private function assetsLoaded(e:Event):void  
		{ 
			e.target.removeEventListener(Event.COMPLETE, assetsLoaded); 
			camera.addChild(treesBM); 
			camera.addChildAt(map, 0); 
			 
			// asset is our statics.png which was just loaded 
			var asset:Bitmap = Bitmap(e.target.content); 
 
			// staticsData is the path to the list of mission static objects in Assets.xml 
			var staticsData:XMLList = _assets.missionAssets.mission[map.mission].staticObjects.object; 
			 
			// for in loop searches thru XMLList of objects, sets each object's position and type, and adds it to the treesBMD 
			// the experience shows that "for in" loop works about 30% quicker than "for" loop. So I prefer to use "for in's" 
			for (var i in staticsData) 
			{ 
				// the next 3 variables are the attributes of each object node in Assets.xml 
				var objectName:String = String(staticsData[i].@name); 
				var objectX:Number = Number(staticsData[i].@positionX); 
				var objectY:Number = Number(staticsData[i].@positionY); 
				switch (objectName) 
				{ 
					// this switch statement checks the name of each object and according to this 
					// decides what part of the statics.png to copy 
					case "house": 
					treesBMD.copyPixels(asset.bitmapData, new Rectangle(0, 0, 200, 200), new Point(objectX, objectY), null, null, true); 
					break; 
					case "trees": 
					treesBMD.copyPixels(asset.bitmapData, new Rectangle(200, 0, 200, 200), new Point(objectX, objectY), null, null, true); 
					break; 
					case "house2": 
					treesBMD.copyPixels(asset.bitmapData, new Rectangle(400, 0, 200, 200), new Point(objectX, objectY), null, null, true); 
					break;	 
				} 
			} 
		}

In each case we copy pixels to treesBMD bitmap data from our statics.png's bitmap data. The pixels are copied from the rectangular area which represents the area in our statics.png. The Point objects is the coordinates of the place in treesBMD where the pixels will be copied to. These coordinates are taken out of Assets.xml.

Check Milestone


Step 29: Organizing Environment

Having the engine built, all we need to create a mission map is to type all necessary objects into our Assets.xml. I've already made one map:

 
<?xml version="1.0" encoding="utf-8" ?> 
<missions> 
	<mission name="First Mission of the Game"> 
		<map>maps/map.jpg</map>	 
		<assets>maps/statics.png</assets> 
		<tiles rows="6" columns="5">5,2,2,2,3, 	 5,3,10,11,6, 	 9,4,13,14,6,	 12,4,15,15,6,		15,4,15,12,6,		11,4,15,15,6</tiles> 
		<staticObjects> 
			<!--Adding trees--> 
			<object name="trees" positionX="100" positionY="100">trees</object> 
			<object name="trees" positionX="280" positionY="100">trees</object> 
			<object name="trees" positionX="400" positionY="600">trees</object> 
			<object name="trees" positionX="580" positionY="620">trees</object> 
			<object name="trees" positionX="700" positionY="430">trees</object> 
			<object name="trees" positionX="0" positionY="800">trees</object> 
			 
			<!--Adding houses of the first type--> 
			<object name="house" positionX="130" positionY="350">trees</object> 
			<object name="house" positionX="420" positionY="1000">trees</object> 
			<!--And the second type--> 
			<object name="house2" positionX="50" positionY="550">trees</object> 
			<object name="house2" positionX="400" positionY="800">trees</object> 
		</staticObjects> 
	</mission> 
</missions>

You can use this or create your own map.

After you finish with the map, open up MapBuilder class and comment out setDebugger() method call

 
//setDebugger();

to remove the debugging grid.

Check Milestone


Step 30: First Test Character

By now we have a map and a camera but still we can't see how the camera follows the characters since we don't have a single character. It's time to create one for testing.
I've modeled I very primitive tank in Google SketchUp:


And rendered the top of its body and turret with V-Ray. Basically it has only turret and body. No tracks, no wheels etc. You won't need a detailed tank because it will be so small that it's going to be hard to see any details.

Next I opened turret and tank body in one Photoshop document:


...went to Image > Image Size and made it 30 pixels wide with "constant proportions" checked. Then I right-clicked each layer and converted it to smart objects. Once again I right-clicked these smart objects and chose "Edit Contents". Thus I could open tank's body and turret in separate documents and then save them for web and devices as png 24.


Step 31: Assembling Tank MovieClip in Flash

If you're following along, open up Flash and go to File > Import > Import to Library. Navigate to the folder where you saved your tank and turret images, select them both and import to library. Or you can just select the images in the folder and simply drag them onto your library panel. As they appear in the library, you have to change their properties. Right-click each image and choose Properties. For the compression select lossless and check allow smoothing.


Now go to Insert – New Symbol. Create new MovieClip symbol and call it Tank. After the symbol is created go to library and drag the tank's body onto the stage, align it like this:


Then convert this body to Movie Clip (Hit F8). Give it any name (it doesn't matter).
Go to its filters panel and add new drop shadow filter.


Lock the body's layer and create new layer above it. Call it turret, and drag an instance of the turret from the library.


Also convert the turret to Movie Clip and give it any name. Then click it to select (if it's not selected yet) and give it an instance name of mTurret


Don't forget to save the file.


Step 32: Custom Class for the Tank.

Go back to FlashDevelop, right-click projects file and chose Add > New Folder. Name it "tanks" (without quotes). Then right click this folder and add new class. Give it a name of Ptank (for player tank). The class must extend MovieClip since we'll need its timeline for animation.

 
package tanks  
{ 
	import flash.display.MovieClip; 
 
	public class Ptank extends MovieClip 
	{ 
		 
		public function Ptank()  
		{ 
			 
		} 
	} 
}

Now open up Flash and find your Tank MovieClip in the library. Right-click it and go to Properties (or Linkage in CS3). Check Export For Actionscript and give it a full path to your Ptank class. Also clear the "Base class" entry box since you don't need this line any more.


Click OL, and if it doesn't give you any errors, everything's right, and you've successfully attached Tank MovieClip to your custom class.


Step 33: First Modification to Ptank's Constructor Method

The tank will be added to stage just like camera. So first thing to do is modify its constructor method.

 
public function Ptank(parent:DisplayObjectContainer, location:Point, rotation:Number)  
{ 
	this.x = location.x; 
	this.y = location.y; 
	this.rotation = rotation; 
	parent.addChild(this); 
}

The first two parameters are the same as parameters for camera, and the third one will let the program know what angle the tank should be at when it's first added to stage.
Right now we have all means to add the tank to the screen. Let's do it. Open up GameStructure.as in FlashDevelop and in the assetsLoaded() method, right below this line:

 
e.target.removeEventListener(Event.COMPLETE, assetsLoaded);

...add new tank:

 
var playerTank:Ptank = new Ptank(camera, new Point(100, 100), 90);

The parent object will be camera, position it at 100 x and 100 y and rotate the tank by 90 degrees. One more thing you need now is to create an array of player's objects and push tank into it. You'll understand why later, just create the array now:

 
public var playerObjects:Array = [];

Then, right after the line where you created tank, push it into this array:

 
var playerTank:Ptank = new Ptank(camera, new Point(100, 100), 90); 
playerObjects.push(playerTank);

If you test it now, you should see new tank in the top left corner, but of course it won't respond to your mouse or keyboard input.


Step 34: prepareToMove() Method

In order to move tank, we need to add several variables to Ptank class:

 
private var _curentPositionX:Number; 
private var _curentPositionY:Number; 
private var _previousPositionX:Number; 
private var _previousPositionY:Number; 
private var _moveToPositionX:Number; 
private var _moveToPositionY:Number; 
 
 
public function prepareToMove():void 
{ 
	_previousPositionX = this.x; 
	_previousPositionY = this.y; 
	_moveToPositionX = this.parent.mouseX; 
	_moveToPositionY = this.parent.mouseY; 
	 
		// the next code will not let the tank move in a strange manner, you can see what I mean  
		// if you comment out all the lines from this point to tween.  
		 
		var imTurnedAt:Number = this.rotation; // this variable is necessary because we cannot reassign thi.rotation property without moving the tank 
		var _tankToDestinationAngle:Number = Math.atan2(_moveToPositionY - this.y, _moveToPositionX - this.x) * Globals.RAD_DEG + 90; 
		if (_tankToDestinationAngle >= 180 && _tankToDestinationAngle <= 270) 
		{ 
			_tankToDestinationAngle = _tankToDestinationAngle - 360; 
		} 
		if (imTurnedAt > 0 && _moveToPositionX > this.x && _moveToPositionY > this.y) 
		{ 
			imTurnedAt = this.rotation; 
		} 
		else if (imTurnedAt > 0 && _moveToPositionX < this.x && _moveToPositionY > this.y) 
		{ 
			imTurnedAt -= 360; 
		} 
		else if (imTurnedAt < 0 && _moveToPositionX < this.x && _moveToPositionY > this.y) 
		{ 
			imTurnedAt = this.rotation; 
		} 
		else if (imTurnedAt < 0 && _moveToPositionX > this.x && _moveToPositionY > this.y) 
		{ 
			imTurnedAt += 360; 
		} 
		else  
		{ 
			imTurnedAt = this.rotation; 
		} 
		var turnToDestinationPoint:Tween = new Tween(this, "rotation", None.easeOut, imTurnedAt, _tankToDestinationAngle, 0.7, true); 
		 
}

Step 35: Tween Rotation Drawback

As you can see I used tween to rotate tank to the destination point. It would be just perfect if tween had not had a little problem. When you use tweens for changing rotation property of an object it always chooses the shortest way from one angle to another between 0 and – 180 and 0 and 180 and if you get rig of all negative angles it will choose the angle between 0 and 360.
I know it sounds very confusing, so I'll try to explain it in a more practical example.
Imagine your tank is turned at 350 degrees and you click at 10 degrees:


The red point is where you click. Now guess what will the tank do? Will it rotate Clockwise or Counter Clockwise?
It will rotate clockwise because if it rotated CCW it would have to go to "370 degrees" but it cannot figure this out itself because there's no 370 angle in its coordinate system, even though it would look more natural. The code I've designed resolves this problem and the object rotates naturally every time. It's pretty complicated but you can guess what it does if you look through it very carefully.


Step 36: Method to Move Player Objects

All of our game objects that belong to player will be moved from inside GameStructure class. For this purpose we need to add MouseEvent.CLICK listener to the Camera object.

Open up GameStructure.as, find where you added listeners for KEY_DOWN and KEY_UP events and add MouseEvent.CLICK listener right below them:

 
camera.addEventListener(MouseEvent.CLICK, moveObjects);

Now create event handler called moveObjects.

 
private function moveObjects(e:MouseEvent):void  
		{ 
			// this "if" statement is a part of "defensive" programming. If playerObjects array is empty, it's obvious that there's nothing to move 
			// this code will not let the program generate error in this case 
			if (playerObjects.length > 0) 
			{ 
				// remember the object at 0 index is our tank now 
				// when you click _camera it calls  prepareToMove() method inside Ptank's code 
				playerObjects[0].prepareToMove(); 
			} 
		}

Check Milestone


Step 37: moveMe() Method

If you test the game right now, you can see how tank reacts to the mouse clicks. It turns but doesn't move. Lets create a method that will fix this. But before we can move the tank we should define its speed.
Add one more variable:

 
private var tankSpeed:Number;

And initialize it inside of prepareToMove() method:

 
tankSpeed = 2;

So every time you click the speed resets to 2 despite its current value.
Go to prepareToMove() method and find this line:

 
var turnToDestinationPoint:Tween = new Tween(this, "rotation", None.easeOut, imTurnedAt, _tankToDestinationAngle, 0.7, true);

Add Event.ENTER_FRAME listener right after it.

 
this.addEventListener(Event.ENTER_FRAME, moveMe);

And create event handler:

 
private function moveMe(e:Event):void  
{ 
	 
	var angleToTarget:Number = Math.atan2(_moveToPositionY - _previousPositionY, _moveToPositionX - _previousPositionX); 
	this.x += Math.cos(angleToTarget) * tankSpeed;  
	this.y += Math.sin(angleToTarget) * tankSpeed; 
}

(I won't explain this code 'cause I had already explained it in one of my previous tutorials http://active.tutsplus.com/tutorials/games/deploy-a-tank-on-a-mission-in-an-isometric-war-zone/)
The tank is able to move now but it never stops. In my previous tut I used Math.abs() to stop tank, nevertheless we won't do it in this game because it stops tank immediately after it reaches the destination but we need it to slow down gradually. So, we'll use a Pythagorean theorem to solve this problem.


Step 38: Slow Down Gradually

Update moveMe() method again:

 
private function moveMe(e:Event):void  
		{ 
			 
			var angleToTarget:Number = Math.atan2(_moveToPositionY - _previousPositionY, _moveToPositionX - _previousPositionX); 
			this.x += Math.cos(angleToTarget) * tankSpeed;  
			this.y += Math.sin(angleToTarget) * tankSpeed; 
			 
			var destinationPoint:Point = new Point(_moveToPositionX, _moveToPositionY); 
			var currentTankPosition:Point = new Point(this.x, this.y); 
			var distanceBetweenPoints:Point = destinationPoint.subtract(currentTankPosition); 
			var distanceLeft:Number = Math.sqrt(Math.pow(distanceBetweenPoints.x, 2) + Math.pow(distanceBetweenPoints.y, 2)); 
			trace(distanceLeft); 
		}

I won't explain the theorem here, I'll just say that the two cathetuses -- shorter sides of a right-angled triangle -- are the distances to destination point at Y and X directions. I just took the tank's current position point, subtracted it from destination point and thus I found 2 cathetuses. And the distance that is left for out tank to go is hypotenuse. It's actually the square root from the sum of cathetuses.

You can trace it to show how it veries when the tank moves.

There's a couple more things to do to stop the tank.
Add an "if statement" to moveMe() method:

 
if (distanceLeft < 50) 
{ 
	tankSpeed -= 0.05; 
	if (tankSpeed <= 0) 
	{ 
		this.removeEventListener(Event.ENTER_FRAME, moveMe); 
	} 
}

I think it's pretty clear. When the distance to go is less than 50 pixels, the tank will decelerate until its speed reaches 0 or below, and then ENTER_FRAME listener that moves tanks is removed.
To reader: If you wanna test your skills, try to figure out a way how to make tank accelerate gradually, using the same approach.


Step 39: Rotate Turret

To rotate the turret to face the direction of your pointer, you will just need to add these three lines to the top of the moveMe() method:

 
var mouseXpos:Number = this.parent.mouseX; 
var mouseYpos:Number = this.parent.mouseY; 
// calculating the angle to the mouse 
this.mTurret.rotation = ((Math.atan2(this.y - mouseYpos, this.x - mouseXpos) * Globals.RAD_DEG) -90) - this.rotation;

Note: mTurret object is nested inside tank and despite the tank's current rotation it thinks it's rotated at 0 degrees so we must take the tank's rotation into consideration and subtract it from the turret's rotation.

There's one little problem with it right now. Remember, when the tank reaches its destination, the ENTER_FRAME listeners are removed so the turret will also stop aiming at the pointer when the tank stops. Actually, we won't use this ENTER_FRAME listener later in the tutorial (when we make a centralized controls system) but at this point let's just fix this little trouble real quick.

Go to the top of your Ptank class and declare one more variable:

 
private var _tankIsMovingRightNow:Boolean = false;

Now go to moveMe() method and add all code you see there (except for the code that rotates the turret) into this conditional statements:

 
if (_tankIsMovingRightNow) 
{ 
 
}

And one last thing find where you added event listener for ENTER_FRAME, and add this line right after it:

 
_tankIsMovingRightNow = true;

and add this line:

 
_tankIsMovingRightNow = false;

instead of this:

 
this.removeEventListener(Event.ENTER_FRAME, moveMe);

Test the game.

Check Milestone


Step 40: Camera Tracking

The last thing we do in this part of the tutorial will be active camera tracking.

Before the camera can follow the tank, it should know where it's moving. So we need two more variables for Ptank.as

 
private var _tankHorizontalDirection:String; 
private var _tankVerticalDirection:String;

Then modify moveMe() method adding this code to the end of it:

 
// to calculate the direction of the tank's movement we need to check it's initial position 
// and final position at the end of every frame 
// the first of the next conditional statement line is the part of "defensive" programming. There's a possibility that  
// previous or final position of the tank might not be a number. In this case flash will generate an error, which we don't need 
if (isNaN(_curentPositionX) || isNaN(_curentPositionY)) 
{ 
	_previousPositionX = this.x; 
	_previousPositionY = this.y; 
} else { 
	_previousPositionX = _curentPositionX; 
	_previousPositionY = _curentPositionY; 
}	 
_curentPositionX = this.x; 
_curentPositionY = this.y; 
 
// if we take the tank's parent (Camera object) its child at 0 index will be map. 
// let's assign it to a variable to simplify our coding 
var myMap:DisplayObject = this.parent.getChildAt(0); 
// the next line calculates the maps screen position relatively to its parent 
// it might look a little strange at first sight, but if you don't do it, the map will not stop moving  
// when it reaches the edge of the stage 
var mapLocalToGlobal:Point = myMap.localToGlobal(new Point(myMap.x, myMap.y));		 
// myStgPosition doesn almost the same but calculates the tank's global position 
var myStgPosition:Point = this.localToGlobal(new Point(stage.x, stage.y)); 
 
// calculating the tank's direction. The calculation is based on the discrepancy between  
// the tank's current an previous position 
if (_previousPositionX > _curentPositionX) 
{ 
	_tankHorizontalDirection = "left"; 
}  
else if (_previousPositionX < _curentPositionX) 
{ 
	_tankHorizontalDirection = "right"; 
}  
if (_previousPositionY > _curentPositionY) 
{ 
	_tankVerticalDirection = "up"; 
}  
else if (_previousPositionY < _curentPositionY) 
{ 
	_tankVerticalDirection = "down"; 
}  
 
// the next 4 blocks of code move the map in the direction opposite to the tank's 
// 300 means the center of the stage because wee need the camera to start moving only after the tank reaches it 
if (_tankHorizontalDirection == "right" && myMap.x + 300 <= this.x && mapLocalToGlobal.x + myMap.width >= stage.stageWidth + 1 + tankSpeed && myStgPosition.x > 300) 
{ 
	this.parent.x -= Math.cos(angleToTarget) * tankSpeed; 
} 
if (_tankHorizontalDirection == "left" && mapLocalToGlobal.x <= 0 - tankSpeed && myMap.x + (myMap.width - 300) >= this.x && myStgPosition.x < 300) 
{ 
	this.parent.x -= Math.cos(angleToTarget) * tankSpeed; 
} 
if (_tankVerticalDirection == "down" && myMap.y + 300 <= this.y && mapLocalToGlobal.y + myMap.height >= stage.stageHeight + tankSpeed && myStgPosition.y > 300) 
{ 
	this.parent.y -= Math.sin(angleToTarget) * tankSpeed; 
} 
if (_tankVerticalDirection == "up" && mapLocalToGlobal.y <= 0 - tankSpeed && myMap.y + (myMap.height - 300) >= this.y && myStgPosition.y < 300) 
{ 
	this.parent.y -= Math.sin(angleToTarget) * tankSpeed; 
}

Just to make sure there's no confusion about anything, look the the class's code in its entirety:

 
package tanks  
{ 
	import fl.transitions.easing.None; 
	import fl.transitions.Tween; 
	import fl.transitions.TweenEvent; 
	import flash.display.DisplayObjectContainer; 
	import flash.display.DisplayObject; 
	import flash.display.MovieClip; 
	import flash.events.Event; 
	import flash.events.MouseEvent; 
	import flash.geom.Point; 
 
	public class Ptank extends MovieClip 
	{ 
		private var _curentPositionX:Number; 
		private var _curentPositionY:Number; 
		private var _previousPositionX:Number; 
		private var _previousPositionY:Number; 
		private var _moveToPositionX:Number; 
		private var _moveToPositionY:Number; 
		private var tankSpeed:Number; 
		private var _tankHorizontalDirection:String; 
		private var _tankVerticalDirection:String; 
		 
		public function Ptank(parent:DisplayObjectContainer, location:Point, rotation:Number)  
		{ 
			this.x = location.x; 
			this.y = location.y; 
			this.rotation = rotation; 
			parent.addChild(this); 
			this.parent.addEventListener(MouseEvent.MOUSE_MOVE, rotateTurret); 
		} 
		public function prepareToMove():void 
		{ 
			tankSpeed = 2; 
			_previousPositionX = this.x; 
			_previousPositionY = this.y; 
			_moveToPositionX = this.parent.mouseX; 
			_moveToPositionY = this.parent.mouseY; 
			 
				 
				var imTurnedAt:Number = this.rotation;  
				var _tankToDestinationAngle:Number = Math.atan2(_moveToPositionY - this.y, _moveToPositionX - this.x) * Globals.RAD_DEG + 90; 
				if (_tankToDestinationAngle >= 180 && _tankToDestinationAngle <= 270) 
				{ 
					_tankToDestinationAngle = _tankToDestinationAngle - 360; 
				} 
				if (imTurnedAt > 0 && _moveToPositionX > this.x && _moveToPositionY > this.y) 
				{ 
					imTurnedAt = this.rotation; 
				} 
				else if (imTurnedAt > 0 && _moveToPositionX < this.x && _moveToPositionY > this.y) 
				{ 
					imTurnedAt -= 360; 
				} 
				else if (imTurnedAt < 0 && _moveToPositionX < this.x && _moveToPositionY > this.y) 
				{ 
					imTurnedAt = this.rotation; 
				} 
				else if (imTurnedAt < 0 && _moveToPositionX > this.x && _moveToPositionY > this.y) 
				{ 
					imTurnedAt += 360; 
				} 
				else  
				{ 
					imTurnedAt = this.rotation; 
				} 
				var turnToDestinationPoint:Tween = new Tween(this, "rotation", None.easeOut, imTurnedAt, _tankToDestinationAngle, 0.7, true); 
				this.addEventListener(Event.ENTER_FRAME, moveMe); 
		} 
		 
		private function moveMe(e:Event):void  
		{ 
			 
			var angleToTarget:Number = Math.atan2(_moveToPositionY - _previousPositionY, _moveToPositionX - _previousPositionX); 
			this.x += Math.cos(angleToTarget) * tankSpeed;  
			this.y += Math.sin(angleToTarget) * tankSpeed; 
			 
			var destinationPoint:Point = new Point(_moveToPositionX, _moveToPositionY); 
			var currentTankPosition:Point = new Point(this.x, this.y); 
			var distanceBetweenPoints:Point = destinationPoint.subtract(currentTankPosition); 
			var distanceLeft:Number = Math.sqrt(Math.pow(distanceBetweenPoints.x, 2) + Math.pow(distanceBetweenPoints.y, 2)); 
			 
			if (distanceLeft < 50) 
			{ 
				tankSpeed -= 0.05; 
				if (tankSpeed <= 0) 
				{ 
					this.removeEventListener(Event.ENTER_FRAME, moveMe); 
				} 
			} 
			 
			 
			if (isNaN(_curentPositionX) || isNaN(_curentPositionY)) 
			{ 
				_previousPositionX = this.x; 
				_previousPositionY = this.y; 
			} else { 
				_previousPositionX = _curentPositionX; 
				_previousPositionY = _curentPositionY; 
			}	 
			_curentPositionX = this.x; 
			_curentPositionY = this.y; 
 
			var myMap:DisplayObject = this.parent.getChildAt(0); 
			var mapLocalToGlobal:Point = myMap.localToGlobal(new Point(myMap.x, myMap.y));		 
			var myStgPosition:Point = this.localToGlobal(new Point(stage.x, stage.y)); 
 
			if (_previousPositionX > _curentPositionX) 
			{ 
				_tankHorizontalDirection = "left"; 
			}  
			else if (_previousPositionX < _curentPositionX) 
			{ 
				_tankHorizontalDirection = "right"; 
			}  
			if (_previousPositionY > _curentPositionY) 
			{ 
				_tankVerticalDirection = "up"; 
			}  
			else if (_previousPositionY < _curentPositionY) 
			{ 
				_tankVerticalDirection = "down"; 
			}  
 
			 
			if (_tankHorizontalDirection == "right" && myMap.x + 300 <= this.x && mapLocalToGlobal.x + myMap.width >= stage.stageWidth + 1 + tankSpeed && myStgPosition.x > 300) 
			{ 
				this.parent.x -= Math.cos(angleToTarget) * tankSpeed; 
			} 
			if (_tankHorizontalDirection == "left" && mapLocalToGlobal.x <= 0 - tankSpeed && myMap.x + (myMap.width - 300) >= this.x && myStgPosition.x < 300) 
			{ 
				this.parent.x -= Math.cos(angleToTarget) * tankSpeed; 
			} 
			if (_tankVerticalDirection == "down" && myMap.y + 300 <= this.y && mapLocalToGlobal.y + myMap.height >= stage.stageHeight + tankSpeed && myStgPosition.y > 300) 
			{ 
				this.parent.y -= Math.sin(angleToTarget) * tankSpeed; 
			} 
			if (_tankVerticalDirection == "up" && mapLocalToGlobal.y <= 0 - tankSpeed && myMap.y + (myMap.height - 300) >= this.y && myStgPosition.y < 300) 
			{ 
				this.parent.y -= Math.sin(angleToTarget) * tankSpeed; 
			} 
	 
		} 
		private function rotateTurret(e:MouseEvent):void 
		{ 
			var mouseXpos:Number = this.parent.mouseX; 
			var mouseYpos:Number = this.parent.mouseY; 
			this.mTurret.rotation = ((Math.atan2(this.y - mouseYpos, this.x - mouseXpos) * Globals.RAD_DEG) -90) - this.rotation; 
		} 
	} 
}

Conclusion

Well, that's it for Part 1 of the tutorial, you may test the game now. In the next lesson we'll create the system of collisions with another vehicles and static objects like trees and houses. The system of water detection, some basic artificial intelligence for enemies that will let them find player objects and destroy them, the system of player objects selection, and of course the weapons.

I hope you enjoyed following along so far :)

Advertisement