Creating a Asteroids Flash Game Part 7: Detecting Collisions and Creating Particle Explosions

Chris Moeller Web Asteroids Flash Game Part 7- Adding Explosions

In the last section, created as asteroids class to easily create a variety of asteroids, as well as move them around on screen.

In this section, we’ll create a way to detect collisions between the asteroids and the ship or bullets, and create particle explosions when the collisions happen, as well as break apart asteroids upon collision.

Adding Collision Detection to Bullets, Asteroids and the Ship Class

We now have to detect when a collision has happened between and asteroid and either the ship or a bullet, and create an explosions when one has happened.

Since bullets and asteroids are both approximately square in shape, to check if a point is colliding with them, we’ll check to see if the point is within the four corners.

I decided to modify “GameSprite.as” to add this check.

GameSprite.as

package Entities
{
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.geom.Rectangle;

	public class GameSprite
	{

		public var x:Number;
		public var y:Number;
		public var width:int;
		public var height:int;
		public var angle:Number;
		public var rotate_offset:Point;
		public var visible:Boolean;

		protected var image:BitmapData;
		protected var image_sprite:Sprite;

		public var collis_points:Array;

		public function GameSprite(x:int, y:int, width:int, height:int, angle:int=0)
		{
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.angle = angle;
			visible = true;
			collis_points = new Array(
			new Point(0, 0),
			new Point(width, 0),
			new Point(width, height),
			new Point(0, height)
			);
			rotate_offset = new Point(0, 0);
		}
		public function Render():void
		{
		}
		public function Update():void
		{
			if (angle == 0)
			{
				collis_points = [
					new Point(x, y),
					new Point(x+width, y),
					new Point(x+width, y+height),
					new Point(x, y+height)
				];
			}

		}
		public function CheckIfInNonRotatedRect(obj2:GameSprite):Boolean
		{
			//we'll make sure that this object is a rentangle with no angle
			var intersecting:Boolean = false;
			if (obj2.angle == 0)
			{
				//first we check the top left point
				if (x >= obj2.x && x <= obj2.x + obj2.width)
					if (y >= obj2.y && y <= obj2.y + obj2.height)
						return true;

				//now we'll check the top right point
				if (x+width >= obj2.x && x+width <= obj2.x + obj2.width)
					if (y >= obj2.y && y <= obj2.y + obj2.height)
						return true;

				//now we check the bottom right point
				if (x+width >= obj2.x && x+width <= obj2.x + obj2.width)
					if (y+height >= obj2.y && y+height <= obj2.y + obj2.height)
						return true;

				//And check the bottom left point
				if (x >= obj2.x && x <= obj2.x + obj2.width)
					if (y+height >= obj2.y && y+height <= obj2.y + obj2.height)
						return true;
			}

			return intersecting;
		}
	}
}

Then for the ship, I decided to check each of the 3 corners of the ship, and use these as it's collision points:

Ship.as

package Entities
{
	import Entities.GameSprite;
	import flash.display.Sprite;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;

	public class Ship extends GameSprite
	{
		private var rotate_amount:int;

		public var speed:Point;
		private var speed_multi:Number = .5;
		private var friction:Number = .95;

		public function Ship(x:int, y:int, width:int, height:int)
		{
			super(x, y, width, height);
			image_sprite = new Sprite();
			image_sprite.graphics.lineStyle(2, 0xFFFFFF);
			rotate_amount = 15;
			speed =  new Point(0, 0);
			rotate_offset = new Point(width / 2, height / 2);

			//create the ship drawing
			image_sprite.graphics.moveTo(0, height);
			image_sprite.graphics.lineTo(width / 2, 0);
			image_sprite.graphics.lineTo(width, height);

			//draw the line across
			image_sprite.graphics.moveTo((7*height/8 -height)/(-height/(width/2)), 7*height/8 );
			image_sprite.graphics.lineTo((7*height/8 -height)/(height/(width/2))+width, 7*height/8);
		}
		override public function Render():void
		{
			if (!visible)
				return;

			var matrix:Matrix = new Matrix();
			matrix.translate(-rotate_offset.x, -rotate_offset.y);
			matrix.rotate(angle);
			matrix.translate(rotate_offset.x, rotate_offset.y);
			matrix.translate(x, y);

			Game.Renderer.draw(image_sprite, matrix);
		}

		override public function Update():void
		{
			x += speed.x;
			y += speed.y;
			speed.x *= friction;
			speed.y *= friction;
			var angle_deg:int = angle * (180.0 / Math.PI);

			if (x + width <= 0)
				x = Game.Renderer.width - width;
			else if(x >= Game.Renderer.width)
				x = 0;

			if (y + height <= 0)
				y = Game.Renderer.height - height;
			else if(y >= Game.Renderer.height)
				y = 0;

			//now find the collision points
			var x_top:int = 0 * Math.cos(angle) + rotate_offset.y * Math.sin(angle)+x+rotate_offset.x;
			var y_top:int = 0 * Math.sin(angle) - rotate_offset.y * Math.cos(angle) + y + rotate_offset.y;

			var x_b_left:int = (-rotate_offset.x) * Math.cos(angle) - (height-rotate_offset.y) * Math.sin(angle)+x+rotate_offset.x;
			var y_b_left:int = (-rotate_offset.x) * Math.sin(angle) + (height-rotate_offset.y)  * Math.cos(angle) + y + rotate_offset.y;

			var x_b_right:int = (rotate_offset.x) * Math.cos(angle) - (height-rotate_offset.y) * Math.sin(angle)+x+rotate_offset.x;
			var y_b_right:int = (rotate_offset.x) * Math.sin(angle) + (height - rotate_offset.y) * Math.cos(angle) + y + rotate_offset.y;

			collis_points = [new Point(x_top, y_top), new Point(x_b_left, y_b_left), new Point(x_b_right, y_b_right)];
		}
		override public function CheckIfInNonRotatedRect(obj2:GameSprite):Boolean
		{
			var intersecting:Boolean = false;
			if (!visible)
				return false;

			if (obj2.angle == 0)
			{
				for (var i:int = 0; i < collis_points.length; i++)
					if (collis_points[i].x >= obj2.x && collis_points[i].x <= obj2.x + obj2.width)
						if (collis_points[i].y >= obj2.y && collis_points[i].y <= obj2.y + obj2.height)
							return true;
			}
			return intersecting;

		}
		public function RotateLeft():void
		{
			//first convert angle in rads
			var angle_deg:int = Math.round(angle * (180.0 / Math.PI));
			angle_deg -= rotate_amount;
			angle = angle_deg * (Math.PI / 180.0);
		}
		public function RotateRight():void
		{
			var angle_deg:int = angle * (180.0 / Math.PI);
			angle_deg += rotate_amount;
			angle = angle_deg * (Math.PI / 180.0);
		}
		public function Thrust(dir:int=1):void
		{
			var angle_deg:int = angle * (180.0 / Math.PI);
			if (dir == 1)
			{
				speed.x +=speed_multi * Math.sin(angle);
				speed.y -= speed_multi * Math.cos(angle);
			}
			else
			{
				speed.x -=speed_multi * Math.sin(angle);
				speed.y += speed_multi * Math.cos(angle);
			}
		}
	}
}

Next, we'll want to create an explosion object to be created whenever a collision has occured. So create a new class with a base class of GameSprite.as.

Explosion.as

package Entities
{
	import flash.display.Sprite;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	/**
	 * ...
	 * @author Chris Moeller
	 */
	public class Explosion extends GameSprite
	{
		private var radius:Number;
		public var finished:Boolean;
		private var size:int;

		private var random_offsets:Array;
		private var random_sizes:Array;

		private const num_points:int = 12;
		public var life:Number;
		public var max_life:Number;

		public function Explosion(x:int, y:int, max_life:Number, size:int = 0 )
		{
			super(x, y, 0, 0, angle);
			this.size = size;
			this.life = Game.current_time;
			this.max_life = max_life;

			image_sprite = new Sprite();
			image_sprite.graphics.lineStyle(2, 0x333333);
			finished = false;

			random_offsets = new Array();
			random_sizes = new Array();

			var high:Number = 10 ;
			var low:Number = 0;

			var high2:int = 3;
			var low2:int = 1;

			for (var i:int = 0; i < num_points; i++)
			{
				var random_x:Number = Math.floor(Math.random() * (1 + high - low)) + low+i;
				var random_y:Number = Math.floor(Math.random() * (1 + high - low)) + low+1;
				random_offsets.push(new Point(random_x, random_y));
				random_sizes.push(Math.floor(Math.random() * (1 + high2 - low2)) + low2);
			}

		}
		override public function Render():void
		{
			//going to want to draw points around the outside of a circle of radius size, and have it increasing
			//so need to go around the circle (centered at x,y=0,0) and draw lines outward

			var selected_color:int = 16-Math.round((Game.current_time-life) / max_life*16);
			var color_val:String = selected_color.toString(16);

			var color:uint  = uint("0x" + color_val + color_val + color_val + color_val + color_val + color_val);

			for (var i:int = 0; i < num_points; i++)
			{
				Game.Renderer.fillRect(
				new Rectangle(
				x+size * Math.cos(((i + 1) * 360 / num_points) * (Math.PI / 180)) + random_offsets[i].x,
				y + size * Math.sin(((i + 1) * 360 / num_points) * (Math.PI / 180)) + random_offsets[i].y,
				random_sizes[i], random_sizes[i])
				,color);
			}
			size += ((max_life-(Game.current_time-life))/max_life)*5;

			super.Render();
		}
	}
}

We also have to change "game.as" to actually run the collision detection for each frame, and handle when a collision (by creating an explosion, removing the asteroid and creating two smaller asteroids, if applicable).

Game.as

package
{
	import Entities.Asteroid;
	import Entities.Bullet;
	import Entities.Explosion;
	import Entities.Ship;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.getTimer;

	public class Game
	{
		public var bitmap:Bitmap;
		public static var Renderer:BitmapData;

		private var ship:Ship

		private var keys_down:Array

		private const LEFT:int = 37;
		private const UP:int = 38;
		private const RIGHT:int = 39;
		private const DOWN:int = 40;
		private const SPACE:int = 32;	

		public static var current_time:Number;
		private var bullets:Array;
		private var firing_delay:Number;
		private var last_fired:Number;
		private var bullets_max_life:Number;

		private var asteroids:Array;

		private var explosions:Array;

		public var mouse_down:Boolean;
		public var mouse_click:Boolean;
		public var mouse_pos:Point;

		public function Game(stageWidth:int, stageHeight:int)
		{
			trace("Game created");
			current_time = getTimer();
			Renderer = new BitmapData(stageWidth, stageHeight, false, 0x000000);
			bitmap = new Bitmap(Renderer);

			ship = new Ship(Renderer.width / 2 - 6, Renderer.height / 2 - 10, 10, 18);

			keys_down = new Array();
			bullets = new Array();
			firing_delay = 200;
			last_fired = 0;
			bullets_max_life = 1000;

			asteroids = new Array();

			//setup four first asteroids
			asteroids.push(new Asteroid(Renderer.width / 5, 2 * Renderer.height / 3, 0, 0, 1));
			asteroids.push(new Asteroid(Renderer.width / 5, Renderer.height / 4, 0, 1, 0));

			asteroids.push(new Asteroid(2*Renderer.width / 3, 1 * Renderer.height / 3, 0, 2, 1));
			asteroids.push(new Asteroid(2*Renderer.width / 3, 5 * Renderer.height / 5, 0, 3, 0));

			explosions = new Array();

		}
		public function Render():void
		{
			Renderer.lock();
			Renderer.fillRect(new Rectangle(0, 0, Renderer.width, Renderer.height), 0x000000);

			ship.Render();
			for (var i:int = 0; i < bullets.length; i++)
				bullets[i].Render();

			for (var j:int = 0; j < asteroids.length; j++)
				asteroids[j].Render();

			for (var k:int = 0; k < explosions.length; k++)
				explosions[k].Render();

			Renderer.unlock();
		}

		public function Update():void
		{
			current_time = getTimer();

			if (ship.visible)
			{
				if (CheckKeyDown(LEFT))
					ship.RotateLeft();

				if (CheckKeyDown(RIGHT))
					ship.RotateRight();

				if (CheckKeyDown(UP))
					ship.Thrust(1);
				if (CheckKeyDown(DOWN))
					ship.Thrust( -1);				

				if (CheckKeyDown(SPACE) && current_time-last_fired > firing_delay)
				{
					var x:int = 0 * Math.cos(ship.angle) + ship.rotate_offset.y * Math.sin(ship.angle)+ship.x+ship.rotate_offset.x;
					var y:int = 0 * Math.sin(ship.angle) - ship.rotate_offset.y * Math.cos(ship.angle)+ship.y+ship.rotate_offset.y;

					bullets.push(new Bullet(x, y, current_time, ship.angle));
					last_fired = current_time;
				}
				ship.Update();
			}

			var bullets_to_delete:Array = new Array();
			for (var i:int = 0; i < bullets.length; i++)
			{
				bullets[i].Update();
				if (current_time-bullets[i].life > bullets_max_life)
				{
					bullets_to_delete.push(i);
					continue;
				}

				var asteroid_hit:int = -1;
				for (var i2:int = 0; i2 < asteroids.length;i2++)
					if (bullets[i].CheckIfInNonRotatedRect(asteroids[i2]))
					{
						asteroid_hit = i2;
						break;
					}
				if (asteroid_hit != -1)
				{
					DestroyAsteroid(asteroid_hit);
					bullets_to_delete.push(i);
				}
			}
			for (var j:int = 0; j < bullets_to_delete.length;j++ )
			{
				bullets.splice(bullets_to_delete[j], 1);
			}
			var asteroid_ship_hit:int = -1;
			for (var k:int = 0; k < asteroids.length; k++ )
			{
				asteroids[k].Update();
				if (ship.CheckIfInNonRotatedRect(asteroids[k]))
				{
					asteroid_ship_hit = k;
					break;
				}
			}
			if (asteroid_ship_hit != -1)
			{
				ship.visible = false;
				explosions.push(new Explosion(ship.x, ship.y, 1500,2));
				explosions.push(new Explosion(ship.x, ship.y, 500));
				DestroyAsteroid(asteroid_ship_hit);
			}

			var explosions_to_delete:Array = new Array();
			for (var m:int = 0; m < explosions.length;m++ )
				if (current_time-explosions[m].life > explosions[m].max_life)
					explosions_to_delete.push(m);
			for (var n:int = 0; n < explosions_to_delete.length; n++)
				explosions.splice(explosions_to_delete[n], 1);

		}
		public function DestroyAsteroid(asteroid_hit:int):void
		{
			explosions.push(new Explosion(
			asteroids[asteroid_hit].x + asteroids[asteroid_hit].width / 2,
			asteroids[asteroid_hit].y + asteroids[asteroid_hit].height / 2, 1000, asteroids[asteroid_hit].width/4));	

			//now delete the old asteroid, and add 2 new ones in it's place if there are any more sizes left
			var old_asteroid:Asteroid = asteroids[asteroid_hit];
			//if there are more sizes to chose from
			if (old_asteroid.size != Asteroid.Sizes.length - 1)
			{

				var rand_dir:int = Math.floor(Math.random() * (1 + Asteroid.Directions.length - 1 ));

				var rand_dir2:int = rand_dir - 2;
				if (rand_dir - 2 < 0)
					rand_dir2 = rand_dir + 2;

				var rand_type:int = Math.floor(Math.random() * (1 + Asteroid.Types.length - 1 ));
				var rand_type2:int = Math.floor(Math.random() * (1 + Asteroid.Types.length - 1 ));

				//add 2 new asteroids at half the size
				asteroids.push(new Asteroid(
				old_asteroid.x,
				old_asteroid.y,
				old_asteroid.size + 1,
				rand_dir,
				rand_type));

				asteroids.push(new Asteroid(
				old_asteroid.x,
				old_asteroid.y,
				old_asteroid.size + 1,
				rand_dir2,
				rand_type2));
			}
			asteroids.splice(asteroid_hit, 1);
		}
		public function KeyUp(e:KeyboardEvent):void
		{
			//trace(e.keyCode);
			//position of key in the array
			var key_pos:int = -1;
			for (var i:int = 0; i < keys_down.length; i++)
				if (e.keyCode == keys_down[i])
				{
					//the key is found/was pressed before, so store the position
					key_pos = i;
					break;
				}
			//remove the keycode from keys_down if found
			if(key_pos!=-1)
				keys_down.splice(key_pos, 1);
		}

		public function KeyDown(e:KeyboardEvent):void
		{
			//check to see if the key that is being pressed is already in the array of pressed keys
			var key_down:Boolean = false;
			for (var i:int = 0; i < keys_down.length; i++)
				if (keys_down[i] == e.keyCode)
					key_down = true;

			//add the key to the array of pressed keys if it wasn't already in there
			if (!key_down)
				keys_down.push(e.keyCode);
		}

		public function CheckKeyDown(keycode:int):Boolean
		{
			var answer:Boolean = false;
			for (var i:int = 0; i < keys_down.length; i++)
				if (keys_down[i] == keycode)
				{
					answer = true;
					break;
				}
			return answer;
		}
	}
}

Tutorial Demo

In the next section we’ll create a way to keep track of the lives and score, as well as create the levels, the ability to pause and restart, and a basic menu GUI.

Download the source Code and please leave me any comments or feedback you have.

Other Articles in this Series

Bookmark the permalink.

10 Comments

  1. Great tutorial! One problem on this part (7) is that the changes needed for Asteroid.as are not in the tutorial, though they are present in the source code available for download. Without those changes, you get compile errors regarding the types and size public vars in the Asteroid class. Aside from the small oversight, this is a very enlightening tutorial.

    • Thanks for checking out the tutorial Ted!

      I’ll have to fix that- I had written out the tutorials after I programmed each section, but then realized some things that would have been better added earlier, updated the earlier tutorials, and tried to make sure the newer ones reflected those changes smoothly 😮

      Thanks for finding that! Let me know if you’re interested in any other types of tutorials!

  2. I’m having trouble getting collision detection between the asteroids and the bullets to work. Frustratingly, if the ship runs into the asteroids it blows up just fine. I’ve been searching through the code posted here and downloaded the source code (which works properly) and looked through that too and can’t find what I’m missing. Can you guess what I may be missing?

    • Hey, yeah- the collision detection for the ship is handled differently than that for the asteroids.

      On line 130 of ‘Game.as’ above: bullets[i].CheckIfInNonRotatedRect(asteroids[i2])

      so that would be where the problem is (or associated with)- if you’re having trouble you can also email me a zip of the code and I can check it out – (email me through the contact form and I’ll email you back with the email)

      • Thank you! I just forgot the g in length on line 129. Since beginning to learn programming I have quickly developed a newfound appreciation for the horrors of debugging.

  3. Thanks for the tutorial!! This is very helpful.

  4. Hi. There are a few inconsistencies in the code that I found, mostly for the asteroid splitting void. It might just be me being careless and leaving stuff out, but you seemed to be accessing arrays and variables that don’t exist. I was able to fix it, but I like this tutorial and it would be a shame if a complete beginner was unable to complete it because of a small inconsistency in the code.

    • Shoot- Sorry, it’s the same problem consistently- whenever I edit a entry after putting it in, wordpress changes all the less than and greater than signs ( > and < ) into the html character code for them & lt; and & gt; (no spaces) I'll go through the tutorial series again and check if any of them have them- but that is probably why it was not compiling. Sorry about that!

  5. Hi, I figured out a way to highlight the collision points to help with changing them once I add my own graphics in. Here’s the code:

    public function highlightcollisionpts():void
    {
    collisionptsvis = new Sprite();
    collisionptsvis.graphics.lineStyle(2, 0xFFFFFF)
    for (var i:int = collisionpts.length – 1; i >= 0; i–)
    {
    collisionptsvis.graphics.drawRect(collisionpts[i].x, collisionpts[i].y, 2, 2);
    }
    Game.archetect.draw(collisionptsvis);
    }

  6. Hi, i keep getting an error in part 7 referring to lines including Old_asteroid.size, the error says

    “Access of possibly undefined property size through a reference with static type Entities.Asteroid”

    and i am also getting the error

    “Access of possibly undefined property Types through a reference with static type Class”

Leave a Reply

Your email address will not be published. Required fields are marked *