Away3D Shoot’em’Up Tutorial – Powerups

by The Tech Labs 0

In this article we add some powerups to the game.

Result


Download Source Files

Requirements

Adobe Flex v3.02

Away3D v3.3.3 source code

Flint 2.1.0 source code

Pre-Requisites

You should read the previous articles in this series.

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

Powerups

In the last article we implemented a new system for defining and creating the weapons. There was no way for the player to change his weapons though – for that we need to add powerups.

Powerup.as

package
{
	import away3d.materials.BitmapMaterial;
	import away3d.primitives.Plane;

	public class Powerup extends MeshObject
	{
		protected static const POWERUP_SIZE:Number = 6;
		protected static const POWERUP_SPEED:Number = 30;
		protected static const POWERUP_X_LIMIT:Number = 70;
		protected static const POWERUP_Y_LIMIT:Number = 60;
		protected static const POWERUP_X_START:Number = 65;
		protected static const POWERUP_Y_START:Number = 50;
		protected var weapon:int = 0;
		protected var shields:int = 0;
		protected var score:int = 0;

		public function get Weapon():int
		{
			return weapon;
		}

		public function get Shields():int
		{
			return shields;
		}

		public function get Score():int
		{
			return score;
		}

		public function Powerup()
		{
			super();
		}

		protected function startupPowerup(engineManager:EngineManager, material:BitmapMaterial, weapon:int, score:int, shields:int):Powerup
		{
			var plane:Plane = new Plane(
				{material:material,
				width:POWERUP_SIZE,
				height:POWERUP_SIZE,
				yUp:false});
			super.startupMeshObject(engineManager, plane);

			this.collisionName = CollisionIdentifiers.POWERUP;
			this.model.x = POWERUP_X_LIMIT;
			this.model.y = MathUtils.randRange(-POWERUP_Y_START, POWERUP_Y_START);
			this.weapon = weapon;
			this.shields = shields;
			this.score = score;

			return this;
		}

		public function startupRandomPowerup(engineManager:EngineManager):Powerup
		{
			var random:int = int(Math.random() * 2);
			switch (random)
			{
				case 0:
					return startupWeapon1Powerup(engineManager);
				case 1:
					return startupWeapon2Powerup(engineManager);
			}

			return null;
		}

		public function startupWeapon1Powerup(engineManager:EngineManager):Powerup
		{
			return startupPowerup(
				engineManager,
				ResourceManager.Powerup1_Tex,
				1,
				0,
				0);
		}

		public function startupWeapon2Powerup(engineManager:EngineManager):Powerup
		{
			return startupPowerup(
				engineManager,
				ResourceManager.Powerup2_Tex,
				2,
				0,
				0);
		}

		override public function enterFrame(dt:Number):void
		{
			super.enterFrame(dt);

			this.model.x -= POWERUP_SPEED * dt;

			if (this.model.x > POWERUP_X_LIMIT ||
				this.model.x < -POWERUP_X_LIMIT || 				this.model.y > POWERUP_Y_LIMIT ||
				this.model.y < -POWERUP_Y_LIMIT)
				this.shutdown();
		}

		override public function collision(other:MeshObject):void
		{
			this.shutdown();
		}
	}
}

This code should look familiar. We have extended the MeshObject class, which allows us to put a mesh on the screen. Like the weapons and the enemies, the powerup mesh is a Plane orientated to face the camera, and textured with a BitmapMaterial embedded by the ResourceManager class. The collisionName has been set to CollisionIdentifiers.POWERUP to identify this as a powerup to the collision system. The enterFrame functions moves the Powerup across the screen until it is no longer visible, at which point it is shutdown.

The thing that makes the powerup unique are the three properties Shield, Score and Weapon. Each represents the reward that the player will receive when this powerup is collected. You would probably only create powerups that have one of these three properties set to something other than zero, but there is also no reason why you couldn’t have a powerup that gave the player points and some additional shields.

The Powerup class has a number of startup functions, along with a startup function that creates a random Powerup. You may recall from the last article that we spent some time breaking up the Weapon class to avoid using that one class to create multiple enemy types. In this case though, because the properties defined by the Powerup class are shared by all the different types of powerups (every powerup will have a shield, score and weapon property, and move in a straight line across the screen), there is no downside to having the Powerup startup functions create the various different types of powerups.

So here we have defined two powerups, one for weapon 1 (startupWeapon1Powerup), and for weapon 2 (startupWeapon2Powerup). The code for creating powerups for shields and points would be very similar.

Player.as

package
{
	import away3d.core.math.Number3D;
	import away3d.events.MouseEvent3D;
	import away3d.primitives.*;

	import flash.events.*;
	import flash.geom.*;
	import flash.media.*;

	import mx.core.*;

	public class Player extends DamageableObject
	{
		protected static const PLAYER_X_LIMIT:Number = 52;
		protected static const PLAYER_Y_LIMIT:Number = 38;
		protected static const PLAYER_WIDTH:Number = 16;
		protected static const PLAYER_HEIGHT:Number = 5;
		protected static const COLLISION_PLANE_WIDTH:Number = 150;
		protected static const COLLISION_PLANE_HEIGHT:Number = 100;
		protected static const PLAYER_SHIELDS:int = 10;
		protected var collisionPlane:MeshObject = null;
		protected var shooting:Boolean = false;
		protected var timeToNextShot:Number = 0;

		public function Player()
		{
			super();
		}

		public function startupPlayer(engineManager:EngineManager):Player
		{
			var plane:Plane = new Plane(
				{material:ResourceManager.Player_Tex,
				width:PLAYER_WIDTH,
				height:PLAYER_HEIGHT,
				yUp:false});
			super.startupMeshObject(engineManager, plane);

			var collisionPlaneMesh:Plane = new Plane(
				{material:new NullMaterial(),
				width:COLLISION_PLANE_WIDTH,
				height:COLLISION_PLANE_HEIGHT,
				yUp:false});
			collisionPlane = new MeshObject().startupMeshObject(engineManager, collisionPlaneMesh);
			collisionPlaneMesh.addEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D);

			this.collisionName = CollisionIdentifiers.PLAYER;
			this.shields = PLAYER_SHIELDS;

			Application.application.pbarLife.setProgress(this.shields, PLAYER_SHIELDS);

			return this;
		}

		override public function shutdown():void
		{
			collisionPlane.model.removeEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D);
			collisionPlane.shutdown();
			collisionPlane = null;
			super.shutdown();
		}

		override public function enterFrame(dt:Number):void
		{
			super.enterFrame(dt);

			timeToNextShot -= dt;
			timeToNextShot = timeToNextShot<0?0:timeToNextShot; 			if (timeToNextShot == 0 && shooting) 			{ 				var weaponPosition:Number3D = new Number3D(); 				weaponPosition.add(this.model.position, new Number3D(PLAYER_WIDTH / 2, 0, 0)); 				 				timeToNextShot = this.engineManager.MyApplicationManager.MyWeaponManager.createWeapon( 					this.engineManager.MyApplicationManager.MyPlayerWeapon, 					this.engineManager.MyApplicationManager.MyPlayerWeaponLevel,  					weaponPosition); 			} 				 			if (this.model.x > PLAYER_X_LIMIT)
				this.model.x = PLAYER_X_LIMIT;
			if (this.model.x < -PLAYER_X_LIMIT) 				this.model.x = -PLAYER_X_LIMIT; 			 			if (this.model.y > PLAYER_Y_LIMIT)
				this.model.y = PLAYER_Y_LIMIT;
			if (this.model.y < -PLAYER_Y_LIMIT)
				this.model.y = -PLAYER_Y_LIMIT;
		}

		public function mouseMove3D(event:MouseEvent3D):void
		{
			if (this.model)
			{
				this.model.x = event.sceneX;
				this.model.y = event.sceneY;
			}
		}

		public override function collision(other:MeshObject):void
		{
			if (other.collisionName == CollisionIdentifiers.POWERUP)
			{
				var powerup:Powerup = other as Powerup;
				this.shields += powerup.Shields;
				this.engineManager.MyApplicationManager.MyScore += powerup.Score;
				if (powerup.Weapon != 0)
				{
					this.engineManager.MyApplicationManager.changePlayerWeapon(powerup.Weapon);
				}
			}
		}

		public override function mouseDown(event:MouseEvent):void
		{
			shooting = true;
		}

		public override function damage(amount:int):void
		{
			super.damage(amount);
			Application.application.pbarLife.setProgress(this.shields, PLAYER_SHIELDS);
		}

		public override function mouseUp(event:MouseEvent):void
		{
			shooting = false;
		}

		protected override function die():void
		{
			engineManager.MyApplicationManager.levelEnded();
			super.die();
		}
	}
}

The Player class needs to be modified to respond to a collision with a Powerup.

if (other.collisionName == CollisionIdentifiers.POWERUP)
{
	var powerup:Powerup = other as Powerup;
	this.shields += powerup.Shields;
	this.engineManager.MyApplicationManager.MyScore += powerup.Score;
	if (powerup.Weapon != 0)
	{
		this.engineManager.MyApplicationManager.changePlayerWeapon(powerup.Weapon);
	}
}

In the collision function we test for a collision with a Powerup. When such a collision has been detected, the players shields and score are updated. If the Shields and Score properties have been set to zero then these updates have no effect.

If the Weapon property is not zero we ask the ApplicationManager to update the players weapons by calling the changePlayerWeapon function.

ApplicationManager.as

package
{
	import mx.core.Application;

	public class ApplicationManager extends BaseObject
	{
		protected static const TIME_BETWEEN_BUILDINGS:Number = 2;
		protected static const TIME_BETWEEN_ENEMIES:Number = 1;
		protected static const TIME_BETWEEN_POWERUPS:Number = 10;
		protected static const TIME_TO_LEVEL_END:Number = 1;
		protected var timeToEndGame:Number = TIME_TO_LEVEL_END;
		protected var timeToNextBuilding:Number = 0;
		protected var timeToNextEnemy:Number = TIME_BETWEEN_ENEMIES;
		protected var timeToNextPowerup:Number = 0;//TIME_BETWEEN_POWERUPS;
		protected var levelHasEnded:Boolean = false;
		protected var myPlayerWeapon:int = 1;
		protected var myPlayerWeaponLevel:int = 1;
		protected var myWeaponManager:WeaponManager = null;
		protected var myScore:int = 0;

		public function get MyWeaponManager():WeaponManager
		{
			return myWeaponManager;
		}

		public function get MyPlayerWeapon():int
		{
			return myPlayerWeapon;
		}

		public function get MyPlayerWeaponLevel():int
		{
			return myPlayerWeaponLevel;
		}

		public function set MyScore(value:int):void
		{
			myScore = value;
			Application.application.lblScore.text = myScore;
		}

		public function get MyScore():int
		{
			return myScore;
		}

		public function ApplicationManager()
		{
			super();
		}

		public function startupApplicationManager(engineManager:EngineManager):ApplicationManager
		{
			this.startupBaseObject(engineManager);
			this.engineManager.addCollidingPair(CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYER);
			this.engineManager.addCollidingPair(CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYERWEAPON);
			this.engineManager.addCollidingPair(CollisionIdentifiers.PLAYER, CollisionIdentifiers.POWERUP);
			this.myWeaponManager = new WeaponManager().startupWeaponManager(this.engineManager);

			return this;
		}

		public function startLevel1():void
		{
			timeToNextBuilding = 0;
			MyScore = 0;

			new BackgroundPlane().startupBackgroundPlane(engineManager);
			new Player().startupPlayer(engineManager);

			// prepopulate the city
			for (var i:int = 0; i < 5; ++i)
			{
				var building:BackgroundBuilding = new BackgroundBuilding();
				building.startupBackgroundBuilding(engineManager);
				building.enterFrame((i + 1) * 2);
			}
		}

		public function levelEnded():void
		{
			levelHasEnded = true;
		}

		public override function shutdown():void
		{
			super.shutdown();
		}

		public override function enterFrame(dt:Number):void
		{
			timeToNextBuilding -= dt;
			if (timeToNextBuilding <= 0)
			{
				timeToNextBuilding = TIME_BETWEEN_BUILDINGS;
				new BackgroundBuilding().startupBackgroundBuilding(engineManager);
			}

			timeToNextEnemy -= dt;
			if (timeToNextEnemy <= 0)
			{
				timeToNextEnemy = TIME_BETWEEN_ENEMIES;
				new Enemy().startupBasicEnemy(engineManager);
			}

			timeToNextPowerup -= dt;
			if (timeToNextPowerup <= 0)
			{
				timeToNextPowerup = TIME_BETWEEN_POWERUPS;
				new Powerup().startupRandomPowerup(engineManager);
			}

			if (levelHasEnded)
			{
				timeToEndGame -= dt;
				if (timeToEndGame <= 0)
				{
					engineManager.nextStateChange = "MainMenu";
				}
			}
		}

		public function changePlayerWeapon(weapon:int):void
		{
			if (weapon == this.myPlayerWeapon &&
				this.myWeaponManager.isValidWeapon(
					this.myPlayerWeapon,
					this.myPlayerWeaponLevel + 1))
			{
				++this.myPlayerWeaponLevel;
			}
			else if (this.myWeaponManager.isValidWeapon(weapon, 1))
			{
				this.myPlayerWeapon = weapon;
				while (!this.myWeaponManager.isValidWeapon(
					this.myPlayerWeapon,
					this.myPlayerWeaponLevel))
				{
					--this.myPlayerWeaponLevel;
				}
			}
		}
	}
}

In order for the players weapons to change in response to a powerup, the ApplicationManager gets a new function called changePlayerWeapon.

public function changePlayerWeapon(weapon:int):void
{
	if (weapon == this.myPlayerWeapon &&
		this.myWeaponManager.isValidWeapon(
			this.myPlayerWeapon,
			this.myPlayerWeaponLevel + 1))
	{
		++this.myPlayerWeaponLevel;
	}

If the player has collected a powerup for the current weapon (i.e. the powerups Weapon property equals the ApplicationManagers myPlayerWeapon property) the ApplicationManager will attempt to increase the weapon level. Obviously this can only be done if the current weapon has not been maxed out, so a call to the WeaponManager isValidWeapon function is used to see if such an upgrade is possible.

	else if (this.myWeaponManager.isValidWeapon(weapon, 1))
	{
		this.myPlayerWeapon = weapon;
		while (!this.myWeaponManager.isValidWeapon(
			this.myPlayerWeapon,
			this.myPlayerWeaponLevel))
		{
			--this.myPlayerWeaponLevel;
		}
	}
}

If the player has collected a powerup for a new weapon then the ApplicationManager changes to that weapon. Because weapons may have different levels defined (weapon 1 may have 4 levels, whereas weapon 2 may only have 3), the ApplicationManager will downgrade the weapon level where necessary.

timeToNextPowerup -= dt;
if (timeToNextPowerup <= 0)
{
	timeToNextPowerup = TIME_BETWEEN_POWERUPS;
	new Powerup().startupRandomPowerup(engineManager);
}

The Powerups are added to the game just like the enemies and background buildings. The ApplicationManager is set to add one powerup after a specified amount of time has passed in the enterFrame function.

WeaponManager.as

package
{
	import away3d.core.math.Number3D;
	import away3d.materials.BitmapMaterial;

	import flash.utils.Dictionary;

	public class WeaponManager
	{
		protected var engineManager:EngineManager = null;
		protected var weaponDatabase:Dictionary = new Dictionary();

		public function WeaponManager()
		{
			super();
		}

		public function startupWeaponManager(engineManager:EngineManager):WeaponManager
		{
			this.engineManager = engineManager;

			var weapon1:Dictionary = new Dictionary();
			weaponDatabase[1] = weapon1;

			weapon1[1] = weapon1Level1;
			weapon1[2] = weapon1Level2;

			var weapon2:Dictionary = new Dictionary();
			weaponDatabase[2] = weapon2;

			weapon2[1] = weapon2Level1;
			weapon2[2] = weapon2Level2;

			return this;
		}

		public function createWeapon(weapon:int, weaponLevel:int, position:Number3D):Number
		{
			if (isValidWeapon(weapon, weaponLevel))
			{
				return weaponDatabase[weapon][weaponLevel](position);
			}

			return 0;
		}

		public function isValidWeapon(weapon:int, weaponLevel:int):Boolean
		{
			if (weaponDatabase[weapon] != null)
			{
				if (weaponDatabase[weapon][weaponLevel] != null)
					return true;
			}

			return false;
		}

		public function weapon1Level1(weaponPosition:Number3D):Number
		{
			var material:BitmapMaterial = ResourceManager.Bullet_Tex;
			var timeToNextShot:Number = 0.4;
			var damage:int = 1;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

		public function weapon1Level2(weaponPosition:Number3D):Number
		{
			var timeToNextShot:Number = 0.4;

			var material:BitmapMaterial = ResourceManager.Bullet_Tex;
			var damage:int = 1;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var yDir2:Number = 0.5;
			var yDir3:Number = -0.5;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir2, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir3, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

		public function weapon2Level1(weaponPosition:Number3D):Number
		{
			var material:BitmapMaterial = ResourceManager.Bullet2_Tex;
			var timeToNextShot:Number = 1;
			var damage:int = 2;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

		public function weapon2Level2(weaponPosition:Number3D):Number
		{
			var material:BitmapMaterial = ResourceManager.Bullet2_Tex;
			var timeToNextShot:Number = 1;
			var damage:int = 1;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var yDir2:Number = 0.5;
			var yDir3:Number = -0.5;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir2, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, material, weaponPosition, damage, xDir, yDir3, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

	}
}

In order for the player to have weapons to powerup to, we need to add some more weapon definitions to the WeaponManager.

var weapon2:Dictionary = new Dictionary();
weaponDatabase[2] = weapon2;

weapon2[1] = weapon2Level1;
weapon2[2] = weapon2Level2;

The weapon2Level1 and weapon2Level2 functions are added to the weaponDatabase in the startupWeaponManager function. The code for doing this is exactly the same as the code that added the original weapon definitions. This is one of the benefits of having all the weapon creation code separated into it’s own class. With a few lines of code, and a few simple functions, we can create new weapon definitions, without a whole mess of if/else/switch statements in the middle of the Players enterFrame function.

The code required to implement the powerups has been quite straight forward. As we create the base framework additional functionality, like powerups, becomes quite easy to add to the game. In the next article we will look at finalising this base framework by moving away from the ApplicationManager creating an endless number of enemies towards specifically defined the levels.

Leave a Reply

Your email address will not be published.

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