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

Build a Stage3D Shoot-'Em-Up: Full-Screen Boss Battles and Polish

Scroll to top
Read Time: 63 min
This post is part of a series called Shoot-'Em-Up.
Pixel-Level Collision Detection Based on Pixel Colors

In this tutorial series we will create a high-performance 2D shoot-em-up using Flash 11's new hardware-accelerated Stage3D rendering engine. We will be taking advantage of several hardcore optimization techniques to achieve great 2D sprite rendering performance.


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: the final game, complete with blazingly fast sprite rendering, sound and music, multiple detailed levels, numerous enemies to destroy, score, health, lives, particle systems, level transitions, full screen rendering, an NPC character, slow-mo, a preloader progress bar, and a boss battle.


Introduction: Welcome to Level Six!

This is the final installment in the of the Stage3D shoot-em-up tutorial series. Let's finish our epic quest 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.

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.

And in the fifth part, we made it such that when the player is hit they will take damage, and when out of health they die in a firey explosion. We added game states to support game overs and multiple levels, a "final credits screen" plus all sorts of visual feedback (such as level transition messages and a health bar).

In this, the last part, we are going to put the final layer of polish on our game:

  • We'll add boss battles, complete with a glowing health bar and bullets everywhere.
  • We implement full screen HD rendering at any screen resolution by using liquid layout.
  • Because our game is just over a meg in size, we'll implement a preloader progress bar.
  • Just for fun, we'll add NPC (non-player character) voiceovers to motivate players.
  • For dramatic effect, we'll implement slow motion time dilation.
  • We'll tweak the movement speed of the player, enemies and bullets.
  • We will add autofire to the game so players can concentrate solely upon movement.

When we're done, the game will be complete. The final product is a "real" videogame that has everything players expect, with all the bells and whistles.


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 five (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 CS6 to Flash Builder, as long as you target Flash 11.


Step 2: Set Up a Preloader

Any game that requires more than a couple seconds to download should have a progress bar preloader screen. This gives players some visual confirmation that the game is indeed loading, so that they never worry that their browser has hung. A good rule of thumb is any game that is more than one meg in size should have a preloader.

Although the majority of users nowadays have extremely high bandwidth and this Flash file will only take a couple seconds to load, congested web servers, users with slower internet access, and times that the PC might be really busy mean that occasionally it will take a few moments to download. A preloader progress bar is also one of the many little touches of polish that set tech demos and "real" games apart.

Creating a preloader in FlashDevelop is extremely simple. First, begin by making a brand new file in your project called Preloader.as and right-click it in your project manager window. Set it to be the project's primary "Document Class" (which will replace the original document class, which was Main.as) as shown in the image below:



Step 3: Include the Game Itself

Before we fill in the details for rendering a nice-looking progress bar in our Preloader.as, we need to tell Flash to load the rest of the game. This is important because as coded the preloader won't import any of the game classes.

Why? because they aren't actually referred to in Preloader.as. If we don't specify that we also want to include Main.as in the .SWF, the compiler is smart enough to assume it is an unused file and will not include it. Skipping this step will mean that the .SWF we create upon compilation is only a couple kilobytes in size. We want to ensure the entire project is included in the downloaded even though it isn't referred to in the preloader code.

To do so, go into the Project menu, select Properties, go into the Compiler Options tab and click Additional Compiler Options to add a snippet to the compiler command-line. This snippet is -frame main Main which means that a second "frame" in the flash timeline will use the "Main" class that used to be the primary document class for our project.

If you aren't using FlashDevelop, simply add this to your make file command line options. If you're using pure Flex, you can also do it automatically by including the following in your preloader as3 source: [frame (factoryClass="Main")].


Step 4: Init the Progress Bar

We're now ready to implement the progress bar preloader in our currently blank Preloader.as class. Add the following code to set everything up:

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

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

4
// Created for active.tutsplus.com 

5
 
6
// Preloader.as 

7
// displays a progress bar while 

8
// the swf is being downloaded 

9
package  
10
{ 
11
	[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] 
12
	 
13
	import flash.display.Bitmap; 
14
	import flash.display.DisplayObject; 
15
	import flash.display.MovieClip; 
16
	import flash.events.Event; 
17
	import flash.events.ProgressEvent; 
18
	import flash.text.Font; 
19
	import flash.utils.getDefinitionByName; 
20
	import flash.display.Sprite; 
21
	import flash.text.TextField; 
22
	 
23
	// Force the 3d game to be on frame two 

24
	 
25
	// In FlashDevelop, add this to your compiler command-line: 

26
	// Project > Properties >  

27
	// Compiler Options >  

28
	// Additional Compiler Options: 

29
	// -frame main Main 

30
	 
31
	// In Flex, uncomment this line: 

32
	// [frame (factoryClass="Main")] 

33
	 
34
	public class Preloader extends MovieClip  
35
	{ 
36
		private var preloader_square:Sprite = new Sprite(); 
37
		private var preloader_border:Sprite = new Sprite(); 
38
		private var preloader_text:TextField = new TextField(); 
39
		 
40
		public function Preloader()  
41
		{ 
42
			addEventListener(Event.ENTER_FRAME, checkFrame); 
43
			 
44
			loaderInfo.addEventListener( 
45
				ProgressEvent.PROGRESS, progress); 
46
 
47
			addChild(preloader_square); 
48
			preloader_square.x = 200; 
49
			preloader_square.y = stage.stageHeight / 2; 
50
			 
51
			addChild(preloader_border); 
52
			preloader_border.x = 200-4; 
53
			preloader_border.y = stage.stageHeight / 2 - 4; 
54
		 
55
			addChild(preloader_text); 
56
			preloader_text.x = 194; 
57
			preloader_text.y = stage.stageHeight / 2 - 30; 
58
			preloader_text.width = 256; 
59
			 
60
		}

Step 5: Animate the Progress Bar

Continuing with Preloader.as, implement the event handler that will be called repeatedly during the download.

1
 
2
		private function progress(e:ProgressEvent):void  
3
		{ 
4
			// update loader 

5
			preloader_square.graphics.beginFill(0xAAAAAA); 
6
			preloader_square.graphics.drawRect(0, 0, 
7
				(loaderInfo.bytesLoaded / loaderInfo.bytesTotal) 
8
				* 200,20); 
9
			preloader_square.graphics.endFill(); 
10
			 
11
			preloader_border.graphics.lineStyle(2,0xDDDDDD); 
12
			preloader_border.graphics.drawRect(0, 0, 208, 28); 
13
			 
14
			preloader_text.textColor = 0xAAAAAA; 
15
			preloader_text.text = "Loaded " + Math.ceil( 
16
				(loaderInfo.bytesLoaded /  
17
				loaderInfo.bytesTotal)*100) + "% (" + 
18
				+ loaderInfo.bytesLoaded + " of " +  
19
				loaderInfo.bytesTotal + " bytes)"; 
20
			 
21
		} 
22
		 
23
		private function checkFrame(e:Event):void  
24
		{ 
25
			if (currentFrame == totalFrames)  
26
			//if (loaderInfo.bytesLoaded >= loaderInfo.bytesTotal) 

27
			{ 
28
				removeEventListener(Event.ENTER_FRAME, checkFrame); 
29
				preloader_startup(); 
30
			} 
31
		} 
32
		 
33
		private function preloader_startup():void  
34
		{ 
35
			// stop loader 

36
			stop(); 
37
			loaderInfo.removeEventListener( 
38
				ProgressEvent.PROGRESS, progress); 
39
			// remove progress bar 

40
			if (contains(preloader_square))  
41
				removeChild(preloader_square); 
42
			if (contains(preloader_border))  
43
				removeChild(preloader_border); 
44
			if (contains(preloader_text))  
45
				removeChild(preloader_text); 
46
			// start the game 

47
			var mainClass:Class =  
48
				getDefinitionByName("Main") 
49
				as Class; 
50
			addChild(new mainClass() as DisplayObject); 
51
		} 
52
		 
53
	} // end class 

54
} // end package

In the two steps above, we added a few simple elements to the stage and animated them. A rectangular progress bar that changes size as the download progresses, plus some text that tells the user how many bytes have been downloaded, how many in total are required, and the completion percentage.

This is updated every frame as the .SWF continues to download. When the download is complete, we use the getDefinitionByName function to locate the actual game class and add it to the stage. This will start the game.

This is what the preloader will look like while the game is downloading:


That's it for the preloader! You can use this handy class in all your projects as a quick and easy way to ensure that larger downloads don't get skipped. In the age of Stage3D and high definition games filled with sprites, sounds and more, it is entirely reasonable to have a game that is many megs in size. Forcing a user to sit at a blank screen for more than a second or two will lose potential players.


Step 6: Create BOSS Entity Vars

In this final version of our game, we're going to implement a "boss battle" which will be triggered at the end of each level. This will add some additional tension at the end of a harrowing dogfight, and is a tried-and-true shooter game convention. This mechanic adds a climax to the level.

A boss battle usually involved a larger and much more powerful enemy that requires many more hits to destroy. It also won't scroll past the edge of the, thus forcing the player to deal with it. Instead of basic random single shots aimed at the player, we'll code a more varied firing pattern and force the player to dodge and weave just to stay alive.

Open the existing Entity.as, and add a few new properties to the top of the file as follows:

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

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
		// v6 if this is a boss, when it dies the game state (level) increases 

20
		public var isBoss:Boolean = false; 
21
		// used by the boss battles for "burst, delay, burst" firing 

22
		public var burstTimerStart:Number = 0; 
23
		public var burstTimerEnd:Number = 0; 
24
		public var burstPauseTime:Number = 2; 
25
		public var burstLength:Number = 2; 
26
		public var burstShootInterval:Number = 0.2;

The rest of the entity class variables remain unchanged except, however, as a result of playtesting during the course of development, a few values have been tweaked. Locate fireDelayMin and change it to 4, so that non-boss enemies wait a little longer between firing. Change fireDelayMax to 12 for the same reason. This cuts down on bullet spam a little, since when the screen is filled with baddies we want the game to be hard but not impossible.


Step 7: Code BOSS A.I.

The last upgrade that needs to be implemented in our Entity.as file is the new artificial intelligence function for the boss. At the very bottom of the file, below all the other behaviors such as sentryAI, droneAI and the rest, add a new AI routine as follows:

1
 
2
	// v6 // boss battle: stay on the screen 

3
	public function bossAI(seconds:Number):void 
4
	{ 
5
		age += seconds; 
6
		 
7
		// spammy with breaks in between 

8
		if (age > burstTimerStart) 
9
		{ 
10
			if (age > burstTimerEnd) 
11
			{ 
12
				// one final "circle burst" 

13
				for (var deg:int = 0; deg < 20; deg++) 
14
				{ 
15
					gfx.shootBullet(1, this, deg * 18 * gfx.DEGREES_TO_RADIANS); 
16
					gfx.sfx.playBoss(); 
17
				} 
18
				burstTimerStart = age + burstPauseTime; 
19
				burstTimerEnd = burstTimerStart + burstLength; 
20
			} 
21
			else 
22
			{ 
23
				maybeShoot(2, burstShootInterval, burstShootInterval); 
24
			} 
25
		} 
26
		 
27
		if (gfx.thePlayer) 
28
		{ 
29
			// point at player 

30
			sprite.rotation = gfx.pointAtRad( 
31
				gfx.thePlayer.sprite.position.x - sprite.position.x, 
32
				gfx.thePlayer.sprite.position.y - sprite.position.y)  
33
				- (90 * gfx.DEGREES_TO_RADIANS); 
34
				 
35
			// slowly move to a good spot: 256 pixels to the right of the player 

36
			speedX = (gfx.thePlayer.sprite.position.x + 256 - sprite.position.x); 
37
			aiPathOffsetY = (Math.sin(age) / Math.PI) * 256; 
38
		} 
39
	}

In the boss AI code above, we keep track of time passing in order to trigger different behaviors at different times. Firstly, the boss will fire a rapid long line, like a machine-gun, every few seconds. At the end of that burst, it will fire a single round fo different bullets in every direction, which results in a "circle" of bullets that are hard to didge if you are too close to the boss. To make things more challenging, the boss measures the distance to the player and smoothly interpolates its position to be nearby - it prefers to sit just 256 pixels to the right of wherever the player is. That's all that is required for the boss battle upgrades to our entity class.


Step 8: Add the BOSS Sprite

Although we have all the behaviors coded for our boss battle, there's one final thing to do so that it appears in-game. We need to draw a big boss and add it to our spritesheet. Using Photoshop, Gimp or the image editor of your choice, replace some unnecessary sprites with a larger boss sprite. Later on, we'll change the way our spritesheet is "chopped up" so that the larger boss image is rendered properly. You may also notice that the bullet sprites have been tweaked to use different colors of glow, just for fun. Here's the final spritesheet texture as used in the example project:



Step 9: BOSS Health GUI Vars

Because our epic boss battles are going to feature a big enemy that can take many hits before being destroyed, we're going to update our GameGUI.as class to inlude a big red "health bar" for the boss. This will give players the visual feedback required to confirm that, yes, the boss is taking damage when being hit.

Start by adding the following lines of code for some new class variables to the top of the file, alongside the similar TextField definitions for the player's health bar and such.

1
 
2
public var bosshealthTf:TextField; // v6 

3
public var bosshealth:int = 100; // v6

Step 10: Init the BOSS Health GUI

Continuing with GameGUI.as, add the following initialization code to the onAddedHandler function. This will create a new textfield for the boss health meter in a large glowing red font. Note that we don't yet add it to the stage - we only want it to be visible during the actual boss battle.

1
 
2
			// v6 - a boss health meter 

3
			bosshealthTf = new TextField(); 
4
			bosshealthTf.defaultTextFormat = myFormatCENTER; 
5
			bosshealthTf.embedFonts = true; 
6
			bosshealthTf.x = 0; 
7
			bosshealthTf.y = 48; 
8
			bosshealthTf.selectable = false; 
9
			bosshealthTf.antiAliasType = 'advanced'; 
10
			bosshealthTf.text = "BOSS: |||||||||||||"; 
11
			bosshealthTf.filters = [new GlowFilter(0xFF0000, 1, 8, 8, 4, 2)]; 
12
			bosshealthTf.width = 600;

Step 10: A Reusable Health Bar Function

In previous versions of the game, the only health bar belonged to the player. Now that there is another used by the boss, we should make a reusable function that generates the proper health display for any entity so that we avoid having copy-n-pasted duplicate code in our gui class. Add the following function to GameGUI.as as follows:

1
 
2
		private function healthBar(num:int):String // v6 

3
		{ 
4
			if (num >= 99) return "|||||||||||||"; 
5
			else if (num >= 92) return "||||||||||||"; 
6
			else if (num >= 84) return "|||||||||||"; 
7
			else if (num >= 76) return "||||||||||"; 
8
			else if (num >= 68) return "|||||||||"; 
9
			else if (num >= 60) return "||||||||"; 
10
			else if (num >= 52) return "|||||||"; 
11
			else if (num >= 44) return "||||||"; 
12
			else if (num >= 36) return "|||||"; 
13
			else if (num >= 28) return "||||"; 
14
			else if (num >= 20) return "|||"; 
15
			else if (num >= 12) return "||"; 
16
			else return "|"; 
17
		}

The last three steps of the tutorial added a simple health bar that will look like this when we're done:



Step 11: NPC Dialog Vars

NPCs are often used as the "quest givers" in games, and since adding a little popup dialog bar to the bottom of the screen is a trivial effort, it will tgive the game such much more pizazz with very little extra work. Therefore, just for fun, we're going to add a non-player-character (NPC) to our game.

This character will provide encouragement and will add a human touch. By using a pretty girl's face (which was sculpted and rendered in Poser Pro 2010 in this example) we add a little personality - and a reason to fight all those enemies. She will congratulate you when a boss is defeated, and will sympathize with you if you die.

Add the following variables to the top of the GameGUI.as class, right next to where you added the corresponding ones for the boss health bar GUI.

1
 
2
		[Embed (source = "../assets/npc_overlay.png")]  
3
		private var npcOverlayData:Class; 
4
		private var npcOverlay:Bitmap = new npcOverlayData(); 
5
 
6
		public var npcTf:TextField; // v6 

7
		public var npcText : String = ""; // v6

Step 11: NPC Dialog Inits

Just as we did for the boss health meter, we need to initialize the text field that will contain the NPC's dialog. During the game, we'll also trigger some voiceover sounds to go alogn with them. This text (and the overlay image specified above) are not normally visible during the game and will only be used during "transitions" such the the beginning of a level, just efore the boss battle, and when you reach a game over state.

Continuing upgrading GameGUI.as by adding the following initialization code to the onAddedHandler function.

1
 
2
			// v6 - an NPC "mission text" character 

3
			npcTf = new TextField(); 
4
			npcTf.defaultTextFormat = myFormat; 
5
			npcTf.embedFonts = true; 
6
			npcTf.x = 0; 
7
			npcTf.y = 400-64; 
8
			npcTf.selectable = false; 
9
			npcTf.antiAliasType = 'advanced'; 
10
			npcTf.text = ""; 
11
			npcTf.width = 600;

Step 12: The NPC Overlay Image

In the steps above we created an overlay sprite as well as some text that will appear on-screen when the NPC needs to do some talking. The image we need for this overlay, which we will float to the bottom of the screen, should be a small bar that is 600x64 pixels in size. Create the background for this overlay in your image editor now. It looks like this in our example game:



Step 13: Liquid GUI Layout

Now that we've added a boss health bar and NPC dialog overlay to our GameGUI.as class, all we need to do is upgrade the update functions to deal with this new functionality. To begin with, we know that the final version of the game is going to support full screen mode. Since there are many different monotor resolutions in use, we can't know for sure what the size of the game is going to be.

This is a situation where, just like when creating an HTML page, the best solution to different screen sizes is to create a "liquid layout" function that moves everything around to the proper places on screen no matter how big it is. For our GUI class, we simply calculate what the center position of the screen is and move things around whenever a RESIZE event is fired. Continue upgrading GameGUI.as as follows:

1
 
2
		public function setPosition(view:Rectangle):void // v6 

3
		{ 
4
			trace('Moving GUI'); 
5
			var mid:Number = view.width / 2; 
6
			hudOverlay.x = mid - hudOverlay.width / 2; 
7
			debugStatsTf.x = mid - 300 + 18; 
8
			scoreTf.x = mid - 300 + 442; 
9
			highScoreTf.x = mid - 300 + 208; 
10
			healthTf.x = mid - 300 + 208; 
11
			bosshealthTf.x = mid - 300; 
12
			bosshealthTf.y = 48; 
13
			transitionTf.y = view.height / 2 - 80; 
14
			transitionTf.x = mid - 300; 
15
			npcOverlay.x = mid - npcOverlay.width / 2; // v6 

16
			npcOverlay.y = view.height - npcOverlay.height - 8; // v6 

17
			npcTf.y = view.height - 64; // v6 

18
			npcTf.x = mid - 220; // v6 

19
		}

Step 14: Upgrade the GUI Updater

The final set of upgrades required by all this new GUI functionality is to account for our new items during the render loop. As an optimization we will only change things when required (not every frame) by checking to see if the values have changed. Modify these two functions in GameGUI.as as follows:

1
 
2
		// only updates textfields if they have changed 

3
		private function updateScore():void 
4
		{ 
5
			// NPC dialog toggle // v6 

6
			if (npcText != npcTf.text) 
7
			{ 
8
				npcTf.text = npcText; 
9
				if (npcText != "") 
10
				{ 
11
					if (!contains(npcOverlay)) 
12
						addChild(npcOverlay);					 
13
					if (!contains(npcTf)) 
14
						addChild(npcTf);					 
15
				} 
16
				else 
17
				{ 
18
					if (contains(npcOverlay)) 
19
						removeChild(npcOverlay);					 
20
					if (contains(npcTf)) 
21
						removeChild(npcTf);					 
22
				} 
23
			} 
24
			 
25
			if (transitionText != transitionTf.text) 
26
			{ 
27
				transitionTf.text = transitionText; 
28
				if (transitionTf.text != "") 
29
				{ 
30
					if (!contains(transitionTf)) 
31
						addChild(transitionTf); 
32
				} 
33
				else 
34
				{ 
35
					if (contains(transitionTf)) 
36
						removeChild(transitionTf); 
37
				} 
38
			} 
39
			 
40
			if (statsTarget && statsTarget.thePlayer) 
41
			{ 
42
				// v6 optional boss health meter 

43
				if (statsTarget.theBoss) 
44
				{ 
45
					if (bosshealth != statsTarget.theBoss.health) 
46
					{ 
47
						bosshealth = statsTarget.theBoss.health; 
48
						bosshealthTf.text = "BOSS: " + healthBar(bosshealth); 
49
					} 
50
				} 
51
				 
52
				if (health != statsTarget.thePlayer.health) 
53
				{ 
54
					health = statsTarget.thePlayer.health; 
55
					healthTf.text = "HP: " + healthBar(health); 
56
				} 
57
				if ((score != statsTarget.thePlayer.score) || (lives != statsTarget.thePlayer.lives)) 
58
				{ 
59
					score = statsTarget.thePlayer.score; 
60
					lives = statsTarget.thePlayer.lives; 
61
					if (lives == -1) 
62
						scoreTf.text = scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' +'GAME OVER'; 
63
					else 
64
						scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' + lives +  
65
							(lives != 1 ? ' LIVES' : ' LIFE') + ' LEFT'; 
66
					// we may be beating the high score right now 

67
					if (score > highScore) highScore = score; 
68
				} 
69
			} 
70
			if (prevHighScore != highScore) 
71
			{ 
72
				prevHighScore = highScore; 
73
				highScoreTf.text = "HIGH SCORE: " + pad0s(highScore); 
74
			} 
75
		} 
76
		 
77
		private function onEnterFrame(evt:Event):void 
78
		{ 
79
			timer = getTimer(); 
80
			 
81
			updateScore(); 
82
			 
83
			if( timer - 1000 > ms_prev ) 
84
			{ 
85
				lastfps = Math.round(frameCount/(timer-ms_prev)*1000); 
86
				ms_prev = timer; 
87
 
88
 
89
				// v6 - we don't want sprite or memory stats in the "final" version 

90
				/* 

91
				var mem:Number = Number((System.totalMemory * 0.000000954).toFixed(2)); // v6 

92
				// grab the stats from the entity manager 

93
				if (statsTarget) 

94
				{ 

95
					statsText =  

96
						statsTarget.numCreated + '/' + 

97
						statsTarget.numReused + ' sprites'; 

98
				} 

99
				debugStatsTf.text = titleText + lastfps + 'FPS - ' + mem + 'MB' + '\n' + statsText; 

100
				*/ 
101
				debugStatsTf.text = titleText + lastfps + ' FPS\n' + statsText; 
102
				frameCount = 0; 
103
			} 
104
		 
105
			// count each frame to determine the framerate 

106
			frameCount++; 
107
				 
108
		} 
109
	} // end class 

110
} // end package

In the code above, we have added update functionality for our two new GUI items (the boss health bar and the NPC dialog popup). We also simplified the "debug" stats that appear in the top left of the screen. Instead of cryptic sprite counts and RAM useage stats, we simply include an FPS display and a custom message that will show what level we are on during gameplay.


Step 15: New Entity Manager Vars

We're going to make of number of minor changes to our most important class, the entity manager. Open the existing file EntityManager.as in your project and begin by adding some new class variables related to the boss. In addition, a few values related to speed have been tweaked, so replace the existing definitions with these ones at the very top of your class, as follows:

1
 
2
	public class EntityManager 
3
	{ 
4
		// v6 - the boss entity if it exists 

5
		public var theBoss:Entity; 
6
		// v6 - function that is run when the boss is killed 

7
		public var bossDestroyedCallback:Function = null; 
8
		// v6 how fast the default scroll (enemy flying) speed is 

9
		public var defaultSpeed:Number = 160; 
10
		// v6 how fast player bullets go per second 

11
		public var playerBulletSpeed:Number = 300; 
12
		// v6 how fast enemy bullets go per second 

13
		public var enemyBulletSpeed:Number = 200; 
14
		// v6 how big the bullet sprites are 

15
		public var bulletScale:Number = 1; 
16
		// v6 used to enable full screen liquid layout 

17
		public var levelTopOffset:int;

All the other class vars in the section above remain unchanged and aren't included here for brevity.


Step 16: Liquid Layout

In the same way that we are now using a liquid layout sceme for the game to support running at any resolution, we need to tweak the setPosition function in EntityManager.as to ensure that no matter what size of monitor the play has the game takes place in the middle of the screen. Modify this function as follows:

1
 
2
		public function setPosition(view:Rectangle):void  
3
		{ 
4
			// allow moving fully offscreen before 

5
			// automatically being culled (and reused) 

6
			maxX = view.width + cullingDistance; 
7
			minX = view.x - cullingDistance; 
8
			maxY = view.height + cullingDistance; 
9
			minY = view.y - cullingDistance; 
10
			midpoint = view.height / 2; 
11
			// during fullscreen, we may have more screen than 

12
			// the level data would fill: to avoid everything being 

13
			// at the top of the screen, center the level 

14
			levelTopOffset = midpoint - 200; // v6 

15
		}

Step 17: Upgrade the Respawner

A few minor upgrades are required for our respawn function to account for the fact that some entities might have invalid timer data (such as age or when it should fire next) left over from when it was last destroyed. In particular, some values used the bosses needs to be reset. We don't want new versions of these same sprites to never shoot when respawned due to having incorrect ages, which can mess up the AI routines. Continuing with EntityManager.as, make these tweaks to correct this oversight:

1
 
2
		// search the entity pool for unused entities and reuse one 

3
		// if they are all in use, create a brand new one 

4
		public function respawn(sprID:uint=0):Entity 
5
		{ 
6
			var currentEntityCount:int = entityPool.length; 
7
			var anEntity:Entity; 
8
			var i:int = 0; 
9
			// search for an inactive entity 

10
			for (i = 0; i < currentEntityCount; i++ )  
11
			{ 
12
				anEntity = entityPool[i]; 
13
				if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) 
14
				{ 
15
					//trace('Reusing Entity #' + i); 

16
					anEntity.active = true; 
17
					anEntity.sprite.visible = true; 
18
					anEntity.recycled = true; 
19
					anEntity.age = 0; // v6 

20
					anEntity.burstTimerStart = 0; // v6 

21
					anEntity.burstTimerEnd = 0; // v6 

22
					anEntity.fireTime = 0; // v6 

23
					numReused++; 
24
					return anEntity; 
25
				} 
26
			} 
27
			// none were found so we need to make a new one 

28
			//trace('Need to create a new Entity #' + i); 

29
			var sprite:LiteSprite; 
30
			sprite = batch.createChild(sprID); 
31
			anEntity = new Entity(sprite, this); 
32
			anEntity.age = 0; // v6 

33
			anEntity.burstTimerStart = 0; // v6 

34
			anEntity.burstTimerEnd = 0; // v6 

35
			anEntity.fireTime = 0; // v6 

36
			entityPool.push(anEntity); 
37
			numCreated++; 
38
			return anEntity; 
39
		}

Step 18: Upgrade the Bullets

We've implemented a new, cool-looking firing mode to our bosses that spews tons of bullets in a circular pattern. The original shootBullet function assumed that all entities would always only fire bullets in the direction that they are facing. We need to upgrade this routine to allow for a specific angle to be passed in the function parameters. If it is not specified, then the original behavior applies.

Additionally, in previous versions of the game all bullet sprites were facing backwards and a line of code was used to correct this error. In this final version, the actual spritesheet was fixed and this hack is no longer required.

Finally, we are now using different bullet speeds for the player's bullets compared to those shot by enemies. After playtesting, giving the player a bit of an edge (by having faster bullets) just "felt right". Therefore, we take into account who the shooter is and give our projectiles the appropriate speeds.

1
 
2
		// shoot a bullet 

3
		public function shootBullet(powa:uint=1, shooter:Entity = null, angle:Number = NaN):Entity // v6 

4
		{ 
5
			// just in case the AI is running during the main menu 

6
			// and we've not yet created the player entity 

7
			if (thePlayer == null) return null; 
8
 
9
			var theBullet:Entity; 
10
			// assume the player shot it 

11
			// otherwise maybe an enemy did 

12
			if (shooter == null)  
13
				shooter = thePlayer; 
14
				 
15
			// three possible bullets, progressively larger 

16
			if (powa == 1)  
17
				theBullet = respawn(spritenumBullet1); 
18
			else if (powa == 2)  
19
				theBullet = respawn(spritenumBullet2); 
20
			else  
21
				theBullet = respawn(spritenumBullet3); 
22
			theBullet.sprite.position.x = shooter.sprite.position.x + 8; 
23
			theBullet.sprite.position.y = shooter.sprite.position.y + 2; 
24
			//theBullet.sprite.rotation = 180 * DEGREES_TO_RADIANS; // v6 fixed in the spritesheet 

25
			theBullet.sprite.scaleX = theBullet.sprite.scaleY = bulletScale;  // v6 

26
			if (shooter == thePlayer) 
27
			{ 
28
				theBullet.speedX = playerBulletSpeed; // v6 

29
				theBullet.speedY = 0; 
30
			} 
31
			else // enemy bullets move slower and towards the player // v6 UNLESS SPECIFIED 

32
			{ 
33
				if (isNaN(angle)) 
34
				{ 
35
					theBullet.sprite.rotation =  
36
						pointAtRad(theBullet.sprite.position.x - thePlayer.sprite.position.x, 
37
							theBullet.sprite.position.y - thePlayer.sprite.position.y)  
38
							- (90 * DEGREES_TO_RADIANS); 
39
				} 
40
				else 
41
				{ 
42
					theBullet.sprite.rotation = angle; 
43
				} 
44
				 
45
				// move in the direction we're facing // v6 

46
				theBullet.speedX = enemyBulletSpeed*Math.cos(theBullet.sprite.rotation); 
47
				theBullet.speedY = enemyBulletSpeed*Math.sin(theBullet.sprite.rotation); 
48
    
49
				// optionally, we could just fire straight ahead in the direction we're heading: 

50
				// theBullet.speedX = shooter.speedX * 1.5; 

51
				// theBullet.speedY = shooter.speedY * 1.5; 

52
				// and we could point where we're going like this: 

53
				// pointAtRad(theBullet.speedX,theBullet.speedY) - (90*DEGREES_TO_RADIANS); 

54
			} 
55
			theBullet.owner = shooter; 
56
			theBullet.collideradius = 10; 
57
			theBullet.collidemode = 1; 
58
			theBullet.isBullet = true; 
59
			if (!theBullet.recycled) 
60
				allBullets.push(theBullet); 
61
			return theBullet; 
62
		}

Step 19: Upgrade the Collision Responses

Now that we have a boss battle to consider, we need to upgrade the function that handles bullet collisions. A special case has been added at the end of the checkCollisions function to detect when the boss has been hit. Instead of blindly destroying all enemies on the first hit, we deduct health and change the game state if the boss is destroyed.

Additionally, just for fun and to add a little extra eye-candy, the player and boss explosions have been made bigger by scattering multiple explosions near the point of impact. These two explosions are more important, from a gameplay perspective, and deserve a little extra "oomph".

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; 
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
							// accumulate score only when playing 

30
							if (thePlayer.sprite.visible) 
31
								thePlayer.score += anEntity.collidepoints; 
32
							break; 
33
						} 
34
					} 
35
				} 
36
			} 
37
			if (collided) 
38
			{ 
39
				// handle player health and possible gameover 

40
				if ((anEntity == thePlayer) || (checkMe == thePlayer)) 
41
				{ 
42
					// when the player gets damaged, they become 

43
					// invulnerable for a short perod of time 

44
					if (thePlayer.invulnerabilityTimeLeft <= 0) 
45
					{ 
46
						thePlayer.health -= anEntity.damage; 
47
						thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit; 
48
						// extra explosions for a bigger boom 

49
						var explosionPos:Point = new Point(); 
50
						for (var numExplosions:int = 0; numExplosions < 6; numExplosions++) 
51
						{ 
52
							explosionPos.x = thePlayer.sprite.position.x + fastRandom() * 64 - 32;  
53
							explosionPos.y = thePlayer.sprite.position.y + fastRandom() * 64 - 32;  
54
							particles.addExplosion(explosionPos); 
55
						} 
56
						if (thePlayer.health > 0) 
57
						{ 
58
							trace("Player was HIT!"); 
59
						} 
60
						else 
61
						{ 
62
							trace('Player was HIT... and DIED!'); 
63
							thePlayer.lives--; 
64
							// will be reset after transition 

65
							// thePlayer.health = 100; 

66
							thePlayer.invulnerabilityTimeLeft =  
67
								thePlayer.invulnerabilitySecsWhenHit + thePlayer.transitionSeconds; 
68
							thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
69
						} 
70
					} 
71
					else // we are currently invulnerable and flickering 

72
					{	// ignore the collision 

73
						collided = false; 
74
					} 
75
				} 
76
				 
77
				if (collided) // still 

78
				{ 
79
					//trace('Collision!'); 

80
					if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); 
81
					particles.addExplosion(checkMe.sprite.position); 
82
					// v6 

83
					if (anEntity == theBoss) 
84
					{ 
85
						theBoss.health -= 2; // 50 hits to destroy 

86
						trace("Boss hit. HP = " + theBoss.health); 
87
						// knockback for more vidual feedback 

88
						theBoss.sprite.position.x += 8; 
89
						if (theBoss.health < 1) 
90
						{ 
91
							trace("Boss has been destroyed!"); 
92
 
93
							// huge shockwave 

94
							particles.addParticle(spritenumShockwave, theBoss.sprite.position.x,  
95
								theBoss.sprite.position.y, 0.01, 0, 0, 1, NaN, NaN, -1, 30); 
96
							// extra explosions for a bigger boom 

97
							var bossexpPos:Point = new Point(); 
98
							for (var bossnumExps:int = 0; bossnumExps < 6; bossnumExps++) 
99
							{ 
100
								bossexpPos.x = theBoss.sprite.position.x + fastRandom() * 128 - 64;  
101
								bossexpPos.y = theBoss.sprite.position.y + fastRandom() * 128 - 64;  
102
								particles.addExplosion(bossexpPos); 
103
							}							 
104
							 
105
							theBoss.die(); 
106
							theBoss = null; 
107
							if (bossDestroyedCallback != null) 
108
								bossDestroyedCallback(); 
109
						} 
110
					} 
111
					else if ((anEntity != theOrb) && ((anEntity != thePlayer)))  
112
						anEntity.die(); // the victim 

113
					if ((checkMe != theOrb) && (checkMe != thePlayer))  
114
						checkMe.die(); // the bullet 

115
					return anEntity; 
116
				} 
117
			} 
118
			return null; 
119
		}

Step 20: Upgrade the Level Streaming

The last upgrade we need to make to EntityManager.as is a subtle change to the routine that streams level data during gameplay. In previous tutorials we made it parse the level data and spawn new tiles as old ones are scrolled off-screen.

None of this logic has changed apart from one tiny change: to enable full screen and liquid layout at any resolution, we vertically center the level data so that if the screen is larger than the available level it isn't all sitting on the very top of the screen.

This way, no matter what size screen you play the game on, the action takes place near the middle. To make this change, upgrade the streamLevelEntities function as follows:

1
 
2
		// check to see if another row from the level data should be spawned  

3
		public function streamLevelEntities(theseAreEnemies:Boolean = false):void  
4
		{ 
5
			var anEntity:Entity; 
6
			var sprID:int; 
7
			// time-based with overflow remembering (increment and floor) 

8
			levelCurrentScrollX += defaultSpeed * currentFrameSeconds; 
9
			// is it time to spawn the next col from our level data? 

10
			if (levelCurrentScrollX >= levelTilesize) 
11
			{ 
12
				levelCurrentScrollX = 0; 
13
				levelPrevCol++; 
14
				 
15
				// this prevents small "seams" due to floating point inaccuracies over time 

16
				var currentLevelXCoord:Number; 
17
				if (lastTerrainEntity && !theseAreEnemies)  
18
					currentLevelXCoord = lastTerrainEntity.sprite.position.x + levelTilesize; 
19
				else 
20
					currentLevelXCoord = maxX; 
21
				 
22
				var rows:int = level.data.length; 
23
				//trace('levelCurrentScrollX = ' + levelCurrentScrollX +  

24
				//' - spawning next level column ' + levelPrevCol + ' row count: ' + rows); 

25
								 
26
				if (level.data && level.data.length) 
27
				{ 
28
					for (var row:int = 0; row < rows; row++) 
29
					{ 
30
						if (level.data[row].length > levelPrevCol) // data exists? NOP? 

31
						{ 
32
							//trace('Next row data: ' + String(level.data[row])); 

33
							sprID = level.data[row][levelPrevCol]; 
34
							if (sprID > -1) // zero is a valid number, -1 means blank 

35
							{ 
36
								anEntity = respawn(sprID); 
37
								anEntity.sprite.position.x = currentLevelXCoord; 
38
								// this change will allow the level to be vertically centered on screen 

39
								// using liquid layout so that in full screen mode it is properly 

40
								// positioned no matter what the player's screen resolution 

41
								anEntity.sprite.position.y = (row * levelTilesize)  
42
									+ (levelTilesize/2) + levelTopOffset; // v6 

43
								//trace('Spawning a level sprite ID ' + sprID + ' at '  

44
								//	+ anEntity.sprite.position.x + ',' + anEntity.sprite.position.y); 

45
								anEntity.speedX = -defaultSpeed; 
46
								anEntity.speedY = 0; 
47
								anEntity.sprite.scaleX = defaultScale; 
48
								anEntity.sprite.scaleY = defaultScale; 
49
								 
50
								if (theseAreEnemies) 
51
								{ 
52
									// which AI should we give this enemy? 

53
									switch (sprID) 
54
									{ 
55
										case 1: 
56
										case 2: 
57
										case 3: 
58
										case 4: 
59
										case 5: 
60
										case 6: 
61
										case 7: 
62
											// move forward at a random angle 

63
											anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); 
64
											anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); 
65
											anEntity.aiFunction = anEntity.straightAI; 
66
											break; 
67
										case 8: 
68
										case 9: 
69
										case 10: 
70
										case 11: 
71
										case 12: 
72
										case 13: 
73
										case 14: 
74
										case 15: 
75
											// move straight with a wobble 

76
											anEntity.aiFunction = anEntity.wobbleAI; 
77
											break 
78
										case 16: 
79
										case 24: // sentry guns don't move + always look at the player 

80
											anEntity.aiFunction = anEntity.sentryAI; 
81
											anEntity.speedX = -90; // same speed as background 

82
											break; 
83
										case 17: 
84
										case 18: 
85
										case 19: 
86
										case 20: 
87
										case 21: 
88
										case 22: 
89
										case 23: 
90
											// move at a random angle with a wobble 

91
											anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); 
92
											anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); 
93
											anEntity.aiFunction = anEntity.wobbleAI; 
94
											break; 
95
										case 32: 
96
										case 40: 
97
										case 48: // asteroids don't move or shoot: they spin and drift 

98
											anEntity.aiFunction = null; 
99
											anEntity.rotationSpeed = fastRandom() * 8 - 4 
100
											anEntity.speedY = fastRandom() * 64 - 32; 
101
											break; 
102
										default: // follow a complex random spline curve path 

103
											anEntity.aiFunction = anEntity.droneAI; 
104
											break; 
105
									} 
106
									 
107
									anEntity.sprite.rotation = pointAtRad(anEntity.speedX,  
108
										anEntity.speedY) - (90*DEGREES_TO_RADIANS); 
109
									anEntity.collidemode = 1; 
110
									anEntity.collideradius = 16; 
111
									if (!anEntity.recycled) 
112
										allEnemies.push(anEntity); 
113
								} // end if these were enemies 

114
							}// end loop for level data rows 

115
						} 
116
					} 
117
				} 
118
				// remember the last created terrain entity 

119
				// (might be null if the level data was blank for this column) 

120
				// to avoid slight seams due to terrain scrolling speed over time 

121
				if (!theseAreEnemies) lastTerrainEntity = anEntity; 
122
			} 
123
		} 
124
	} // end class 

125
} // end package

That's it for the entity manager class upgrades. In the code snippets above we enabled out boss battle action to be detected, tweaked the way bullets are fired to support extra bullet directions, added a bit more pizazz to our explosions, and ensured that the game could be played full-screen.


Step 21: Make the Background Fullscreen

We need to make a couple very minor changes to the existing GameBackground.as class to support fullscreen liquid layout. In previous tutorials, we confined the game to a mere 400 pixels in height, and thus a single 512x512 background texture, tiled horizontally, was enough to fill the background.

In this final version, we're going to add two more rows of background tiles, above and below those in the middle of the screen, so that even at 1080p HD resolution the entire screen is filled. The changes are very minor and are marked with the //v6 code comment. Virtually everything else remains the same but because the changes are scattered around such a small file, it is included here in its entirety to avoid confusion.

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

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

4
 
5
// GameBackground.as 

6
// A very simple batch of background stars that scroll 

7
// with a subtle vertical parallax effect 

8
 
9
package 
10
{ 
11
	import flash.display.Bitmap; 
12
	import flash.display3D.*; 
13
	import flash.geom.Point; 
14
	import flash.geom.Rectangle; 
15
	 
16
	public class GameBackground extends EntityManager 
17
	{ 
18
		// how fast the stars move 

19
		public var bgSpeed:int = -1; 
20
		// the sprite sheet image 

21
		public const bgSpritesPerRow:int = 1; 
22
		public const bgSpritesPerCol:int = 1; 
23
		[Embed(source="../assets/stars.gif")] 
24
		public var bgSourceImage : Class; 
25
		 
26
		// since the image is larger than the screen we have some extra pixels to play with 

27
		public var yParallaxAmount:Number = 128; // v6 

28
		public var yOffset:Number = 0; 
29
 
30
		public function GameBackground(view:Rectangle) 
31
		{ 
32
			// run the init functions of the EntityManager class 

33
			super(view); 
34
		} 
35
		 
36
		override public function createBatch(context3D:Context3D, uvPadding:Number = 0) : LiteSpriteBatch  
37
		{ 
38
			var bgsourceBitmap:Bitmap = new bgSourceImage(); 
39
 
40
			// create a spritesheet with single giant sprite 

41
			spriteSheet = new LiteSpriteSheet(bgsourceBitmap.bitmapData, bgSpritesPerRow, bgSpritesPerCol); 
42
			 
43
			// Create new render batch  

44
			batch = new LiteSpriteBatch(context3D, spriteSheet); 
45
			 
46
			return batch; 
47
		} 
48
 
49
		override public function setPosition(view:Rectangle):void  
50
		{ 
51
			// allow moving fully offscreen before looping around 

52
			maxX = 256+512+512+512+512; 
53
			minX = -256; 
54
			maxY = view.height; 
55
			minY = view.y; 
56
			yParallaxAmount = 128; // v6 

57
			yOffset = (maxY / 2) + (-1 * yParallaxAmount * 0.5); // v6 

58
		} 
59
		 
60
		// for this test, create random entities that move  

61
		// from right to left with random speeds and scales 

62
		public function initBackground():void  
63
		{ 
64
			// we need several 512x512 sprites  

65
			var anEntity1:Entity = respawn(0) 
66
			anEntity1.sprite.position.x = 256; 
67
			anEntity1.sprite.position.y = maxY / 2; 
68
			anEntity1.speedX = bgSpeed; 
69
			var anEntity2:Entity = respawn(0) 
70
			anEntity2.sprite.position.x = 256+512; 
71
			anEntity2.sprite.position.y = maxY / 2; 
72
			anEntity2.speedX = bgSpeed; 
73
			var anEntity3:Entity = respawn(0) 
74
			anEntity3.sprite.position.x = 256+512+512; 
75
			anEntity3.sprite.position.y = maxY / 2; 
76
			anEntity3.speedX = bgSpeed; 
77
			// v6  

78
			var anEntity4:Entity = respawn(0) 
79
			anEntity4.sprite.position.x = 256+512+512+512; 
80
			anEntity4.sprite.position.y = maxY / 2; 
81
			anEntity4.speedX = bgSpeed; 
82
			var anEntity5:Entity = respawn(0) 
83
			anEntity5.sprite.position.x = 256+512+512+512+512; 
84
			anEntity5.sprite.position.y = maxY / 2; 
85
			anEntity5.speedX = bgSpeed; 
86
			 
87
			// upper row 

88
			var anEntity1a:Entity = respawn(0) 
89
			anEntity1a.sprite.position.x = 256; 
90
			anEntity1a.sprite.position.y = maxY / 2 + 512; 
91
			anEntity1a.speedX = bgSpeed; 
92
			var anEntity2a:Entity = respawn(0) 
93
			anEntity2a.sprite.position.x = 256+512; 
94
			anEntity2a.sprite.position.y = maxY / 2 + 512; 
95
			anEntity2a.speedX = bgSpeed; 
96
			var anEntity3a:Entity = respawn(0) 
97
			anEntity3a.sprite.position.x = 256+512+512; 
98
			anEntity3a.sprite.position.y = maxY / 2 + 512; 
99
			anEntity3a.speedX = bgSpeed; 
100
			var anEntity4a:Entity = respawn(0) 
101
			anEntity4a.sprite.position.x = 256+512+512+512; 
102
			anEntity4a.sprite.position.y = maxY / 2 + 512; 
103
			anEntity4a.speedX = bgSpeed; 
104
			var anEntity5a:Entity = respawn(0) 
105
			anEntity5a.sprite.position.x = 256+512+512+512+512; 
106
			anEntity5a.sprite.position.y = maxY / 2 + 512; 
107
			anEntity5a.speedX = bgSpeed; 
108
			 
109
			// lower row 

110
			var anEntity1b:Entity = respawn(0) 
111
			anEntity1b.sprite.position.x = 256; 
112
			anEntity1b.sprite.position.y = maxY / 2 - 512; 
113
			anEntity1b.speedX = bgSpeed; 
114
			var anEntity2b:Entity = respawn(0) 
115
			anEntity2b.sprite.position.x = 256+512; 
116
			anEntity2b.sprite.position.y = maxY / 2 - 512; 
117
			anEntity2b.speedX = bgSpeed; 
118
			var anEntity3b:Entity = respawn(0) 
119
			anEntity3b.sprite.position.x = 256+512+512; 
120
			anEntity3b.sprite.position.y = maxY / 2 - 512; 
121
			anEntity3b.speedX = bgSpeed; 
122
			var anEntity4b:Entity = respawn(0) 
123
			anEntity4b.sprite.position.x = 256+512+512+512; 
124
			anEntity4b.sprite.position.y = maxY / 2 - 512; 
125
			anEntity4b.speedX = bgSpeed; 
126
			var anEntity5b:Entity = respawn(0) 
127
			anEntity5b.sprite.position.x = 256+512+512+512+512; 
128
			anEntity5b.sprite.position.y = maxY / 2 - 512; 
129
			anEntity5b.speedX = bgSpeed; 
130
		} 
131
		 
132
		// scroll slightly up or down to give more parallax 

133
		public function yParallax(OffsetPercent:Number = 0) : void 
134
		{ 
135
			yOffset = (maxY / 2) + (-1 * yParallaxAmount * OffsetPercent); // v6 

136
		} 
137
		 
138
		// called every frame: used to update the scrolling background 

139
		override public function update(currentTime:Number) : void 
140
		{		 
141
			var anEntity:Entity; 
142
			 
143
			// handle all other entities 

144
			for(var i:int=0; i<entityPool.length;i++) 
145
			{ 
146
				anEntity = entityPool[i]; 
147
				if (anEntity.active) 
148
				{ 
149
					anEntity.sprite.position.x += anEntity.speedX; 
150
					anEntity.sprite.position.y = yOffset; 
151
					// upper row // v6 

152
					if (i > 9) anEntity.sprite.position.y += 512; 
153
					// lower row // v6 

154
					else if (i > 4) anEntity.sprite.position.y -= 512; 
155
 
156
					if (anEntity.sprite.position.x >= maxX) 
157
					{ 
158
						anEntity.sprite.position.x = minX; 
159
					} 
160
					else if (anEntity.sprite.position.x <= minX) 
161
					{ 
162
						anEntity.sprite.position.x = maxX; 
163
					} 
164
				} 
165
			} 
166
		} 
167
	} // end class 

168
} // end package

Step 22: Autofire!

Based on beta playtesting user feedback, we're going to add the capability to enable AUTO-FIRE to our game. There are two primary reasons for doing so. One, because the majority of users simply hold down the space bar the entire time they are playing anyways. Two, because of security restrictions in the full screen mode of Flash which disable any typing on the keyboard apart from the arrow keys.

The reason that Flash won't allow full screen .SWFs to access the entire keyboard is that they could be used as keyloggers: surrepticiously recording keystrokes or faking a bank login page by drawing normal-looking web browser chrome in a "phishing" scam.

In future version of Flash (11.3 and beyond) it is technically possible to have full keyboard input in fullscreen games, but it will force users to confirm with a pop-up security warning. This gives a bad impression, but regardless, the vast majory of players (right now) won't have the latest version of Flash installed.

It should be noted that the arrow keys and the space bar are allowed in regular full screen mode, but sadly most PC keyboards are incapable of registering left+up+space at the same time. This means that although we could turn off auto-fire and go full screen and simply use the arrow keys and the space bar, any time players tried to move up and back while firing the computer would beep and movement would stop. Not all keyboards suffer from this technical constraint but standard cheap ones do.

In light of these deficiances, and to simplify the gameplay experience to the most essential aspect of the game, we are going to enable autofire during play. The code is set up so that you can easily turn it off or on depending on your needs.

Begin by opening the existing GameControls.as class and adding one extra class variable near the top as follows:

1
 
2
	// v6 - autofire during gameplay 

3
	public var autofire:Boolean = false;

Now tweak one line in the lostFocus function:

1
 
2
		pressing.fire = autofire; // v6

Finally, add one new line at the very bottom of the keyHandler function:

1
 
2
		// override the actual event response 

3
		if (autofire) pressing.fire = true; // v6

Step 23: Embed the Voiceovers

Now that we've added a boss and an NPC character to our game, lets give them some sound effects. This will increase the production values of our game a little, and should give both characters a little more personality.

Record some fun voiceovers in the sound editing program of your choosing (Audacity, CoolEditPro, etc.) Remember that we want to record in high quality (44.1khz) but save as low quality mp3 files (11khz, mono) so that our SWF doesn't get too big.

The voiceovers I created for our demo game are a bit cheesy and were recorded in a single take, but they will suffice for our purposes. Feel free to laugh at my silly pitch-shifted voice. You can play them in your browser just for fun:

  1. sfxboss.mp3
  2. sfxNPCwelcome.mp3
  3. sfxNPCdeath.mp3
  4. sfxNPCboss.mp3
  5. sfxNPCnextlevel.mp3
  6. sfxNPCgameover.mp3
  7. sfxNPCthanks.mp3

Once you're happy with your new voiceovers, embed them in the GameSound.as file. Simply add the new sounds to the top of the class alongside all the other MP3 files from before:

1
 
2
		// v6 - boss and NPC mission-giving character 

3
		[Embed (source = "../assets/sfxboss.mp3")] 
4
		private var _bossMp3:Class; 
5
		private var _bossSound:Sound = (new _bossMp3) as Sound; 
6
		[Embed (source = "../assets/sfxNPCdeath.mp3")] 
7
		private var _NPCdeathMp3:Class; 
8
		private var _NPCdeathSound:Sound = (new _NPCdeathMp3) as Sound; 
9
		[Embed (source = "../assets/sfxNPCboss.mp3")] 
10
		private var _NPCbossMp3:Class; 
11
		private var _NPCbossSound:Sound = (new _NPCbossMp3) as Sound; 
12
		[Embed (source = "../assets/sfxNPCwelcome.mp3")] 
13
		private var _NPCwelcomeMp3:Class; 
14
		private var _NPCwelcomeSound:Sound = (new _NPCwelcomeMp3) as Sound; 
15
		[Embed (source = "../assets/sfxNPCnextlevel.mp3")] 
16
		private var _NPCnextlevelMp3:Class; 
17
		private var _NPCnextlevelSound:Sound = (new _NPCnextlevelMp3) as Sound; 
18
		[Embed (source = "../assets/sfxNPCgameover.mp3")] 
19
		private var _NPCgameoverMp3:Class; 
20
		private var _NPCgameoverSound:Sound = (new _NPCgameoverMp3) as Sound; 
21
		[Embed (source = "../assets/sfxNPCthanks.mp3")] 
22
		private var _NPCthanksMp3:Class; 
23
		private var _NPCthanksSound:Sound = (new _NPCthanksMp3) as Sound;

Step 24: Voiceover Trigger Functions

Continuing with GameSound.as, create functions that we will use during gameplay to trigger the new sounds as follows:

1
 
2
		public function playBoss():void 
3
		{ 
4
			_bossSound.play(); 
5
		} 
6
		 
7
		public function playNPCdeath():void 
8
		{ 
9
			_NPCdeathSound.play(); 
10
		} 
11
 
12
		public function playNPCboss():void 
13
		{ 
14
			_NPCbossSound.play(); 
15
		} 
16
 
17
		public function playNPCwelcome():void 
18
		{ 
19
			_NPCwelcomeSound.play(); 
20
		} 
21
 
22
		public function playNPCnextlevel():void 
23
		{ 
24
			_NPCnextlevelSound.play(); 
25
		} 
26
 
27
		public function playNPCgameover():void 
28
		{ 
29
			_NPCgameoverSound.play(); 
30
		} 
31
 
32
		public function playNPCthanks():void 
33
		{ 
34
			_NPCthanksSound.play(); 
35
		}

Step 25: Upgrade the Game Class

We've finished upgrading all the supplementary classes used by our game. All we need to do now is enable this enhanced functionality in the game by upgrading our primary game class. The majority of this file remains unchanged since last time, but there are 27 different minor edits to make. Search for the // v6 code comment which points out each change.

Open the existing Main.as file and begin by adding one new import, tweaking the player speed and adding few new class variables:

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

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.display.StageDisplayState; // v6 for fullscreen 

16
	import flash.events.Event; 
17
	import flash.events.ErrorEvent; 
18
	import flash.events.MouseEvent; 
19
	import flash.geom.Rectangle; 
20
	import flash.utils.getTimer; 
21
	import flash.geom.Point; 
22
		 
23
	public class Main extends Sprite  
24
	{ 
25
		// v6 fill the entire screen for HD gaming 

26
		public var enableFullscreen:Boolean = true; 
27
 
28
		// v6 players generally hold down the fire button anyway 

29
		// plus in fullscreen only arrow keys can be relied upon 

30
		public var enableAutofire:Boolean = true; 
31
 
32
		// v6 this allows for SLOW-MO and fast forward 

33
		public var timeDilation:Number = 1; 
34
		 
35
		// the game save/load system 

36
		public var saved:GameSaves; 
37
		 
38
		// the entity spritesheet (ships, particles) 

39
		[Embed(source="../assets/sprites.png")] 
40
		private var EntitySourceImage : Class; 
41
 
42
		// the terrain spritesheet 

43
		[Embed(source="../assets/terrain.png")] 
44
		private var TerrainSourceImage : Class; 
45
		 
46
		// the keyboard control system 

47
		private var _controls : GameControls; 
48
		// don't update the menu too fast 

49
		private var nothingPressedLastFrame:Boolean = false; 
50
		// timestamp of the current frame 

51
		public var currentTime:int; 
52
		// for framerate independent speeds 

53
		public var currentFrameMs:int; 
54
		public var previousFrameTime:int; 
55
		 
56
		// player one's entity 

57
		public var thePlayer:Entity; 
58
		// v6 movement speed in pixels per second 

59
		public var playerSpeed:Number = 180; 
60
		// timestamp when next shot can be fired 

61
		private var nextFireTime:uint = 0; 
62
		// how many ms between shots 

63
		private var fireDelay:uint = 200; 
64
		 
65
		// main menu = 0 or current level number 

66
		private var _state:int = 0; 
67
		// the title screen batch 

68
		private var _mainmenu:GameMenu; 
69
		// the sound system 

70
		private var _sfx:GameSound;	 
71
		// the background stars 

72
		private var _bg:GameBackground;	 
73
		 
74
		private var _terrain:EntityManager; 
75
		private var _entities:EntityManager; 
76
		private var _spriteStage:LiteSpriteStage; 
77
		private var _gui:GameGUI; 
78
		private var _width:Number = 600; 
79
		private var _height:Number = 400; 
80
		public var context3D:Context3D;

Step 26: Upgrade the Inits

There is only one minor change to make to all the init functions in Main.as. We simply fill in the one new GUI variable that will eventually list what level we are on. Before the game begins, we list the game version instead.

1
 
2
		// constructor function for our game 

3
		public function Main():void  
4
		{ 
5
			if (stage) init(); 
6
			else addEventListener(Event.ADDED_TO_STAGE, init); 
7
		} 
8
		 
9
		// called once flash is ready 

10
		private function init(e:Event = null):void  
11
		{ 
12
			_controls = new GameControls(stage); 
13
			removeEventListener(Event.ADDED_TO_STAGE, init); 
14
			stage.quality = StageQuality.LOW; 
15
			stage.align = StageAlign.TOP_LEFT; 
16
			stage.scaleMode = StageScaleMode.NO_SCALE; 
17
			stage.addEventListener(Event.RESIZE, onResizeEvent); 
18
			trace("Init Stage3D..."); 
19
			_gui = new GameGUI(""); 
20
			_gui.statsText = "Kaizen v1.6"; // v6 

21
			addChild(_gui); 
22
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); 
23
			stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); 
24
			stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); 
25
			trace("Stage3D requested...");		 
26
			_sfx = new GameSound(); 
27
		} 
28
				 
29
		// this is called when the 3d card has been set up 

30
		// and is ready for rendering using stage3d 

31
		private function onContext3DCreate(e:Event):void  
32
		{ 
33
			trace("Stage3D context created! Init sprite engine..."); 
34
			context3D = stage.stage3Ds[0].context3D; 
35
			initSpriteEngine(); 
36
		} 
37
		 
38
		// this can be called when using an old version of flash 

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

40
		private function errorHandler(e:ErrorEvent):void  
41
		{ 
42
			trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); 
43
		}

Step 27: Ensure Liquid Layout

Since we need to be able to adapt the game to fit any screen resolution, we need to tweak the onResizeEvent function such that it calls the setPosition function on more of our classes, so that each in turn can move things around as appropriate. Continuing with Main.as:

1
 
2
		protected function onResizeEvent(event:Event) : void // v6 

3
		{ 
4
			trace("resize event..."); 
5
			 
6
			// Set correct dimensions if we resize 

7
			_width = stage.stageWidth; 
8
			_height = stage.stageHeight;  
9
			 
10
			// Resize Stage3D to continue to fit screen 

11
			var view:Rectangle = new Rectangle(0, 0, _width, _height); 
12
			if ( _spriteStage != null ) { 
13
				_spriteStage.position = view; 
14
			} 
15
			if (_terrain != null) { 
16
				_terrain.setPosition(view); 
17
			} 
18
			if (_entities != null) { 
19
				_entities.setPosition(view); 
20
			} 
21
			if (_mainmenu != null) { 
22
				_mainmenu.setPosition(view); 
23
			} 
24
			if (_bg != null) { 
25
				_bg.setPosition(view); 
26
			} 
27
			if (_gui != null) 
28
				_gui.setPosition(view); 
29
		} 
30
		 
31
		private function initSpriteEngine():void  
32
		{ 
33
			// this forces the game to fill the screen 

34
			onResizeEvent(null); // v6 

35
 
36
			// init a gpu sprite system 

37
			var stageRect:Rectangle = new Rectangle(0, 0, _width, _height);  
38
			_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); 
39
			_spriteStage.configureBackBuffer(_width,_height); 
40
			 
41
			// create the background stars 

42
			trace("Init background..."); 
43
			_bg = new GameBackground(stageRect); 
44
			_bg.createBatch(context3D); 
45
			_spriteStage.addBatch(_bg.batch); 
46
			_bg.initBackground(); 
47
			 
48
			// create the terrain spritesheet and batch 

49
			trace("Init Terrain..."); 
50
			_terrain = new EntityManager(stageRect); 
51
			_terrain.SourceImage = TerrainSourceImage; 
52
			_terrain.SpritesPerRow = 16; 
53
			_terrain.SpritesPerCol = 16; 
54
			_terrain.defaultSpeed = 90; 
55
			_terrain.defaultScale = 1.5;  
56
			_terrain.levelTilesize = 48; 
57
			_terrain.createBatch(context3D, 0.001); // a little UV padding required 

58
			_spriteStage.addBatch(_terrain.batch); 
59
			_terrain.changeLevels('terrain' + _state); 
60
 
61
			// create a single rendering batch 

62
			// which will draw all sprites in one pass 

63
			trace("Init Entities..."); 
64
			_entities = new EntityManager(stageRect); 
65
			_entities.SourceImage = EntitySourceImage; 
66
			_entities.defaultScale = 1.5;  
67
			_entities.levelTilesize = 48; 
68
			_entities.createBatch(context3D, 0.0005); // UV padding required // v6 

69
			_entities.sfx = _sfx; 
70
			_spriteStage.addBatch(_entities.batch); 
71
			_entities.changeLevels('level' + _state); 
72
			_entities.streamLevelEntities(true); // spawn first row of the level immediately 

73
			 
74
			// create the logo/titlescreen main menu 

75
			_mainmenu = new GameMenu(stageRect); 
76
			_mainmenu.createBatch(context3D); 
77
			_spriteStage.addBatch(_mainmenu.batch); 
78
			 
79
			// tell the gui where to grab statistics from 

80
			_gui.statsTarget = _entities;  
81
			 
82
			// start the render loop 

83
			stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); 
84
 
85
			// only used for the menu 

86
			stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);    
87
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);  
88
 
89
			// set up the savegame system 

90
			saved = new GameSaves(); 
91
			_gui.highScore = saved.score; 
92
			_gui.level = saved.level; 
93
 
94
			// this forces the game to fill the screen 

95
			onResizeEvent(null); // v6 

96
		}

Step 28: Begin the Boss Battle

It is finally time to add boss battles to the game! Out new boss sprite, pictured above, will pose a significant challenge to the player due to its complex firing pattern of green machine-gun shots from the front and a circular burst every few seconds:


To implement our new boss battle system, we will need to create two new functions. One will initialize a new boss battle, and the other is a callback function that will be triggered when the boss is destroyed.

Our old spritesheet system assumed that all sprites in our texture are the same size. Since the boss is much bigger, we need to manually define it using the proper pixel coordinates of sprites.png.

We also need to give our new boss the proper AI function as created above, ensure that it is in the middle of the screen, and give it enough health to take a significant amount of damage before blowing up. Because the boss is a special case in the entity manager, we tell it which sprite it is so that it can be treated differently in the collision response function.

Add this new function to Main.as as follows:

1
 
2
		// v6 initialize a boss battle 

3
		private var bossSpriteID:uint = 0; 
4
		private function bossBattle():void 
5
		{ 
6
			trace("Boss battle begins!"); 
7
			// a special sprite that is larger than the rest 

8
			if (!bossSpriteID) bossSpriteID = _entities.spriteSheet.defineSprite(160, 128, 96, 96); // v6 

9
			var anEntity:Entity; 
10
			anEntity = _entities.respawn(bossSpriteID); 
11
			anEntity.sprite.position.x = _width + 64; 
12
			anEntity.sprite.position.y = _height / 2; 
13
			anEntity.sprite.scaleX = anEntity.sprite.scaleY = 2; // v6 

14
			anEntity.aiFunction = anEntity.bossAI; 
15
			anEntity.isBoss = true; 
16
			anEntity.collideradius = 96; 
17
			anEntity.collidemode = 1; 
18
			_gui.addChild(_gui.bosshealthTf); 
19
			anEntity.health = 100; 
20
			// ensure that our bullets can hit it 

21
			if (!anEntity.recycled) 
22
				_entities.allEnemies.push(anEntity); 
23
			_entities.theBoss = anEntity; 
24
			_entities.bossDestroyedCallback = bossComplete; 
25
		} 
26
		 
27
/sourcecode]</pre>  
28
 
29
<hr /> 
30
<h2><span>Step 29:</span> End the Boss Battle</h2> 
31
 
32
 When the boss is destroyed, the following callback function is executed. It gives the player some more points as a reward for surviving such a harrowing experience, removes the boss health bar from the screen, informs the entity manager that there is no longer a boss to contend with, and reverts the game state back to a regular level number (the current level plus one).   
33
 
34
 The number 999 is used here because boss battles are a special state for the game: regular levels are numbered 1 to 999 and boss battles occur in-between levels and thus are given a special state of 1000 plus whatever the current level is. For example, the boss at the end of level 2 sets the game state to 1002, and when destroyed the game switches state to 3 - the next level.  
35
 
36
<pre>[sourcecode language="actionscript3"] 
37
		// the entity manager calls this when a boss is destroyed 

38
		public function bossComplete():void 
39
		{ 
40
			trace("bossComplete!"); 
41
 
42
			thePlayer.score += 1000; 
43
				 
44
			// remove the boss health bar 

45
			if (_gui.contains(_gui.bosshealthTf)) 
46
				_gui.removeChild(_gui.bosshealthTf); 
47
				 
48
			// so next time we get a fresh one 

49
			_entities.theBoss = null; 
50
			 
51
			// remove the +1000 boss battle state  

52
			// and add one so that we go to the next level 

53
			_state -= 999; 
54
			// trigger a "level complete" transition 

55
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
56
		}

Step 30: Upgrade the Game Transitions

In the previous tutorial, we created a simple state-driven game transition handler. It would announce the upcoming level or display a game over message as appropriate. Much of this function remains unchanged, except we are going to upgrade it to include the new boss battle announcements as well as the NPC voiceover sounds and subtitles. We're also going to add a fun and simple effect to player deaths: SLOW MOTION. Continuing with Main.as, modify the handleTransitions function 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) && (_state < 1000)) // v6 

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
							timeDilation = 0.5; // slow mo 

30
							 
31
							// v6 

32
							if (_gui.npcText == "") _sfx.playNPCthanks(); 
33
							_gui.npcText = "You saved us!\nThank you!\nMy hero!"; 
34
						} 
35
						else if (_state == 0) 
36
						{ 
37
							_gui.transitionText = "GAME OVER\nYou got to level " + thePlayer.level  
38
								+ "\nwith " + thePlayer.score + " points."; 
39
							 
40
							if (_gui.npcText == "") _sfx.playNPCgameover(); 
41
							_gui.npcText = "You were incredible.\nThere were simply too many of them.\nYou'll win next time. I know it.";	 
42
							 
43
							timeDilation = 0.5; // slow mo 

44
						} 
45
						else if (_state > 1) 
46
						{ 
47
							_gui.transitionText = "\nLEVEL " + (_state-1) + " COMPLETE!"; 
48
 
49
							if (_gui.npcText == "") _sfx.playNPCnextlevel(); 
50
							_gui.npcText = "That was amazing!\nYou destroyed it!\nYour skill is legendary.";	 
51
						} 
52
						else 
53
						{ 
54
							_gui.transitionText = "\nLEVEL " + _state;  
55
 
56
							if (_gui.npcText == "") _sfx.playNPCwelcome(); 
57
							_gui.npcText = "We're under attack! Please help us!\nYou're our only hope for survival.\nUse the arrow keys to move.";	 
58
						} 
59
					} 
60
					else // must be a death or boss battle 

61
					{ 
62
						if ((_state > 1000) && (thePlayer.health > 0)) // v6 

63
						{ 
64
							_gui.transitionText = "\nINCOMING BOSS BATTLE!"; 
65
 
66
							if (_gui.npcText == "") _sfx.playNPCboss(); 
67
							_gui.npcText = "Be careful! That ship is HUGE!\nKeep moving and watch out for\nany burst attacks. Good luck!"; 
68
						} 
69
						else 
70
						{ 
71
							_gui.transitionText = "Your ship was destroyed.\n\nYou have "  
72
								+ thePlayer.lives + (thePlayer.lives != 1 ? " lives" : " life") + " left."; 
73
 
74
							if (_gui.npcText == "") _sfx.playNPCdeath(); 
75
							_gui.npcText = "Nooooo!\nDon't give up! I believe in you!\nYou can do it."; 
76
							 
77
							timeDilation = 0.5; // slow mo 

78
						} 
79
					} 
80
					if (thePlayer.lives < 0 || thePlayer.health <= 0) 
81
					{ 
82
						// during the death transition, spawn tons of explosions just for fun 

83
						if (_entities.fastRandom() < 0.2) 
84
						{ 
85
							var explosionPos:Point = new Point(); 
86
							explosionPos.x = thePlayer.sprite.position.x + _entities.fastRandom() * 128 - 64;  
87
							explosionPos.y = thePlayer.sprite.position.y + _entities.fastRandom() * 128 - 64;  
88
							_entities.particles.addExplosion(explosionPos); 
89
						} 
90
					} 
91
				} 
92
				else // transition time has elapsed 

93
				{ 
94
					_gui.npcText = ""; // v6 

95
					timeDilation = 1; // turn off slow-mo 

96
					currentTransitionSeconds = 0; 
97
					 
98
					thePlayer.transitionTimeLeft = 0; 
99
					 
100
					if (_state == -1) _state = 0; 
101
					_gui.transitionTf.scrollRect = new Rectangle(0,0,600,160); 
102
					_gui.transitionText = ""; 
103
					 
104
					if ((thePlayer.health <= 0) && (_state != 0)) // we died 

105
					{ 
106
						trace("Death transition over. Respawning player."); 
107
						thePlayer.sprite.position.y = _entities.midpoint; 
108
						thePlayer.sprite.position.x = 64; 
109
						thePlayer.health = 100; 
110
						// failed to kill boss: 

111
						if (_state > 1000) 
112
						{ 
113
							trace('Filed to kill boss. Resetting.'); 
114
							_state -= 1000; 
115
							_gui.bosshealth = -999; 
116
							// remove the boss health bar 

117
							if (_gui.contains(_gui.bosshealthTf)) 
118
								_gui.removeChild(_gui.bosshealthTf); 
119
							// remove the boss itself 

120
							if (_entities.theBoss) 
121
							{ 
122
								_entities.theBoss.die(); 
123
								_entities.theBoss = null; 
124
							} 
125
						} 
126
						// start the level again 

127
						_entities.changeLevels('level' + _state); 
128
						_terrain.changeLevels('terrain' + _state); 
129
					} 
130
					if ((thePlayer.level != _state) && (_state < 1000)) 
131
					{ 
132
						trace('Level transition over. Starting level ' + _state); 
133
						thePlayer.level = _state; 
134
						if (_state > 1) // no need to reload at startGame 

135
						{ 
136
							_entities.changeLevels('level' + _state); 
137
							_terrain.changeLevels('terrain' + _state); 
138
							_gui.statsText = "Level " + _state; // v6 

139
						} 
140
						if (_state == 0) // game over 

141
						{ 
142
							trace('Game Over transition over: starting main menu'); 
143
							thePlayer.health = 100; 
144
							thePlayer.lives = 3; 
145
							thePlayer.sprite.visible = false; 
146
							_entities.theOrb.sprite.visible = false; 
147
							_entities.changeLevels('level' + _state); 
148
							_terrain.changeLevels('terrain' + _state); 
149
							_spriteStage.addBatch(_mainmenu.batch); 
150
							_gui.statsText = "GAME OVER"; // v6 

151
							_gui.bosshealth = 0; 
152
							// remove the boss health bar if any 

153
							if (_gui.contains(_gui.bosshealthTf)) 
154
								_gui.removeChild(_gui.bosshealthTf); 
155
							// go back to normal size 

156
							if (enableFullscreen) 
157
							{ 
158
								trace('Leaving fullscreen...'); 
159
								stage.displayState = StageDisplayState.NORMAL; 
160
							}							 
161
						} 
162
					} 
163
				} 
164
			} 
165
		}

The next few functions (playerLogic, mouseDown, mouseMove, processInput) all remain unchanged since last time and are not included here.


Step 31: Go Fullscreen

Since we're going to be going fullscreen after the player presses the start button, we need to upgrade the stageGame function as follows:

1
 
2
 
3
		private function startGame():void 
4
		{ 
5
			trace("Starting game!"); 
6
			 
7
			_state = 1; 
8
			_spriteStage.removeBatch(_mainmenu.batch); 
9
			_sfx.playMusic(); 
10
			 
11
			if (enableAutofire) // v6 

12
			{ 
13
				_controls.autofire = true;  
14
			} 
15
			 
16
			// v6 fullscreen mode! 

17
			// Note: security blocks keyboard except  

18
			// arrows and space, so WASD keys don't work... 

19
			// also pressing left+up+space doesn't work on  

20
			// normal keyboards (therefore we implemented autofire) 

21
			if (enableFullscreen) 
22
			{ 
23
				try 
24
				{ 
25
					trace('Going fullscreen...'); 
26
					// remember to add this to your HTML: 

27
					// <param name="allowFullScreen" value="true" /> 

28
					stage.displayState = StageDisplayState.FULL_SCREEN; 
29
				} 
30
				catch (err:Error) 
31
				{ 
32
					trace("Error going fullscreen."); 
33
				} 
34
				// in Flash 11.3 (summer 2012) you can use the following 

35
				// for full keyboard access but it asks the user for permission first 

36
				// stage.displayState = StageDisplayState.FULL_SCREEN_INTERACTIVE; 

37
				// you also need to add this to your html 

38