Advertisement

Build a Stage3D Shoot-'Em-Up: Score, Health, Lives, HUD and Transitions

by

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

This post is part of a series called Shoot-'Em-Up.
Activetuts+ Workshop #5: Frantic 2 - Critique
Pixel-Level Collision Detection Based on Pixel Colors

In this part of the series, we’re adding gameplay elements such as health, score, and lives, the GUI elements to display them, and game logic transitions to deal with dying, game overs, level changes, and the final credits screen.


Also available in this series:

  1. Build a Stage3D Shoot-’Em-Up: Sprite Test
  2. Build a Stage3D Shoot-’Em-Up: Interaction
  3. Build a Stage3D Shoot-’Em-Up: Explosions, Parallax, and Collisions
  4. Build a Stage3D Shoot-'Em-Up: Terrain, Enemy AI, and Level Data
  5. Build a Stage3D Shoot-’Em-Up: Score, Health, Lives, HUD and Transitions
  6. Build a Stage3D Shoot-’Em-Up: Full-Screen Boss Battles and Polish

Final Result Preview

Let's take a look at the final result we will be working towards: a hardware-accelerated shoot-em-up demo that includes everything from parts one to four of this series, plus gameplay elements such as health, score, lives, the GUI elements to display them, and game logic transitions to deal with dying, game overs, level changes, and the final credits screen.



Introduction: Welcome to Level Five!

Let's continue to make a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius in actionscript.

In the first part of this series, we implemented a basic 2D sprite engine that achieves great performance through the use of Stage3D hardware rendering and as several optimizations.

In the first part, we implemented a title screen, the main menu, sound and music, and an input system so that the player can control their spaceship using the keyboard.

In the third part, we added all the eye-candy: a particle system complete with sparks, flying debris, shockwaves, engine fire trails and tons of explosions. We also added accurate timers, collision detection and an R-Type inspired orbiting "power orb" companion that circles the player's ship.

And in the fourth part, we added A.I. (artificial intelligence) to our enemies by creating several different behaviors and movement styles, a level data parsing mechanism that allowed the use of a level editor, and a terrain background layer.

In this part, we are going to finally make this game fully playable from start to finish. When the player is hit they will take damage, and when they run out of health they are going to die in a firey explosion. If they die too many times it will be game over for them. If they make it to the end of a level's terrain, they will move on to the next level - unless they've cleared the entire game. When that happens, we'll display a "credits screen" which congratulates the player for a job well done.

The game will also track the player's high score, and all sorts of visual feedback (such as level transition messages, a health bar and even more eye candy) will help to add to the fun-factor. We will have taken what was a mere game demo and fleshed it out into a complete game!


Step 1: Open Your Existing Project

We're going to be building on the source code written in the previous tutorials, much of which will not change. If you don't already have it, be sure to download the source code from part four (download here). Open the project file in FlashDevelop (info here) and get ready to upgrade your game! This source code will work in any other AS3 compiler, from CS5.5 to Flash Builder, as long as you target Flash 11.


Step 2: Implement Game Saves

In the same way that browsers have regular cookies, Flash can also store temporary information that can be access in subsequent visits. Since this game is inspired by old-school arcade shooters, it seems only natural that it includes a high score display.

In order to save the player's current high score, we'll take advantage of the SharedObject package in Flash. You can read more about it in this tutorial. Basically, it serves as a simplistic repository of data - we can store anything there.

The one caveat is that - just like browser cookies - users can clear them in their security settings and they might even be turned off due to privacy concerns. Therefore, they cannot be relied upon and serve only as a little fun extra that we can add to our game when available. This way, if a player returns to the game their old high score will still be visible. We will also be saving which level the player reached, which would be handy in a future version of the game to implement an unlocking "level select" screen.

Create a brand new file in your project called GameSaves.as and start off by creating a new SharedObject instance in the class constructor as follows:

 
// Stage3D Shoot-em-up Tutorial Part 5 
// by Christer Kaitila - www.mcfunkypants.com 
 
// GameSaves.as 
// A simple highscore and level save game system. 
// For now, only high score is used (by the GUI), but you 
// could implement an "unlocked" levels menu to allow players 
// to skip levels they have completed when starting a new game. 
 
package 
{ 
	// For more information on "Flash Cookies" see: 
	// http://gamedev.michaeljameswilliams.com/2009/03/18/avoider-game-tutorial-11/ 
	// http://en.wikipedia.org/wiki/Local_shared_object 
 
	public class GameSaves 
	{ 
		import flash.net.SharedObject; 
 
		private var _saves:SharedObject; 
		 
		// class constructor 
		public function GameSaves() 
		{ 
			trace("Initializing game save system"); 
			try 
			{ 
				_saves = SharedObject.getLocal("SaveGame"); 
			} 
			catch ( sharedObjectError:Error ) 
			{ 
				trace( "Unable to init game save system: "  
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
			} 
		}

Step 3: Store Highscore and Level Data

Continuing with GameSaves.as, we will implement get and set functions that either access the saved data or write new values to the Flash "cookie" respectively. We need to ensure that errors are completely ignored and valid data is returned, just in case the user is running with privacy settings set on high.

		 
		public function get level():int 
		{ 
			try 
			{ 
				if (!_saves) return 0; 
				if (_saves.data.level == null) return 0; 
				trace("Loaded level is " + _saves.data.level); 
				return _saves.data.level; 
			} 
			catch ( sharedObjectError:Error ) 
			{ 
				trace( "Unable to load score: "  
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
			} 
			return 0; 
		} 
 
		public function get score():int 
		{ 
			try 
			{ 
				if (!_saves) return 0; 
				if (_saves.data.score == null) return 0; 
				trace("Loaded score is " + _saves.data.score); 
				return _saves.data.score; 
			} 
			catch ( sharedObjectError:Error ) 
			{ 
				trace( "Unable to load score: "  
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
			} 
			return 0; 
		} 
 
		public function set level(num:int):void 
		{ 
			try 
			{ 
				if (!_saves) return; 
				_saves.data.level = num; 
				_saves.flush(); 
				trace("Saved level set to: " + num); 
			} 
			catch ( sharedObjectError:Error ) 
			{ 
				trace( "Unable to save level: "  
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
			} 
		} 
 
		public function set score(num:int):void 
		{ 
			try 
			{ 
				if (!_saves) return; 
				_saves.data.score = num; 
				_saves.flush(); 
				trace("Saved score set to: " + num); 
			} 
			catch ( sharedObjectError:Error ) 
			{ 
				trace( "Unable to save score: "  
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
			} 
		} 
 
	} // end class 
} // end package

That's it for the high score save game system. You could flesh this out to allow users to save their game at any point in a longer experience, and as you might imagine all sorts of interesting stats could be saved, from the number of shots fired, total playing time or total distance travelled to the ratio of hits to misses. For this example game, we're just going to use this new class to update the high score display in our brand new GUI heads-up-display, which we will implement below.


Step 4: Add a Heads-Up-Display

Almost every well-polished game needs to display information to the player as an overlay that sits on top of the action. This is often merely the player's score, and perhaps a health bar. We're going to create a great-looking HUD (heads-up-display) GUI overlay system that will show all sorts of information and should help to add some visual flair and polish to our game, such as:

  • The framerate (FPS)
  • How much RAM is being used
  • How many sprites are currently in our entity batch
  • (As well as how many have been reused)
  • The all-time high score
  • A health bar that decreases as the player gets damaged
  • The player's current score/li>
  • How many lives the player has remaining until the game is over.

This is how it is going to look:



Step 5: Upgrade the GUI Class

We are going to perform a complete re-write of the existing GameGUI.as, so open that file in your project and replace the minimalistic debug stats display from previous tutorials with this more feature-rich HUD class as follows. To begin with, we need to embed a fancy font and the background for our HUD, as well as create several new class variables.

 
// Stage3D Shoot-em-up Tutorial Part 5 
// by Christer Kaitila - www.mcfunkypants.com 
 
// GameGUI.as 
// A typical simplistic framerate display for benchmarking performance, 
// plus a way to track rendering statistics from the entity manager. 
// In this version, we include all sorts of GUI displays such as score, 
// highscore, lives left as well as a "health bar". 
 
package 
{ 
	import flash.display.Sprite; 
	import flash.display.Bitmap; 
	import flash.events.Event; 
	import flash.events.TimerEvent; 
	import flash.text.TextField; 
	import flash.text.TextFormat; 
	import flash.text.Font; 
	import flash.utils.getTimer; 
	import flash.filters.GlowFilter; 
	import flash.system.System; 
	import flash.geom.Rectangle; 
	 
	public class GameGUI extends Sprite 
	{

Step 6: Embed a Font

You can choose any truetype font that you're allowed to use and embed it into your .swf. Take note of the unicodeRange parameter of the embed below. This is important to cut down the size of your flash file - it serves to eliminate the potentially thousands of glyphs that your game won't use such as international symbols, accented characters and more.

 
		// Font used by the GUI - only embed the chars we need to save space 
		[Embed (source = '../assets/gui_font.ttf',  
			embedAsCFF = 'false',  
			fontFamily = 'guiFont', 
			mimeType = 'application/x-font-truetype',  
			unicodeRange='U+0020-U+002F, U+0030-U+0039, U+003A-U+0040, U+0041-U+005A, U+005B-U+0060, U+0061-U+007A, U+007B-U+007E')] 
		private const GUI_FONT:Class; 
		private var myFormat:TextFormat; 
		private var myFormatRIGHT:TextFormat; 
		private var myFormatCENTER:TextFormat;

Step 7: Embed an Overlay Image

The hudOverlay below is a simple .PNG file that was created in Photoshop and is 50% transparent, so that the game can be seen underneath:


 
		// GUI bitmap overlay 
		[Embed (source = "../assets/hud_overlay.png")]  
		private var hudOverlayData:Class; 
		private var hudOverlay:Bitmap = new hudOverlayData();

Step 8: GUI Class Vars

Continuing with GameGUI.as, add some TextField objects and data variables that we will use to hold and display the HUD stats.

 
		// text on the screen 
		public var scoreTf:TextField; 
		public var highScoreTf:TextField; 
		public var levelTf:TextField; 
		public var healthTf:TextField; 
		public var transitionTf:TextField; 
		public var transitionTf_y_location:int = 162; 
 
		// numeric values for the text above 
		// used to cache player stats to detect changes 
		// so that textfields will only be updated if needed 
		public var score:int = 0; 
		public var prevHighScore:int = 0; 
		public var highScore:int = 0; 
		public var level:int = 0; 
		public var lives:int = 3; 
		public var health:int = 100; 
		public var transitionText:String = ""; 
 
		// debug stats (only used during development) 
		public var debugStatsTf:TextField; 
		public var titleText : String = ""; 
		public var statsText : String = ""; 
		public var statsTarget : EntityManager; 
		public var frameCount:int = 0; 
		public var timer:int; 
		public var ms_prev:int; 
		public var lastfps : Number = 60;

Step 9: GUI Inits

During the class constrcutor for our fancy new HUD, we need to setup a TextFormat objects that we be reused for variout displays. These objects are used to store the style information such as font, text size and color. We then spawn a bunch of text fields on screen at the appropriate locations along with the hud overlay image. Finally, we start listening for the ENTER_FRAME event so that we can update the HUD during gameplay.

		 
		public function GameGUI(title:String = "", inX:Number=0, inY:Number=0, inCol:int = 0xFFFFFF) 
		{ 
			super(); 
			x = inX; 
			y = inY; 
			titleText = title; 
 
			// used for most GUI text 
			var myFont:Font = new GUI_FONT(); 
			myFormat = new TextFormat();   
			myFormat.color = inCol; 
			myFormat.size = 16; 
			myFormat.font = myFont.fontName; 
			 
			// used only by the score 
			myFormatRIGHT = new TextFormat();   
			myFormatRIGHT.color = inCol; 
			myFormatRIGHT.size = 16; 
			myFormatRIGHT.font = myFont.fontName; 
			myFormatRIGHT.align = 'right'; 
 
			// used by the transition texts 
			myFormatCENTER = new TextFormat();   
			myFormatCENTER.color = inCol; 
			myFormatCENTER.size = 32; 
			myFormatCENTER.font = myFont.fontName; 
			myFormatCENTER.align = 'center'; 
 
			this.addEventListener(Event.ADDED_TO_STAGE, onAddedHandler); 
		} 
		 
		public function onAddedHandler(e:Event):void  
		{ 
			trace("GameGUI was added to the stage"); 
			 
			addChild(hudOverlay); 
			 
			// used for FPS display 
			debugStatsTf = new TextField(); 
			debugStatsTf.defaultTextFormat = myFormat; 
			debugStatsTf.embedFonts = true; 
			debugStatsTf.x = 18; 
			debugStatsTf.y = 0; 
			debugStatsTf.width = 320; 
			debugStatsTf.selectable = false; 
			debugStatsTf.text = titleText; 
			debugStatsTf.antiAliasType = 'advanced'; 
			addChild(debugStatsTf); 
 
			// create a score display 
			scoreTf = new TextField(); 
			scoreTf.defaultTextFormat = myFormatRIGHT; 
			scoreTf.embedFonts = true; 
			scoreTf.x = 442; 
			scoreTf.y = 0; 
			scoreTf.selectable = false; 
			scoreTf.antiAliasType = 'advanced'; 
			scoreTf.text = "SCORE: 000000\n3 LIVES LEFT"; 
			scoreTf.width = 140; 
			addChild(scoreTf); 
 
			// high score display 
			highScoreTf = new TextField(); 
			highScoreTf.defaultTextFormat = myFormat; 
			highScoreTf.embedFonts = true; 
			highScoreTf.x = 232; 
			highScoreTf.y = 0; 
			highScoreTf.selectable = false; 
			highScoreTf.antiAliasType = 'advanced'; 
			highScoreTf.text = "HIGH SCORE: 000000"; 
			highScoreTf.width = 320; 
			addChild(highScoreTf); 
 
			// add a health meter 
			healthTf = new TextField(); 
			healthTf.defaultTextFormat = myFormat; 
			healthTf.embedFonts = true; 
			healthTf.x = 232; 
			healthTf.y = 15; 
			healthTf.selectable = false; 
			healthTf.antiAliasType = 'advanced'; 
			healthTf.text = "HP: |||||||||||||"; 
			healthTf.width = 320; 
			addChild(healthTf); 
			 
			// add a "transition text" display 
			transitionTf = new TextField(); 
			transitionTf.defaultTextFormat = myFormatCENTER; 
			transitionTf.embedFonts = true; 
			transitionTf.x = 0; 
			transitionTf.y = transitionTf_y_location; 
			transitionTf.selectable = false; 
			transitionTf.filters = [new GlowFilter(0xFF0000, 1, 8, 8, 4, 2)]; 
			transitionTf.antiAliasType = 'advanced'; 
			transitionTf.text = ""; 
			transitionTf.width = 600; 
			transitionTf.height = 2000; 
			transitionTf.scrollRect = new Rectangle(0, 0, 600, 160); 
			// keep off screen until needed 
			// addChild(transitionTf); 
	 
			stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); 
		}

Step 10: Format the Values

Continuing with GameGUI.as, implement some handy formatting routines that will do things like pad the score with zeroes as seen in most arcade-style games, as well as create a simple "health bar" by converting a number into a a string of horizontal lines. Since the player is going to start out with 100 health and die when it reaches zero, and since the GUI happens to fit 13 characters nicely, we simply update the health text as approprite.

One important optimization that has been made is the use of temporary data variables that store the previously set state for each text field. This way, even if the game blindly sends a steady stream of updates to our GUI class, the on-screen representations only change when the value is actually different. This has a massive effect upon the framerate. For example, it could be many thousands of frames until the health or number of lives changes - there's no need to touch that HUD item until it does.

		 
		private function pad0s(num:int):String 
		{ 
			if (num < 10) return '00000' + num; 
			else if (num < 100) return '0000' + num; 
			else if (num < 1000) return '000' + num; 
			else if (num < 10000) return '00' + num; 
			else if (num < 100000) return '0' + num; 
			else return '' + num; 
		} 
		 
		// only updates textfields if they have changed 
		private function updateScore():void 
		{ 
			if (transitionText != transitionTf.text) 
			{ 
				transitionTf.text = transitionText; 
				if (transitionTf.text != "") 
				{ 
					if (!contains(transitionTf)) 
						addChild(transitionTf); 
				} 
				else 
				{ 
					if (contains(transitionTf)) 
						removeChild(transitionTf); 
				} 
			} 
			 
			if (statsTarget && statsTarget.thePlayer) 
			{ 
				if (health != statsTarget.thePlayer.health) 
				{ 
					health = statsTarget.thePlayer.health; 
					// generate the health bar (13 chars simply happens to fit the gui nicely) 
					if (statsTarget.thePlayer.health >= 99) healthTf.text = "HP: |||||||||||||"; 
					else if (statsTarget.thePlayer.health >= 92) healthTf.text = "HP: ||||||||||||"; 
					else if (statsTarget.thePlayer.health >= 84) healthTf.text = "HP: |||||||||||"; 
					else if (statsTarget.thePlayer.health >= 76) healthTf.text = "HP: ||||||||||"; 
					else if (statsTarget.thePlayer.health >= 68) healthTf.text = "HP: |||||||||"; 
					else if (statsTarget.thePlayer.health >= 60) healthTf.text = "HP: ||||||||"; 
					else if (statsTarget.thePlayer.health >= 52) healthTf.text = "HP: |||||||"; 
					else if (statsTarget.thePlayer.health >= 44) healthTf.text = "HP: ||||||"; 
					else if (statsTarget.thePlayer.health >= 36) healthTf.text = "HP: |||||"; 
					else if (statsTarget.thePlayer.health >= 28) healthTf.text = "HP: ||||"; 
					else if (statsTarget.thePlayer.health >= 20) healthTf.text = "HP: |||"; 
					else if (statsTarget.thePlayer.health >= 12) healthTf.text = "HP: ||"; 
					else healthTf.text = "HP: |"; 
				} 
				if ((score != statsTarget.thePlayer.score) || (lives != statsTarget.thePlayer.lives)) 
				{ 
					score = statsTarget.thePlayer.score; 
					lives = statsTarget.thePlayer.lives; 
					if (lives == -1) 
						scoreTf.text = scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' +'GAME OVER'; 
					else 
						scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' + lives +  
							(lives != 1 ? ' LIVES' : ' LIFE') + ' LEFT'; 
					// we may be beating the high score right now 
					if (score > highScore) highScore = score; 
				} 
			} 
			if (prevHighScore != highScore) 
			{ 
				prevHighScore = highScore; 
				highScoreTf.text = "HIGH SCORE: " + pad0s(highScore); 
			} 
		}

The transitionTf text area is formatted differently than the regular HUD text. Firstly, it is not added to the stage during the inits, since it will not be visible initially. It is rendered in a larger font, centered, and has a red glow. It will be used in the middle of the screen during level transitions, when the player has died, or when the game over screen needs to be displayed. Since it is usually offscreen, its appearance is controlled by our main game class. It will look like this:



Step 11: Render the GUI

The final routine to implement for our new HUD display GUI is the render loop which is called every frame. Most of the time, this function will do nothing and return control to our main game. Once per second we'll update the framerate, memory and entity stats, and only when values have changed will the updateScore function touch the screen.

		 
		private function onEnterFrame(evt:Event):void 
		{ 
			timer = getTimer(); 
			 
			updateScore(); 
			 
			if( timer - 1000 > ms_prev ) 
			{ 
				lastfps = Math.round(frameCount/(timer-ms_prev)*1000); 
				ms_prev = timer; 
				 
 
				var mem:Number = Number((System.totalMemory * 0.000000954).toFixed(1)); 
 
				// grab the stats from the entity manager 
				if (statsTarget) 
				{ 
					statsText =  
						statsTarget.numCreated + '/' + 
						statsTarget.numReused + ' sprites'; 
				} 
				 
				debugStatsTf.text = titleText + lastfps + 'FPS - ' + mem + 'MB' + '\n' + statsText; 
				frameCount = 0; 
			} 
		 
			// count each frame to determine the framerate 
			frameCount++; 
				 
		} 
	} // end class 
} // end package

We're done with the newly upgraded GameGUI.as. This simple addition really brings our game from mere sprite demo to something that looks like a real videogame. All we need to do now is implement the systems required to actually changes these values - a way for te player to store health and score, plus a way for enemies to destroy the player.


Step 12: Upgrade the Entity Class

We are now going to upgrade our game so that entities retain a record of their current health, score, level and number of lives. The Player is an entity, and for the most part these new stats will only be used by the game on the player, but in future versions of our game we could give different enemies varying amounts of health and damage.

Open the existing file Entity.as and add a few new class variables as follows. Note that we are NOT going to include the entire code listing for the entity class here since we're only making one change at the very top of the file. All the A.I. routines, collision detection code and the like remain unchanged.

 
// Stage3D Shoot-em-up Tutorial Part 5 
// by Christer Kaitila - www.mcfunkypants.com 
 
// Entity.as 
// The Entity class will eventually hold all game-specific entity stats 
// for the spaceships, bullets and effects in our game. 
// It stores a reference to a gpu sprite and a few demo properties. 
// This is where you would add hit points, weapons, ability scores, etc. 
// This class handles any AI (artificial intelligence) for enemies as well. 
 
package 
{ 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	 
	public class Entity 
	{ 
		// v5 stats used by the player entity 
		// but could optionally be used by some enemies too 
		public var health:int = 100; 
		// how many reserve ships you have until game over 
		public var lives:int = 3; 
		// matches the score on the GUI 
		public var score:int = 0; 
		// what level the player is on 
		public var level:int = 0; 
		// when you get hit, for a short while you are impervious 
		// to damage (otherwise nearby ships could add up to instant death) 
		public var invulnerabilityTimeLeft:Number = 0; 
		public var invulnerabilitySecsWhenHit:Number = 4; 
		// when you die, change levels or get to a game over state, 
		// this is the period of time that the GUI tells you 
		// before switching to the new state 
		public var transitionTimeLeft:Number = 0; 
		public var transitionSeconds:Number = 5; 
		// how much damage the player will take when getting hit 
		// since the player has 100HP, 49 means you can be hit 3x 
		public var damage:int = 49; 
		 
		// ... the rest of this class remains unchanged below ...

Step 13: Add Some More Levels

Since we are going to implement transition between multiple levels, we need more than one level in our game. We also need to determine how "long" a level is so that our game knows when the player has reached the end of it. Open the existing GameLevels.as and make a few changes as follows.

To begin with, we need to embed some more level data. Using OGMO (or the level editor of your choice - even hand-coded .CSV data if you wish) create a few more levels. Since we're testing transitions, the example levels, which are included in the source code .zip file above, are intentially extremely short - only a couple screens long. For your real game, you will naturally want to design much larger levels. If we did that here, however, it would take too long to test out all the new functionality we're programming.

 
// Stage3D Shoot-em-up Tutorial Part 5 
// by Christer Kaitila - www.mcfunkypants.com 
 
// GameLevels.as 
// This class parses .CSV level data strings 
// that define the locations of tiles from a spritesheet 
// Example levels were created using the OGMO editor, 
// but could be designed by hand or any number of other 
// freeware game level editors that can output .csv 
// This can be a .txt, .csv, .oel, .etc file 
// - we will strip all xml/html tags (if any)  
// - we only care about raw csv data 
// Our game can access the current level with: 
// spriteId = myLevel.data[x][y]; 
 
package 
{ 
	import flash.display3D.Context3DProgramType; 
	public class GameLevels 
	{ 
		// v5 the farthest column in the level 
		// used to detect when the map is complete 
		public var levelLength:int = 0; 
 
		// the "demo" level seen during the title screen 
		[Embed(source = '../assets/level0.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL0:Class; 
		private var level0data:String = new LEVEL0; 
		[Embed(source = '../assets/terrain0.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL0TERRAIN:Class; 
		private var level0terrain:String = new LEVEL0TERRAIN; 
 
		// level 1 
		[Embed(source = '../assets/level1.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL1:Class; 
		private var level1data:String = new LEVEL1; 
		[Embed(source = '../assets/terrain1.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL1TERRAIN:Class; 
		private var level1terrain:String = new LEVEL1TERRAIN; 
 
		// level 2 
		[Embed(source = '../assets/level2.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL2:Class; 
		private var level2data:String = new LEVEL2; 
		[Embed(source = '../assets/terrain2.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL2TERRAIN:Class; 
		private var level2terrain:String = new LEVEL2TERRAIN; 
 
		// level 3 
		[Embed(source = '../assets/level3.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL3:Class; 
		private var level3data:String = new LEVEL3; 
		[Embed(source = '../assets/terrain3.oel', mimeType = 'application/octet-stream')]  
		private static const LEVEL3TERRAIN:Class; 
		private var level3terrain:String = new LEVEL3TERRAIN; 
 
		// the currently loaded level data 
		public var data:Array = [];

Step 14: Upgrade the Level Parsing

Apart from some new levels being embeded above, we are tracking the maximum length found in the level during the parsing stage. We need to do it this way because some rows of level data may be trimmed (when spaces to the far right are left blank). Therefore, we remember the "longest known" row of level data and compare it with the current row so that we are sure to see every tile in our map during gameplay.

 
		public function GameLevels() 
		{ 
		} 
		 
		private function stripTags(str:String):String 
		{ 
			var pattern:RegExp = /<\/?[a-zA-Z0-9]+.*?>/gim; 
			return str.replace(pattern, ""); 
		} 
	 
		private function parseLevelData(lvl:String):Array 
		{ 
			var levelString:String; 
			var temps:Array; 
			var nextValue:int; 
			var output:Array = []; 
			var nextrow:int; 
 
			// how many columns wide is the map? 
			// note: some rows may be shorter 
			levelLength = 0; 
			 
			switch (lvl) 
			{ 
				case "level0" : levelString = stripTags(level0data); break; 
				case "terrain0" : levelString = stripTags(level0terrain); break; 
				case "level1" : levelString = stripTags(level1data); break; 
				case "terrain1" : levelString = stripTags(level1terrain); break; 
				case "level2" : levelString = stripTags(level2data); break; 
				case "terrain2" : levelString = stripTags(level2terrain); break; 
				case "level3" : levelString = stripTags(level3data); break; 
				case "terrain3" : levelString = stripTags(level3terrain); break; 
				default: 
					return output;  
			} 
			 
			//trace("Level " + num + " data:\n" + levelString); 
			var lines:Array = levelString.split(/\r\n|\n|\r/); 
			for (var row:int = 0; row < lines.length; row++) 
			{ 
				// split the string by comma 
				temps = lines[row].split(","); 
				if (temps.length > 1) 
				{ 
					nextrow = output.push([]) - 1; 
					// turn the string values into integers 
					for (var col:int = 0; col < temps.length; col++) 
					{ 
						if (temps[col] == "") temps[col] = "-1"; 
						nextValue = parseInt(temps[col]); 
						if (nextValue < 0) nextValue = -1; // we still need blanks 
						// trace('row '+ nextrow + ' nextValue=' + nextValue); 
						 
						// v5 remember longest column so we know when we've reached the end 
						if (col > levelLength) levelLength = col; 
						 
						output[nextrow].push(nextValue); 
						 
					} 
					//trace('Level row '+nextrow+':\n' + String(output[nextrow])); 
				} 
			} 
			//trace('Level output data:\n' + String(output)); 
			return output; 
		} 
		 
		public function loadLevel(lvl:String):void 
		{ 
			trace("Loading level " + lvl); 
			data = parseLevelData(lvl); 
		} 
		 
	} // end class 
} // end package

These two simple changes to the level parsing class are all that are needed for the rest of our new gameplay functionality.


Step 15: Upgrade the Title Screen

Just for fun, I've created a brand new title screen spritesheet texture. After some Google searching, I learned that the invented name of the game, Kaizen - which was simply the first three letters in my last name and the word Zen - is in fact a real word!

In Japanese, Kaizen can be roughly translated to mean "continuous improvement" - what an apt title for a game that has been iteratively developed in small steps over the course of this tutorial series! Because it is a real Japanese phrase, and since arcade shoot-em-ups were often created in Japan in the heyday of the arcade era, the characters were added to the sprite. Additional minor changes to the controls menu item were also in order.

Here is the newly upgraded titlescreen spritesheet texture:


There are only two functions in GameMenu.as that have changed, where we account for the slightly different size of the logo.

 
		public function setPosition(view:Rectangle):void  
		{ 
			logoX = view.width / 2; 
			logoY = view.height / 2 - 56; // v5 
			menuX = view.width / 2; 
			menuY = view.height / 2 + 64; 
			menuY1 = menuY - (menuItemHeight / 2); 
			menuY2 = menuY - (menuItemHeight / 2) + menuItemHeight; 
			menuY3 = menuY - (menuItemHeight / 2) + (menuItemHeight * 2); 
		} 
		 
		// called every frame: used to update the animation 
		public function update(currentTime:Number) : void 
		{		 
			logoSprite.position.x = logoX; 
			logoSprite.position.y = logoY; 
			var wobble:Number = (Math.cos(currentTime / 500) / Math.PI) * 0.2; 
			logoSprite.scaleX = 0.8 + wobble; // v5 
			logoSprite.scaleY = 0.8 + wobble; // v5 
			wobble = (Math.cos(currentTime / 777) / Math.PI) * 0.1; 
			logoSprite.rotation = wobble; 
			 
			// pulse the active menu item 
			wobble = (Math.cos(currentTime / 150) / Math.PI) * 0.1; 
			amenuAboutSprite.scaleX =  
			amenuAboutSprite.scaleY =  
			amenuControlsSprite.scaleX =  
			amenuControlsSprite.scaleY =  
			amenuPlaySprite.scaleX =  
			amenuPlaySprite.scaleY =  
				1.1 + wobble; 
			 
			// show the about/controls for a while 
			if (showingAbout) 
			{ 
				if (showingAboutUntil > currentTime) 
				{ 
					aboutSprite.alpha = 1; 
				} 
				else 
				{ 
					aboutSprite.alpha = 0; 
					showingAbout = false; 
					updateState(); 
				} 
			} 
 
			if (showingControls) 
			{ 
				if (showingControlsUntil > currentTime) 
				{ 
					controlsSprite.alpha = 1; 
				} 
				else 
				{ 
					controlsSprite.alpha = 0; 
					showingControls = false; 
					updateState(); 
				} 
			}		 
		}

Step 16: Upgrade the Entity Manager

Because EntityManager.as calls each entity's collision function when appropriate, we need to take into account our new entity properties for the player such as health. Almost everything remains as-is except that we will be upgrading the collision checking routine, the level changing function and the render loop.

Open EntityManager.as and upgrade it as follows. We only need to change three functions. The first step is to change checkCollisions() to not grant the player any points when they destroy something during the main menu. This can happen between multiple plays, since after the game is first run the player entity exists and is simply hidden at gameover until the start of the subsequent game.

 
// as an optimization to save millions of checks, only 
// the player's bullets check for collisions with all enemy ships 
// (enemy bullets only check to hit the player) 
public function checkCollisions(checkMe:Entity):Entity 
{ 
	var anEntity:Entity; 
	var collided:Boolean = false; 
	if (!thePlayer) return null; // v5 
	 
	if (checkMe.owner != thePlayer) 
	{	// quick check ONLY to see if we have hit the player 
		anEntity = thePlayer; 
		if (checkMe.colliding(anEntity))  
		{ 
			collided = true; 
		} 
	} 
	else // check all active enemies 
	{ 
		for(var i:int=0; i< allEnemies.length;i++) 
		{ 
			anEntity = allEnemies[i]; 
			if (anEntity.active && anEntity.collidemode) 
			{ 
				if (checkMe.colliding(anEntity))  
				{ 
					collided = true; 
					// v5 accumulate score only when playing 
					if (thePlayer.sprite.visible) 
						thePlayer.score += anEntity.collidepoints; 
					break; 
				} 
			} 
		} 
	}

Step 17: Invulnerability

One important gameplay change that really adds to the polish is to "debounce" the player's collisions. Debouncing is a coding term that refers to avoiding multiple successive calls to a function that are extremely close in time. In this case, imagine a situation where the player is about to be hit by an enemy bullet while surrounded by dozens of nearby threats. If we don't debounce the hit event, the player could conceivably start from 100% health, get hit ten times in a single frame, and suffer from an "insta kill".

A far better approach is to register the very first hit, subtract some health, and then switch to a temporary invulnerability state for a few seconds, which gives the player enough time to get out of harm's way. As per genre conventions, after getting hit we make the player "flicker" in and out by cycling the opacity of the player's sprite to indicate to the player that his or her ship is no longer susceptible to damage. After a few seconds, we switch back to being vulnerable.

As an extra bit of eye-candy and user feedback, we spawn a large number of explosions nearby to the player so that there is even more visual feedback. After all, getting damaged is a major event in the game and warrants an even bigger explosion. We then reduce the player's health which will eventually be reflected in the heads-up-display GUI. If the player's health goes below zero, reduce the number of lives and trigger a death transition (something that we will implement later on).

Continuing with the checkCollisions() function in EntityManager.as, code these upgrades as follows.

 
	if (collided) 
	{ 
		// v5 - handle player health and possible gameover 
		if ((anEntity == thePlayer) || (checkMe == thePlayer)) 
		{ 
			// when the player gets damaged, they become 
			// invulnerable for a short perod of time 
			if (thePlayer.invulnerabilityTimeLeft <= 0) 
			{ 
				thePlayer.health -= anEntity.damage; 
				thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit; 
				// extra explosions for a bigger boom 
				var explosionPos:Point = new Point(); 
				for (var numExplosions:int = 0; numExplosions < 6; numExplosions++) 
				{ 
					explosionPos.x = thePlayer.sprite.position.x + fastRandom() * 64 - 32;  
					explosionPos.y = thePlayer.sprite.position.y + fastRandom() * 64 - 32;  
					particles.addExplosion(explosionPos); 
				} 
				if (thePlayer.health > 0) 
				{ 
					trace("Player was HIT!"); 
				} 
				else 
				{ 
					trace('Player was HIT... and DIED!'); 
					thePlayer.lives--; 
					// will be reset after transition 
					// thePlayer.health = 100; 
					thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit 
						+ thePlayer.transitionSeconds; 
					thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
				} 
			} 
			else // we are currently invulnerable and flickering 
			{	// ignore the collision 
				collided = false; 
			} 
		} 
		 
		if (collided) // still 
		{ 
			//trace('Collision!'); 
			if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); 
			particles.addExplosion(checkMe.sprite.position); 
			if ((checkMe != theOrb) && (checkMe != thePlayer))  
				checkMe.die(); // the bullet 
			if ((anEntity != theOrb) && ((anEntity != thePlayer)))  
				anEntity.die(); // the victim 
			return anEntity; 
		} 
	} 
	return null; 
}

Step 19: Hide Things When Required

The update() function needs only minor tweaks to account for the fact that we do not want the orb or its particle trail to be visible after the game over occurs. Finally, one extra line is added to the changeLevels() function to ensure that levels start scrolling from the beginning location.

 
// called every frame: used to update the simulation 
// this is where you would perform AI, physics, etc. 
// in this version, currentTime is seconds since the previous frame 
public function update(currentTime:Number) : void 
{		 
	var anEntity:Entity; 
	var i:int; 
	var max:int; 
	 
	// what portion of a full second has passed since the previous update? 
	currentFrameSeconds = currentTime / 1000; 
	 
	// handle all other entities 
	max = entityPool.length; 
	for (i = 0; i < max; i++) 
	{ 
		anEntity = entityPool[i]; 
		if (anEntity.active) 
		{ 
			// subtract the previous aiPathOffset 
			anEntity.sprite.position.x -= anEntity.aiPathOffsetX; 
			anEntity.sprite.position.y -= anEntity.aiPathOffsetY; 
 
			// calculate location on screen with scrolling 
			anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds; 
			anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds; 
								 
			// is a custom AI specified? if so, run it now 
			if (anEntity.aiFunction != null) 
			{ 
				anEntity.aiFunction(currentFrameSeconds); 
			} 
 
			// add the new aiPathOffset 
			anEntity.sprite.position.x += anEntity.aiPathOffsetX; 
			anEntity.sprite.position.y += anEntity.aiPathOffsetY; 
			 
			// collision detection 
			if (anEntity.collidemode) // && anEntity.isBullet 
			{ 
				checkCollisions(anEntity); 
			} 
			 
			// entities can orbit other entities  
			// (uses their rotation as the position) 
			if (anEntity.orbiting != null) 
			{ 
				anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x +  
					((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); 
				anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y -  
					((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); 
			} 
 
			// entities can leave an engine emitter trail 
			if (anEntity.leavesTrail) 
			{ 
				// leave a trail of particles 
				if (anEntity == theOrb) 
				{ 
					if (theOrb.sprite.visible) // v5 don't show during gameover 
						particles.addParticle(63,  
						anEntity.sprite.position.x, anEntity.sprite.position.y,  
						0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1); 
				} 
				else // other enemies 
				{ 
					particles.addParticle(63, anEntity.sprite.position.x + 12,  
					anEntity.sprite.position.y + 2,  
					0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1); 
				} 
				 
			} 
			 
			if ((anEntity.sprite.position.x > maxX) || 
				(anEntity.sprite.position.x < minX) || 
				(anEntity.sprite.position.y > maxY) || 
				(anEntity.sprite.position.y < minY))							 
			{ 
				// if we go past any edge, become inactive 
				// so the sprite can be respawned 
				if ((anEntity != thePlayer) && (anEntity != theOrb))  
					anEntity.die(); 
			} 
			 
			if (anEntity.rotationSpeed != 0) 
				anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds; 
				 
			if (anEntity.fadeAnim != 0) 
			{ 
				anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds; 
				if (anEntity.sprite.alpha <= 0.001) 
				{ 
					anEntity.die(); 
				} 
				else if (anEntity.sprite.alpha > 1) 
				{ 
					anEntity.sprite.alpha = 1; 
				} 
			} 
			if (anEntity.zoomAnim != 0) 
			{ 
				anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds; 
				anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds; 
				if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0) 
					anEntity.die(); 
			} 
		} 
	} 
} 
 
// load a new level for entity generation 
public function changeLevels(lvl:String):void 
{ 
		killEmAll(); 
		level.loadLevel(lvl); 
		levelCurrentScrollX = 0; 
		levelPrevCol = -1; 
		lastTerrainEntity = null; // v5 
}

That's all we need to upgrade in the entity manager class. The last step required is to upgrade the main game class to take all these awesome new features into account.


Step 20: Upgrade the Game Itself!

We need to implement several major changes to the existing Main.as, so it is included here in full to avoid confusion. New sections are marked with a // v5 comment to help you skip over sections that remain unchanged.

To begin with, we're going to be working with points so we need to make one subtle change to our imports. All of the inits in our game are identical to those from the previous tutorial, apart from the creation of an instance of our fancy new GameSaves class.

 
// Stage3D Shoot-em-up Tutorial Part 5 
// by Christer Kaitila - www.mcfunkypants.com 
// Created for active.tutsplus.com 
 
package  
{ 
	[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] 
 
	import flash.display3D.*; 
	import flash.display.Sprite; 
	import flash.display.StageAlign; 
	import flash.display.StageQuality; 
	import flash.display.StageScaleMode; 
	import flash.events.Event; 
	import flash.events.ErrorEvent; 
	import flash.events.MouseEvent; 
	import flash.geom.Rectangle; 
	import flash.utils.getTimer; 
	import flash.geom.Point; // v5 
		 
	public class Main extends Sprite  
	{ 
		// the game save/load system 
		public var saved:GameSaves; // v5 
		 
		// the entity spritesheet (ships, particles) 
		[Embed(source="../assets/sprites.png")] 
		private var EntitySourceImage : Class; 
 
		// the terrain spritesheet 
		[Embed(source="../assets/terrain.png")] 
		private var TerrainSourceImage : Class; 
		 
		// the keyboard control system 
		private var _controls : GameControls; 
		// don't update the menu too fast 
		private var nothingPressedLastFrame:Boolean = false; 
		// timestamp of the current frame 
		public var currentTime:int; 
		// for framerate independent speeds 
		public var currentFrameMs:int; 
		public var previousFrameTime:int; 
		 
		// player one's entity 
		public var thePlayer:Entity; 
		// movement speed in pixels per second 
		public var playerSpeed:Number = 128; 
		// timestamp when next shot can be fired 
		private var nextFireTime:uint = 0; 
		// how many ms between shots 
		private var fireDelay:uint = 200; 
		 
		// main menu = 0 or current level number 
		private var _state:int = 0; 
		// the title screen batch 
		private var _mainmenu:GameMenu; 
		// the sound system 
		private var _sfx:GameSound;	 
		// the background stars 
		private var _bg:GameBackground;	 
		 
		private var _terrain:EntityManager; 
		private var _entities:EntityManager; 
		private var _spriteStage:LiteSpriteStage; 
		private var _gui:GameGUI; 
		private var _width:Number = 600; 
		private var _height:Number = 400; 
		public var context3D:Context3D; 
		 
		// constructor function for our game 
		public function Main():void  
		{ 
			if (stage) init(); 
			else addEventListener(Event.ADDED_TO_STAGE, init); 
		} 
		 
		// called once flash is ready 
		private function init(e:Event = null):void  
		{ 
			_controls = new GameControls(stage); 
			removeEventListener(Event.ADDED_TO_STAGE, init); 
			stage.quality = StageQuality.LOW; 
			stage.align = StageAlign.TOP_LEFT; 
			stage.scaleMode = StageScaleMode.NO_SCALE; 
			stage.addEventListener(Event.RESIZE, onResizeEvent); 
			trace("Init Stage3D..."); 
			_gui = new GameGUI(""); 
			addChild(_gui); 
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); 
			stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); 
			stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); 
			trace("Stage3D requested...");		 
			_sfx = new GameSound(); 
		} 
				 
		// this is called when the 3d card has been set up 
		// and is ready for rendering using stage3d 
		private function onContext3DCreate(e:Event):void  
		{ 
			trace("Stage3D context created! Init sprite engine..."); 
			context3D = stage.stage3Ds[0].context3D; 
			initSpriteEngine(); 
		} 
		 
		// this can be called when using an old version of flash 
		// or if the html does not include wmode=direct 
		private function errorHandler(e:ErrorEvent):void  
		{ 
			trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); 
		} 
 
		protected function onResizeEvent(event:Event) : void 
		{ 
			trace("resize event..."); 
			 
			// Set correct dimensions if we resize 
			_width = stage.stageWidth; 
			_height = stage.stageHeight; 
			 
			// Resize Stage3D to continue to fit screen 
			var view:Rectangle = new Rectangle(0, 0, _width, _height); 
			if ( _spriteStage != null ) { 
				_spriteStage.position = view; 
			} 
			if(_terrain != null) { 
				_terrain.setPosition(view); 
			} 
			if(_entities != null) { 
				_entities.setPosition(view); 
			} 
			if(_mainmenu != null) { 
				_mainmenu.setPosition(view); 
			} 
		} 
		 
		private function initSpriteEngine():void  
		{ 
			// init a gpu sprite system 
			//var view:Rectangle = new Rectangle(0,0,_width,_height) 
			var stageRect:Rectangle = new Rectangle(0, 0, _width, _height);  
			_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); 
			_spriteStage.configureBackBuffer(_width,_height); 
			 
			// create the background stars 
			trace("Init background..."); 
			_bg = new GameBackground(stageRect); 
			_bg.createBatch(context3D); 
			_spriteStage.addBatch(_bg.batch); 
			_bg.initBackground(); 
			 
			// create the terrain spritesheet and batch 
			trace("Init Terrain..."); 
			_terrain = new EntityManager(stageRect); 
			_terrain.SourceImage = TerrainSourceImage; 
			_terrain.SpritesPerRow = 16; 
			_terrain.SpritesPerCol = 16; 
			_terrain.defaultSpeed = 90; 
			_terrain.defaultScale = 1.5; 
			_terrain.levelTilesize = 48;  
			_terrain.createBatch(context3D, 0.001); // a little UV padding required 
			_spriteStage.addBatch(_terrain.batch); 
			_terrain.changeLevels('terrain' + _state); 
 
			// create a single rendering batch 
			// which will draw all sprites in one pass 
			trace("Init Entities..."); 
			_entities = new EntityManager(stageRect); 
			_entities.SourceImage = EntitySourceImage; 
			_entities.defaultScale = 1.5; // 1 
			_entities.levelTilesize = 48;  
			_entities.createBatch(context3D); 
			_entities.sfx = _sfx; 
			_spriteStage.addBatch(_entities.batch); 
			_entities.changeLevels('level' + _state); 
			_entities.streamLevelEntities(true); // spawn first row of the level immediately 
			 
			// create the logo/titlescreen main menu 
			_mainmenu = new GameMenu(stageRect); 
			_mainmenu.createBatch(context3D); 
			_spriteStage.addBatch(_mainmenu.batch); 
			 
			// tell the gui where to grab statistics from 
			_gui.statsTarget = _entities;  
			 
			// start the render loop 
			stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); 
 
			// only used for the menu 
			stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);    
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);  
 
			// set up the savegame system 
			saved = new GameSaves(); 
			_gui.highScore = saved.score; 
			_gui.level = saved.level; 
		}

Step 21: Transition Logic

This next function makes a huge impact on the game. We are going to implement the handleTransitions function. Our render loop checks the player's state each frame and if the player entity has any time remaining in its transitionTimeLeft property, an appropriate message is displayed via our upgraded game GUI class. A large glowing red message will be displayed in the center of the screen telling the player either what level they just cleared, that they died, or that the game is over.

Just for fun, if all levels have been cleared and none remain, a special game state of -1 is set which means that it is time to "roll the credits". This is another genre convention of most videogames: just like the end of a Hollywood movie, once the credits start rolling you know that you've reached the end. Add the transition logic function to Main.as as follows:

		 
		// check player transition state (deaths, game over, etc) 
		private var currentTransitionSeconds:Number = 0; 
		private function handleTransitions(seconds:Number):void 
		{ 
			// are we at a pending transition (death or level change)? 
			if (thePlayer.transitionTimeLeft > 0) 
			{ 
				currentTransitionSeconds += seconds; 
				 
				thePlayer.transitionTimeLeft -= seconds; 
				 
				if (thePlayer.transitionTimeLeft > 0) 
				{	//was it a level change? 
					if (thePlayer.level != _state) 
					{ 
						if (_state == -1) 
						{ 
							_gui.transitionText = "\n\n\n\n\n\nCONGRATULATIONS\n\n" + 
								"You fought bravely and defended\n" + 
								"the universe from certain doom.\n\nYou got to level " +  
								thePlayer.level + "\nwith " + thePlayer.score + " points." +  
								"\n\nCREDITS:\n\nProgramming: McFunkypants\n(mcfunkypants.com)\n\n" + 
								"Art: Daniel Cook\n(lostgarden.com)\n\n" + 
								"Music: MaF\n(maf464.com)\n\n" + 
								"Thanks for playing!"; 
							_gui.transitionTf.scrollRect = new Rectangle(0,currentTransitionSeconds * 40,600,160); 
						} 
						else if (_state == 0) 
							_gui.transitionText = "GAME OVER\nYou got to level " + thePlayer.level  
								+ "\nwith " + thePlayer.score + " points."; 
						else if (_state > 1) 
							_gui.transitionText = "\nLEVEL " + (_state-1) + " COMPLETE!"; 
						else 
							_gui.transitionText = "\nLEVEL " + _state;  
					} 
					else // must be a death or start of a map 
					{ 
						_gui.transitionText = "Your ship was destroyed.\n\nYou have "  
							+ thePlayer.lives + (thePlayer.lives != 1 ? " lives" : " life") + " left."; 
					} 
					if (thePlayer.lives < 0 || thePlayer.health <= 0) 
					{ 
						// during the death transition, spawn tons of explosions just for fun 
						if (_entities.fastRandom() < 0.2) 
						{ 
							var explosionPos:Point = new Point(); 
							explosionPos.x = thePlayer.sprite.position.x + _entities.fastRandom() * 128 - 64;  
							explosionPos.y = thePlayer.sprite.position.y + _entities.fastRandom() * 128 - 64;  
							_entities.particles.addExplosion(explosionPos); 
						} 
					} 
				} 
				else // transition time has elapsed 
				{ 
					currentTransitionSeconds = 0; 
					 
					thePlayer.transitionTimeLeft = 0; 
					 
					if (_state == -1) _state = 0; 
					_gui.transitionTf.scrollRect = new Rectangle(0,0,600,160); 
					_gui.transitionText = ""; 
					 
					if ((thePlayer.health <= 0) && (_state != 0)) // we died 
					{ 
						trace("Death transition over. Respawning player."); 
						thePlayer.sprite.position.y = _entities.midpoint; 
						thePlayer.sprite.position.x = 64; 
						thePlayer.health = 100; 
						// start the level again 
						_entities.changeLevels('level' + _state); 
						_terrain.changeLevels('terrain' + _state); 
					} 
					if (thePlayer.level != _state) 
					{ 
						trace('Level transition over. Starting level ' + _state); 
						thePlayer.level = _state; 
						if (_state > 1) // no need to reload at startGame 
						{ 
							_entities.changeLevels('level' + _state); 
							_terrain.changeLevels('terrain' + _state); 
						} 
						if (_state == 0) // game over 
						{ 
							thePlayer.health = 100; 
							thePlayer.lives = 3; 
							thePlayer.sprite.visible = false; 
							_entities.theOrb.sprite.visible = false; 
							_spriteStage.addBatch(_mainmenu.batch); 
							_entities.changeLevels('level' + _state); 
							_terrain.changeLevels('terrain' + _state); 
						} 
					} 
				} 
			} 
		}

Step 22: Upgrade the Player Logic

Now that our gameplay can boast player death, gameover states, and health, our entity AI function that is run for the player sprite needs to be upgraded to take advantage of all this new stuff. We are going to add a few nice little upgrades to our game here.

Firstly, we want to ensure that players can't fire when they're dead. Secondly, as an additional bit of visual feedback and as a warning of impending doom, when the player's health is nearly depleted we will spawn a sorts of sparks. This will add to the tension and is sure to communicate to the player that they should be extra careful. Thirdly, as implemented above, just after being hit the player is invulnerable for a few seconds; during this time we want to flicker the opacity of the player's sprite to communicate this invulnerability.

When the player is almost dead, this is what it will look like:


Continuing with Main.as, upgrade the player logic function as follows:

		 
		// run every frame by the entity manager as the player ai function 
		public function playerLogic(seconds:Number):void 
		{ 
			thePlayer.age += seconds; 
			handleTransitions(seconds); 
			thePlayer.speedY = thePlayer.speedX = 0; 
			 
			if (_state == 0) return; 
			 
			if (_controls.pressing.up) 
				thePlayer.speedY = -playerSpeed; 
			if (_controls.pressing.down) 
				thePlayer.speedY = playerSpeed; 
			if (_controls.pressing.left) 
				thePlayer.speedX = -playerSpeed; 
			if (_controls.pressing.right) 
				thePlayer.speedX = playerSpeed; 
				 
			// v5 
			if (_controls.pressing.fire && (thePlayer.health > 0)) 
			{ 
				// is it time to fire again? 
				if (currentTime >= nextFireTime) 
				{ 
					//trace("Fire!"); 
					nextFireTime = currentTime + fireDelay; 
					_sfx.playGun(1); 
					_entities.shootBullet(3); 
				} 
			} 
				 
			// keep on screen 
			if (thePlayer.sprite.position.x < 0) 
				thePlayer.sprite.position.x = 0; 
			if (thePlayer.sprite.position.x > _width) 
				thePlayer.sprite.position.x = _width; 
			if (thePlayer.sprite.position.y < 0) 
				thePlayer.sprite.position.y = 0; 
			if (thePlayer.sprite.position.y > _height) 
				thePlayer.sprite.position.y = _height; 
				 
			// leave a trail of particles 
			_entities.particles.addParticle(63,  
				thePlayer.sprite.position.x - 12,  
				thePlayer.sprite.position.y + 2,  
				0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5); 
				 
			// v5 if we are about to die, spew sparks as a warning 
			if (thePlayer.health < 10) 
				_entities.particles.addSparks(thePlayer.sprite.position, 1, 2);	 
				 
			// when the player gets damaged, they become 
			// invulnerable for a short perod of time 
			if (thePlayer.invulnerabilityTimeLeft > 0) 
			{ 
				thePlayer.invulnerabilityTimeLeft -= seconds; 
				if (thePlayer.invulnerabilityTimeLeft <= 0) 
				{ 
					trace("Invulnerability wore off."); 
					thePlayer.sprite.alpha = 1; 
				} 
				else // while invulnerable, flicker 
				{ 
					thePlayer.sprite.alpha = Math.sin(thePlayer.age * 30) / Math.PI  + 0.25; 
				} 
			} 
		}

Step 23: Simplify the Input Handler

We just moved the gun firing code to the playerLogic function. Therefore, we need to make one small change to the existing processInput function to account for this upgrade. Remove the obsolete shooting code.

 
		// handle any player input 
		private function processInput():void 
		{ 
			if (_state == 0) // are we at the main menu? 
			{ 
				// select menu items via keyboard 
				if (_controls.pressing.down || _controls.pressing.right) 
				{ 
					if (nothingPressedLastFrame)  
					{ 
						_sfx.playGun(1); 
						_mainmenu.nextMenuItem(); 
						nothingPressedLastFrame = false; 
					} 
				} 
				else if (_controls.pressing.up || _controls.pressing.left) 
				{ 
					if (nothingPressedLastFrame)  
					{ 
						_sfx.playGun(1); 
						_mainmenu.prevMenuItem(); 
						nothingPressedLastFrame = false; 
					} 
				} 
				else if (_controls.pressing.fire) 
				{ 
					if (_mainmenu.activateCurrentMenuItem(getTimer())) 
					{ // if the above returns true we should start the game 
						startGame(); 
					} 
				} 
				else 
				{ 
					// this ensures the menu doesn't change too fast 
					nothingPressedLastFrame = true; 
				} 
			} 
			// v5 - player firing moved to playerLogic function 
		}

Step 24: Upgrade the Game Start

Continuing with Main.as make a few adjustments to the startGame function. In particular, we need to account for the fact that the player (and orb companion) are only spawned on the first game and thereafter are simply hidden when not needed. Additionally, now that we have implemented a game state transition mechanism, we simply trigger one by setting the player's transitionTimeLeft property and let our transition handler, which we coded above, take care of everything.

 
		private function startGame():void 
		{ 
			trace("Starting game!"); 
			_state = 1; 
			_spriteStage.removeBatch(_mainmenu.batch); 
			_sfx.playMusic(); 
			 
			// add the player entity to the game! 
			if (!thePlayer)  
				thePlayer = _entities.addPlayer(playerLogic); 
			else // on subsequent games // v5 
				thePlayer.sprite.visible = true; 
			if (_entities.theOrb) 
				_entities.theOrb.sprite.visible = true; 
				 
			// load level one (and clear demo entities) 
			_entities.changeLevels('level' + _state); 
			_terrain.changeLevels('terrain' + _state); 
			 
			// reset the player position 
			thePlayer.level = 0; // it will transition to 1 
			thePlayer.score = 0; 
			thePlayer.lives = 3; 
			thePlayer.sprite.position.x = 64; 
			thePlayer.sprite.position.y = _entities.midpoint; 
 
			// add a "welcome message" 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
			 
			// make the player invulnerable at first 
			thePlayer.invulnerabilityTimeLeft = thePlayer.transitionSeconds + thePlayer.invulnerabilitySecsWhenHit; 
			 
		}

Step 25: Handle Player State Changes

Now that the player can die and the game can end, our render loop below is going to check to see whether any of these new actions need to be processed.

Firstly, when the game ends we use our new GameSaves class to record the current high score so that if the player visits the site that hosts our game at a later date it remembers their best score. Secondly, we also need to start checking the player's state in order to trigger this new game over if all lives have been lost. Game over can also occur when the player "beats" the game.

 
		// v5 triggered if the player loses all lives 
		private function gameOver():void 
		{ 
			trace("================ GAME OVER ================"); 
 
			// save game 
			if (saved.level < thePlayer.level) 
				saved.level = thePlayer.level; 
			if (saved.score < thePlayer.score) 
			{ 
				saved.score = thePlayer.score; 
				_gui.highScore = thePlayer.score; 
			} 
 
			_state = 0; 
 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
 
		} 
		 
		// v5 detect if we just died, etc. 
		private function checkPlayerState():void 
		{ 
			if (_state == 0) return; 
			if (thePlayer) 
			{ 
				if (thePlayer.lives < 0) 
				{ 
					gameOver(); 
				} 
			} 
		}

Step 26: Handle Map Changes

Just like the player state checking above, we also need to check to see if it is time to load the next map, or if the game has been cleared and no more maps remain. Continue adding to Main.as as follows:

		 
		// v5 check to see if we reached the end of the map/game 
		private function checkMapState():void 
		{ 
			// main menu or gameover credits? 
			if (_state < 1) return; 
			// already transitioning? 
			if (thePlayer.level != _state) return; 
			 
			// allow some extra spaces for the level to scroll past 
			// the player and then call the level complete. 
			if (_terrain.levelPrevCol > _terrain.level.levelLength + 16) 
			{ 
				trace("LEVEL " + _state  + " COMPLETED!"); 
				 
				_state++; 
				 
				thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
				 
				if (_entities.level.levelLength == 0) 
				{ 
					trace("NO MORE LEVELS REMAIN! GAME OVER!"); 
					rollTheCredits(); 
				} 
			} 
		} 
		 
		// display the "game cleared" screen 
		private function rollTheCredits():void 
		{ 
			gameOver(); 
			_state = -1; 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds * 3; 
		}

Step 27: Upgrade the Render Loop

The final set of upgrades we need to make is to the render loop, which is the onEnterFrame() function which is run every frame up to 60 times a second. We have removed the old debug GUI display from previous tutorials, since we now have a better FPS display as part of our fancy new heads-up-display GUI class. We also check the player and map state to determine when it is time to trigger a transition.

 
		// this function draws the scene every frame 
		private function onEnterFrame(e:Event):void  
		{ 
			try  
			{ 
				// grab timestamp of current frame 
				currentTime = getTimer(); 
				currentFrameMs = currentTime - previousFrameTime; 
				previousFrameTime = currentTime; 
				 
				// erase the previous frame 
				context3D.clear(0, 0, 0, 1); 
				 
				// for debugging the input manager, update the gui 
				// _gui.titleText = _controls.textDescription(); 
				 
				// process any player input 
				processInput(); 
 
				// scroll the background 
				if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height); 
				_bg.update(currentTime); 
				 
				// update the main menu titlescreen 
				if (_state == 0) 
					_mainmenu.update(currentTime); 
				 
				// move/animate all entities 
				_terrain.update(currentFrameMs); 
				_entities.update(currentFrameMs); 
				 
				// keep adding more sprites - IF we need to 
				_terrain.streamLevelEntities(false); 
				_entities.streamLevelEntities(true); 
				 
				// draw all entities 
				_spriteStage.render(); 
 
				// update the screen 
				context3D.present(); 
				 
				// check for gameover/death 
				checkPlayerState(); 
				 
				// check for the end of the level 
				checkMapState(); 
			} 
			catch (e:Error)  
			{ 
				// this can happen if the computer goes to sleep and 
				// then re-awakens, requiring reinitialization of stage3D 
				// (the onContext3DCreate will fire again) 
			} 
		} 
	} // end class 
} // end package

Step 28: Compile and Play!

We're done! Compile your project, fix any typos, and run the game. If you're having trouble with the code you typed in or just want the instant gratification of everything in one place, remember that you can download the full source code here.

Here are a few tips if you experience problems. If you do use FlashBuilder, be sure to include "-default-frame-rate 60" in your compiler options to ensure you get the best performance. If you are using Linux or a Mac, you can compile this from the command-line (or in a makefile) using something similar to "mxmlc -load-config+=obj\shmup_tutorial_part4Config.xml -swf-version=13", depending on your working environment. Remember that since we're using Flash 11 you will need to be compiling using the latest version of the Flex compiler and playerglobal.swc. Most importantly, remember that your Flash embed HTML has to include "wmode=direct" to enable Stage3D. This source has only been tested using FlashDevelop on Windows, and the tips above have been kindly submitted by your fellow readers.

Once everything compiles and runs properly you should see something that looks like this: a fast-action Stage3d shoot-em-up game complete with parallax scrolling terrain, tons of enemies to destroy, sounds, music and last but not least, a silky-smooth 60 frames per second framerate!



Part Five Complete: Prepare for Level Six!

That's it for tutorial number five in this series. We can now boast a detailed game world filled with things that can actually destroy the player, plus all sorts of fancy GUI elements like the high score and a health meter to give it a true arcade feel. We give the player a lot more in-game feedback now, whether in the form of "LEVEL COMPLETE" messages, sparks flying from our ship when we are about to die, or a period of innulnerability after we get hit so we have a chance to recover before being bombarded by the next wave of deadly enemies. Our game is now quite challenging.

We've taken what was initially a mere tech demo and brought it to the point that it really feels like a game, with a beginning, middle and end: a main menu, level transitions and the final credits. Congratulations, brave warrior! You've made it to the final boss.

In the next and final tutorial in this series, we will get to add that final layer of polish and call the game complete. Many coders have likely heard the old expression, "when you think you are 90% done, you are really only 50% done" or perhaps more commonly, "the devil's in the details." In addition to minor upgrades here and there, polish aplenty and subtle tweaks and optimizations, we are going to implement an EPIC BOSS BATTLE!

I'd love to hear from you regarding this tutorial. I warmly welcome all readers to get in touch with me via twitter: @McFunkypants, my blog mcfunkypants.com or on Google+ any time. In particular, I'd love to see the games you make using this code and I'm always looking for new topics to write future tutorials on. Get in touch with me any time.

If you have enjoyed these tutorials thus far, perhaps you'd like to learn more about Stage3D? If so, why not buy my Stage3D book?

Good luck and HAVE FUN!

Advertisement