Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

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

Scroll to top
Read Time: 51 min
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:

1
 
2
// Stage3D Shoot-em-up Tutorial Part 5 

3
// by Christer Kaitila - www.mcfunkypants.com 

4
 
5
// GameSaves.as 

6
// A simple highscore and level save game system. 

7
// For now, only high score is used (by the GUI), but you 

8
// could implement an "unlocked" levels menu to allow players 

9
// to skip levels they have completed when starting a new game. 

10
 
11
package 
12
{ 
13
	// For more information on "Flash Cookies" see: 

14
	// http://gamedev.michaeljameswilliams.com/2009/03/18/avoider-game-tutorial-11/ 

15
	// http://en.wikipedia.org/wiki/Local_shared_object 

16
 
17
	public class GameSaves 
18
	{ 
19
		import flash.net.SharedObject; 
20
 
21
		private var _saves:SharedObject; 
22
		 
23
		// class constructor 

24
		public function GameSaves() 
25
		{ 
26
			trace("Initializing game save system"); 
27
			try 
28
			{ 
29
				_saves = SharedObject.getLocal("SaveGame"); 
30
			} 
31
			catch ( sharedObjectError:Error ) 
32
			{ 
33
				trace( "Unable to init game save system: "  
34
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
35
			} 
36
		}

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.

1
		 
2
		public function get level():int 
3
		{ 
4
			try 
5
			{ 
6
				if (!_saves) return 0; 
7
				if (_saves.data.level == null) return 0; 
8
				trace("Loaded level is " + _saves.data.level); 
9
				return _saves.data.level; 
10
			} 
11
			catch ( sharedObjectError:Error ) 
12
			{ 
13
				trace( "Unable to load score: "  
14
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
15
			} 
16
			return 0; 
17
		} 
18
 
19
		public function get score():int 
20
		{ 
21
			try 
22
			{ 
23
				if (!_saves) return 0; 
24
				if (_saves.data.score == null) return 0; 
25
				trace("Loaded score is " + _saves.data.score); 
26
				return _saves.data.score; 
27
			} 
28
			catch ( sharedObjectError:Error ) 
29
			{ 
30
				trace( "Unable to load score: "  
31
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
32
			} 
33
			return 0; 
34
		} 
35
 
36
		public function set level(num:int):void 
37
		{ 
38
			try 
39
			{ 
40
				if (!_saves) return; 
41
				_saves.data.level = num; 
42
				_saves.flush(); 
43
				trace("Saved level set to: " + num); 
44
			} 
45
			catch ( sharedObjectError:Error ) 
46
			{ 
47
				trace( "Unable to save level: "  
48
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
49
			} 
50
		} 
51
 
52
		public function set score(num:int):void 
53
		{ 
54
			try 
55
			{ 
56
				if (!_saves) return; 
57
				_saves.data.score = num; 
58
				_saves.flush(); 
59
				trace("Saved score set to: " + num); 
60
			} 
61
			catch ( sharedObjectError:Error ) 
62
			{ 
63
				trace( "Unable to save score: "  
64
					+ sharedObjectError.name + " " + sharedObjectError.message ); 
65
			} 
66
		} 
67
 
68
	} // end class 

69
} // 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.

1
 
2
// Stage3D Shoot-em-up Tutorial Part 5 

3
// by Christer Kaitila - www.mcfunkypants.com 

4
 
5
// GameGUI.as 

6
// A typical simplistic framerate display for benchmarking performance, 

7
// plus a way to track rendering statistics from the entity manager. 

8
// In this version, we include all sorts of GUI displays such as score, 

9
// highscore, lives left as well as a "health bar". 

10
 
11
package 
12
{ 
13
	import flash.display.Sprite; 
14
	import flash.display.Bitmap; 
15
	import flash.events.Event; 
16
	import flash.events.TimerEvent; 
17
	import flash.text.TextField; 
18
	import flash.text.TextFormat; 
19
	import flash.text.Font; 
20
	import flash.utils.getTimer; 
21
	import flash.filters.GlowFilter; 
22
	import flash.system.System; 
23
	import flash.geom.Rectangle; 
24
	 
25
	public class GameGUI extends Sprite 
26
	{

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.

1
 
2
		// Font used by the GUI - only embed the chars we need to save space 

3
		[Embed (source = '../assets/gui_font.ttf',  
4
			embedAsCFF = 'false',  
5
			fontFamily = 'guiFont', 
6
			mimeType = 'application/x-font-truetype',  
7
			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')] 
8
		private const GUI_FONT:Class; 
9
		private var myFormat:TextFormat; 
10
		private var myFormatRIGHT:TextFormat; 
11
		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:


1
 
2
		// GUI bitmap overlay 

3
		[Embed (source = "../assets/hud_overlay.png")]  
4
		private var hudOverlayData:Class; 
5
		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.

1
 
2
		// text on the screen 

3
		public var scoreTf:TextField; 
4
		public var highScoreTf:TextField; 
5
		public var levelTf:TextField; 
6
		public var healthTf:TextField; 
7
		public var transitionTf:TextField; 
8
		public var transitionTf_y_location:int = 162; 
9
 
10
		// numeric values for the text above 

11
		// used to cache player stats to detect changes 

12
		// so that textfields will only be updated if needed 

13
		public var score:int = 0; 
14
		public var prevHighScore:int = 0; 
15
		public var highScore:int = 0; 
16
		public var level:int = 0; 
17
		public var lives:int = 3; 
18
		public var health:int = 100; 
19
		public var transitionText:String = ""; 
20
 
21
		// debug stats (only used during development) 

22
		public var debugStatsTf:TextField; 
23
		public var titleText : String = ""; 
24
		public var statsText : String = ""; 
25
		public var statsTarget : EntityManager; 
26
		public var frameCount:int = 0; 
27
		public var timer:int; 
28
		public var ms_prev:int; 
29
		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.

1
		 
2
		public function GameGUI(title:String = "", inX:Number=0, inY:Number=0, inCol:int = 0xFFFFFF) 
3
		{ 
4
			super(); 
5
			x = inX; 
6
			y = inY; 
7
			titleText = title; 
8
 
9
			// used for most GUI text 

10
			var myFont:Font = new GUI_FONT(); 
11
			myFormat = new TextFormat();   
12
			myFormat.color = inCol; 
13
			myFormat.size = 16; 
14
			myFormat.font = myFont.fontName; 
15
			 
16
			// used only by the score 

17
			myFormatRIGHT = new TextFormat();   
18
			myFormatRIGHT.color = inCol; 
19
			myFormatRIGHT.size = 16; 
20
			myFormatRIGHT.font = myFont.fontName; 
21
			myFormatRIGHT.align = 'right'; 
22
 
23
			// used by the transition texts 

24
			myFormatCENTER = new TextFormat();   
25
			myFormatCENTER.color = inCol; 
26
			myFormatCENTER.size = 32; 
27
			myFormatCENTER.font = myFont.fontName; 
28
			myFormatCENTER.align = 'center'; 
29
 
30
			this.addEventListener(Event.ADDED_TO_STAGE, onAddedHandler); 
31
		} 
32
		 
33
		public function onAddedHandler(e:Event):void  
34
		{ 
35
			trace("GameGUI was added to the stage"); 
36
			 
37
			addChild(hudOverlay); 
38
			 
39
			// used for FPS display 

40
			debugStatsTf = new TextField(); 
41
			debugStatsTf.defaultTextFormat = myFormat; 
42
			debugStatsTf.embedFonts = true; 
43
			debugStatsTf.x = 18; 
44
			debugStatsTf.y = 0; 
45
			debugStatsTf.width = 320; 
46
			debugStatsTf.selectable = false; 
47
			debugStatsTf.text = titleText; 
48
			debugStatsTf.antiAliasType = 'advanced'; 
49
			addChild(debugStatsTf); 
50
 
51
			// create a score display 

52
			scoreTf = new TextField(); 
53
			scoreTf.defaultTextFormat = myFormatRIGHT; 
54
			scoreTf.embedFonts = true; 
55
			scoreTf.x = 442; 
56
			scoreTf.y = 0; 
57
			scoreTf.selectable = false; 
58
			scoreTf.antiAliasType = 'advanced'; 
59
			scoreTf.text = "SCORE: 000000\n3 LIVES LEFT"; 
60
			scoreTf.width = 140; 
61
			addChild(scoreTf); 
62
 
63
			// high score display 

64
			highScoreTf = new TextField(); 
65
			highScoreTf.defaultTextFormat = myFormat; 
66
			highScoreTf.embedFonts = true; 
67
			highScoreTf.x = 232; 
68
			highScoreTf.y = 0; 
69
			highScoreTf.selectable = false; 
70
			highScoreTf.antiAliasType = 'advanced'; 
71
			highScoreTf.text = "HIGH SCORE: 000000"; 
72
			highScoreTf.width = 320; 
73
			addChild(highScoreTf); 
74
 
75
			// add a health meter 

76
			healthTf = new TextField(); 
77
			healthTf.defaultTextFormat = myFormat; 
78
			healthTf.embedFonts = true; 
79
			healthTf.x = 232; 
80
			healthTf.y = 15; 
81
			healthTf.selectable = false; 
82
			healthTf.antiAliasType = 'advanced'; 
83
			healthTf.text = "HP: |||||||||||||"; 
84
			healthTf.width = 320; 
85
			addChild(healthTf); 
86
			 
87
			// add a "transition text" display 

88
			transitionTf = new TextField(); 
89
			transitionTf.defaultTextFormat = myFormatCENTER; 
90
			transitionTf.embedFonts = true; 
91
			transitionTf.x = 0; 
92
			transitionTf.y = transitionTf_y_location; 
93
			transitionTf.selectable = false; 
94
			transitionTf.filters = [new GlowFilter(0xFF0000, 1, 8, 8, 4, 2)]; 
95
			transitionTf.antiAliasType = 'advanced'; 
96
			transitionTf.text = ""; 
97
			transitionTf.width = 600; 
98
			transitionTf.height = 2000; 
99
			transitionTf.scrollRect = new Rectangle(0, 0, 600, 160); 
100
			// keep off screen until needed 

101
			// addChild(transitionTf); 

102
	 
103
			stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); 
104
		}

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.

1
		 
2
		private function pad0s(num:int):String 
3
		{ 
4
			if (num < 10) return '00000' + num; 
5
			else if (num < 100) return '0000' + num; 
6
			else if (num < 1000) return '000' + num; 
7
			else if (num < 10000) return '00' + num; 
8
			else if (num < 100000) return '0' + num; 
9
			else return '' + num; 
10
		} 
11
		 
12
		// only updates textfields if they have changed 

13
		private function updateScore():void 
14
		{ 
15
			if (transitionText != transitionTf.text) 
16
			{ 
17
				transitionTf.text = transitionText; 
18
				if (transitionTf.text != "") 
19
				{ 
20
					if (!contains(transitionTf)) 
21
						addChild(transitionTf); 
22
				} 
23
				else 
24
				{ 
25
					if (contains(transitionTf)) 
26
						removeChild(transitionTf); 
27
				} 
28
			} 
29
			 
30
			if (statsTarget && statsTarget.thePlayer) 
31
			{ 
32
				if (health != statsTarget.thePlayer.health) 
33
				{ 
34
					health = statsTarget.thePlayer.health; 
35
					// generate the health bar (13 chars simply happens to fit the gui nicely) 

36
					if (statsTarget.thePlayer.health >= 99) healthTf.text = "HP: |||||||||||||"; 
37
					else if (statsTarget.thePlayer.health >= 92) healthTf.text = "HP: ||||||||||||"; 
38
					else if (statsTarget.thePlayer.health >= 84) healthTf.text = "HP: |||||||||||"; 
39
					else if (statsTarget.thePlayer.health >= 76) healthTf.text = "HP: ||||||||||"; 
40
					else if (statsTarget.thePlayer.health >= 68) healthTf.text = "HP: |||||||||"; 
41
					else if (statsTarget.thePlayer.health >= 60) healthTf.text = "HP: ||||||||"; 
42
					else if (statsTarget.thePlayer.health >= 52) healthTf.text = "HP: |||||||"; 
43
					else if (statsTarget.thePlayer.health >= 44) healthTf.text = "HP: ||||||"; 
44
					else if (statsTarget.thePlayer.health >= 36) healthTf.text = "HP: |||||"; 
45
					else if (statsTarget.thePlayer.health >= 28) healthTf.text = "HP: ||||"; 
46
					else if (statsTarget.thePlayer.health >= 20) healthTf.text = "HP: |||"; 
47
					else if (statsTarget.thePlayer.health >= 12) healthTf.text = "HP: ||"; 
48
					else healthTf.text = "HP: |"; 
49
				} 
50
				if ((score != statsTarget.thePlayer.score) || (lives != statsTarget.thePlayer.lives)) 
51
				{ 
52
					score = statsTarget.thePlayer.score; 
53
					lives = statsTarget.thePlayer.lives; 
54
					if (lives == -1) 
55
						scoreTf.text = scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' +'GAME OVER'; 
56
					else 
57
						scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' + lives +  
58
							(lives != 1 ? ' LIVES' : ' LIFE') + ' LEFT'; 
59
					// we may be beating the high score right now 

60
					if (score > highScore) highScore = score; 
61
				} 
62
			} 
63
			if (prevHighScore != highScore) 
64
			{ 
65
				prevHighScore = highScore; 
66
				highScoreTf.text = "HIGH SCORE: " + pad0s(highScore); 
67
			} 
68
		}

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.

1
		 
2
		private function onEnterFrame(evt:Event):void 
3
		{ 
4
			timer = getTimer(); 
5
			 
6
			updateScore(); 
7
			 
8
			if( timer - 1000 > ms_prev ) 
9
			{ 
10
				lastfps = Math.round(frameCount/(timer-ms_prev)*1000); 
11
				ms_prev = timer; 
12
				 
13
 
14
				var mem:Number = Number((System.totalMemory * 0.000000954).toFixed(1)); 
15
 
16
				// grab the stats from the entity manager 

17
				if (statsTarget) 
18
				{ 
19
					statsText =  
20
						statsTarget.numCreated + '/' + 
21
						statsTarget.numReused + ' sprites'; 
22
				} 
23
				 
24
				debugStatsTf.text = titleText + lastfps + 'FPS - ' + mem + 'MB' + '\n' + statsText; 
25
				frameCount = 0; 
26
			} 
27
		 
28
			// count each frame to determine the framerate 

29
			frameCount++; 
30
				 
31
		} 
32
	} // end class 

33
} // 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.

1
 
2
// Stage3D Shoot-em-up Tutorial Part 5 

3
// by Christer Kaitila - www.mcfunkypants.com 

4
 
5
// Entity.as 

6
// The Entity class will eventually hold all game-specific entity stats 

7
// for the spaceships, bullets and effects in our game. 

8
// It stores a reference to a gpu sprite and a few demo properties. 

9
// This is where you would add hit points, weapons, ability scores, etc. 

10
// This class handles any AI (artificial intelligence) for enemies as well. 

11
 
12
package 
13
{ 
14
	import flash.geom.Point; 
15
	import flash.geom.Rectangle; 
16
	 
17
	public class Entity 
18
	{ 
19
		// v5 stats used by the player entity 

20
		// but could optionally be used by some enemies too 

21
		public var health:int = 100; 
22
		// how many reserve ships you have until game over 

23
		public var lives:int = 3; 
24
		// matches the score on the GUI 

25
		public var score:int = 0; 
26
		// what level the player is on 

27
		public var level:int = 0; 
28
		// when you get hit, for a short while you are impervious 

29
		// to damage (otherwise nearby ships could add up to instant death) 

30
		public var invulnerabilityTimeLeft:Number = 0; 
31
		public var invulnerabilitySecsWhenHit:Number = 4; 
32
		// when you die, change levels or get to a game over state, 

33
		// this is the period of time that the GUI tells you 

34
		// before switching to the new state 

35
		public var transitionTimeLeft:Number = 0; 
36
		public var transitionSeconds:Number = 5; 
37
		// how much damage the player will take when getting hit 

38
		// since the player has 100HP, 49 means you can be hit 3x 

39
		public var damage:int = 49; 
40
		 
41
		// ... 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.

1
 
2
// Stage3D Shoot-em-up Tutorial Part 5 

3
// by Christer Kaitila - www.mcfunkypants.com 

4
 
5
// GameLevels.as 

6
// This class parses .CSV level data strings 

7
// that define the locations of tiles from a spritesheet 

8
// Example levels were created using the OGMO editor, 

9
// but could be designed by hand or any number of other 

10
// freeware game level editors that can output .csv 

11
// This can be a .txt, .csv, .oel, .etc file 

12
// - we will strip all xml/html tags (if any)  

13
// - we only care about raw csv data 

14
// Our game can access the current level with: 

15
// spriteId = myLevel.data[x][y]; 

16
 
17
package 
18
{ 
19
	import flash.display3D.Context3DProgramType; 
20
	public class GameLevels 
21
	{ 
22
		// v5 the farthest column in the level 

23
		// used to detect when the map is complete 

24
		public var levelLength:int = 0; 
25
 
26
		// the "demo" level seen during the title screen 

27
		[Embed(source = '../assets/level0.oel', mimeType = 'application/octet-stream')]  
28
		private static const LEVEL0:Class; 
29
		private var level0data:String = new LEVEL0; 
30
		[Embed(source = '../assets/terrain0.oel', mimeType = 'application/octet-stream')]  
31
		private static const LEVEL0TERRAIN:Class; 
32
		private var level0terrain:String = new LEVEL0TERRAIN; 
33
 
34
		// level 1 

35
		[Embed(source = '../assets/level1.oel', mimeType = 'application/octet-stream')]  
36
		private static const LEVEL1:Class; 
37
		private var level1data:String = new LEVEL1; 
38
		[Embed(source = '../assets/terrain1.oel', mimeType = 'application/octet-stream')]  
39
		private static const LEVEL1TERRAIN:Class; 
40
		private var level1terrain:String = new LEVEL1TERRAIN; 
41
 
42
		// level 2 

43
		[Embed(source = '../assets/level2.oel', mimeType = 'application/octet-stream')]  
44
		private static const LEVEL2:Class; 
45
		private var level2data:String = new LEVEL2; 
46
		[Embed(source = '../assets/terrain2.oel', mimeType = 'application/octet-stream')]  
47
		private static const LEVEL2TERRAIN:Class; 
48
		private var level2terrain:String = new LEVEL2TERRAIN; 
49
 
50
		// level 3 

51
		[Embed(source = '../assets/level3.oel', mimeType = 'application/octet-stream')]  
52
		private static const LEVEL3:Class; 
53
		private var level3data:String = new LEVEL3; 
54
		[Embed(source = '../assets/terrain3.oel', mimeType = 'application/octet-stream')]  
55
		private static const LEVEL3TERRAIN:Class; 
56
		private var level3terrain:String = new LEVEL3TERRAIN; 
57
 
58
		// the currently loaded level data 

59
		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.

1
 
2
		public function GameLevels() 
3
		{ 
4
		} 
5
		 
6
		private function stripTags(str:String):String 
7
		{ 
8
			var pattern:RegExp = /<\/?[a-zA-Z0-9]+.*?>/gim; 
9
			return str.replace(pattern, ""); 
10
		} 
11
	 
12
		private function parseLevelData(lvl:String):Array 
13
		{ 
14
			var levelString:String; 
15
			var temps:Array; 
16
			var nextValue:int; 
17
			var output:Array = []; 
18
			var nextrow:int; 
19
 
20
			// how many columns wide is the map? 

21
			// note: some rows may be shorter 

22
			levelLength = 0; 
23
			 
24
			switch (lvl) 
25
			{ 
26
				case "level0" : levelString = stripTags(level0data); break; 
27
				case "terrain0" : levelString = stripTags(level0terrain); break; 
28
				case "level1" : levelString = stripTags(level1data); break; 
29
				case "terrain1" : levelString = stripTags(level1terrain); break; 
30
				case "level2" : levelString = stripTags(level2data); break; 
31
				case "terrain2" : levelString = stripTags(level2terrain); break; 
32
				case "level3" : levelString = stripTags(level3data); break; 
33
				case "terrain3" : levelString = stripTags(level3terrain); break; 
34
				default: 
35
					return output;  
36
			} 
37
			 
38
			//trace("Level " + num + " data:\n" + levelString); 

39
			var lines:Array = levelString.split(/\r\n|\n|\r/); 
40
			for (var row:int = 0; row < lines.length; row++) 
41
			{ 
42
				// split the string by comma 

43
				temps = lines[row].split(","); 
44
				if (temps.length > 1) 
45
				{ 
46
					nextrow = output.push([]) - 1; 
47
					// turn the string values into integers 

48
					for (var col:int = 0; col < temps.length; col++) 
49
					{ 
50
						if (temps[col] == "") temps[col] = "-1"; 
51
						nextValue = parseInt(temps[col]); 
52
						if (nextValue < 0) nextValue = -1; // we still need blanks 

53
						// trace('row '+ nextrow + ' nextValue=' + nextValue); 

54
						 
55
						// v5 remember longest column so we know when we've reached the end 

56
						if (col > levelLength) levelLength = col; 
57
						 
58
						output[nextrow].push(nextValue); 
59
						 
60
					} 
61
					//trace('Level row '+nextrow+':\n' + String(output[nextrow])); 

62
				} 
63
			} 
64
			//trace('Level output data:\n' + String(output)); 

65
			return output; 
66
		} 
67
		 
68
		public function loadLevel(lvl:String):void 
69
		{ 
70
			trace("Loading level " + lvl); 
71
			data = parseLevelData(lvl); 
72
		} 
73
		 
74
	} // end class 

75
} // 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.

1
 
2
		public function setPosition(view:Rectangle):void  
3
		{ 
4
			logoX = view.width / 2; 
5
			logoY = view.height / 2 - 56; // v5 

6
			menuX = view.width / 2; 
7
			menuY = view.height / 2 + 64; 
8
			menuY1 = menuY - (menuItemHeight / 2); 
9
			menuY2 = menuY - (menuItemHeight / 2) + menuItemHeight; 
10
			menuY3 = menuY - (menuItemHeight / 2) + (menuItemHeight * 2); 
11
		} 
12
		 
13
		// called every frame: used to update the animation 

14
		public function update(currentTime:Number) : void 
15
		{		 
16
			logoSprite.position.x = logoX; 
17
			logoSprite.position.y = logoY; 
18
			var wobble:Number = (Math.cos(currentTime / 500) / Math.PI) * 0.2; 
19
			logoSprite.scaleX = 0.8 + wobble; // v5 

20
			logoSprite.scaleY = 0.8 + wobble; // v5 

21
			wobble = (Math.cos(currentTime / 777) / Math.PI) * 0.1; 
22
			logoSprite.rotation = wobble; 
23
			 
24
			// pulse the active menu item 

25
			wobble = (Math.cos(currentTime / 150) / Math.PI) * 0.1; 
26
			amenuAboutSprite.scaleX =  
27
			amenuAboutSprite.scaleY =  
28
			amenuControlsSprite.scaleX =  
29
			amenuControlsSprite.scaleY =  
30
			amenuPlaySprite.scaleX =  
31
			amenuPlaySprite.scaleY =  
32
				1.1 + wobble; 
33
			 
34
			// show the about/controls for a while 

35
			if (showingAbout) 
36
			{ 
37
				if (showingAboutUntil > currentTime) 
38
				{ 
39
					aboutSprite.alpha = 1; 
40
				} 
41
				else 
42
				{ 
43
					aboutSprite.alpha = 0; 
44
					showingAbout = false; 
45
					updateState(); 
46
				} 
47
			} 
48
 
49
			if (showingControls) 
50
			{ 
51
				if (showingControlsUntil > currentTime) 
52
				{ 
53
					controlsSprite.alpha = 1; 
54
				} 
55
				else 
56
				{ 
57
					controlsSprite.alpha = 0; 
58
					showingControls = false; 
59
					updateState(); 
60
				} 
61
			}		 
62
		}

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.

1
 
2
// as an optimization to save millions of checks, only 

3
// the player's bullets check for collisions with all enemy ships 

4
// (enemy bullets only check to hit the player) 

5
public function checkCollisions(checkMe:Entity):Entity 
6
{ 
7
	var anEntity:Entity; 
8
	var collided:Boolean = false; 
9
	if (!thePlayer) return null; // v5 

10
	 
11
	if (checkMe.owner != thePlayer) 
12
	{	// quick check ONLY to see if we have hit the player 

13
		anEntity = thePlayer; 
14
		if (checkMe.colliding(anEntity))  
15
		{ 
16
			collided = true; 
17
		} 
18
	} 
19
	else // check all active enemies 

20
	{ 
21
		for(var i:int=0; i< allEnemies.length;i++) 
22
		{ 
23
			anEntity = allEnemies[i]; 
24
			if (anEntity.active && anEntity.collidemode) 
25
			{ 
26
				if (checkMe.colliding(anEntity))  
27
				{ 
28
					collided = true; 
29
					// v5 accumulate score only when playing 

30
					if (thePlayer.sprite.visible) 
31
						thePlayer.score += anEntity.collidepoints; 
32
					break; 
33
				} 
34
			} 
35
		} 
36
	}

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.

1
 
2
	if (collided) 
3
	{ 
4
		// v5 - handle player health and possible gameover 

5
		if ((anEntity == thePlayer) || (checkMe == thePlayer)) 
6
		{ 
7
			// when the player gets damaged, they become 

8
			// invulnerable for a short perod of time 

9
			if (thePlayer.invulnerabilityTimeLeft <= 0) 
10
			{ 
11
				thePlayer.health -= anEntity.damage; 
12
				thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit; 
13
				// extra explosions for a bigger boom 

14
				var explosionPos:Point = new Point(); 
15
				for (var numExplosions:int = 0; numExplosions < 6; numExplosions++) 
16
				{ 
17
					explosionPos.x = thePlayer.sprite.position.x + fastRandom() * 64 - 32;  
18
					explosionPos.y = thePlayer.sprite.position.y + fastRandom() * 64 - 32;  
19
					particles.addExplosion(explosionPos); 
20
				} 
21
				if (thePlayer.health > 0) 
22
				{ 
23
					trace("Player was HIT!"); 
24
				} 
25
				else 
26
				{ 
27
					trace('Player was HIT... and DIED!'); 
28
					thePlayer.lives--; 
29
					// will be reset after transition 

30
					// thePlayer.health = 100; 

31
					thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit 
32
						+ thePlayer.transitionSeconds; 
33
					thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
34
				} 
35
			} 
36
			else // we are currently invulnerable and flickering 

37
			{	// ignore the collision 

38
				collided = false; 
39
			} 
40
		} 
41
		 
42
		if (collided) // still 

43
		{ 
44
			//trace('Collision!'); 

45
			if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); 
46
			particles.addExplosion(checkMe.sprite.position); 
47
			if ((checkMe != theOrb) && (checkMe != thePlayer))  
48
				checkMe.die(); // the bullet 

49
			if ((anEntity != theOrb) && ((anEntity != thePlayer)))  
50
				anEntity.die(); // the victim 

51
			return anEntity; 
52
		} 
53
	} 
54
	return null; 
55
}

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.

1
 
2
// called every frame: used to update the simulation 

3
// this is where you would perform AI, physics, etc. 

4
// in this version, currentTime is seconds since the previous frame 

5
public function update(currentTime:Number) : void 
6
{		 
7
	var anEntity:Entity; 
8
	var i:int; 
9
	var max:int; 
10
	 
11
	// what portion of a full second has passed since the previous update? 

12
	currentFrameSeconds = currentTime / 1000; 
13
	 
14
	// handle all other entities 

15
	max = entityPool.length; 
16
	for (i = 0; i < max; i++) 
17
	{ 
18
		anEntity = entityPool[i]; 
19
		if (anEntity.active) 
20
		{ 
21
			// subtract the previous aiPathOffset 

22
			anEntity.sprite.position.x -= anEntity.aiPathOffsetX; 
23
			anEntity.sprite.position.y -= anEntity.aiPathOffsetY; 
24
 
25
			// calculate location on screen with scrolling 

26
			anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds; 
27
			anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds; 
28
								 
29
			// is a custom AI specified? if so, run it now 

30
			if (anEntity.aiFunction != null) 
31
			{ 
32
				anEntity.aiFunction(currentFrameSeconds); 
33
			} 
34
 
35
			// add the new aiPathOffset 

36
			anEntity.sprite.position.x += anEntity.aiPathOffsetX; 
37
			anEntity.sprite.position.y += anEntity.aiPathOffsetY; 
38
			 
39
			// collision detection 

40
			if (anEntity.collidemode) // && anEntity.isBullet 

41
			{ 
42
				checkCollisions(anEntity); 
43
			} 
44
			 
45
			// entities can orbit other entities  

46
			// (uses their rotation as the position) 

47
			if (anEntity.orbiting != null) 
48
			{ 
49
				anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x +  
50
					((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); 
51
				anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y -  
52
					((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); 
53
			} 
54
 
55
			// entities can leave an engine emitter trail 

56
			if (anEntity.leavesTrail) 
57
			{ 
58
				// leave a trail of particles 

59
				if (anEntity == theOrb) 
60
				{ 
61
					if (theOrb.sprite.visible) // v5 don't show during gameover 

62
						particles.addParticle(63,  
63
						anEntity.sprite.position.x, anEntity.sprite.position.y,  
64
						0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1); 
65
				} 
66
				else // other enemies 

67
				{ 
68
					particles.addParticle(63, anEntity.sprite.position.x + 12,  
69
					anEntity.sprite.position.y + 2,  
70
					0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1); 
71
				} 
72
				 
73
			} 
74
			 
75
			if ((anEntity.sprite.position.x > maxX) || 
76
				(anEntity.sprite.position.x < minX) || 
77
				(anEntity.sprite.position.y > maxY) || 
78
				(anEntity.sprite.position.y < minY))							 
79
			{ 
80
				// if we go past any edge, become inactive 

81
				// so the sprite can be respawned 

82
				if ((anEntity != thePlayer) && (anEntity != theOrb))  
83
					anEntity.die(); 
84
			} 
85
			 
86
			if (anEntity.rotationSpeed != 0) 
87
				anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds; 
88
				 
89
			if (anEntity.fadeAnim != 0) 
90
			{ 
91
				anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds; 
92
				if (anEntity.sprite.alpha <= 0.001) 
93
				{ 
94
					anEntity.die(); 
95
				} 
96
				else if (anEntity.sprite.alpha > 1) 
97
				{ 
98
					anEntity.sprite.alpha = 1; 
99
				} 
100
			} 
101
			if (anEntity.zoomAnim != 0) 
102
			{ 
103
				anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds; 
104
				anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds; 
105
				if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0) 
106
					anEntity.die(); 
107
			} 
108
		} 
109
	} 
110
} 
111
 
112
// load a new level for entity generation 

113
public function changeLevels(lvl:String):void 
114
{ 
115
		killEmAll(); 
116
		level.loadLevel(lvl); 
117
		levelCurrentScrollX = 0; 
118
		levelPrevCol = -1; 
119
		lastTerrainEntity = null; // v5 

120
}

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.

1
 
2
// Stage3D Shoot-em-up Tutorial Part 5 

3
// by Christer Kaitila - www.mcfunkypants.com 

4
// Created for active.tutsplus.com 

5
 
6
package  
7
{ 
8
	[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] 
9
 
10
	import flash.display3D.*; 
11
	import flash.display.Sprite; 
12
	import flash.display.StageAlign; 
13
	import flash.display.StageQuality; 
14
	import flash.display.StageScaleMode; 
15
	import flash.events.Event; 
16
	import flash.events.ErrorEvent; 
17
	import flash.events.MouseEvent; 
18
	import flash.geom.Rectangle; 
19
	import flash.utils.getTimer; 
20
	import flash.geom.Point; // v5 

21
		 
22
	public class Main extends Sprite  
23
	{ 
24
		// the game save/load system 

25
		public var saved:GameSaves; // v5 

26
		 
27
		// the entity spritesheet (ships, particles) 

28
		[Embed(source="../assets/sprites.png")] 
29
		private var EntitySourceImage : Class; 
30
 
31
		// the terrain spritesheet 

32
		[Embed(source="../assets/terrain.png")] 
33
		private var TerrainSourceImage : Class; 
34
		 
35
		// the keyboard control system 

36
		private var _controls : GameControls; 
37
		// don't update the menu too fast 

38
		private var nothingPressedLastFrame:Boolean = false; 
39
		// timestamp of the current frame 

40
		public var currentTime:int; 
41
		// for framerate independent speeds 

42
		public var currentFrameMs:int; 
43
		public var previousFrameTime:int; 
44
		 
45
		// player one's entity 

46
		public var thePlayer:Entity; 
47
		// movement speed in pixels per second 

48
		public var playerSpeed:Number = 128; 
49
		// timestamp when next shot can be fired 

50
		private var nextFireTime:uint = 0; 
51
		// how many ms between shots 

52
		private var fireDelay:uint = 200; 
53
		 
54
		// main menu = 0 or current level number 

55
		private var _state:int = 0; 
56
		// the title screen batch 

57
		private var _mainmenu:GameMenu; 
58
		// the sound system 

59
		private var _sfx:GameSound;	 
60
		// the background stars 

61
		private var _bg:GameBackground;	 
62
		 
63
		private var _terrain:EntityManager; 
64
		private var _entities:EntityManager; 
65
		private var _spriteStage:LiteSpriteStage; 
66
		private var _gui:GameGUI; 
67
		private var _width:Number = 600; 
68
		private var _height:Number = 400; 
69
		public var context3D:Context3D; 
70
		 
71
		// constructor function for our game 

72
		public function Main():void  
73
		{ 
74
			if (stage) init(); 
75
			else addEventListener(Event.ADDED_TO_STAGE, init); 
76
		} 
77
		 
78
		// called once flash is ready 

79
		private function init(e:Event = null):void  
80
		{ 
81
			_controls = new GameControls(stage); 
82
			removeEventListener(Event.ADDED_TO_STAGE, init); 
83
			stage.quality = StageQuality.LOW; 
84
			stage.align = StageAlign.TOP_LEFT; 
85
			stage.scaleMode = StageScaleMode.NO_SCALE; 
86
			stage.addEventListener(Event.RESIZE, onResizeEvent); 
87
			trace("Init Stage3D..."); 
88
			_gui = new GameGUI(""); 
89
			addChild(_gui); 
90
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); 
91
			stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); 
92
			stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); 
93
			trace("Stage3D requested...");		 
94
			_sfx = new GameSound(); 
95
		} 
96
				 
97
		// this is called when the 3d card has been set up 

98
		// and is ready for rendering using stage3d 

99
		private function onContext3DCreate(e:Event):void  
100
		{ 
101
			trace("Stage3D context created! Init sprite engine..."); 
102
			context3D = stage.stage3Ds[0].context3D; 
103
			initSpriteEngine(); 
104
		} 
105
		 
106
		// this can be called when using an old version of flash 

107
		// or if the html does not include wmode=direct 

108
		private function errorHandler(e:ErrorEvent):void  
109
		{ 
110
			trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); 
111
		} 
112
 
113
		protected function onResizeEvent(event:Event) : void 
114
		{ 
115
			trace("resize event..."); 
116
			 
117
			// Set correct dimensions if we resize 

118
			_width = stage.stageWidth; 
119
			_height = stage.stageHeight; 
120
			 
121
			// Resize Stage3D to continue to fit screen 

122
			var view:Rectangle = new Rectangle(0, 0, _width, _height); 
123
			if ( _spriteStage != null ) { 
124
				_spriteStage.position = view; 
125
			} 
126
			if(_terrain != null) { 
127
				_terrain.setPosition(view); 
128
			} 
129
			if(_entities != null) { 
130
				_entities.setPosition(view); 
131
			} 
132
			if(_mainmenu != null) { 
133
				_mainmenu.setPosition(view); 
134
			} 
135
		} 
136
		 
137
		private function initSpriteEngine():void  
138
		{ 
139
			// init a gpu sprite system 

140
			//var view:Rectangle = new Rectangle(0,0,_width,_height) 

141
			var stageRect:Rectangle = new Rectangle(0, 0, _width, _height);  
142
			_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); 
143
			_spriteStage.configureBackBuffer(_width,_height); 
144
			 
145
			// create the background stars 

146
			trace("Init background..."); 
147
			_bg = new GameBackground(stageRect); 
148
			_bg.createBatch(context3D); 
149
			_spriteStage.addBatch(_bg.batch); 
150
			_bg.initBackground(); 
151
			 
152
			// create the terrain spritesheet and batch 

153
			trace("Init Terrain..."); 
154
			_terrain = new EntityManager(stageRect); 
155
			_terrain.SourceImage = TerrainSourceImage; 
156
			_terrain.SpritesPerRow = 16; 
157
			_terrain.SpritesPerCol = 16; 
158
			_terrain.defaultSpeed = 90; 
159
			_terrain.defaultScale = 1.5; 
160
			_terrain.levelTilesize = 48;  
161
			_terrain.createBatch(context3D, 0.001); // a little UV padding required 

162
			_spriteStage.addBatch(_terrain.batch); 
163
			_terrain.changeLevels('terrain' + _state); 
164
 
165
			// create a single rendering batch 

166
			// which will draw all sprites in one pass 

167
			trace("Init Entities..."); 
168
			_entities = new EntityManager(stageRect); 
169
			_entities.SourceImage = EntitySourceImage; 
170
			_entities.defaultScale = 1.5; // 1 

171
			_entities.levelTilesize = 48;  
172
			_entities.createBatch(context3D); 
173
			_entities.sfx = _sfx; 
174
			_spriteStage.addBatch(_entities.batch); 
175
			_entities.changeLevels('level' + _state); 
176
			_entities.streamLevelEntities(true); // spawn first row of the level immediately 

177
			 
178
			// create the logo/titlescreen main menu 

179
			_mainmenu = new GameMenu(stageRect); 
180
			_mainmenu.createBatch(context3D); 
181
			_spriteStage.addBatch(_mainmenu.batch); 
182
			 
183
			// tell the gui where to grab statistics from 

184
			_gui.statsTarget = _entities;  
185
			 
186
			// start the render loop 

187
			stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); 
188
 
189
			// only used for the menu 

190
			stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);    
191
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);  
192
 
193
			// set up the savegame system 

194
			saved = new GameSaves(); 
195
			_gui.highScore = saved.score; 
196
			_gui.level = saved.level; 
197
		}

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:

1
		 
2
		// check player transition state (deaths, game over, etc) 

3
		private var currentTransitionSeconds:Number = 0; 
4
		private function handleTransitions(seconds:Number):void 
5
		{ 
6
			// are we at a pending transition (death or level change)? 

7
			if (thePlayer.transitionTimeLeft > 0) 
8
			{ 
9
				currentTransitionSeconds += seconds; 
10
				 
11
				thePlayer.transitionTimeLeft -= seconds; 
12
				 
13
				if (thePlayer.transitionTimeLeft > 0) 
14
				{	//was it a level change? 

15
					if (thePlayer.level != _state) 
16
					{ 
17
						if (_state == -1) 
18
						{ 
19
							_gui.transitionText = "\n\n\n\n\n\nCONGRATULATIONS\n\n" + 
20
								"You fought bravely and defended\n" + 
21
								"the universe from certain doom.\n\nYou got to level " +  
22
								thePlayer.level + "\nwith " + thePlayer.score + " points." +  
23
								"\n\nCREDITS:\n\nProgramming: McFunkypants\n(mcfunkypants.com)\n\n" + 
24
								"Art: Daniel Cook\n(lostgarden.com)\n\n" + 
25
								"Music: MaF\n(maf464.com)\n\n" + 
26
								"Thanks for playing!"; 
27
							_gui.transitionTf.scrollRect = new Rectangle(0,currentTransitionSeconds * 40,600,160); 
28
						} 
29
						else if (_state == 0) 
30
							_gui.transitionText = "GAME OVER\nYou got to level " + thePlayer.level  
31
								+ "\nwith " + thePlayer.score + " points."; 
32
						else if (_state > 1) 
33
							_gui.transitionText = "\nLEVEL " + (_state-1) + " COMPLETE!"; 
34
						else 
35
							_gui.transitionText = "\nLEVEL " + _state;  
36
					} 
37
					else // must be a death or start of a map 

38
					{ 
39
						_gui.transitionText = "Your ship was destroyed.\n\nYou have "  
40
							+ thePlayer.lives + (thePlayer.lives != 1 ? " lives" : " life") + " left."; 
41
					} 
42
					if (thePlayer.lives < 0 || thePlayer.health <= 0) 
43
					{ 
44
						// during the death transition, spawn tons of explosions just for fun 

45
						if (_entities.fastRandom() < 0.2) 
46
						{ 
47
							var explosionPos:Point = new Point(); 
48
							explosionPos.x = thePlayer.sprite.position.x + _entities.fastRandom() * 128 - 64;  
49
							explosionPos.y = thePlayer.sprite.position.y + _entities.fastRandom() * 128 - 64;  
50
							_entities.particles.addExplosion(explosionPos); 
51
						} 
52
					} 
53
				} 
54
				else // transition time has elapsed 

55
				{ 
56
					currentTransitionSeconds = 0; 
57
					 
58
					thePlayer.transitionTimeLeft = 0; 
59
					 
60
					if (_state == -1) _state = 0; 
61
					_gui.transitionTf.scrollRect = new Rectangle(0,0,600,160); 
62
					_gui.transitionText = ""; 
63
					 
64
					if ((thePlayer.health <= 0) && (_state != 0)) // we died 

65
					{ 
66
						trace("Death transition over. Respawning player."); 
67
						thePlayer.sprite.position.y = _entities.midpoint; 
68
						thePlayer.sprite.position.x = 64; 
69
						thePlayer.health = 100; 
70
						// start the level again 

71
						_entities.changeLevels('level' + _state); 
72
						_terrain.changeLevels('terrain' + _state); 
73
					} 
74
					if (thePlayer.level != _state) 
75
					{ 
76
						trace('Level transition over. Starting level ' + _state); 
77
						thePlayer.level = _state; 
78
						if (_state > 1) // no need to reload at startGame 

79
						{ 
80
							_entities.changeLevels('level' + _state); 
81
							_terrain.changeLevels('terrain' + _state); 
82
						} 
83
						if (_state == 0) // game over 

84
						{ 
85
							thePlayer.health = 100; 
86
							thePlayer.lives = 3; 
87
							thePlayer.sprite.visible = false; 
88
							_entities.theOrb.sprite.visible = false; 
89
							_spriteStage.addBatch(_mainmenu.batch); 
90
							_entities.changeLevels('level' + _state); 
91
							_terrain.changeLevels('terrain' + _state); 
92
						} 
93
					} 
94
				} 
95
			} 
96
		}

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:

1
		 
2
		// run every frame by the entity manager as the player ai function 

3
		public function playerLogic(seconds:Number):void 
4
		{ 
5
			thePlayer.age += seconds; 
6
			handleTransitions(seconds); 
7
			thePlayer.speedY = thePlayer.speedX = 0; 
8
			 
9
			if (_state == 0) return; 
10
			 
11
			if (_controls.pressing.up) 
12
				thePlayer.speedY = -playerSpeed; 
13
			if (_controls.pressing.down) 
14
				thePlayer.speedY = playerSpeed; 
15
			if (_controls.pressing.left) 
16
				thePlayer.speedX = -playerSpeed; 
17
			if (_controls.pressing.right) 
18
				thePlayer.speedX = playerSpeed; 
19
				 
20
			// v5 

21
			if (_controls.pressing.fire && (thePlayer.health > 0)) 
22
			{ 
23
				// is it time to fire again? 

24
				if (currentTime >= nextFireTime) 
25
				{ 
26
					//trace("Fire!"); 

27
					nextFireTime = currentTime + fireDelay; 
28
					_sfx.playGun(1); 
29
					_entities.shootBullet(3); 
30
				} 
31
			} 
32
				 
33
			// keep on screen 

34
			if (thePlayer.sprite.position.x < 0) 
35
				thePlayer.sprite.position.x = 0; 
36
			if (thePlayer.sprite.position.x > _width) 
37
				thePlayer.sprite.position.x = _width; 
38
			if (thePlayer.sprite.position.y < 0) 
39
				thePlayer.sprite.position.y = 0; 
40
			if (thePlayer.sprite.position.y > _height) 
41
				thePlayer.sprite.position.y = _height; 
42
				 
43
			// leave a trail of particles 

44
			_entities.particles.addParticle(63,  
45
				thePlayer.sprite.position.x - 12,  
46
				thePlayer.sprite.position.y + 2,  
47
				0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5); 
48
				 
49
			// v5 if we are about to die, spew sparks as a warning 

50
			if (thePlayer.health < 10) 
51
				_entities.particles.addSparks(thePlayer.sprite.position, 1, 2);	 
52
				 
53
			// when the player gets damaged, they become 

54
			// invulnerable for a short perod of time 

55
			if (thePlayer.invulnerabilityTimeLeft > 0) 
56
			{ 
57
				thePlayer.invulnerabilityTimeLeft -= seconds; 
58
				if (thePlayer.invulnerabilityTimeLeft <= 0) 
59
				{ 
60
					trace("Invulnerability wore off."); 
61
					thePlayer.sprite.alpha = 1; 
62
				} 
63
				else // while invulnerable, flicker 

64
				{ 
65
					thePlayer.sprite.alpha = Math.sin(thePlayer.age * 30) / Math.PI  + 0.25; 
66
				} 
67
			} 
68
		}

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.

1
 
2
		// handle any player input 

3
		private function processInput():void 
4
		{ 
5
			if (_state == 0) // are we at the main menu? 

6
			{ 
7
				// select menu items via keyboard 

8
				if (_controls.pressing.down || _controls.pressing.right) 
9
				{ 
10
					if (nothingPressedLastFrame)  
11
					{ 
12
						_sfx.playGun(1); 
13
						_mainmenu.nextMenuItem(); 
14
						nothingPressedLastFrame = false; 
15
					} 
16
				} 
17
				else if (_controls.pressing.up || _controls.pressing.left) 
18
				{ 
19
					if (nothingPressedLastFrame)  
20
					{ 
21
						_sfx.playGun(1); 
22
						_mainmenu.prevMenuItem(); 
23
						nothingPressedLastFrame = false; 
24
					} 
25
				} 
26
				else if (_controls.pressing.fire) 
27
				{ 
28
					if (_mainmenu.activateCurrentMenuItem(getTimer())) 
29
					{ // if the above returns true we should start the game 

30
						startGame(); 
31
					} 
32
				} 
33
				else 
34
				{ 
35
					// this ensures the menu doesn't change too fast 

36
					nothingPressedLastFrame = true; 
37
				} 
38
			} 
39
			// v5 - player firing moved to playerLogic function 

40
		}

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.

1
 
2
		private function startGame():void 
3
		{ 
4
			trace("Starting game!"); 
5
			_state = 1; 
6
			_spriteStage.removeBatch(_mainmenu.batch); 
7
			_sfx.playMusic(); 
8
			 
9
			// add the player entity to the game! 

10
			if (!thePlayer)  
11
				thePlayer = _entities.addPlayer(playerLogic); 
12
			else // on subsequent games // v5 

13
				thePlayer.sprite.visible = true; 
14
			if (_entities.theOrb) 
15
				_entities.theOrb.sprite.visible = true; 
16
				 
17
			// load level one (and clear demo entities) 

18
			_entities.changeLevels('level' + _state); 
19
			_terrain.changeLevels('terrain' + _state); 
20
			 
21
			// reset the player position 

22
			thePlayer.level = 0; // it will transition to 1 

23
			thePlayer.score = 0; 
24
			thePlayer.lives = 3; 
25
			thePlayer.sprite.position.x = 64; 
26
			thePlayer.sprite.position.y = _entities.midpoint; 
27
 
28
			// add a "welcome message" 

29
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
30
			 
31
			// make the player invulnerable at first 

32
			thePlayer.invulnerabilityTimeLeft = thePlayer.transitionSeconds + thePlayer.invulnerabilitySecsWhenHit; 
33
			 
34
		}

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.

1
 
2
		// v5 triggered if the player loses all lives 

3
		private function gameOver():void 
4
		{ 
5
			trace("================ GAME OVER ================"); 
6
 
7
			// save game 

8
			if (saved.level < thePlayer.level) 
9
				saved.level = thePlayer.level; 
10
			if (saved.score < thePlayer.score) 
11
			{ 
12
				saved.score = thePlayer.score; 
13
				_gui.highScore = thePlayer.score; 
14
			} 
15
 
16
			_state = 0; 
17
 
18
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
19
 
20
		} 
21
		 
22
		// v5 detect if we just died, etc. 

23
		private function checkPlayerState():void 
24
		{ 
25
			if (_state == 0) return; 
26
			if (thePlayer) 
27
			{ 
28
				if (thePlayer.lives < 0) 
29
				{ 
30
					gameOver(); 
31
				} 
32
			} 
33
		}

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:

1
		 
2
		// v5 check to see if we reached the end of the map/game 

3
		private function checkMapState():void 
4
		{ 
5
			// main menu or gameover credits? 

6
			if (_state < 1) return; 
7
			// already transitioning? 

8
			if (thePlayer.level != _state) return; 
9
			 
10
			// allow some extra spaces for the level to scroll past 

11
			// the player and then call the level complete. 

12
			if (_terrain.levelPrevCol > _terrain.level.levelLength + 16) 
13
			{ 
14
				trace("LEVEL " + _state  + " COMPLETED!"); 
15
				 
16
				_state++; 
17
				 
18
				thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
19
				 
20
				if (_entities.level.levelLength == 0) 
21
				{ 
22
					trace("NO MORE LEVELS REMAIN! GAME OVER!"); 
23
					rollTheCredits(); 
24
				} 
25
			} 
26
		} 
27
		 
28
		// display the "game cleared" screen 

29
		private function rollTheCredits():void 
30
		{ 
31
			gameOver(); 
32
			_state = -1; 
33
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds * 3; 
34
		}

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.

1
 
2
		// this function draws the scene every frame 

3
		private function onEnterFrame(e:Event):void  
4
		{ 
5
			try  
6
			{ 
7
				// grab timestamp of current frame 

8
				currentTime = getTimer(); 
9
				currentFrameMs = currentTime - previousFrameTime; 
10
				previousFrameTime = currentTime; 
11
				 
12
				// erase the previous frame 

13
				context3D.clear(0, 0, 0, 1); 
14
				 
15
				// for debugging the input manager, update the gui 

16
				// _gui.titleText = _controls.textDescription(); 

17
				 
18
				// process any player input 

19
				processInput(); 
20
 
21
				// scroll the background 

22
				if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height); 
23
				_bg.update(currentTime); 
24
				 
25
				// update the main menu titlescreen 

26
				if (_state == 0) 
27
					_mainmenu.update(currentTime); 
28
				 
29
				// move/animate all entities 

30
				_terrain.update(currentFrameMs); 
31
				_entities.update(currentFrameMs); 
32
				 
33
				// keep adding more sprites - IF we need to 

34
				_terrain.streamLevelEntities(false); 
35
				_entities.streamLevelEntities(true); 
36
				 
37
				// draw all entities 

38
				_spriteStage.render(); 
39
 
40
				// update the screen 

41
				context3D.present(); 
42
				 
43
				// check for gameover/death 

44
				checkPlayerState(); 
45
				 
46
				// check for the end of the level 

47
				checkMapState(); 
48
			} 
49
			catch (e:Error)  
50
			{ 
51
				// this can happen if the computer goes to sleep and 

52
				// then re-awakens, requiring reinitialization of stage3D 

53
				// (the onContext3DCreate will fire again) 

54
			} 
55
		} 
56
	} // end class 

57
} // 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
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.