We are now on Twitter!


Penguin wearing teacher's hatGame Development With FlashDevelop Part 4

Shooting

Welcome to part 4 of our tutorial series. In this installment, we're going to add missiles so that the player can shoot down the aliens.

We're going to use the graphic below for our missiles.

First add a boolean variable called active to the Actor class and set it to true in the constructors for Player and Alien. This variable will be used to determine if an actor should be updated and rendered. Change the Update and Render functions for the Player and Alien classes, and also the CheckForDirectionChange function in the Alien class, for these classes to return straight away if active is false. The code below shows how the Player's Update and Render functions should now look.

public override function Render():void
{
	if ( !active ) return;
	
	var p:Point = new Point(x - (width / 2 ), y - (height / 2));
	State.currentState.buffer.copyPixels( graphic, graphic.rect, p );
}
		
public override function Update() :void
{
	if ( !active ) return;
	
	if ( KeyboardManager.keysPressed[Keyboard.LEFT] ) x -= speed;
	if ( KeyboardManager.keysPressed[Keyboard.RIGHT] ) x += speed;			
		
	if ( x - widthHalved < 0 ) x = widthHalved;
	if ( x + widthHalved > Application.application.width ) x = Application.application.width - widthHalved;
}	

Now create a subclass of Actor called Missile...

package  
{
	import flash.display.*;
	import flash.geom.*;
	
	public class Missile extends Actor
	{	
		[Embed(source="../images/missile.png")]
		private var i:Class;
		
		public function Missile(X:Number, Y:Number, Speed:Number) 
		{
			var image:DisplayObject= new i();
			graphic = new BitmapData(image.width, image.height, true, 0x00000000);
			graphic.draw(image);
			
			height = image.height;
			width = image.width;
			
			heightHalved = height / 2;
			widthHalved = width / 2;
			
			x = X;
			y = Y;
			speed = Speed;
			
			active = true;
		}
		
		public override function Render():void
		{
			if ( !active ) return;
			State.currentState.buffer.copyPixels( graphic, graphic.rect, new Point(x - widthHalved, y - heightHalved) );
		}
		
		public override function Update() :void
		{
			if ( !active ) return;
			y += speed;
		}	
	}
}

There's nothing here that we haven't seen before. The Update will function will make the missile move along the y axis so we can set a positive speed for missiles being fired downwards by the aliens, and a negative speed for the player's missiles.

We will also need somewhere to store the missiles - we'll add another Vector to the PlayState class. Note that this vector has been made public so that we can add items to it from other classes. Alternatively, a public function in the PlayState class could be used to access the Vector but we'll stick with just making the missiles vector public.

public var missiles:Vector.<Missile>;

In PlayState's constructor, we must create the missiles vector:

missiles = new Vector.<Missile>();

Just as we did when we introduced the aliens vector, we need to iterate over the missiles vector in the Update and Render functions and call the Update and Render functions for each missile.

public override function Render():void
{
	buffer.fillRect( buffer.rect, 0x000000 );
	
	for each (var missile:Missile in missiles) 
	{
		missile.Render();
	}
	player.Render();	
	for each (var alien:Alien in aliens) 
	{
		alien.Render();
	}		
}
		
public override function Update():void
{
	player.Update();	
	for each (var alien:Alien in aliens) 
	{
		alien.CheckForDirectionChange();
	}			
	for each (alien in aliens) 
	{
		alien.Update();
	}			
	for each (var missile:Missile in missiles) 
	{
		missile.Update();
	}
}

Notice that we are rendering the missiles before any other objects. This is so that we can make a missile start in a position where it is obscured by the ship that fired it and will appear to be coming out of that ship as it moves.

Now we will make the player's spaceship fire a missile on every update when the space bar or up arrow is pressed. Change the Player class Update function as follows:

public override function Update() :void
{
	if ( !active ) return;
		
	if ( KeyboardManager.keysPressed[Keyboard.LEFT] ) x -= speed;
	if ( KeyboardManager.keysPressed[Keyboard.RIGHT] ) x += speed;			
			
	if ( x - widthHalved < 0 ) x = widthHalved;
	if ( x + widthHalved > Application.application.width ) x = Application.application.width - widthHalved;
			
	if ( KeyboardManager.keysPressed[Keyboard.SPACE] || KeyboardManager.keysPressed[Keyboard.UP] )
	{
		(State.currentState as PlayState).missiles.push( new Missile( x, y - 8, -3 ) );
	}
}	

This works, but it works too well - the player will fire a new missile every frame whilst the fire button is held down. We need some means of introducing a delay between each shot. Add a new integer variable to the Player class called fireDelay. We'll only let the player fire a missile if fireDelay is zero at which point we'll set fireDelay to a value greater than zero. Also in the Update function, decrement fireDelay if it is greater than zero. Now we can control the rate of fire of the player's ship.

if ( fireDelay > 0 ) fireDelay--;
if ( KeyboardManager.keysPressed[Keyboard.SPACE] || KeyboardManager.keysPressed[Keyboard.UP] && fireDelay == 0)
{
	(State.currentState as PlayState).missiles.push( new Missile( x, y - 8, -3 ) );
	fireDelay = 60;
}

The player can now shoot at the aliens with a reasonable rate of fire but the missiles pass harmlessly through them. Each frame we need to do some collision detection to see if anything has been hit.

Fortunately, it is easy to do pixel perfect collision detection in Flash using a bitmapData object's hitTest function which allows you to see if any of the non-transparent pixels of two overlapping bitmaps intersect. Add this code at the end of the Update function in PlayState (you'll also need to import flash.geom.*; into the PlayState):

for each ( missile in missiles )
{
	if ( missile.active )
	{
		for each (alien in aliens) 
		{
			if ( alien.active )
			{
				//calculate top left positions of the alien and missile bitmaps
				var alienPosition:Point = new Point(alien.x - alien.widthHalved, alien.y - alien.heightHalved);
				var missilePosition:Point = new Point(missile.x - missile.widthHalved, missile.y - missile.heightHalved);
		
				if ( missile.graphic.hitTest(missilePosition, 255, alien.graphic, alienPosition, 255) )
				{
					alien.active = false;
					missile.active = false;
				}
			}
		}	
	}						
}

Basically we are testing each active missile against each active alien to determine if the missiles have hit anything and if they have, we set the missile and alien to be inactive so that they will play no further role in the game. The values of 255 in the hitTest function are specifying that we should only be checking fully opaque pixels when determining if there is a collision.

There is a small problem at this point. Test the game and you'll notice that it is possible for an alien to be hit even though the missile is some distance to the side. This is because we are checking for a collision with the alien's graphic which includes two frames of animation. To get round this, we'll give the Alien class another bitmapData variable called collisionGraphic with half the width of the original graphic. We can draw the alien.png image onto this bitmap and it will only contain the left hand side of the image. Then we can use collisionGraphic in the hitTest function instead of graphic.

public var collisionGraphic:BitmapData;
		
public function Alien(X:Number, Y:Number, Speed:Number) 
{
	var image:DisplayObject= new i();
	graphic = new BitmapData(image.width, image.height, true, 0x00000000);
	graphic.draw(image);
	
	collisionGraphic = new BitmapData(image.width / 2, image.height, true, 0x00000000);
	collisionGraphic.draw(image);
	...

Go back to the code we just added to PlayState's Update function and change alien.graphic to alien.collisionGraphic.

Next we will make the aliens fire. We'll use a random number generator to give each alien a small chance of firing each frame. Add the following lines to the end of the Alien class Update function. The value can be tweaked to change the aliens' rate of fire.

if ( Math.floor(Math.random() * 1000) > 998 )
	(State.currentState as PlayState).missiles.push( new Missile( x, y + 8, 3 ) );

Now when we run the game, we'll see the aliens disappearing at random - they are firing missiles with which they instantly collide. We'll change the collision detection code to only check for collisions between aliens and missiles with a negative speed (i.e. heading upwards).

for each ( missile in missiles )
{
	if ( missile.active )
	{
		if ( missile.speed < 0 )
		{
			for each (alien in aliens) 
			{
				if ( alien.active )
				{
					var alienPosition:Point = new Point(alien.x - alien.widthHalved, alien.y - alien.heightHalved);
					var missilePosition:Point = new Point(missile.x - missile.widthHalved, missile.y - missile.heightHalved);
		
					if ( missile.graphic.hitTest(missilePosition, 255, alien.collisionGraphic, alienPosition, 255) )
					{
						alien.active = false;
						missile.active = false;
					}
				}
			}	
		}					
	}						
}		

The aliens no longer destroy themselves but the player is immune to the aliens' missiles too. We can modify the collision detection code to check for collisions with the player for all missiles with a positive speed.

for each ( missile in missiles )
{
	if ( missile.active )
	{
		if ( missile.speed < 0 )
		{
			for each (alien in aliens) 
			{
				if ( alien.active )
				{
					var alienPosition:Point = new Point(alien.x - alien.widthHalved, alien.y - alien.heightHalved);
					var missilePosition:Point = new Point(missile.x - missile.widthHalved, missile.y - missile.heightHalved);
		
					if ( missile.graphic.hitTest(missilePosition, 255, alien.collisionGraphic, alienPosition, 255) )
					{
						alien.active = false;
						missile.active = false;
					}
				}
			}	
		}	
		else if ( missile.speed > 0 )
		{						
			var playerPosition:Point = new Point( player.x - player.widthHalved, player.y - player.heightHalved);
			missilePosition = new Point(missile.x - missile.widthHalved, missile.y - missile.heightHalved);
			
			if ( missile.graphic.hitTest(missilePosition, 255, player.graphic, playerPosition, 255) )
			{
				player.active = false;
				missile.active = false;
			}
		}
	}					
}

The player can now be destroyed by the aliens' missiles. The application should look like this:

In the next installment, we will add some more features such as a title screen and a game over screen.