Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Create an Epic War Game in Flash: Part 2

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

In the first lesson we created an engine to build the environment for our game; a camera that can follow active player objects; and one player object, the tank. But the game is still far from finished. During this tutorial we'll fix all of the problems and turn it into an actual game. Let's get started!


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

Step 1: Initial Code for Tank Shell Class

At the beginning we'll create a shell for player tank. Open up FlashDevelop and in the root directory of your project create a folder called "shells". Right-click it and add new class. Call it Shell.


Here's the initial code for it:

 
package shells  
{ 
	import flash.display.DisplayObjectContainer; 
	import flash.display.MovieClip; 
	import flash.geom.Point; 
 
	public class Shell extends MovieClip 
	{ 
		 
		public function Shell(parent:DisplayObjectContainer, location:Point, rotation:Number, destination:Point)  
		{ 
			this.x = location.x; 
			this.y = location.y; 
			this.rotation = rotation; 
			parent.addChild(this); 
		} 
	} 
}

The code should look familiar because we already used similar construction in the first lesson.


Step 2: Tank Shell MovieClip

Open up WarGame.fla and go to Insert > New Symbol. Choose Movie Clip and attach it to PlayerTank.as.


This shouldn't give you any errors, so if it does, check everything once again.

Now choose PolyStar Tool from the instruments palette:


Go to tool's options and set Number of Sides to 3, so you could draw a triangle


For Stroke Color choose none and for Fill > Linear Gradient


In the Color palette transform the gradient fill like this


Now draw a triangle with the top at the registration point


This can be done pretty easily. Just type in these parameters:


To make it look more like a real shell, choose Convert Anchor Point Tool


Click the topmost point of the shell and simply drag the handlers to the right or left to make it look like this, and save it:



Step 3: Explosion MovieClip

Go to Insert menu once again and insert another MovieClip symbol. Call it Explosion and click OK. You don't have to export it for ActionScript because it will be used only as a nested MovieClip inside our Shell.
Pick up Brush Tool, set its width to whatever value you like (the bigger the better). Choose "none" for Stroke Color and yellow for Fill.


Draw some irregularly shaped spot like this:


This shape will represent the explosion. Red and yellow parts are for the fire (as you could guess) and black ones are for smoke. Exit the Symbol Editing Mode and open Shell symbol again.


Step 4: Assembling the Shell

Now right-click the second frame of the Shell timeline and choose Insert Bank Keyframe


Drag Explosion MovieClip from the library to this frame, give it an instance name of mExplosion and change its width and height to 4


Select mExplosion (by clicking it once) and add Blur filter to it


Set Blur X and Blur Y to 12 and Quality to High


Now select the 6th frame and hit F6 to insert another Keyframe. Select this frame


...and change mExplosion's width and height to 40


Right-click any frame between 1st and 6th and choose Create Classic Tween


The last thing you need to do now is to stop the shell's animation from playing. To do this, select the first frame and hit F9. When the actions panel pops up, type in stop(); and close it.



Step 5: First Variables for the Shell

In order to move the shell, we need to define its speed, direction (which is equal to the rotation parameter passed to the constructor), initial position (not this.x and this.y, because these values will change during the shell's flight but we'll need constant values), destination point, and the Boolean variable to check if the shell's hit something.

So, first thing to do now is add these variable to Shell class:

 
// in case there was no destination passed to it fro some reason, give it a defaul destination. This makes  
private var _destination:Point = new Point(this.x, this.y); 
private var _speed:Number; 
private var _hit:Boolean = false; 
private var _initialPosition:Point;

Initialize them in the constructor. Your class should look this way now:

 
package shells  
{ 
	import flash.display.DisplayObjectContainer; 
	import flash.display.MovieClip; 
	import flash.geom.Point; 
 
	public class Shell extends MovieClip 
	{ 
		private var _destination:Point; 
		private var _speed:Number; 
		private var _hit:Boolean = false; 
		private var _initialPosition:Point; 
		 
		public function Shell(parent:DisplayObjectContainer, location:Point, rotation:Number, destination:Point)  
		{ 
			this.x = location.x; 
			this.y = location.y; 
			this.rotation = rotation; 
			_destination = destination; 
			_speed = 15; 
			// the next variable is set only once, when the shell instance is created and does not change while it flies 
			_initialPosition = new Point(this.x, this.y); 
			parent.addChild(this); 
		} 
	} 
}

Step 6: Where To Fly and For How Long?

To move the shell we need either an ENTER_FRAME or TIMER event listener. We'll use ENTER_FRAME here. Go to the constructor method and add Event.ENTER_FRAME listener to it.

 
addEventListener(Event.ENTER_FRAME, moveShell);

then create an event handler for moveShell().

 
private function moveShell(e:Event):void  
{ 
			 
}

We're close to moving the shell now, but what distance should it travel? It's obvious that the destination point is the mouse's position at the moment the shell is created, so we only need to calculate the distance between the destination point and the shell's initial position.

Luckily, Actionscript can do it for us, we only have to pass two points that we need to know the distance between to the Point class's distance() function. Let's do it.
Add one more variable:

 
private var _distanceToTravel:Number;

and initialize it in the constructor:

 
_distanceToTravel = Point.distance(_destination, _initialPosition);

That's it. Now the program knows the distance that the shell should travel when it's created.


Step 7: Choice Between Movement and Shooting

You probably want to test the shell already, but we can't do it until we create some means to launch it. The idea is: when you click somewhere the tank moves to the pointer's position, but if you press Control key and then click, it shoots at the pointer. To do this, open up GameStructure.as in FlashDevelop and find where you declared these variables:

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

Add one more variable right below them

 
private var _controlPressed:Boolean = false;

Go to buttonControls() method and add the next conditional statement (that will set _controlPressed variable to true) to the top of it.

 
if (e.keyCode == Keyboard.CONTROL) 
{ 
	_controlPressed = true; 
}

Since we have code that sets variable to true when Control key is pressed, we need another one that'll set it back to false when the key is released. Go to releaseKey() method and add one more case to its switch statement:

 
case Keyboard.CONTROL: 
	_controlPressed = false; 
	break;

Now we need to delimit when out tank should move and when shoot. Find moveObjects() method. That's how it looks now:

 
private function moveObjects(e:MouseEvent):void  
{ 
	if (playerObjects.length > 0) 
	{ 
		playerObjects[0].prepareToMove(); 
	} 
}

And modify it like this:

 
private function moveObjects(e:MouseEvent):void  
{ 
	if (playerObjects.length > 0) 
	{ 
		if (!_controlPressed) // the exclamation sign means that if control is not pressed, do this block of code 
		{ 
			playerObjects[0].prepareToMove(); 
		} 
		else  
		{ 
			playerObjects[0].shoot(); 
		} 
	} 
}

If you test it now, it will give you a #1069 Error because Ptank class does not implement shoot() method yet.


Step 8: Shoot() Method Test

Open up Ptank.as in FlashDevelop and create public function shoot() somewhere (I'd do it right after the constructor).

 
public function shoot():void 
{ 
	trace("shoot"); 
}

And now, if you test it again, it will trace "shoot" string when you click the stage with Control key pressed.
Remove the trace statement and paste the next line instead:

 
var shell:Shell = new Shell(this.parent, new Point(this.x, this.y), this.rotation, new Point(this.x, this.y));

It's not the correct version of the method, but at least we can see if the tank can create a shell.
Test it now, and if you see a new shell right over the center of the turret every time you Control-Click somewhere > everything is fine.

Note: if you're working in FlashDevelop it will do all the imports for you, but if not, don't forget to import the necessary classes. In the case with shell it'll be:

 
import shells.Shell;

Step 9: Major Modifications to shoot() method

It's time to pass real parameters to Shell.

 
public function shoot():void 
{ 
	var destination:Point = new Point(this.parent.mouseX, this.parent.mouseY); 
	// since we need to rotate the shell in the same direction as turret, add turret's rotation to the global rotation of the tank 
	var shellRotation:Number = this.rotation + this.mTurret.rotation; 
	// offset is the distance at which the shell will appear relatively to the center of the turret. 50 pixels offset will place it almost at the edge of the barrel 
	var shellOffset:int = 50; 
	// we need trigonometry to calculate the shell's initial position because it will differ depending on the turret's curent angle 
	var initialShellX:Number = this.x + Math.sin(shellRotation * Globals.DEG_RAD) * shellOffset; 
	var initialShellY:Number = this.y - Math.cos(shellRotation * Globals.DEG_RAD) * shellOffset; 
	// and don't forget to pass all these new parameters to Shell constructor 
	var shell:Shell = new Shell(this.parent, new Point(initialShellX, initialShellY), shellRotation, destination); 
}

Test it again and you'll see shells appear in the correct positions


Step 10: Major Modifications to Shell.as

Go to moveShell() method and update it like so:

 
private function moveShell(e:Event):void  
{ 
	// turn shell's rotation property to radians 
	var radiansAngle:Number = this.rotation * Globals.DEG_RAD; 
	// just like we calculated the way that the shell is supposed to pass, here we calculate how much it's passed already 
	var passedDistance:Number = Point.distance(new Point(this.x, this.y), _initialPosition); 
	this.y -= Math.cos(radiansAngle) * _speed; 
	this.x += Math.sin(radiansAngle) * _speed; 
	// and if the passed distance surpasses the necessary distance or the shell hits something, remove ENTER_FRAME listener and play explosion animation. 
	// 400 is a maximum distance the shell will travel at, to make the game more interesting 
	if (passedDistance >= _distanceToTravel || _hit || passedDistance > 400) 
	{ 
		removeEventListener(Event.ENTER_FRAME, moveShell); 
		this.gotoAndPlay(2); 
	} 
}

Step 11: What Objects Can the Shell Hit?

That's a reasonable questions. Normally it should hit enemies or some static stuff like trees or houses. And since we don't have any enemies yet, let's make it hit some statics.
Open up GameStructure.as and add one more variable to it

 
//This array will contain all the stuff that out shell or tank can collide with 
// it's gonna be static because we'll need to access it without initializing in another classes 
public static var hittableStuff:Array = [];

Find where you added trees to the camera

 
camera.addChild(treesBM);

And add a line that will push it into the hittableStuff array right after it

 
treesBM = new Bitmap(treesBMD); 
hittableStuff.push(treesBM);

The idea here is that BitmapData has a perfect collision detection method. We can now test whether some object collides with its transparent pixels or not.


Step 12: Shell HitTesting

In Shell.as find moveShell() method and add the next "for in" loop right before the "if statement"

 
for (var i in GameStructure.hittableStuff) 
{ 
	if (GameStructure.hittableStuff[i] is Bitmap) 
	{ 
		trace("hitting bitmap"); 
	} 
}

If you shoot now, it will trace "hitting bitmap" which means that the shell can touch trees or houses. So, now we need to find out what kind of pixel it touches, transparent or not.
Remove trace statement and put the next "if" statement instead of it:

 
if (GameStructure.hittableStuff[i] is Bitmap) 
{ 
	// assign GameStructure.hittableStuff[i] to a variable to shorten the code 
	var bm:Bitmap = Bitmap(GameStructure.hittableStuff[i]); 
	// the first object being tested is bitmap, the second might be whether a Point or a Rectangle or even Object. In this case we only need to know if shell's X and Y touch non-transtapert area of the bitmap and if so, explode the shell.  
	// 255 means the opacity of alpha channel (in this case fully transparent). 
	if (bm.bitmapData.hitTest(new Point(bm.x, bm.y), 255, new Point(this.x, this.y))) 
	{ 
		_hit = true; 
	} 
}

Step 13: Removing the Shell After Explosion

If you test the game at this point, you'll see how the shell explodes if it touches a tree or a house but it is not removed after it and it doesn't look too good.
You probably think now, where do I remove the shell?

That's a reasonable question. If you remove the shell right after the collision you won't see an explosion animation and thus you can't check if it can kill some character or not. So the best place to remove shell is the last frame of explosion animation.

Open up Shell Movie Clip in Flash (by double-clicking its icon in the library) and add one more layer above the animation layer, call it AS3.


Now convert the last frame and the one before it to empty key frames. Select the last one and hit F9 to open Actions Panel and place the code that will remove shell to it:


If you test the game now, you'll see how the shell is removed after explosion. Easy isn't it?

As to frame before the last one, will come back to it later, after we create the enemy tank.


Step 14: Enemy Tank Coloring

Basically, enemy tank should be almost just like the player's one, except for coloring. Open tankBody.png in Photoshop and hit Control + U to open Hue / Saturation panel and adjust colors to whatever values you like. I chose these parameters:


Save it for Web And Devices as enemyTankBody.png. Do the same to the turret graphics. If you don't have Photoshop you can use my graphics from the Source download above.

Drag the graphics to Flash library and assemble enemy tank Movie Clip just like we did in the first part of the tut with the player's tank.


Step 15: Tank Explosion Animation

Close turret and body layers and add a new layer above them. Then select 6th frame and hit F6 to add a blank key frame. You should get this:


Select the last frame and simply drag it to the second position to get this:


Drag Explosion Movie Clip from the library to the second frame of explosion layer. Add Blur filter to it.

  • Blur X: 13
  • Blur Y: 13
  • Quality: High

Select the last frame and hit F6 to convert it to Key Frame. Then scale Explosion Movie Clip to get something similar to this:


Basically it's the same as with shell explosion animation, so you should also create a Classic Tween . And don't forget to add stop(); method to the first frame of the explosion layer as well.


Do the same to the Tank (player tank) Movie Clip.


Step 16: Enemy Tank Class

Enemy tank is supposed to be just like player tank, except for one thing, it will be controlled by Artificial Intelligence. But for now just duplicate Ptank class and call it Etank, and also change the class and constructor names inside:


Attach this class to EnemyTank MovieClip like you did before with player tank:



Step 17: Two More Custom Events

Open up MyEvent.as and add two more "events" (constants) to it:

 
public static const DAMAGE:String = "damaged"; 
public static const SHELL_HIT:String = "shelHit";

Both of these events will be dispatched by a shell. The first one is needed to inform active objects that they have been damaged and the second one will be used to let tank shoot again only when the previous shell is removed. Actually tank could shoot without it but it would cause 2 big problems.
1)If it shoots too many times, flash player might just freeze because it wouldn't be able to process an enormous number of shells at a time.
2)If enemy tank spotted you, it would just give you no chance to survive by shooting a series shells at you.


Step 18: How To Add Flexibility to GameStructure Class?

By now, we have manually added player tank to the display list. This system is far not the most flexible because we can't add active characters for a particular mission without recompiling the whole project. So, the best way to work around this problem now is to add mission characters right to the Assets.xml. Open it up in FlashDevelop and add one node for player tanks and one for enemies. Here's the entire code for Assets.xml.

 
<?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> 
		<playerTanks>  
			<tank Xposition="100" Yposition="300" rotation="90">tank</tank> 
		</playerTanks> 
		 
		<enemyTanks>  
			<tank Xposition="100" Yposition="100" rotation="90">tank</tank> 
		</enemyTanks> 
		 
		<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>

I've also added the position and rotation attributes.


Step 19: Adding Player Tanks Dynamically

Open GameStructure.as and find where you added player tank:

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

and add this code instead:

 
// assigning the XMLList path to a variable fot the purpose of shortening 
var playerTanksData:XMLList = _assets.missionAssets.mission[map.mission].playerTanks.tank; 
 
for (var pTank in playerTanksData) 
{ 
	// getting the necessary parameters from XML list 
	var pTankX:Number = playerTanksData[pTank].@Xposition; 
	var pTankY:Number = playerTanksData[pTank].@Yposition; 
	var pTankRotation:Number = playerTanksData[pTank].@rotation; 
	// this listener will activate a game object after you click upon it. Don't forget to create  selectObject() event handler 
	playerTank.addEventListener(MouseEvent.CLICK, selectObject)			 
	var playerTank:Ptank = new Ptank(camera, new Point(pTankX, pTankY), pTankRotation); 
	// since the tank is also a hittable stuff, don't porget to put each player tank to hittableStuff array 
	hittableStuff.push(playerTank); 
}

Check if you have the same GameStructure code as below, and test the game.

 
package   
{ 
	import builder.MapBuilder; 
	import builder.xmlParser; 
	import flash.display.Bitmap; 
	import flash.display.BitmapData; 
	import flash.display.Loader; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.events.KeyboardEvent; 
	import flash.events.MouseEvent; 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	import flash.net.URLRequest; 
	import flash.ui.Keyboard; 
	import tanks.Ptank; 
	 
	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; 
		private var _controlPressed:Boolean = false; 
		private var _assets:xmlParser; 
		public static var playerObjects:Array = []; 
		 
		private var treesBMD:BitmapData; 
		private var treesBM:Bitmap; 
		public static var hittableStuff:Array = []; 
		 
		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  
		{ 
			_assets = new xmlParser(); 
			_assets.addEventListener(MyEvent.MAP_STRUCTURED, setEnvironment); 
			 
			stage.addEventListener(KeyboardEvent.KEY_DOWN, buttonControls); 
			stage.addEventListener(KeyboardEvent.KEY_UP, releaseKey); 
			camera.addEventListener(MouseEvent.CLICK, moveObjects); 
		} 
		 
		private function moveObjects(e:MouseEvent):void  
		{ 
			if (playerObjects.length > 0) 
			{ 
				if (!_controlPressed) 
				{ 
					playerObjects[0].prepareToMove(); 
				} 
				else 
				{ 
					playerObjects[0].shoot(); 
				} 
			} 
		} 
		 
		private function setEnvironment(e:MyEvent):void  
		{ 
			 
			treesBMD = new BitmapData(map.width, map.height, true, 0xFFFFFF); 
			treesBM = new Bitmap(treesBMD); 
			hittableStuff.push(treesBM); 
			var loader:Loader = new Loader(); 
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, assetsLoaded); 
			loader.load(new URLRequest(_assets.missionAssets.mission[map.mission].assets)); 
		} 
		 
		private function assetsLoaded(e:Event):void  
		{ 
			e.target.removeEventListener(Event.COMPLETE, assetsLoaded); 
			 
			var playerTanksData:XMLList = _assets.missionAssets.mission[map.mission].playerTanks.tank; 
			for (var pTank in playerTanksData) 
			{ 
				var pTankX:Number = playerTanksData[pTank].@Xposition; 
				var pTankY:Number = playerTanksData[pTank].@Yposition; 
				var pTankRotation:Number = playerTanksData[pTank].@rotation; 
				 
				var playerTank:Ptank = new Ptank(camera, new Point(pTankX, pTankY), pTankRotation); 
				playerTank.addEventListener(MouseEvent.CLICK, selectObject) 
				hittableStuff.push(playerTank); 
			} 
			 
			 
			camera.addChild(treesBM); 
			camera.addChildAt(map, 0); 
			 
			var asset:Bitmap = Bitmap(e.target.content); 
 
			var staticsData:XMLList = _assets.missionAssets.mission[map.mission].staticObjects.object; 
			 
			for (var i in staticsData) 
			{ 
				var objectName:String = String(staticsData[i].@name); 
				var objectX:Number = Number(staticsData[i].@positionX); 
				var objectY:Number = Number(staticsData[i].@positionY); 
				switch (objectName) 
				{ 
					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;	 
				} 
			} 
		} 
		 
		private function selectObject(e:MouseEvent):void  
		{ 
			// event handler for mouse clicks 
			trace(e.currentTarget); 
		} 
		 
		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; 
				case Keyboard.CONTROL: 
				_controlPressed = false; 
				break; 
			} 
		} 
		 
		private function buttonControls(e:KeyboardEvent):void  
		{ 
			if (e.keyCode == Keyboard.CONTROL) 
			{ 
				_controlPressed = true; 
			} 
			 
			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); 
			} 
		} 
	} 
}

Step 20: Object Selection

You've noticed that your tank is added to the display list but you can't control it anymore. But if you click the tank you'll get the next text in the output panel:
[object Ptank]

This means that everything works.
Now we just need to push our tank to playerObjects array. But first, add one more variable:

 
private var _shiftPressed:Boolean = false;

and just like before, update releaseKey() method to process shift release:

 
case Keyboard.SHIFT: 
_shiftPressed = false; 
break;

and buttonControls():

 
if (e.keyCode == Keyboard.SHIFT) 
{ 
	_shiftPressed = true; 
}

You also need to modify moveObjects() method:

 
private function moveObjects(e:MouseEvent):void  
{ 
	if (playerObjects.length > 0) 
	{ 
		// make sure the shift key is not and if it is, the tank won't move 
		if (!_controlPressed && !_shiftPressed) 
		{ 
			playerObjects[0].prepareToMove(); 
		} 
		// it won't shoot when shift is pressed and we can select tank not making it move or shoot 
		else if (_controlPressed && !_shiftPressed) 
		{ 
			playerObjects[0].shoot(); 
		} 
	} 
}

And finally, a little modification to selectObject() method:

 
private function selectObject(e:MouseEvent):void  
{ 
	if (_shiftPressed) 
	{ 
		if (playerObjects.length > 0) 
		{ 
			// if array of active objects is not empty, clear it 
			playerObjects = []; 
		} 
	// and push the current event target into it 
	playerObjects.push(e.currentTarget); 
	} 
}

Thus, when you press Shift key you can select some of the objects you have in current mission.


Step 21: Adding Enemy Character

Since enemy tank is already added to Assets.xml, find the loops that adds playerTanks, and add one for enemy tanks right below it.

 
var enemyTanksData:XMLList = _assets.missionAssets.mission[map.mission].enemyTanks.tank; 
for (var eTank in enemyTanksData) 
{ 
	var eTankX:Number = enemyTanksData[eTank].@Xposition; 
	var eTankY:Number = enemyTanksData[eTank].@Yposition; 
	var eTankRotation:Number = enemyTanksData[eTank].@rotation; 
				 
	var enemyTank:Etank = new Etank(camera, new Point(eTankX, eTankY), eTankRotation); 
	hittableStuff.push(enemyTank); 
}

Compile the game and you'll see enemy tank in the top left corner. There is a tank but there is no means to destruct it and we need to fix it.


Step 22: Enemy Tank Destruction Preparations

It might sound a bit complicated at first but actually it's a pretty easy task to accomplish. We've already created custom event that should be dispatched to the character that's about to be destroyed, so all we need to do now is add event listener for this type of event to each character.

Open up Etank.as in FlashDevelop and go to its constructor method. Add one line to it:

 
this.addEventListener(MyEvent.DAMAGE, destroyTank);

Create event handler:

 
private function destroyTank(e:MyEvent):void  
{ 
	// remove radar 
	radar.graphics.clear(); 
	this.parent.removeChild(radar); 
	// remove listener since we no longer need it 
	this.removeEventListener(MyEvent.DAMAGE, destroyTank); 
	// play explosion animation 
	this.gotoAndPlay(2); 
}

Alright, the tank can now receive this event and play explosion animation. The only problems is that after the animation is finished, it will remain on battlefield safe and sound, so we need to remove it right after the animation is complete. For this purpose, open EnemyTank Movie Clip in Flash, select the last frame of explosion layer and hit F9 to open Actions panel. Add the next code to it:


 
// it might have ENTER_FRAME listener when you hit it so if you remove tank and leave this listener, it will give you lots of errors. But if there's no listener, this block will be ignored 
if (this.hasEventListener(Event.ENTER_FRAME)) 
{ 
	this.removeEventListener(Event.ENTER_FRAME, moveMe); 
} 
this.parent.removeChild(this);

Step 23: Dispatch DAMAGE Event

It's obvious that this event should be dispatched by some object that collides with the tank. First of all let's make our shell do it. Open Shell Movie Clip in Flash and go to that blank key frame before the last one. Hit F9 and paste the next code into the Actions panel:

 
for (var target in GameStructure.hittableStuff) 
{ 
	if (this.mExplosion.hitTestObject(GameStructure.hittableStuff[target])) 
	{ 
		// if explosion touches some hittable stuff this stuff dispatched DAMAGE event 
		GameStructure.hittableStuff[target].dispatchEvent(new MyEvent(MyEvent.DAMAGE)); 
	} 
}

By the way, after I tested it, I didn't like that the tank explodes too fast, so I added a few frames to its explosion animation (do the same to player's tank)



Step 24: Fixing A Little Problem With Player Tank

You've noticed that when you select a tank, it does not start rotation its turret until you click somewhere. It happens because there's no ENTER_FRAME listener added to it after the selection. This problem can be fixed by creating another public method that'll add it.

Open up Ptank.as and create this new method somewhere:

 
public function activator():void 
{ 
	this.addEventListener(Event.ENTER_FRAME, moveMe); 
}

Now you need to call this method from GameStructure.as. Find selectObject() method and update it like this:

 
private function selectObject(e:MouseEvent):void  
{ 
    if (_shiftPressed) 
    { 
        if (playerObjects.length > 0) 
        { 
            playerObjects = []; 
        } 
        playerObjects.push(e.currentTarget); 
        playerObjects[0].activator(); 
    } 
}

It could have been so simple but it will make all of your tank instances "look" at the pointer even though only one is supposed to be active. We need to make sure this doesn't happen. Open up Ptank again and add this code to the top of moveMe() method:

 
if (GameStructure.playerObjects[0] != this) 
{ 
	this.removeEventListener(Event.ENTER_FRAME, moveMe); 
}

The code is self-explanatory, so you can understand what it does.


Step 25: Player Tanks Destruction

Just like you did with enemy tank, add DAMAGE event listener to Ptank, and create the same event handler for it. Then, also add this code to the last frame of player tank's explosion animation:

 
if (this.hasEventListener(Event.ENTER_FRAME)) 
{ 
	this.removeEventListener(Event.ENTER_FRAME, moveMe); 
} 
// pay special attention to this block of code! If you don't remove the tank from playerObjects array it will give an error if you click somewhere after the tank is destroyed 
if (GameStructure.playerObjects[0] == this) 
{ 
	GameStructure.playerObjects.splice(GameStructure.playerObjects.indexOf(this), 1); 
} 
this.parent.removeChild(this);

You can test how your tank can now destroy itself by losing in on the trees and shooting at them. The explosion of a shell will also touch your tank and destroy it.


Step 26: Enemy Detector

There's a lot of books written on artificial intelligence and of course it's impossible to explain it all in a single tutorial. But anyway, we need some enemy reaction to our actions. That's why we need some basic AI for it.
First of all, let's create a "radar" for it. Open up Etank.as and declare a new variable:

 
private var radar:Sprite;

then add a new private function that will create a radar:

 
private function detector():void 
{ 
    radar = new Sprite(); 
    radar.mouseChildren = false; 
    radar.cacheAsBitmap = true; 
    radar.graphics.lineStyle(1, 0xFF0000, 1); 
    radar.graphics.beginFill(0xFF0000, 0.5); 
    radar.graphics.moveTo(0, 0); 
    radar.graphics.lineTo( -70, -435); 
    radar.graphics.curveTo(0, -455, 70, -435); 
    radar.graphics.lineTo(0, 0); 
    radar.graphics.endFill(); 
    radar.x = this.x; 
    radar.y = this.y; 
    radar.rotation = this.rotation + this.mTurret.rotation; 
    radar.visible = true; 
    this.parent.addChild(radar); 
}

It simply draws something similar to a radar wave. Call this method from the constructor:

 
public function Etank(parent:DisplayObjectContainer, location:Point, rotation:Number)  
{ 
    this.x = location.x; 
    this.y = location.y; 
    this.rotation = rotation; 
    parent.addChild(this); 
    this.addEventListener(MyEvent.DAMAGE, destroyTank); 
    detector(); 
    // and also add ENTER_FRAME listener, to activate tank right away 
    this.addEventListener(Event.ENTER_FRAME, moveMe); 
}

And you'll see this:


If you compile the game now, enemy tank will turn its turret to the pointer's position. It's nothing, we'll fix it later.


Step 27: Trying To Detect Player

The idea of player detection is based on hitTestObject method. Radar tests for collision every frame and if some player object touches it, the tanks starts to follow this object and shoot at that. Let's add this check to the very top of Etank's moveMe() method

 
// loop thru the array of hittable objects and check if radar touches some of them 
for (var target in GameStructure.hittableStuff) 
{ 
    if (radar.hitTestObject(GameStructure.hittableStuff[target])) 
    { 
        // since enemy tanks are also added to hittableStuff array, we need to make sure that enemy tank attacks only player tanks 
        if (GameStructure.hittableStuff[target] is Ptank) 
        { 
            // for now let's just check if it works by tracing this simple phrase 
            // compile the game and try driving thru enemy radar area.  
            trace("I see target"); 
        } 
    } 
}

If you touch the radar area you should get "I see target" string in the Output panel


Step 28: Removing Reaction to the Pointer

This step is truly the easiest in the whole project. Just go to moveMe() method and remove these lines of code:

 
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;

Step 29: What To Use Instead of the Pointer?

Basically, we need to pass target's position to the tank instead of mouse's, and since the destination point is set in prepareToMove() method, we need to modify this method. But before doing it, change all methods' (except for constructor's) access modifiers from public to private, because you won't need to access any of them from outside this class. Now go to prepareToMove() method and find these two lines (they should be at the very top of it):

 
_moveToPositionX = this.parent.mouseX; 
_moveToPositionY = this.parent.mouseY;

Step 30: Cleaning Unnecessary Lines

As enemy tank's code is a copy of player tank's, it also implements the code that controls camera movement. Of course we don't need it, so go to moveMe() method and remove all of these lines:

 
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; 
}

And remove this line:

 
this.addEventListener(Event.ENTER_FRAME, moveMe);

...from both player and enemy tanks' prepareToMove() methods


Step 31: Attempting To Follow Target

It's time to try and follow the detected target. Go to "for in" loop that we created in Step 27 and modify it like so:

 
for (var target in GameStructure.hittableStuff) 
{ 
    if (radar.hitTestObject(GameStructure.hittableStuff[target])) 
    { 
    	if (GameStructure.hittableStuff[target] != null) 
		{ 
            if (GameStructure.hittableStuff[target] is Ptank) 
            { 
                // Calculate the distance between the tank and its target like we did before 
                var targetPosition:Point = new Point(GameStructure.hittableStuff[target].x, GameStructure.hittableStuff[target].y); 
                var myPosition:Point = new Point(this.x, this.y); 
                var distanceLeft:Number = Point.distance(targetPosition, myPosition); 
                var angle:Number = Math.atan2(targetPosition.y - myPosition.y, targetPosition.x - myPosition.x) + (90 * Globals.DEG_RAD); 
                // remember the variables that were assigned after mouse click, now they're tied to the targets position  
                _moveToPositionX = targetPosition.x; 
                _moveToPositionY = targetPosition.y; 
                // once the target is detected, the tank should start turning its turret towards it 
                this.mTurret.rotation = angle * Globals.RAD_DEG + this.rotation * -1; 
                // an the radar as well 
                radar.rotation = angle * Globals.RAD_DEG; 
                // 250 is how far should the target be from tank until it starts to follow it. You can use your own values 
                if (distanceLeft >= 250) 
                { 
                    prepareToMove(); 
                } 
                else   
                { 
                    //if the target is closer than 250 pixels, stop following it but keep on watching 
                    _tankIsMovingRightNow = false; 
                } 
            } 
        } 
    } 
}

You probably want to ask why I added 90 to some angles. To say the truth, I didn't expect this bug, but it happened. When tank detected target, its turret and radar turned 90 degrees away from it (for some unknown reason), so I thought it was the easiest way to work around this problem, and it did the trick.

Test the game to see how the tank follows you.


Step 32: Fixing A Problem With Etank's Rotation Tween

You may have noticed that when you turn too much away from enemy tank, it can't turn directly to your tank while it moves, thus it moves a bit sideways. This happens because it creates new tween every frame, and so its animation just has no time to complete until you stop giving it new destinations.

I found pretty simple way to work around this issue. Firs of all, we'll need some "toggler" variable, which will not let the tank create new tween if is set to false. Let's create it:

	 
private var _toggler:Boolean = true;

Then put the tween into a conditional statement that will let/forbid it to play.

 
if (_toggler) 
{ 
	_toggler = false; 
	var turnToDestinationPoint:Tween = new Tween(this, "rotation", None.easeOut, imTurnedAt, _tankToDestinationAngle, 0.7, true); 
	// add event listener to the tween. It will change the value of toggle variable back to true after the animation is complete				turnToDestinationPoint.addEventListener(TweenEvent.MOTION_FINISH, toggleTrue); 
}

And finally, create toggleTrue() event handler:

 
private function toggleTrue(e:TweenEvent):void  
{ 
	_toggler = true; 
}

Make sure you have the same Etank code as below, and test the game.

 
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.display.Sprite; 
	import flash.events.Event; 
	import flash.events.MouseEvent; 
	import flash.geom.Point; 
	import shells.Shell; 
 
	public class Etank 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; 
		private var _tankIsMovingRightNow:Boolean = false; 
		private var radar:Sprite; 
		private var _toggler:Boolean = true; 
		 
		public function Etank(parent:DisplayObjectContainer, location:Point, rotation:Number)  
		{ 
			this.x = location.x; 
			this.y = location.y; 
			this.rotation = rotation; 
			parent.addChild(this); 
			this.addEventListener(MyEvent.DAMAGE, destroyTank); 
			detector(); 
			this.addEventListener(Event.ENTER_FRAME, moveMe); 
		} 
		 
		private function destroyTank(e:MyEvent):void  
		{ 
			radar.graphics.clear(); 
			this.parent.removeChild(radar); 
			this.removeEventListener(MyEvent.DAMAGE, destroyTank); 
			this.gotoAndPlay(2); 
		} 
		private function detector():void 
		{ 
			radar = new Sprite(); 
			radar.mouseChildren = false; 
			radar.cacheAsBitmap = true; 
			radar.graphics.lineStyle(1, 0xFF0000, 1); 
			radar.graphics.beginFill(0xFF0000, 0.5); 
			radar.graphics.moveTo(0, 0); 
			radar.graphics.lineTo( -70, -435); 
			radar.graphics.curveTo(0, -455, 70, -435); 
			radar.graphics.lineTo(0, 0); 
			radar.graphics.endFill(); 
			radar.x = this.x; 
			radar.y = this.y; 
			radar.rotation = this.rotation + this.mTurret.rotation; 
			radar.visible = true; 
			this.parent.addChild(radar); 
		} 
		 
		private function shoot():void 
		{ 
			var destination:Point = new Point(this.parent.mouseX, this.parent.mouseY); 
			var shellRotation:Number = this.rotation + this.mTurret.rotation; 
			var shellOffset:int = 50; 
			var initialShellX:Number = this.x + Math.sin(shellRotation * Globals.DEG_RAD) * shellOffset; 
			var initialShellY:Number = this.y - Math.cos(shellRotation * Globals.DEG_RAD) * shellOffset; 
			var shell:Shell = new Shell(this.parent, new Point(initialShellX, initialShellY), shellRotation, destination); 
		} 
		 
		private function prepareToMove():void 
		{ 
			tankSpeed = 2; 
			_previousPositionX = this.x; 
			_previousPositionY = this.y; 
			 
				 
				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; 
				} 
				if (_toggler) 
				{ 
					_toggler = false; 
					var turnToDestinationPoint:Tween = new Tween(this, "rotation", None.easeOut, imTurnedAt, _tankToDestinationAngle, 0.7, true); 
					turnToDestinationPoint.addEventListener(TweenEvent.MOTION_FINISH, toggleTrue); 
				} 
				 
				 
				_tankIsMovingRightNow = true; 
		} 
		 
		private function toggleTrue(e:TweenEvent):void  
		{ 
			_toggler = true; 
		} 
		 
		private function moveMe(e:Event):void  
		{ 
			for (var target in GameStructure.hittableStuff) 
			{ 
				if (radar.hitTestObject(GameStructure.hittableStuff[target])) 
				{ 
					if (GameStructure.hittableStuff[target] is Ptank) 
					{ 
						var targetPosition:Point = new Point(GameStructure.hittableStuff[target].x, GameStructure.hittableStuff[target].y); 
						var myPosition:Point = new Point(this.x, this.y); 
						var distanceLeft:Number = Point.distance(targetPosition, myPosition); 
						var angle:Number = Math.atan2(targetPosition.y - myPosition.y, targetPosition.x - myPosition.x) + (90 * Globals.DEG_RAD); 
						_moveToPositionX = targetPosition.x; 
						_moveToPositionY = targetPosition.y; 
						this.mTurret.rotation = angle * Globals.RAD_DEG + this.rotation * -1; 
						radar.rotation = angle * Globals.RAD_DEG; 
						if (distanceLeft >= 250) 
						{ 
							prepareToMove(); 
						} 
						else   
						{ 
							_tankIsMovingRightNow = false; 
						} 
					} 
				} 
			} 
			radar.x = this.x; 
			radar.y = this.y; 
			 
			if (_tankIsMovingRightNow) 
			{ 
			 
				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) 
					{ 
						_tankIsMovingRightNow = false; 
					} 
				} 
			} 
		} 
	} 
}

Step 33: Shooting Restriction

Of course you wouldn't want enemy tank to destroy your tank with a series of shots right after it spots you. So first, we need to restrict its ability to shoot. It will be able to shoot again only after the first shell is removed. Open Shell Movie Clip in Flash, select the last frame of the AS3 layer and paste this line to the end:

 
dispatchEvent(new MyEvent(MyEvent.SHELL_HIT));

Now it can dispatch SHELL_HIT events. Since enemy tank cannot shoot yet, we'll check this system on Ptank. Open up Ptank.as in FlashDevelop and add a new variable:

 
private var _iHaveShot:Boolean = false;

This variable is supposed to be set to true right after the tank has shot, and set back to false after the shell receives SHELL_HIT event. While it's true, tank won't be able to create another shell.

Update shoot() method:

 
public function shoot():void 
{ 
    if (!_iHaveShot) 
    { 
        var destination:Point = new Point(this.parent.mouseX, this.parent.mouseY); 
        var shellRotation:Number = this.rotation + this.mTurret.rotation; 
        var shellOffset:int = 50; 
        var initialShellX:Number = this.x + Math.sin(shellRotation * Globals.DEG_RAD) * shellOffset; 
        var initialShellY:Number = this.y - Math.cos(shellRotation * Globals.DEG_RAD) * shellOffset; 
        var shell:Shell = new Shell(this.parent, new Point(initialShellX, initialShellY), shellRotation, destination); 
        shell.addEventListener(MyEvent.SHELL_HIT, allowShooting); 
        _iHaveShot = true; 
    } 
} 
	 
// create event handler	 
private function allowShooting(e:MyEvent):void  
{ 
	_iHaveShot = false; 
}

Do the same to enemy tank.


Step 34: Attack Player Or Not?

Since player tank shoots at pointer's position, enemy tank should not do the same. So, let's modify its shoot() method a little bit:

 
// pass destination as parameters 
public function shoot(shootAtX:Number, shootAtY:Number):void 
{ 
    if (!_iHaveShot && this != null) 
    { 
        // change pointer's position to position from the method's parameters 
        var destination:Point = new Point(shootAtX, shootAtY); 
        var shellRotation:Number = this.rotation + this.mTurret.rotation; 
        var shellOffset:int = 50; 
        var initialShellX:Number = this.x + Math.sin(shellRotation * Globals.DEG_RAD) * shellOffset; 
        var initialShellY:Number = this.y - Math.cos(shellRotation * Globals.DEG_RAD) * shellOffset; 
        var shell:Shell = new Shell(this.parent, new Point(initialShellX, initialShellY), shellRotation, destination); 
        shell.addEventListener(MyEvent.SHELL_HIT, allowShooting); 
        _iHaveShot = true; 
    } 
}

Now go down to moveMe() method and find this line:

 
radar.rotation = angle * Globals.RAD_DEG;

and add this code right after it

 
if (Math.random() * 1000 > 990) 
{ 
	shoot(_moveToPositionX, _moveToPositionY); 
}

Now go to detector() method and change its visibility from true to false

 
radar.visible = false;

Compile the game and try to get in front of enemy tank. If you don't like its shooting tempo, experiment with random values. Try changing 990 to something smaller until you're happy with the results


Step 35: Responses To Collisions

At this point, we need to check if the tank collide with something. Open Ptank.as and go to its moveMe() method. Find the lines that move it:

 
this.x += Math.cos(angleToTarget) * tankSpeed;  
this.y += Math.sin(angleToTarget) * tankSpeed;

Add this "for in" loop after them:

 
for (var obstacle in GameStructure.hittableStuff) 
{ 
    if (GameStructure.hittableStuff[obstacle] is Bitmap) 
    { 
        // assign hittable object to a variable to shorten the code 
        var bitmap:Bitmap = GameStructure.hittableStuff[obstacle] as Bitmap; 
        // check if the tank's touching it's transparent pixels like we did before 
        if (bitmap.bitmapData.hitTest(new Point(bitmap.x, bitmap.y), 255, new Point(this.x, this.y))) 
        { 
            // if it collides with something, it moves back for 10 pixels immediately. You can play around with this value and set your own 
            this.x -= Math.cos(angleToTarget) * 10;  
            this.y -= Math.sin(angleToTarget) * 10; 
        } 
    } 
    else 
    { 
        if (this.hitTestObject(GameStructure.hittableStuff[obstacle])) 
        { 
            // since this tank is also in hittableStuff array, make sure it does not respond to collision with itself  
            if (GameStructure.hittableStuff[obstacle] != this) 
            { 
                // after the collision occured, send DAMAGE event to the object it collided with and to itself 
                this.dispatchEvent(new MyEvent(MyEvent.DAMAGE)); 
                GameStructure.hittableStuff[obstacle].dispatchEvent(new MyEvent(MyEvent.DAMAGE)); 
            } 
        } 
    } 
}

Do the very same thing to enemy tank, and test the game


Step 36: Fixing Little Problem with a Hit Tank

Everything seems to be working great, except for one thing. After enemy hits your tank it destroys itself but still remains in the hittableStuff array, so enemy keeps on shooting at the position where your tank was. Lets fix it now.

Open Tank Movie Clip in flash and go to the last frame of Explosion layer. Open Actions panel and find the line where tank removes itself:

 
this.parent.removeChild(this);

Add this code right before it:

 
// it the element is not in an array, it's index is -1. So we need to make sure its index is less than 0 
if (GameStructure.hittableStuff.indexOf(this) >= 0) 
{ 
	// and if not, splice it  
	GameStructure.hittableStuff.splice(GameStructure.hittableStuff.indexOf(this), 1); 
}

Do the same to EnemyTank


Step 37: Water Detection

When some objects moves over a painted area, it can detect what color it is over by using bitwise operators. Our tanks move over Bitmap map, so they need to access its color. To do this, open MapBuilder.as, find where you declared _bmdContainer variable and change its access modifier from public to public static:

 
public static var _bmdContainer:Bitmap;

It can now be accessed from outside this class. So, we only need to determine what is water and what is not. Get back to Ptank class, and in its moveMe() method find these lines:

 
var angleToTarget:Number = Math.atan2(_moveToPositionY - _previousPositionY, _moveToPositionX - _previousPositionX); 
this.x += Math.cos(angleToTarget) * tankSpeed;  
this.y += Math.sin(angleToTarget) * tankSpeed;

Add this code right after them:

 
// set map to a variable to shorten the code 
var map:Bitmap = MapBuilder._bmdContainer; 
// get pixel under tank's X and Y positions while it moves 
var pixel:uint = map.bitmapData.getPixel(this.x, this.y); 
// split the color into separate channels		 
var r:uint = (pixel >> 16)  & 0xff;  
var g:uint = (pixel >> 8)  & 0xff;   
var b:uint = pixel & 0xff;  
// trace all of 3 colors to see how they vary over different colors of the map 
trace(r + " red, " + g + " green, " + b + " blue"); 
 
// I've already found out what values the channels have when tank's over water, so you may remove traces if you want. If not, test it yourself, it's up to you.  
if (r < 80 && g < 150 && b > 160) 
{ 
	trace("I'm drowning"); 
}

Step 38: Drowning Animation

Our tank can drown now but there's now animation for this action. Open Tank MovieClip in Flash, click tank's body to select it, and give it an instance name of mBody (like you did with mTurret earlier). This step is necessary because the tank's body must also be accessible from outside


Select the second frame of explosion layer, then select explosion Movie Clip and also give it an instance name of mExplosion (you should do the same for the last frame too).


Create new layer and call it drowning. Lock all other layers and draw several white circles like this:


This circles are supposed to represent air bubbles when tank drowns. Select all circles and hit F8 to convert them to movie clip. Give it an instance name of mBubbles.


Then open this Movie Clip and create 10 or more layers. Draw more white circles in each frame chaotically so that they form a bubbling animation.


Quit symbol editing mode, select the first frame of explosion layer open Actions panel and add one line to it: mBubbles.visible = false;

And, of course, do all of this with the enemy tank too


Step 39: Making Tank Drown

We already have a method that destroys tank. Basically, drowning should be exactly the same but we'll set mBody, mExplosion and mTurret visibility to false, and mBubble to true.
Find where you traced this (in Ptank):

 
trace("I'm drowning");

And add this code instead of the trace statement:

 
this.mBubbles.visible = true; 
this.mBody.visible = false; 
this.mTurret.visible = false; 
// since mExplosion does not exist on the first frame, we need to make sure its visibility is set to false only when the object itself is not null 
if (this.mExplosion != null) 
{ 
	this.mExplosion.visible = false; 
} 
// dispatch DAMAGE event to destroy tank 
this.dispatchEvent(new MyEvent(MyEvent.DAMAGE));

Test the game


Step 40: Winning Or Losing Setup

The game is almost ready, there's only a few things left to be done. First of all, let's create two events for Winning and Losing, and one for checking whether you lose or not. Open up MyEvent.as and add three more constants:

 
public static const YOU_WIN:String = "winning"; 
public static const GAME_OVER:String = "game_over"; 
public static const CHECK_WINNER:String = "checkWinner";

Now we need another way to initialize game. Right-click the project file in FlashDevelop and add a new class. Call it Initializer. Here's the code for it:

 
package   
{ 
	import flash.display.Sprite; 
	import flash.events.Event; 
 
	public class Initializer extends Sprite 
	{ 
		private var gameStructure:GameStructure; 
		 
		public function Initializer()  
		{ 
			this.addEventListener(Event.ADDED_TO_STAGE, initGame) 
		} 
		 
		private function initGame(e:Event):void  
		{ 
			removeEventListener(Event.ADDED_TO_STAGE, initGame); 
			gameStructure = new GameStructure(stage); 
			stage.addChild(gameStructure); 
		} 
	} 
}

Notice that I've passed "stage" as a parameter to GameStructure. If you test it now, it'll give you an error because GameStructure does not receive any parameters at the moment. Let's fix it.

Open GameStructure class in FlashDevelop, click Ctrl + H to find and replace text, and replace all stage. words to _stage. (don't forget the dot at the end, it's important!)


Add a new variable:

 
private var _stage:Stage;

And modify the constructor

 
public function GameStructure(stage:Stage)  
{ 
	_stage = stage; 
	camera = new Camera(stage, new Point(0,0)); 
	map = new MapBuilder(); 
	map.addEventListener(MyEvent.MAP_BUILT, addMap); 
}

To determine whether you lose or win, you'll need two arrays. One to contain all of the player objects and one for enemies. After a tank is killed it will splice itself from the array; the program will then check if some array is empty and, if so, dispatch YOU_WIN or GAME_OVER event depending on which array is empty. So add these two arrays to GameStructure:

 
public static var player:Array = []; 
public static var enemy:Array = [];

Now find where you created player tanks and enemy tanks and add a line that will push them into respective arrays:

 
var playerTanksData:XMLList = _assets.missionAssets.mission[map.mission].playerTanks.tank; 
for (var pTank in playerTanksData) 
{ 
    var pTankX:Number = playerTanksData[pTank].@Xposition; 
    var pTankY:Number = playerTanksData[pTank].@Yposition; 
    var pTankRotation:Number = playerTanksData[pTank].@rotation; 
     
    var playerTank:Ptank = new Ptank(camera, new Point(pTankX, pTankY), pTankRotation); 
    playerTank.addEventListener(MouseEvent.CLICK, selectObject) 
    hittableStuff.push(playerTank); 
    // here's the line >>>> 
    player.push(playerTank); 
    // if there's no player tanks left, this event is dispatched. We'll create the dispatcher later 
    playerTank.addEventListener(MyEvent.CHECK_WINNER, checkWinner); 
} 
 
var enemyTanksData:XMLList = _assets.missionAssets.mission[map.mission].enemyTanks.tank; 
for (var eTank in enemyTanksData) 
{ 
    var eTankX:Number = enemyTanksData[eTank].@Xposition; 
    var eTankY:Number = enemyTanksData[eTank].@Yposition; 
    var eTankRotation:Number = enemyTanksData[eTank].@rotation; 
     
    var enemyTank:Etank = new Etank(camera, new Point(eTankX, eTankY), eTankRotation); 
    hittableStuff.push(enemyTank); 
    // >>>> 
    enemy.push(enemyTank); 
 
    enemyTank.addEventListener(MyEvent.CHECK_WINNER, checkWinner); 
}

And, of course you'll need an event handler for it:

 
private function checkWinner(e:MyEvent):void  
{ 
	if (e.target is Ptank) 
	{ 
		this.dispatchEvent(new MyEvent(MyEvent.GAME_OVER)); 
	} 
	else if (e.currentTarget is Etank) 
	{ 
		this.dispatchEvent(new MyEvent(MyEvent.YOU_WIN)); 
	} 
}

Step 41: Splicing Killed Objects From Arrays

Splicing tanks from enemy and player arrays is a very easy task. Open up Ptank and find destroyTank() method. Add these lines to the top of it:

 
GameStructure.player.splice(GameStructure.player.indexOf(this), 1); 
if (GameStructure.player.length <= 0) 
{ 
	this.dispatchEvent(new MyEvent(MyEvent.CHECK_WINNER)); 
}

Do the same with Etank (only changing the word player to enemy)


Step 42: Check For Winner

Now we have checkWinner() function that dispatches GAME_OVER or YOU_WIN events, but there are no listeners for these event, and, of course, no event handlers. Let's create them now. Open up Initializer.as and add two event listeners to gameStructure instance:

 
package   
{ 
	import flash.display.Sprite; 
	import flash.events.Event; 
 
	public class Initializer extends Sprite 
	{ 
		private var gameStructure:GameStructure; 
		 
		public function Initializer()  
		{ 
			this.addEventListener(Event.ADDED_TO_STAGE, initGame) 
		} 
		 
		private function initGame(e:Event):void  
		{ 
			removeEventListener(Event.ADDED_TO_STAGE, initGame); 
			gameStructure = new GameStructure(stage); 
			 
			// listeners for winning or losing events. 
			gameStructure.addEventListener(MyEvent.YOU_WIN, winning); 
			gameStructure.addEventListener(MyEvent.GAME_OVER, losing); 
			 
			stage.addChild(gameStructure); 
		} 
		 
		private function losing(e:MyEvent):void  
		{ 
			trace("you lose"); 
		} 
		 
		private function winning(e:MyEvent):void  
		{ 
			trace("you win"); 
		} 
	} 
}

Step 43: Some Graphics to Indicate Game State

Well, maybe graphical representation of "Game Over" or "You Win" phrases is not very important, but I think the game wouldn't look good without them. Open up Flash and insert new Movie Clip symbol (Ctrl + F8). Select Text Tool (T), choose some Fill color, Tahoma font, font size 31, bold, and type "Game Over". Align it on stage:


Press Ctrl + B twice, to turn the font into vector shape. Insert new key frame and type "You Win" on it. Also turn it into vector shape.

Add the third frame with phrase "Next Mission". It will be shown if the mission that you won wasn't the last.


Now you need to export GameState Movie Clip for Actionscript



Step 44: Mission Number

If you remember from the previous lesson, the number of the current mission is set right inside MapBuilder class. We need another way of setting that. So open up MapBuilder.as and go to its constructor method. Add only one line to it:

 
mission = Initializer.currentMission;

Since there's no "currentMission" variable in the Initializer class so far, let's create it.

 
public static var currentMission:int = 0;

From now on, the current mission number depends on this variable's value


Step 45: Major Modifications to the Initializer Class

The idea of these modifications is that when losing() method is triggered, the current mission remains the same and you need to click the phrase "Game Over" to play the mission again, and when winning() is triggered, you click "Next Mission" to play the next mission or "You Win" to start the game over.

To do it all, we need to make a lot of modifications to Initializer, both tanks' classes, and the GameStructure class. First of all, we'll need to load Assets.xml from Initializer to know how many missions are there in the game, then add some cleaner method to GameStructure, to remove all of the present environment before creating another mission. Here's the modifications to Initializer (follow the comments):

 
package   
{ 
	import flash.display.MovieClip; 
	import flash.display.Sprite; 
	import flash.events.Event; 
	import flash.events.MouseEvent; 
	import flash.net.URLLoader; 
	import flash.net.URLRequest; 
 
	public class Initializer extends Sprite 
	{ 
		private var gameStructure:GameStructure; 
		public static var currentMission:int = 0; 
		 
		// 1. create these three variables to load Assets.xml and check how many missions are there in the game 
		private var _request:URLRequest; 
		private var _urlLoader:URLLoader; 
		private var missionsData:XML; 
		private var numberOfMissions:int; 
		 
		public function Initializer()  
		{ 
			// 2. change the called method to initGame here. Now when it's added to stage it will load Assets.xml first 
			this.addEventListener(Event.ADDED_TO_STAGE, initGame) 
		} 
		// 3. create event handler 
		private function initGame(e:Event):void  
		{	 
			// 4. load Assets.xml 
			_request = new URLRequest("Assets.xml"); 
			_urlLoader = new URLLoader(_request); 
			// 5. after it's been loaded COMPLETE event will trigger createEnvironment   
			_urlLoader.addEventListener(Event.COMPLETE, createEnvironment); 
 
		} 
		// 6. Now, createEnvironment method adds all of the stuff  
		private function createEnvironment(e:Event):void 
		{ 
			// 7. Assign loader's data to a variable 
			missionsData = new XML(e.target.data); 
			// 8. Get the numbers of missions. Basically it's the number of nodes called mission 
			numberOfMissions = missionsData.mission.length(); 
			removeEventListener(Event.ADDED_TO_STAGE, initGame); 
			// 9. Create new instance of GameStructure class and add event listeners for winning and losing to it 
			gameStructure = new GameStructure(stage); 
			gameStructure.addEventListener(MyEvent.YOU_WIN, winning); 
			gameStructure.addEventListener(MyEvent.GAME_OVER, losing); 
			// 10. don't forget to add it to the stage 
			stage.addChild(gameStructure); 
		} 
		 
		private function losing(e:MyEvent):void  
		{ 
        	// see item 19.  
        	gameStructure.clearSelectListeners(); 
			// 11. if GAME_OVER event was received splice you're tank from the array of moving objects. If you don't do it it will give you an error once you click somewhere 
			GameStructure.playerObjects.splice(0, 1); 
			// 12. Clear the array of enemies. If it's not done here, enemy tanks will collide with their invisible "predecessors" after the mission is recreated. Note that you don't have to clear player array here because when all of your tanks are killed they will remove themselves  
			GameStructure.enemy.splice(0, GameStructure.enemy.length); 
			// 13. Also clear the array of hittable stuff, for the same purpose 
			GameStructure.hittableStuff.splice(0, GameStructure.hittableStuff.length); 
			 
			// 14. Add the "Game Over / You Win" MovieClip (which we created earlier) to the stage  
			var winLose:MovieClip = new GameState(); 
			// 15. center it on the stage 
			winLose.x = stage.stageWidth / 2; 
			winLose.y = stage.stageHeight / 2; 
			// 16. and as it's a game over event, stop the winLose mc on the first frame (which represents Game Over phrase) 
			winLose.gotoAndStop(1); 
			// 17. Add listener that will let us click on this phrase 
			winLose.addEventListener(MouseEvent.CLICK, turnGameState) 
			// 18. Don't forget to add it to the stage 
			stage.addChild(winLose); 
		} 
		 
		private function winning(e:MyEvent):void  
		{ 
			// 19. This line clears mouse click listeners from player tanks, but as it's not created yet, we'll come back to it later 
			gameStructure.clearSelectListeners(); 
			// 20. Clear player (see item 11) 
			GameStructure.playerObjects.splice(0, 1); 
			// 21. Clear player array. As all enemies have already spliced themselves from enemy array, you don't have to clear it 
			GameStructure.player.splice(0, GameStructure.player.length); 
			// 22. (item 13) 
			GameStructure.hittableStuff.splice(0, GameStructure.hittableStuff.length); 
			 
			// 23. (items 14 - 15) 
			var winLose:MovieClip = new GameState(); 
			winLose.x = stage.stageWidth / 2; 
			winLose.y = stage.stageHeight / 2; 
			 
			// 24. Here's a little difference. If current mission is not the last, it will stop on the third frame of GameState MovieClip (Next Mission) 
			if (currentMission < numberOfMissions - 1) 
			{ 
				winLose.gotoAndStop(3); 
			} 
			else 
			{ 
				// 25. If the mission was last, it'll show You Win phrase 
				winLose.gotoAndStop(2); 
			} 
			// 26. (itmes 17 - 18) 
			winLose.addEventListener(MouseEvent.CLICK, turnGameState) 
			stage.addChild(winLose); 
		} 
 
		// 27. Now we need to define frame is it on our GameStage mc, and take a respective action 
		private function turnGameState(e:MouseEvent):void  
		{ 
			// 28. If game you lose the game, leave the currentMission variable unchanged and reconstruct the mission 
			if (MovieClip(e.target).currentFrame == 1) 
			{ 
				gameStructure.addEventListener(Event.COMPLETE, resetAll); 
				// 29. call clean() method of GameStructure. As it's not created yet, let's leave it as is for a moment, we'll come back to it in the next step 
				gameStructure.clean(); 
				 
			} 
			// 30. If you win and there are more missions left, add 1 to currentMission number and create new mission 
			else if (MovieClip(e.target).currentFrame == 3) 
			{ 
				currentMission += 1; 
				gameStructure.addEventListener(Event.COMPLETE, resetAll); 
				gameStructure.clean(); 
 
			} 
			// 31. If you win, and there's no missions left, get back to the first mission 
			else if (MovieClip(e.target).currentFrame == 2) 
			{ 
				currentMission = 0;	 
				gameStructure.addEventListener(Event.COMPLETE, resetAll); 
				gameStructure.clean(); 
			} 
		} 
		// 32. And last but not least, the method that will reset all stuff 
		private function resetAll(e:Event):void  
		{ 
			// 33. Loop thru the stage's children and remove them all, except for Initializer itself.  
			for (var i:int = stage.numChildren - 1; i > 0; i--) 
			{ 
				stage.removeChildAt(i); 
			} 
			// 34. Create new instance of gameInitializer according to the currentMission number 
			gameStructure = new GameStructure(stage); 
			gameStructure.addEventListener(MyEvent.YOU_WIN, winning); 
			gameStructure.addEventListener(MyEvent.GAME_OVER, losing); 
			stage.addChild(gameStructure); 
		}		 
	} 
}

Step 46: Major Modifications to the Initializer Class

In the Initializer class we call two methods that are supposed to be part of GameStructure but they aren't yet. Let's fix this problem. Open up GameStructure.as and add these two methods:

 
// 1. Clear some listeners added inside this class. (NOTE: Look at item 19 of Initializer, this method is called from there) 
public function clearSelectListeners():void 
{ 
    // 2. loop thru the camera children looking for Ptank and Etank objects. 
    for (var i:int = camera.numChildren - 1; i > 0; i--) 
    { 
        // 3. if Ptanks were found, remove MouseEvent.CLICK listeners from them so that these tanks could not be selected after the battle was won or else it will throw an error 
        if (camera.getChildAt(i) is Ptank) 
        { 
            camera.getChildAt(i).removeEventListener(MouseEvent.CLICK, selectObject); 
        } 
        // 4. Remember, enemy tank is active right after it's created, so we need to remove its ENTER_FRAME listener 
        if (camera.getChildAt(i) is Etank) 
        { 
            // NOTE: This method isn't created yet. It will be the last step of the tut 
            Etank(camera.getChildAt(i)).removeEnterFrame(); 
        } 
    } 
} 
public function clean():void 
{ 
    // 5. First of all remove listener that moves objects. It will "freeze" your tank right after you win 
    camera.removeEventListener(MouseEvent.CLICK, moveObjects); 
    // 6. Loop thru camera's children 
    for (var i:int = camera.numChildren - 1; i > 0; i--) 
    { 
        // 7. And if some of these children are tanks, remove CHECK_WINNER listener from each one, since it's no longer needed 
        if (camera.getChildAt(i) is Etank || camera.getChildAt(i) is Ptank) 
        { 
            camera.getChildAt(i).removeEventListener(MyEvent.CHECK_WINNER, checkWinner); 
        } 
        // 8. Remove all children 
        camera.removeChildAt(i); 
    } 
    // 9. Splice hittableStuff once again. I don't know why it happens but if you don't do it, it will be move prone to errors. I really couldn't figure out what was the problem here  
    hittableStuff.splice(0, GameStructure.hittableStuff.length); 
    // 10. When it's all accomplished, disptch COMPLETE event which will trigger resetAll() method in the Initializer class 
    this.dispatchEvent(new Event(Event.COMPLETE)); 
}

Step 47: Tiny Modification to Etank

Finally, open Etank.as and add one public method to it:

 
public function removeEnterFrame():void 
{ 
	this.removeEventListener(Event.ENTER_FRAME, moveMe); 
}

The only thing left to do now is to create more missions. I've already created three missions, you can use them to test the game or feel free to create your own.

Well, that's it. It's been a lot of work, but I hope you liked my tutorial, and it helped you learn some techniques that you didn't know before :)

Advertisement