Advertisement

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

by

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:

 
// Stage3D Shoot-em-up Tutorial Part 6 
// by Christer Kaitila - www.mcfunkypants.com 
// Created for active.tutsplus.com 
 
// Preloader.as 
// displays a progress bar while 
// the swf is being downloaded 
package  
{ 
	[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] 
	 
	import flash.display.Bitmap; 
	import flash.display.DisplayObject; 
	import flash.display.MovieClip; 
	import flash.events.Event; 
	import flash.events.ProgressEvent; 
	import flash.text.Font; 
	import flash.utils.getDefinitionByName; 
	import flash.display.Sprite; 
	import flash.text.TextField; 
	 
	// Force the 3d game to be on frame two 
	 
	// In FlashDevelop, add this to your compiler command-line: 
	// Project > Properties >  
	// Compiler Options >  
	// Additional Compiler Options: 
	// -frame main Main 
	 
	// In Flex, uncomment this line: 
	// [frame (factoryClass="Main")] 
	 
	public class Preloader extends MovieClip  
	{ 
		private var preloader_square:Sprite = new Sprite(); 
		private var preloader_border:Sprite = new Sprite(); 
		private var preloader_text:TextField = new TextField(); 
		 
		public function Preloader()  
		{ 
			addEventListener(Event.ENTER_FRAME, checkFrame); 
			 
			loaderInfo.addEventListener( 
				ProgressEvent.PROGRESS, progress); 
 
			addChild(preloader_square); 
			preloader_square.x = 200; 
			preloader_square.y = stage.stageHeight / 2; 
			 
			addChild(preloader_border); 
			preloader_border.x = 200-4; 
			preloader_border.y = stage.stageHeight / 2 - 4; 
		 
			addChild(preloader_text); 
			preloader_text.x = 194; 
			preloader_text.y = stage.stageHeight / 2 - 30; 
			preloader_text.width = 256; 
			 
		}

Step 5: Animate the Progress Bar

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

 
		private function progress(e:ProgressEvent):void  
		{ 
			// update loader 
			preloader_square.graphics.beginFill(0xAAAAAA); 
			preloader_square.graphics.drawRect(0, 0, 
				(loaderInfo.bytesLoaded / loaderInfo.bytesTotal) 
				* 200,20); 
			preloader_square.graphics.endFill(); 
			 
			preloader_border.graphics.lineStyle(2,0xDDDDDD); 
			preloader_border.graphics.drawRect(0, 0, 208, 28); 
			 
			preloader_text.textColor = 0xAAAAAA; 
			preloader_text.text = "Loaded " + Math.ceil( 
				(loaderInfo.bytesLoaded /  
				loaderInfo.bytesTotal)*100) + "% (" + 
				+ loaderInfo.bytesLoaded + " of " +  
				loaderInfo.bytesTotal + " bytes)"; 
			 
		} 
		 
		private function checkFrame(e:Event):void  
		{ 
			if (currentFrame == totalFrames)  
			//if (loaderInfo.bytesLoaded >= loaderInfo.bytesTotal) 
			{ 
				removeEventListener(Event.ENTER_FRAME, checkFrame); 
				preloader_startup(); 
			} 
		} 
		 
		private function preloader_startup():void  
		{ 
			// stop loader 
			stop(); 
			loaderInfo.removeEventListener( 
				ProgressEvent.PROGRESS, progress); 
			// remove progress bar 
			if (contains(preloader_square))  
				removeChild(preloader_square); 
			if (contains(preloader_border))  
				removeChild(preloader_border); 
			if (contains(preloader_text))  
				removeChild(preloader_text); 
			// start the game 
			var mainClass:Class =  
				getDefinitionByName("Main") 
				as Class; 
			addChild(new mainClass() as DisplayObject); 
		} 
		 
	} // end class 
} // 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:

 
// Stage3D Shoot-em-up Tutorial Part 6 
// by Christer Kaitila - www.mcfunkypants.com 
 
// Entity.as 
// The Entity class will eventually hold all game-specific entity stats 
// for the spaceships, bullets and effects in our game. 
// It stores a reference to a gpu sprite and a few demo properties. 
// This is where you would add hit points, weapons, ability scores, etc. 
// This class handles any AI (artificial intelligence) for enemies as well. 
 
package 
{ 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	 
	public class Entity 
	{ 
		// v6 if this is a boss, when it dies the game state (level) increases 
		public var isBoss:Boolean = false; 
		// used by the boss battles for "burst, delay, burst" firing 
		public var burstTimerStart:Number = 0; 
		public var burstTimerEnd:Number = 0; 
		public var burstPauseTime:Number = 2; 
		public var burstLength:Number = 2; 
		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:

 
	// v6 // boss battle: stay on the screen 
	public function bossAI(seconds:Number):void 
	{ 
		age += seconds; 
		 
		// spammy with breaks in between 
		if (age > burstTimerStart) 
		{ 
			if (age > burstTimerEnd) 
			{ 
				// one final "circle burst" 
				for (var deg:int = 0; deg < 20; deg++) 
				{ 
					gfx.shootBullet(1, this, deg * 18 * gfx.DEGREES_TO_RADIANS); 
					gfx.sfx.playBoss(); 
				} 
				burstTimerStart = age + burstPauseTime; 
				burstTimerEnd = burstTimerStart + burstLength; 
			} 
			else 
			{ 
				maybeShoot(2, burstShootInterval, burstShootInterval); 
			} 
		} 
		 
		if (gfx.thePlayer) 
		{ 
			// point at player 
			sprite.rotation = gfx.pointAtRad( 
				gfx.thePlayer.sprite.position.x - sprite.position.x, 
				gfx.thePlayer.sprite.position.y - sprite.position.y)  
				- (90 * gfx.DEGREES_TO_RADIANS); 
				 
			// slowly move to a good spot: 256 pixels to the right of the player 
			speedX = (gfx.thePlayer.sprite.position.x + 256 - sprite.position.x); 
			aiPathOffsetY = (Math.sin(age) / Math.PI) * 256; 
		} 
	}

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.

 
public var bosshealthTf:TextField; // v6 
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.

 
			// v6 - a boss health meter 
			bosshealthTf = new TextField(); 
			bosshealthTf.defaultTextFormat = myFormatCENTER; 
			bosshealthTf.embedFonts = true; 
			bosshealthTf.x = 0; 
			bosshealthTf.y = 48; 
			bosshealthTf.selectable = false; 
			bosshealthTf.antiAliasType = 'advanced'; 
			bosshealthTf.text = "BOSS: |||||||||||||"; 
			bosshealthTf.filters = [new GlowFilter(0xFF0000, 1, 8, 8, 4, 2)]; 
			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:

 
		private function healthBar(num:int):String // v6 
		{ 
			if (num >= 99) return "|||||||||||||"; 
			else if (num >= 92) return "||||||||||||"; 
			else if (num >= 84) return "|||||||||||"; 
			else if (num >= 76) return "||||||||||"; 
			else if (num >= 68) return "|||||||||"; 
			else if (num >= 60) return "||||||||"; 
			else if (num >= 52) return "|||||||"; 
			else if (num >= 44) return "||||||"; 
			else if (num >= 36) return "|||||"; 
			else if (num >= 28) return "||||"; 
			else if (num >= 20) return "|||"; 
			else if (num >= 12) return "||"; 
			else return "|"; 
		}

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.

 
		[Embed (source = "../assets/npc_overlay.png")]  
		private var npcOverlayData:Class; 
		private var npcOverlay:Bitmap = new npcOverlayData(); 
 
		public var npcTf:TextField; // v6 
		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.

 
			// v6 - an NPC "mission text" character 
			npcTf = new TextField(); 
			npcTf.defaultTextFormat = myFormat; 
			npcTf.embedFonts = true; 
			npcTf.x = 0; 
			npcTf.y = 400-64; 
			npcTf.selectable = false; 
			npcTf.antiAliasType = 'advanced'; 
			npcTf.text = ""; 
			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:

 
		public function setPosition(view:Rectangle):void // v6 
		{ 
			trace('Moving GUI'); 
			var mid:Number = view.width / 2; 
			hudOverlay.x = mid - hudOverlay.width / 2; 
			debugStatsTf.x = mid - 300 + 18; 
			scoreTf.x = mid - 300 + 442; 
			highScoreTf.x = mid - 300 + 208; 
			healthTf.x = mid - 300 + 208; 
			bosshealthTf.x = mid - 300; 
			bosshealthTf.y = 48; 
			transitionTf.y = view.height / 2 - 80; 
			transitionTf.x = mid - 300; 
			npcOverlay.x = mid - npcOverlay.width / 2; // v6 
			npcOverlay.y = view.height - npcOverlay.height - 8; // v6 
			npcTf.y = view.height - 64; // v6 
			npcTf.x = mid - 220; // v6 
		}

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:

 
		// only updates textfields if they have changed 
		private function updateScore():void 
		{ 
			// NPC dialog toggle // v6 
			if (npcText != npcTf.text) 
			{ 
				npcTf.text = npcText; 
				if (npcText != "") 
				{ 
					if (!contains(npcOverlay)) 
						addChild(npcOverlay);					 
					if (!contains(npcTf)) 
						addChild(npcTf);					 
				} 
				else 
				{ 
					if (contains(npcOverlay)) 
						removeChild(npcOverlay);					 
					if (contains(npcTf)) 
						removeChild(npcTf);					 
				} 
			} 
			 
			if (transitionText != transitionTf.text) 
			{ 
				transitionTf.text = transitionText; 
				if (transitionTf.text != "") 
				{ 
					if (!contains(transitionTf)) 
						addChild(transitionTf); 
				} 
				else 
				{ 
					if (contains(transitionTf)) 
						removeChild(transitionTf); 
				} 
			} 
			 
			if (statsTarget && statsTarget.thePlayer) 
			{ 
				// v6 optional boss health meter 
				if (statsTarget.theBoss) 
				{ 
					if (bosshealth != statsTarget.theBoss.health) 
					{ 
						bosshealth = statsTarget.theBoss.health; 
						bosshealthTf.text = "BOSS: " + healthBar(bosshealth); 
					} 
				} 
				 
				if (health != statsTarget.thePlayer.health) 
				{ 
					health = statsTarget.thePlayer.health; 
					healthTf.text = "HP: " + healthBar(health); 
				} 
				if ((score != statsTarget.thePlayer.score) || (lives != statsTarget.thePlayer.lives)) 
				{ 
					score = statsTarget.thePlayer.score; 
					lives = statsTarget.thePlayer.lives; 
					if (lives == -1) 
						scoreTf.text = scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' +'GAME OVER'; 
					else 
						scoreTf.text = 'SCORE: ' + pad0s(score) + '\n' + lives +  
							(lives != 1 ? ' LIVES' : ' LIFE') + ' LEFT'; 
					// we may be beating the high score right now 
					if (score > highScore) highScore = score; 
				} 
			} 
			if (prevHighScore != highScore) 
			{ 
				prevHighScore = highScore; 
				highScoreTf.text = "HIGH SCORE: " + pad0s(highScore); 
			} 
		} 
		 
		private function onEnterFrame(evt:Event):void 
		{ 
			timer = getTimer(); 
			 
			updateScore(); 
			 
			if( timer - 1000 > ms_prev ) 
			{ 
				lastfps = Math.round(frameCount/(timer-ms_prev)*1000); 
				ms_prev = timer; 
 
 
				// v6 - we don't want sprite or memory stats in the "final" version 
				/* 
				var mem:Number = Number((System.totalMemory * 0.000000954).toFixed(2)); // v6 
				// grab the stats from the entity manager 
				if (statsTarget) 
				{ 
					statsText =  
						statsTarget.numCreated + '/' + 
						statsTarget.numReused + ' sprites'; 
				} 
				debugStatsTf.text = titleText + lastfps + 'FPS - ' + mem + 'MB' + '\n' + statsText; 
				*/ 
				debugStatsTf.text = titleText + lastfps + ' FPS\n' + statsText; 
				frameCount = 0; 
			} 
		 
			// count each frame to determine the framerate 
			frameCount++; 
				 
		} 
	} // end class 
} // 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:

 
	public class EntityManager 
	{ 
		// v6 - the boss entity if it exists 
		public var theBoss:Entity; 
		// v6 - function that is run when the boss is killed 
		public var bossDestroyedCallback:Function = null; 
		// v6 how fast the default scroll (enemy flying) speed is 
		public var defaultSpeed:Number = 160; 
		// v6 how fast player bullets go per second 
		public var playerBulletSpeed:Number = 300; 
		// v6 how fast enemy bullets go per second 
		public var enemyBulletSpeed:Number = 200; 
		// v6 how big the bullet sprites are 
		public var bulletScale:Number = 1; 
		// v6 used to enable full screen liquid layout 
		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:

 
		public function setPosition(view:Rectangle):void  
		{ 
			// allow moving fully offscreen before 
			// automatically being culled (and reused) 
			maxX = view.width + cullingDistance; 
			minX = view.x - cullingDistance; 
			maxY = view.height + cullingDistance; 
			minY = view.y - cullingDistance; 
			midpoint = view.height / 2; 
			// during fullscreen, we may have more screen than 
			// the level data would fill: to avoid everything being 
			// at the top of the screen, center the level 
			levelTopOffset = midpoint - 200; // v6 
		}

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:

 
		// search the entity pool for unused entities and reuse one 
		// if they are all in use, create a brand new one 
		public function respawn(sprID:uint=0):Entity 
		{ 
			var currentEntityCount:int = entityPool.length; 
			var anEntity:Entity; 
			var i:int = 0; 
			// search for an inactive entity 
			for (i = 0; i < currentEntityCount; i++ )  
			{ 
				anEntity = entityPool[i]; 
				if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) 
				{ 
					//trace('Reusing Entity #' + i); 
					anEntity.active = true; 
					anEntity.sprite.visible = true; 
					anEntity.recycled = true; 
					anEntity.age = 0; // v6 
					anEntity.burstTimerStart = 0; // v6 
					anEntity.burstTimerEnd = 0; // v6 
					anEntity.fireTime = 0; // v6 
					numReused++; 
					return anEntity; 
				} 
			} 
			// none were found so we need to make a new one 
			//trace('Need to create a new Entity #' + i); 
			var sprite:LiteSprite; 
			sprite = batch.createChild(sprID); 
			anEntity = new Entity(sprite, this); 
			anEntity.age = 0; // v6 
			anEntity.burstTimerStart = 0; // v6 
			anEntity.burstTimerEnd = 0; // v6 
			anEntity.fireTime = 0; // v6 
			entityPool.push(anEntity); 
			numCreated++; 
			return anEntity; 
		}

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.

 
		// shoot a bullet 
		public function shootBullet(powa:uint=1, shooter:Entity = null, angle:Number = NaN):Entity // v6 
		{ 
			// just in case the AI is running during the main menu 
			// and we've not yet created the player entity 
			if (thePlayer == null) return null; 
 
			var theBullet:Entity; 
			// assume the player shot it 
			// otherwise maybe an enemy did 
			if (shooter == null)  
				shooter = thePlayer; 
				 
			// three possible bullets, progressively larger 
			if (powa == 1)  
				theBullet = respawn(spritenumBullet1); 
			else if (powa == 2)  
				theBullet = respawn(spritenumBullet2); 
			else  
				theBullet = respawn(spritenumBullet3); 
			theBullet.sprite.position.x = shooter.sprite.position.x + 8; 
			theBullet.sprite.position.y = shooter.sprite.position.y + 2; 
			//theBullet.sprite.rotation = 180 * DEGREES_TO_RADIANS; // v6 fixed in the spritesheet 
			theBullet.sprite.scaleX = theBullet.sprite.scaleY = bulletScale;  // v6 
			if (shooter == thePlayer) 
			{ 
				theBullet.speedX = playerBulletSpeed; // v6 
				theBullet.speedY = 0; 
			} 
			else // enemy bullets move slower and towards the player // v6 UNLESS SPECIFIED 
			{ 
				if (isNaN(angle)) 
				{ 
					theBullet.sprite.rotation =  
						pointAtRad(theBullet.sprite.position.x - thePlayer.sprite.position.x, 
							theBullet.sprite.position.y - thePlayer.sprite.position.y)  
							- (90 * DEGREES_TO_RADIANS); 
				} 
				else 
				{ 
					theBullet.sprite.rotation = angle; 
				} 
				 
				// move in the direction we're facing // v6 
				theBullet.speedX = enemyBulletSpeed*Math.cos(theBullet.sprite.rotation); 
				theBullet.speedY = enemyBulletSpeed*Math.sin(theBullet.sprite.rotation); 
    
				// optionally, we could just fire straight ahead in the direction we're heading: 
				// theBullet.speedX = shooter.speedX * 1.5; 
				// theBullet.speedY = shooter.speedY * 1.5; 
				// and we could point where we're going like this: 
				// pointAtRad(theBullet.speedX,theBullet.speedY) - (90*DEGREES_TO_RADIANS); 
			} 
			theBullet.owner = shooter; 
			theBullet.collideradius = 10; 
			theBullet.collidemode = 1; 
			theBullet.isBullet = true; 
			if (!theBullet.recycled) 
				allBullets.push(theBullet); 
			return theBullet; 
		}

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

 
		// as an optimization to save millions of checks, only 
		// the player's bullets check for collisions with all enemy ships 
		// (enemy bullets only check to hit the player) 
		public function checkCollisions(checkMe:Entity):Entity 
		{ 
			var anEntity:Entity; 
			var collided:Boolean = false; 
			if (!thePlayer) return null; 
			 
			if (checkMe.owner != thePlayer) 
			{	// quick check ONLY to see if we have hit the player 
				anEntity = thePlayer; 
				if (checkMe.colliding(anEntity))  
				{ 
					collided = true; 
				} 
			} 
			else // check all active enemies 
			{ 
				for(var i:int=0; i< allEnemies.length;i++) 
				{ 
					anEntity = allEnemies[i]; 
					if (anEntity.active && anEntity.collidemode) 
					{ 
						if (checkMe.colliding(anEntity))  
						{ 
							collided = true; 
							// accumulate score only when playing 
							if (thePlayer.sprite.visible) 
								thePlayer.score += anEntity.collidepoints; 
							break; 
						} 
					} 
				} 
			} 
			if (collided) 
			{ 
				// handle player health and possible gameover 
				if ((anEntity == thePlayer) || (checkMe == thePlayer)) 
				{ 
					// when the player gets damaged, they become 
					// invulnerable for a short perod of time 
					if (thePlayer.invulnerabilityTimeLeft <= 0) 
					{ 
						thePlayer.health -= anEntity.damage; 
						thePlayer.invulnerabilityTimeLeft = thePlayer.invulnerabilitySecsWhenHit; 
						// extra explosions for a bigger boom 
						var explosionPos:Point = new Point(); 
						for (var numExplosions:int = 0; numExplosions < 6; numExplosions++) 
						{ 
							explosionPos.x = thePlayer.sprite.position.x + fastRandom() * 64 - 32;  
							explosionPos.y = thePlayer.sprite.position.y + fastRandom() * 64 - 32;  
							particles.addExplosion(explosionPos); 
						} 
						if (thePlayer.health > 0) 
						{ 
							trace("Player was HIT!"); 
						} 
						else 
						{ 
							trace('Player was HIT... and DIED!'); 
							thePlayer.lives--; 
							// will be reset after transition 
							// thePlayer.health = 100; 
							thePlayer.invulnerabilityTimeLeft =  
								thePlayer.invulnerabilitySecsWhenHit + thePlayer.transitionSeconds; 
							thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
						} 
					} 
					else // we are currently invulnerable and flickering 
					{	// ignore the collision 
						collided = false; 
					} 
				} 
				 
				if (collided) // still 
				{ 
					//trace('Collision!'); 
					if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); 
					particles.addExplosion(checkMe.sprite.position); 
					// v6 
					if (anEntity == theBoss) 
					{ 
						theBoss.health -= 2; // 50 hits to destroy 
						trace("Boss hit. HP = " + theBoss.health); 
						// knockback for more vidual feedback 
						theBoss.sprite.position.x += 8; 
						if (theBoss.health < 1) 
						{ 
							trace("Boss has been destroyed!"); 
 
							// huge shockwave 
							particles.addParticle(spritenumShockwave, theBoss.sprite.position.x,  
								theBoss.sprite.position.y, 0.01, 0, 0, 1, NaN, NaN, -1, 30); 
							// extra explosions for a bigger boom 
							var bossexpPos:Point = new Point(); 
							for (var bossnumExps:int = 0; bossnumExps < 6; bossnumExps++) 
							{ 
								bossexpPos.x = theBoss.sprite.position.x + fastRandom() * 128 - 64;  
								bossexpPos.y = theBoss.sprite.position.y + fastRandom() * 128 - 64;  
								particles.addExplosion(bossexpPos); 
							}							 
							 
							theBoss.die(); 
							theBoss = null; 
							if (bossDestroyedCallback != null) 
								bossDestroyedCallback(); 
						} 
					} 
					else if ((anEntity != theOrb) && ((anEntity != thePlayer)))  
						anEntity.die(); // the victim 
					if ((checkMe != theOrb) && (checkMe != thePlayer))  
						checkMe.die(); // the bullet 
					return anEntity; 
				} 
			} 
			return null; 
		}

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:

 
		// check to see if another row from the level data should be spawned  
		public function streamLevelEntities(theseAreEnemies:Boolean = false):void  
		{ 
			var anEntity:Entity; 
			var sprID:int; 
			// time-based with overflow remembering (increment and floor) 
			levelCurrentScrollX += defaultSpeed * currentFrameSeconds; 
			// is it time to spawn the next col from our level data? 
			if (levelCurrentScrollX >= levelTilesize) 
			{ 
				levelCurrentScrollX = 0; 
				levelPrevCol++; 
				 
				// this prevents small "seams" due to floating point inaccuracies over time 
				var currentLevelXCoord:Number; 
				if (lastTerrainEntity && !theseAreEnemies)  
					currentLevelXCoord = lastTerrainEntity.sprite.position.x + levelTilesize; 
				else 
					currentLevelXCoord = maxX; 
				 
				var rows:int = level.data.length; 
				//trace('levelCurrentScrollX = ' + levelCurrentScrollX +  
				//' - spawning next level column ' + levelPrevCol + ' row count: ' + rows); 
								 
				if (level.data && level.data.length) 
				{ 
					for (var row:int = 0; row < rows; row++) 
					{ 
						if (level.data[row].length > levelPrevCol) // data exists? NOP? 
						{ 
							//trace('Next row data: ' + String(level.data[row])); 
							sprID = level.data[row][levelPrevCol]; 
							if (sprID > -1) // zero is a valid number, -1 means blank 
							{ 
								anEntity = respawn(sprID); 
								anEntity.sprite.position.x = currentLevelXCoord; 
								// this change will allow the level to be vertically centered on screen 
								// using liquid layout so that in full screen mode it is properly 
								// positioned no matter what the player's screen resolution 
								anEntity.sprite.position.y = (row * levelTilesize)  
									+ (levelTilesize/2) + levelTopOffset; // v6 
								//trace('Spawning a level sprite ID ' + sprID + ' at '  
								//	+ anEntity.sprite.position.x + ',' + anEntity.sprite.position.y); 
								anEntity.speedX = -defaultSpeed; 
								anEntity.speedY = 0; 
								anEntity.sprite.scaleX = defaultScale; 
								anEntity.sprite.scaleY = defaultScale; 
								 
								if (theseAreEnemies) 
								{ 
									// which AI should we give this enemy? 
									switch (sprID) 
									{ 
										case 1: 
										case 2: 
										case 3: 
										case 4: 
										case 5: 
										case 6: 
										case 7: 
											// move forward at a random angle 
											anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); 
											anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); 
											anEntity.aiFunction = anEntity.straightAI; 
											break; 
										case 8: 
										case 9: 
										case 10: 
										case 11: 
										case 12: 
										case 13: 
										case 14: 
										case 15: 
											// move straight with a wobble 
											anEntity.aiFunction = anEntity.wobbleAI; 
											break 
										case 16: 
										case 24: // sentry guns don't move + always look at the player 
											anEntity.aiFunction = anEntity.sentryAI; 
											anEntity.speedX = -90; // same speed as background 
											break; 
										case 17: 
										case 18: 
										case 19: 
										case 20: 
										case 21: 
										case 22: 
										case 23: 
											// move at a random angle with a wobble 
											anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); 
											anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); 
											anEntity.aiFunction = anEntity.wobbleAI; 
											break; 
										case 32: 
										case 40: 
										case 48: // asteroids don't move or shoot: they spin and drift 
											anEntity.aiFunction = null; 
											anEntity.rotationSpeed = fastRandom() * 8 - 4 
											anEntity.speedY = fastRandom() * 64 - 32; 
											break; 
										default: // follow a complex random spline curve path 
											anEntity.aiFunction = anEntity.droneAI; 
											break; 
									} 
									 
									anEntity.sprite.rotation = pointAtRad(anEntity.speedX,  
										anEntity.speedY) - (90*DEGREES_TO_RADIANS); 
									anEntity.collidemode = 1; 
									anEntity.collideradius = 16; 
									if (!anEntity.recycled) 
										allEnemies.push(anEntity); 
								} // end if these were enemies 
							}// end loop for level data rows 
						} 
					} 
				} 
				// remember the last created terrain entity 
				// (might be null if the level data was blank for this column) 
				// to avoid slight seams due to terrain scrolling speed over time 
				if (!theseAreEnemies) lastTerrainEntity = anEntity; 
			} 
		} 
	} // end class 
} // 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.

 
// Stage3D Shoot-em-up Tutorial Part 6 
// by Christer Kaitila - www.mcfunkypants.com 
 
// GameBackground.as 
// A very simple batch of background stars that scroll 
// with a subtle vertical parallax effect 
 
package 
{ 
	import flash.display.Bitmap; 
	import flash.display3D.*; 
	import flash.geom.Point; 
	import flash.geom.Rectangle; 
	 
	public class GameBackground extends EntityManager 
	{ 
		// how fast the stars move 
		public var bgSpeed:int = -1; 
		// the sprite sheet image 
		public const bgSpritesPerRow:int = 1; 
		public const bgSpritesPerCol:int = 1; 
		[Embed(source="../assets/stars.gif")] 
		public var bgSourceImage : Class; 
		 
		// since the image is larger than the screen we have some extra pixels to play with 
		public var yParallaxAmount:Number = 128; // v6 
		public var yOffset:Number = 0; 
 
		public function GameBackground(view:Rectangle) 
		{ 
			// run the init functions of the EntityManager class 
			super(view); 
		} 
		 
		override public function createBatch(context3D:Context3D, uvPadding:Number = 0) : LiteSpriteBatch  
		{ 
			var bgsourceBitmap:Bitmap = new bgSourceImage(); 
 
			// create a spritesheet with single giant sprite 
			spriteSheet = new LiteSpriteSheet(bgsourceBitmap.bitmapData, bgSpritesPerRow, bgSpritesPerCol); 
			 
			// Create new render batch  
			batch = new LiteSpriteBatch(context3D, spriteSheet); 
			 
			return batch; 
		} 
 
		override public function setPosition(view:Rectangle):void  
		{ 
			// allow moving fully offscreen before looping around 
			maxX = 256+512+512+512+512; 
			minX = -256; 
			maxY = view.height; 
			minY = view.y; 
			yParallaxAmount = 128; // v6 
			yOffset = (maxY / 2) + (-1 * yParallaxAmount * 0.5); // v6 
		} 
		 
		// for this test, create random entities that move  
		// from right to left with random speeds and scales 
		public function initBackground():void  
		{ 
			// we need several 512x512 sprites  
			var anEntity1:Entity = respawn(0) 
			anEntity1.sprite.position.x = 256; 
			anEntity1.sprite.position.y = maxY / 2; 
			anEntity1.speedX = bgSpeed; 
			var anEntity2:Entity = respawn(0) 
			anEntity2.sprite.position.x = 256+512; 
			anEntity2.sprite.position.y = maxY / 2; 
			anEntity2.speedX = bgSpeed; 
			var anEntity3:Entity = respawn(0) 
			anEntity3.sprite.position.x = 256+512+512; 
			anEntity3.sprite.position.y = maxY / 2; 
			anEntity3.speedX = bgSpeed; 
			// v6  
			var anEntity4:Entity = respawn(0) 
			anEntity4.sprite.position.x = 256+512+512+512; 
			anEntity4.sprite.position.y = maxY / 2; 
			anEntity4.speedX = bgSpeed; 
			var anEntity5:Entity = respawn(0) 
			anEntity5.sprite.position.x = 256+512+512+512+512; 
			anEntity5.sprite.position.y = maxY / 2; 
			anEntity5.speedX = bgSpeed; 
			 
			// upper row 
			var anEntity1a:Entity = respawn(0) 
			anEntity1a.sprite.position.x = 256; 
			anEntity1a.sprite.position.y = maxY / 2 + 512; 
			anEntity1a.speedX = bgSpeed; 
			var anEntity2a:Entity = respawn(0) 
			anEntity2a.sprite.position.x = 256+512; 
			anEntity2a.sprite.position.y = maxY / 2 + 512; 
			anEntity2a.speedX = bgSpeed; 
			var anEntity3a:Entity = respawn(0) 
			anEntity3a.sprite.position.x = 256+512+512; 
			anEntity3a.sprite.position.y = maxY / 2 + 512; 
			anEntity3a.speedX = bgSpeed; 
			var anEntity4a:Entity = respawn(0) 
			anEntity4a.sprite.position.x = 256+512+512+512; 
			anEntity4a.sprite.position.y = maxY / 2 + 512; 
			anEntity4a.speedX = bgSpeed; 
			var anEntity5a:Entity = respawn(0) 
			anEntity5a.sprite.position.x = 256+512+512+512+512; 
			anEntity5a.sprite.position.y = maxY / 2 + 512; 
			anEntity5a.speedX = bgSpeed; 
			 
			// lower row 
			var anEntity1b:Entity = respawn(0) 
			anEntity1b.sprite.position.x = 256; 
			anEntity1b.sprite.position.y = maxY / 2 - 512; 
			anEntity1b.speedX = bgSpeed; 
			var anEntity2b:Entity = respawn(0) 
			anEntity2b.sprite.position.x = 256+512; 
			anEntity2b.sprite.position.y = maxY / 2 - 512; 
			anEntity2b.speedX = bgSpeed; 
			var anEntity3b:Entity = respawn(0) 
			anEntity3b.sprite.position.x = 256+512+512; 
			anEntity3b.sprite.position.y = maxY / 2 - 512; 
			anEntity3b.speedX = bgSpeed; 
			var anEntity4b:Entity = respawn(0) 
			anEntity4b.sprite.position.x = 256+512+512+512; 
			anEntity4b.sprite.position.y = maxY / 2 - 512; 
			anEntity4b.speedX = bgSpeed; 
			var anEntity5b:Entity = respawn(0) 
			anEntity5b.sprite.position.x = 256+512+512+512+512; 
			anEntity5b.sprite.position.y = maxY / 2 - 512; 
			anEntity5b.speedX = bgSpeed; 
		} 
		 
		// scroll slightly up or down to give more parallax 
		public function yParallax(OffsetPercent:Number = 0) : void 
		{ 
			yOffset = (maxY / 2) + (-1 * yParallaxAmount * OffsetPercent); // v6 
		} 
		 
		// called every frame: used to update the scrolling background 
		override public function update(currentTime:Number) : void 
		{		 
			var anEntity:Entity; 
			 
			// handle all other entities 
			for(var i:int=0; i<entityPool.length;i++) 
			{ 
				anEntity = entityPool[i]; 
				if (anEntity.active) 
				{ 
					anEntity.sprite.position.x += anEntity.speedX; 
					anEntity.sprite.position.y = yOffset; 
					// upper row // v6 
					if (i > 9) anEntity.sprite.position.y += 512; 
					// lower row // v6 
					else if (i > 4) anEntity.sprite.position.y -= 512; 
 
					if (anEntity.sprite.position.x >= maxX) 
					{ 
						anEntity.sprite.position.x = minX; 
					} 
					else if (anEntity.sprite.position.x <= minX) 
					{ 
						anEntity.sprite.position.x = maxX; 
					} 
				} 
			} 
		} 
	} // end class 
} // 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:

 
	// v6 - autofire during gameplay 
	public var autofire:Boolean = false;

Now tweak one line in the lostFocus function:

 
		pressing.fire = autofire; // v6

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

 
		// override the actual event response 
		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:

 
		// v6 - boss and NPC mission-giving character 
		[Embed (source = "../assets/sfxboss.mp3")] 
		private var _bossMp3:Class; 
		private var _bossSound:Sound = (new _bossMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCdeath.mp3")] 
		private var _NPCdeathMp3:Class; 
		private var _NPCdeathSound:Sound = (new _NPCdeathMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCboss.mp3")] 
		private var _NPCbossMp3:Class; 
		private var _NPCbossSound:Sound = (new _NPCbossMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCwelcome.mp3")] 
		private var _NPCwelcomeMp3:Class; 
		private var _NPCwelcomeSound:Sound = (new _NPCwelcomeMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCnextlevel.mp3")] 
		private var _NPCnextlevelMp3:Class; 
		private var _NPCnextlevelSound:Sound = (new _NPCnextlevelMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCgameover.mp3")] 
		private var _NPCgameoverMp3:Class; 
		private var _NPCgameoverSound:Sound = (new _NPCgameoverMp3) as Sound; 
		[Embed (source = "../assets/sfxNPCthanks.mp3")] 
		private var _NPCthanksMp3:Class; 
		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:

 
		public function playBoss():void 
		{ 
			_bossSound.play(); 
		} 
		 
		public function playNPCdeath():void 
		{ 
			_NPCdeathSound.play(); 
		} 
 
		public function playNPCboss():void 
		{ 
			_NPCbossSound.play(); 
		} 
 
		public function playNPCwelcome():void 
		{ 
			_NPCwelcomeSound.play(); 
		} 
 
		public function playNPCnextlevel():void 
		{ 
			_NPCnextlevelSound.play(); 
		} 
 
		public function playNPCgameover():void 
		{ 
			_NPCgameoverSound.play(); 
		} 
 
		public function playNPCthanks():void 
		{ 
			_NPCthanksSound.play(); 
		}

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:

 
// Stage3D Shoot-em-up Tutorial Part 6 
// by Christer Kaitila - www.mcfunkypants.com 
// Created for active.tutsplus.com 
 
package  
{ 
	[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] 
 
	import flash.display3D.*; 
	import flash.display.Sprite; 
	import flash.display.StageAlign; 
	import flash.display.StageQuality; 
	import flash.display.StageScaleMode; 
	import flash.display.StageDisplayState; // v6 for fullscreen 
	import flash.events.Event; 
	import flash.events.ErrorEvent; 
	import flash.events.MouseEvent; 
	import flash.geom.Rectangle; 
	import flash.utils.getTimer; 
	import flash.geom.Point; 
		 
	public class Main extends Sprite  
	{ 
		// v6 fill the entire screen for HD gaming 
		public var enableFullscreen:Boolean = true; 
 
		// v6 players generally hold down the fire button anyway 
		// plus in fullscreen only arrow keys can be relied upon 
		public var enableAutofire:Boolean = true; 
 
		// v6 this allows for SLOW-MO and fast forward 
		public var timeDilation:Number = 1; 
		 
		// the game save/load system 
		public var saved:GameSaves; 
		 
		// the entity spritesheet (ships, particles) 
		[Embed(source="../assets/sprites.png")] 
		private var EntitySourceImage : Class; 
 
		// the terrain spritesheet 
		[Embed(source="../assets/terrain.png")] 
		private var TerrainSourceImage : Class; 
		 
		// the keyboard control system 
		private var _controls : GameControls; 
		// don't update the menu too fast 
		private var nothingPressedLastFrame:Boolean = false; 
		// timestamp of the current frame 
		public var currentTime:int; 
		// for framerate independent speeds 
		public var currentFrameMs:int; 
		public var previousFrameTime:int; 
		 
		// player one's entity 
		public var thePlayer:Entity; 
		// v6 movement speed in pixels per second 
		public var playerSpeed:Number = 180; 
		// timestamp when next shot can be fired 
		private var nextFireTime:uint = 0; 
		// how many ms between shots 
		private var fireDelay:uint = 200; 
		 
		// main menu = 0 or current level number 
		private var _state:int = 0; 
		// the title screen batch 
		private var _mainmenu:GameMenu; 
		// the sound system 
		private var _sfx:GameSound;	 
		// the background stars 
		private var _bg:GameBackground;	 
		 
		private var _terrain:EntityManager; 
		private var _entities:EntityManager; 
		private var _spriteStage:LiteSpriteStage; 
		private var _gui:GameGUI; 
		private var _width:Number = 600; 
		private var _height:Number = 400; 
		public var context3D:Context3D;

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.

 
		// constructor function for our game 
		public function Main():void  
		{ 
			if (stage) init(); 
			else addEventListener(Event.ADDED_TO_STAGE, init); 
		} 
		 
		// called once flash is ready 
		private function init(e:Event = null):void  
		{ 
			_controls = new GameControls(stage); 
			removeEventListener(Event.ADDED_TO_STAGE, init); 
			stage.quality = StageQuality.LOW; 
			stage.align = StageAlign.TOP_LEFT; 
			stage.scaleMode = StageScaleMode.NO_SCALE; 
			stage.addEventListener(Event.RESIZE, onResizeEvent); 
			trace("Init Stage3D..."); 
			_gui = new GameGUI(""); 
			_gui.statsText = "Kaizen v1.6"; // v6 
			addChild(_gui); 
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); 
			stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); 
			stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); 
			trace("Stage3D requested...");		 
			_sfx = new GameSound(); 
		} 
				 
		// this is called when the 3d card has been set up 
		// and is ready for rendering using stage3d 
		private function onContext3DCreate(e:Event):void  
		{ 
			trace("Stage3D context created! Init sprite engine..."); 
			context3D = stage.stage3Ds[0].context3D; 
			initSpriteEngine(); 
		} 
		 
		// this can be called when using an old version of flash 
		// or if the html does not include wmode=direct 
		private function errorHandler(e:ErrorEvent):void  
		{ 
			trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); 
		}

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:

 
		protected function onResizeEvent(event:Event) : void // v6 
		{ 
			trace("resize event..."); 
			 
			// Set correct dimensions if we resize 
			_width = stage.stageWidth; 
			_height = stage.stageHeight;  
			 
			// Resize Stage3D to continue to fit screen 
			var view:Rectangle = new Rectangle(0, 0, _width, _height); 
			if ( _spriteStage != null ) { 
				_spriteStage.position = view; 
			} 
			if (_terrain != null) { 
				_terrain.setPosition(view); 
			} 
			if (_entities != null) { 
				_entities.setPosition(view); 
			} 
			if (_mainmenu != null) { 
				_mainmenu.setPosition(view); 
			} 
			if (_bg != null) { 
				_bg.setPosition(view); 
			} 
			if (_gui != null) 
				_gui.setPosition(view); 
		} 
		 
		private function initSpriteEngine():void  
		{ 
			// this forces the game to fill the screen 
			onResizeEvent(null); // v6 
 
			// init a gpu sprite system 
			var stageRect:Rectangle = new Rectangle(0, 0, _width, _height);  
			_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); 
			_spriteStage.configureBackBuffer(_width,_height); 
			 
			// create the background stars 
			trace("Init background..."); 
			_bg = new GameBackground(stageRect); 
			_bg.createBatch(context3D); 
			_spriteStage.addBatch(_bg.batch); 
			_bg.initBackground(); 
			 
			// create the terrain spritesheet and batch 
			trace("Init Terrain..."); 
			_terrain = new EntityManager(stageRect); 
			_terrain.SourceImage = TerrainSourceImage; 
			_terrain.SpritesPerRow = 16; 
			_terrain.SpritesPerCol = 16; 
			_terrain.defaultSpeed = 90; 
			_terrain.defaultScale = 1.5;  
			_terrain.levelTilesize = 48; 
			_terrain.createBatch(context3D, 0.001); // a little UV padding required 
			_spriteStage.addBatch(_terrain.batch); 
			_terrain.changeLevels('terrain' + _state); 
 
			// create a single rendering batch 
			// which will draw all sprites in one pass 
			trace("Init Entities..."); 
			_entities = new EntityManager(stageRect); 
			_entities.SourceImage = EntitySourceImage; 
			_entities.defaultScale = 1.5;  
			_entities.levelTilesize = 48; 
			_entities.createBatch(context3D, 0.0005); // UV padding required // v6 
			_entities.sfx = _sfx; 
			_spriteStage.addBatch(_entities.batch); 
			_entities.changeLevels('level' + _state); 
			_entities.streamLevelEntities(true); // spawn first row of the level immediately 
			 
			// create the logo/titlescreen main menu 
			_mainmenu = new GameMenu(stageRect); 
			_mainmenu.createBatch(context3D); 
			_spriteStage.addBatch(_mainmenu.batch); 
			 
			// tell the gui where to grab statistics from 
			_gui.statsTarget = _entities;  
			 
			// start the render loop 
			stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); 
 
			// only used for the menu 
			stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);    
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);  
 
			// set up the savegame system 
			saved = new GameSaves(); 
			_gui.highScore = saved.score; 
			_gui.level = saved.level; 
 
			// this forces the game to fill the screen 
			onResizeEvent(null); // v6 
		}

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:

 
		// v6 initialize a boss battle 
		private var bossSpriteID:uint = 0; 
		private function bossBattle():void 
		{ 
			trace("Boss battle begins!"); 
			// a special sprite that is larger than the rest 
			if (!bossSpriteID) bossSpriteID = _entities.spriteSheet.defineSprite(160, 128, 96, 96); // v6 
			var anEntity:Entity; 
			anEntity = _entities.respawn(bossSpriteID); 
			anEntity.sprite.position.x = _width + 64; 
			anEntity.sprite.position.y = _height / 2; 
			anEntity.sprite.scaleX = anEntity.sprite.scaleY = 2; // v6 
			anEntity.aiFunction = anEntity.bossAI; 
			anEntity.isBoss = true; 
			anEntity.collideradius = 96; 
			anEntity.collidemode = 1; 
			_gui.addChild(_gui.bosshealthTf); 
			anEntity.health = 100; 
			// ensure that our bullets can hit it 
			if (!anEntity.recycled) 
				_entities.allEnemies.push(anEntity); 
			_entities.theBoss = anEntity; 
			_entities.bossDestroyedCallback = bossComplete; 
		} 
		 
/sourcecode]</pre>  
 
<hr /> 
<h2><span>Step 29:</span> End the Boss Battle</h2> 
 
 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).   
 
 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.  
 
<pre>[sourcecode language="actionscript3"] 
		// the entity manager calls this when a boss is destroyed 
		public function bossComplete():void 
		{ 
			trace("bossComplete!"); 
 
			thePlayer.score += 1000; 
				 
			// remove the boss health bar 
			if (_gui.contains(_gui.bosshealthTf)) 
				_gui.removeChild(_gui.bosshealthTf); 
				 
			// so next time we get a fresh one 
			_entities.theBoss = null; 
			 
			// remove the +1000 boss battle state  
			// and add one so that we go to the next level 
			_state -= 999; 
			// trigger a "level complete" transition 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
		}

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:

 
		// check player transition state (deaths, game over, etc) 
		private var currentTransitionSeconds:Number = 0; 
		private function handleTransitions(seconds:Number):void 
		{ 
			// are we at a pending transition (death or level change)? 
			if (thePlayer.transitionTimeLeft > 0) 
			{ 
				currentTransitionSeconds += seconds; 
				 
				thePlayer.transitionTimeLeft -= seconds; 
				 
				if (thePlayer.transitionTimeLeft > 0) 
				{	//was it a level change? 
					if ((thePlayer.level != _state) && (_state < 1000)) // v6 
					{ 
						if (_state == -1) 
						{ 
							_gui.transitionText = "\n\n\n\n\n\nCONGRATULATIONS\n\n" + 
								"You fought bravely and defended\n" + 
								"the universe from certain doom.\n\nYou got to level " +  
								thePlayer.level + "\nwith " + thePlayer.score + " points." +  
								"\n\nCREDITS:\n\nProgramming: McFunkypants\n(mcfunkypants.com)\n\n" + 
								"Art: Daniel Cook\n(lostgarden.com)\n\n" + 
								"Music: MaF\n(maf464.com)\n\n" + 
								"Thanks for playing!"; 
							_gui.transitionTf.scrollRect = new Rectangle(0, currentTransitionSeconds * 40, 600, 160); 
							 
							timeDilation = 0.5; // slow mo 
							 
							// v6 
							if (_gui.npcText == "") _sfx.playNPCthanks(); 
							_gui.npcText = "You saved us!\nThank you!\nMy hero!"; 
						} 
						else if (_state == 0) 
						{ 
							_gui.transitionText = "GAME OVER\nYou got to level " + thePlayer.level  
								+ "\nwith " + thePlayer.score + " points."; 
							 
							if (_gui.npcText == "") _sfx.playNPCgameover(); 
							_gui.npcText = "You were incredible.\nThere were simply too many of them.\nYou'll win next time. I know it.";	 
							 
							timeDilation = 0.5; // slow mo 
						} 
						else if (_state > 1) 
						{ 
							_gui.transitionText = "\nLEVEL " + (_state-1) + " COMPLETE!"; 
 
							if (_gui.npcText == "") _sfx.playNPCnextlevel(); 
							_gui.npcText = "That was amazing!\nYou destroyed it!\nYour skill is legendary.";	 
						} 
						else 
						{ 
							_gui.transitionText = "\nLEVEL " + _state;  
 
							if (_gui.npcText == "") _sfx.playNPCwelcome(); 
							_gui.npcText = "We're under attack! Please help us!\nYou're our only hope for survival.\nUse the arrow keys to move.";	 
						} 
					} 
					else // must be a death or boss battle 
					{ 
						if ((_state > 1000) && (thePlayer.health > 0)) // v6 
						{ 
							_gui.transitionText = "\nINCOMING BOSS BATTLE!"; 
 
							if (_gui.npcText == "") _sfx.playNPCboss(); 
							_gui.npcText = "Be careful! That ship is HUGE!\nKeep moving and watch out for\nany burst attacks. Good luck!"; 
						} 
						else 
						{ 
							_gui.transitionText = "Your ship was destroyed.\n\nYou have "  
								+ thePlayer.lives + (thePlayer.lives != 1 ? " lives" : " life") + " left."; 
 
							if (_gui.npcText == "") _sfx.playNPCdeath(); 
							_gui.npcText = "Nooooo!\nDon't give up! I believe in you!\nYou can do it."; 
							 
							timeDilation = 0.5; // slow mo 
						} 
					} 
					if (thePlayer.lives < 0 || thePlayer.health <= 0) 
					{ 
						// during the death transition, spawn tons of explosions just for fun 
						if (_entities.fastRandom() < 0.2) 
						{ 
							var explosionPos:Point = new Point(); 
							explosionPos.x = thePlayer.sprite.position.x + _entities.fastRandom() * 128 - 64;  
							explosionPos.y = thePlayer.sprite.position.y + _entities.fastRandom() * 128 - 64;  
							_entities.particles.addExplosion(explosionPos); 
						} 
					} 
				} 
				else // transition time has elapsed 
				{ 
					_gui.npcText = ""; // v6 
					timeDilation = 1; // turn off slow-mo 
					currentTransitionSeconds = 0; 
					 
					thePlayer.transitionTimeLeft = 0; 
					 
					if (_state == -1) _state = 0; 
					_gui.transitionTf.scrollRect = new Rectangle(0,0,600,160); 
					_gui.transitionText = ""; 
					 
					if ((thePlayer.health <= 0) && (_state != 0)) // we died 
					{ 
						trace("Death transition over. Respawning player."); 
						thePlayer.sprite.position.y = _entities.midpoint; 
						thePlayer.sprite.position.x = 64; 
						thePlayer.health = 100; 
						// failed to kill boss: 
						if (_state > 1000) 
						{ 
							trace('Filed to kill boss. Resetting.'); 
							_state -= 1000; 
							_gui.bosshealth = -999; 
							// remove the boss health bar 
							if (_gui.contains(_gui.bosshealthTf)) 
								_gui.removeChild(_gui.bosshealthTf); 
							// remove the boss itself 
							if (_entities.theBoss) 
							{ 
								_entities.theBoss.die(); 
								_entities.theBoss = null; 
							} 
						} 
						// start the level again 
						_entities.changeLevels('level' + _state); 
						_terrain.changeLevels('terrain' + _state); 
					} 
					if ((thePlayer.level != _state) && (_state < 1000)) 
					{ 
						trace('Level transition over. Starting level ' + _state); 
						thePlayer.level = _state; 
						if (_state > 1) // no need to reload at startGame 
						{ 
							_entities.changeLevels('level' + _state); 
							_terrain.changeLevels('terrain' + _state); 
							_gui.statsText = "Level " + _state; // v6 
						} 
						if (_state == 0) // game over 
						{ 
							trace('Game Over transition over: starting main menu'); 
							thePlayer.health = 100; 
							thePlayer.lives = 3; 
							thePlayer.sprite.visible = false; 
							_entities.theOrb.sprite.visible = false; 
							_entities.changeLevels('level' + _state); 
							_terrain.changeLevels('terrain' + _state); 
							_spriteStage.addBatch(_mainmenu.batch); 
							_gui.statsText = "GAME OVER"; // v6 
							_gui.bosshealth = 0; 
							// remove the boss health bar if any 
							if (_gui.contains(_gui.bosshealthTf)) 
								_gui.removeChild(_gui.bosshealthTf); 
							// go back to normal size 
							if (enableFullscreen) 
							{ 
								trace('Leaving fullscreen...'); 
								stage.displayState = StageDisplayState.NORMAL; 
							}							 
						} 
					} 
				} 
			} 
		}

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:

 
 
		private function startGame():void 
		{ 
			trace("Starting game!"); 
			 
			_state = 1; 
			_spriteStage.removeBatch(_mainmenu.batch); 
			_sfx.playMusic(); 
			 
			if (enableAutofire) // v6 
			{ 
				_controls.autofire = true;  
			} 
			 
			// v6 fullscreen mode! 
			// Note: security blocks keyboard except  
			// arrows and space, so WASD keys don't work... 
			// also pressing left+up+space doesn't work on  
			// normal keyboards (therefore we implemented autofire) 
			if (enableFullscreen) 
			{ 
				try 
				{ 
					trace('Going fullscreen...'); 
					// remember to add this to your HTML: 
					// <param name="allowFullScreen" value="true" /> 
					stage.displayState = StageDisplayState.FULL_SCREEN; 
				} 
				catch (err:Error) 
				{ 
					trace("Error going fullscreen."); 
				} 
				// in Flash 11.3 (summer 2012) you can use the following 
				// for full keyboard access but it asks the user for permission first 
				// stage.displayState = StageDisplayState.FULL_SCREEN_INTERACTIVE; 
				// you also need to add this to your html 
				// <param name="allowFullScreenInteractive" value="true" /> 
			} 
			 
			// add the player entity to the game! 
			if (!thePlayer)  
				thePlayer = _entities.addPlayer(playerLogic); 
			else // on subsequent games 
				thePlayer.sprite.visible = true; 
			if (_entities.theOrb) 
				_entities.theOrb.sprite.visible = true; 
				 
			// load level one (and clear demo entities) 
			_entities.changeLevels('level' + _state); 
			_terrain.changeLevels('terrain' + _state); 
			_gui.statsText = "Level " + _state; // v6 
			 
			// reset the player position 
			thePlayer.level = 0; // it will transition to 1 
			thePlayer.score = 0; 
			thePlayer.lives = 3; 
			thePlayer.sprite.position.x = 64; 
			thePlayer.sprite.position.y = _entities.midpoint; 
 
			// add a "welcome message" 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
			 
			// make the player invulnerable at first 
			thePlayer.invulnerabilityTimeLeft = thePlayer.transitionSeconds + thePlayer.invulnerabilitySecsWhenHit;	 
		}

Step 32: Turn Off Autofire

We need to upgrade our game over function to turn off autofire when the game ends to avoid the menu being automatically triggered. Continuing with Main.as add this little change to the end of the function:

 
		// triggered if the player loses all lives 
		private function gameOver():void 
		{ 
			trace("================ GAME OVER ================"); 
 
			// save game 
			if (saved.level < thePlayer.level) 
				saved.level = thePlayer.level; 
			if (saved.score < thePlayer.score) 
			{ 
				saved.score = thePlayer.score; 
				_gui.highScore = thePlayer.score; 
			} 
 
			_state = 0; 
			 
			thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
			 
			if (enableAutofire) 
			{ 
				_controls.autofire = false; // v6 
				_controls.pressing.fire = false; 
			} 
		}

The checkPlayerState function that appears next in the code from last time remains unchanged.


Step 33: Upgrade the Map State Check

We need to make one tiny change to the function that determined when it is time to switch maps. We don't want this to happen during a boss battle and instead much wait for the battle to be over.

 
		// check to see if we reached the end of the map/game 
		private function checkMapState():void 
		{ 
			// main menu or gameover credits? 
			if (_state < 1) return; 
			// in the middle of a boss battle? // v6 
			if (_state > 1000) return; 
			 
			// already transitioning? 
			if (thePlayer.level != _state) return; 
			 
			// allow some extra spaces for the level to scroll past 
			// the player and then call the level complete. 
			//if (_terrain.levelPrevCol > _terrain.level.levelLength + 16) // v6 
			if (_entities.levelPrevCol > _entities.level.levelLength) 
			{ 
				trace("LEVEL " + _state  + " COMPLETED!"); 
				 
				// special state: boss battle is 100 + current level 
				// _state++; 
				bossBattle(); 
				_state += 1000; 
				thePlayer.transitionTimeLeft = thePlayer.transitionSeconds; 
				 
				if (_entities.level.levelLength == 0) 
				{ 
					trace("NO MORE LEVELS REMAIN! GAME OVER!"); 
					rollTheCredits(); 
				} 
			} 
		}

The rollTheCredits function that appears next in the original source is also unchanged since last time.


Step 34: Upgrade the Render Loop

We've made it to the last engine uprgrade! The final function that we need to tweak is also only subtle different since last time. During player death transition animations, we set the timeDilation variable to be less than 1. This is multiplied by the elapsed milliseconds since the previous frame, which is sent to all the update functions for each class. This way, whenever it is set to 0.5, for example, all animations will run at half speed.

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

That's it for the many little upgrades to Main.as. The final version of our game is now ready to compile and run. It now boasts slow-mo, fullscreen, boss battles, voiceovers and an NPC, a preloader progress bar, and much more.


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:



Part Six Complete. Final Boss Defeated!

That's it for the Stage3D Shoot-em-up tutorial series.

We started by implementing a simple (and super fast) batched geometry sprite rendering system that made use of reusable object pools for extra speed. By taking advantage of Stage3D, our game is rendered using the hardware GPU and should achieve a silky smooth 60fps on most new computers.

We then added game entities with AI, collision detection and timers, scores and health bars, sound and music, a menu and all the other little bells-n-whistles required to call this a "real" game.

You now have in your hands an MVP (minimum viable product) from which you can expend to your heart's desire to create a world-class "bullet-hell" sidescrolling shooter. This your new quest. The student has become the master. Take this code and bring it to new heights on your own. I sincerely hope you make something amazing with it. Good luck - and have fun!

I'd love to hear from you regarding this series. 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 anytime. 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