Build a Stage3D Shoot-'Em-Up: Full-Screen Boss Battles and Polish
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:
- Build a Stage3D Shoot-’Em-Up: Sprite Test
- Build a Stage3D Shoot-’Em-Up: Interaction
- Build a Stage3D Shoot-’Em-Up: Explosions, Parallax, and Collisions
- Build a Stage3D Shoot-'Em-Up: Terrain, Enemy AI, and Level Data
- Build a Stage3D Shoot-’Em-Up: Score, Health, Lives, HUD and Transitions
- 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:
-
sfxboss.mp3
-
sfxNPCwelcome.mp3
-
sfxNPCdeath.mp3
-
sfxNPCboss.mp3
-
sfxNPCnextlevel.mp3
-
sfxNPCgameover.mp3
-
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 |