In this tutorial series (part free, part Premium) we're creating a high-performance 2D shoot-em-up using the new hardware-accelerated Stage3D
rendering engine. In this part, we'll extend our rendering demo by adding an animated title screen and menu, music and sound effects, and a keyboard-controlled ship that can fire bullets.
Final Result Preview
Let's take a look at the final result we will be working towards: a hardware accelerated shoot-em-up demo in the making that includes an animated title screen and menu, sounds, music, and keyboard controls.
Click the logo to gain keyboard focus, then use the arrow keys to move and space bar to fire. Your shots will be reflected back at you once they reach the edge of the screen.
Introduction: Welcome to Level Two!
We're going to continue making a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius, in AS3, using Flash 11's Stage3D API and the freeware tool FlashDevelop.
In the first part of this tutorial series, we implemented a basic 2D sprite engine that achieves great performance through the use of Stage3D hardware rendering as well as several optimizations.
These optimizations include the use of a spritesheet (or texture atlas), the creation of an entity object pool which reuses incactive entities instead of creating and destroying objects at runtime, and a batched geometry rendering system which draws all of our game's sprites in a single pass rather than individually. These optimizations are the perfect foundation upon which we will build our high performance next-gen 3d shooter.
In this part, we are going to work toward evolving what is currently a mere tech demo into something that is more like a videogame. We are going to create a title screen and main menu, add some sound and music, and code an input system that lets the player control their spaceship using the keyboard.
Step 1: Open Your Existing Project
If you don't already have it, be sure to download the source code from part one (download here). Open the project file in FlashDevelop (info here) and get ready to upgrade your game!
Step 2: The Input Class
We need to allow the player to control the game, so create a new file called GameControls.as
and start by initializing the class as follows:
// Stage3D Shoot-em-up Tutorial Part 2 // by Christer Kaitila - www.mcfunkypants.com // Created for active.tutsplus.com // GameControls.as // A simple keyboard input class package { import flash.display.Stage; import flash.ui.Keyboard; import flash.events.*; public class GameControls { // the current state of the keyboard controls public var pressing:Object = { up:0, down:0, left:0, right:0, fire:0, hasfocus:0 }; // the game's main stage public var stage:Stage; // class constructor public function GameControls(theStage:Stage) { stage = theStage; // get keypresses and detect the game losing focus stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed); stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased); stage.addEventListener(Event.DEACTIVATE, lostFocus); stage.addEventListener(Event.ACTIVATE, gainFocus); }
In the code above, we import the Flash functionality required to handle the keyboard. We also define some class variables that will be used to report the current state of the keyboard to the game.
In order to support international keyboards, special consideration has been made to ensure the game will "just work" when players try to control the game. Although most of your players will be using a English "QWERTY" keyboard, a significant proportion will use alternate international keyboard layouts, and to them a big pet peeve when playing games is when they can't use standard "WASD" style movement. Therefore, we make sure that "AZERTY" and "DVORAK" keyboards are also supported.
Keyboard controls that work on QWERTY, AZERTY and DVORAK.
Step 3: Handle Keyboard Events
This class will be listening to the key up and key down events and will set the state of the class so that the game knows when a certain key is pressed or not. In order to support players from different parts of the world, instead of just forcing the use of the arrow keys we are going to add alternative controls such as W,A,S,D and similar schemes that work on international keyboards.
This way, no matter what the player uses to control the game, it will probably "just work" without the player having to think about it. There's no harm in making different keys do the same thing in-game.
Detect the appropriate key events as follows:
private function keyPressed(event:KeyboardEvent):void { keyHandler(event, true); } private function keyReleased(event:KeyboardEvent):void { keyHandler(event, false); } // if the game loses focus, don't keep keys held down // we could optionally pause the game here private function lostFocus(event:Event):void { trace("Game lost keyboard focus."); pressing.up = false; pressing.down = false; pressing.left = false; pressing.right = false; pressing.fire = false; pressing.hasfocus = false; } // we could optionally unpause the game here private function gainFocus(event:Event):void { trace("Game received keyboard focus."); pressing.hasfocus = true; }
In the code above we handle the key up and key down events, as well as the events that are fired when the game gains or loses keyboard focus. This is important because sometimes when a key is held down, the key up event is not received if the user clicks another window, switches browser tabs, or gets a popup from another program. It's also common for Flash games to pause the game when focus is lost, and this is the ideal place to handle that.
Step 4: Record the Input State
The final function required by our simple GameInput
class is the one that both keyboard events above call. If a key has been pressed, this function sets the appropriate flag to true
so that our main game logic can query the current state of all the controls when needed.
Our game will be controllable via the arrow keys, as well as the typical upper-left keyboard area that is most often used in first person shooters, as follows:
// used only for debugging public function textDescription():String { return ("Controls: " + (pressing.up?"up ":"") + (pressing.down?"down ":"") + (pressing.left?"left ":"") + (pressing.right?"right ":"") + (pressing.fire?"fire":"")); } private function keyHandler(event:KeyboardEvent, isDown:Boolean):void { //trace('Key code: ' + event.keyCode); // alternate "fire" buttons if (event.ctrlKey || event.altKey || event.shiftKey) pressing.fire = isDown; // key codes that support international keyboards: // QWERTY = W A S D // AZERTY = Z Q S D // DVORAK = , A O E switch(event.keyCode) { case Keyboard.UP: case 87: // W case 90: // Z case 188:// , pressing.up = isDown; break; case Keyboard.DOWN: case 83: // S case 79: // O pressing.down = isDown; break; case Keyboard.LEFT: case 65: // A case 81: // Q pressing.left = isDown; break; case Keyboard.RIGHT: case 68: // D case 69: // E pressing.right = isDown; break; case Keyboard.SPACE: case Keyboard.SHIFT: case Keyboard.CONTROL: case Keyboard.ENTER: case 88: // x case 67: // c pressing.fire = isDown; break; } } } // end class } // end package
That's it for GameInput.as
- in future versions we might want to add mouse and touch events to support playing the game without the keyboard or on mobile, but for now this is enough to support almost all possible players.
Step 5: Add Input Variables
In the inits as defined in the Main.as
file, add two new class variables to the top of our existing class definition, just before all the other variables from the sprite test. We need a reference to our new input class, and a state flag that we'll use to make sure the menu doesn't scroll too fast when a key is held down.
public class Main extends Sprite { // the keyboard control system private var _controls : GameControls; // don't update the menu too fast private var nothingPressedLastFrame:Boolean = false;
Step 6: Init the Inputs
Next, create a new input object during the game's inits. Add the following line of code to the very top of the existing init function. We will pass in a reference to the stage which is needed by the class.
private function init(e:Event = null):void { _controls = new GameControls(stage);
Step 7: Debug the Controls
In the main game loop in your Main.as
file, we want to check the state of the player input. In the existing onEnterFrame
function, add this line of code:
// this function draws the scene every frame private function onEnterFrame(e:Event):void { try { // for debugging the input manager, update the gui _gui.titleText = _controls.textDescription();
With this in place, we should now have a working input manager. If you compile the SWF now and run it, you should see the following:
Debug display showing controls in use.
Note the stats debug text only updates once per second, to avoid spamming the displaylist and to keep the framerate high, but this is enough to prove that we have a decent set of input routines all ready to go.
Step 8: Design the Title Screen
Nearly every videogame starts with the display of a big splash screen logo and a main menu. This is often called "attract mode" and is meant to be the idle state of the game while it waits for the player to decide to dive in and start playing. This is the perfect place to put the credits and copyrights you desire, as well as a little bit of information on how to control the game.
Our title screen and menu display is going to be another LiteSprite batch layer that sits over top of the once we made in the first part of this series. It will also take advantage of the spritesheet and geometry batching system we designed, so that we can rotate, scale and move our sprites around smoothly, without any jaggies and while retaining our silky-smooth 60fps. Fire up Photoshup, GIMP, or the image editor of your choice and create a logo and name for your game.
I've decided to use the name "Kaizen", formed the first three letters in my last name, "Kai", and the word "zen", which to me implies the zen-like state of mind that you get in when you are "in the zone," dodging bullets and destroying enemies. It also evokes a Japanese arcade style, and harkens back to titles like Raiden.
Finally, add the on and off states for the menu items as well as the about and controls info screens. Since we will be using batching and a spritesheet, we need to include all images that the title screen will include in a single, power-of-two sized square image that is 512x512 and has alpha transparency. This is the final image we will be using:
Step 9: The GameMenu Class
We now need to create a quick-n-dirty menu class. Create a brand new file in your project called GameMenu.as
and begin by importing the Stage3D functionality required and defining the class variables that we need to handle as follows:
// Stage3D Shoot-em-up Tutorial Part 2 // by Christer Kaitila - www.mcfunkypants.com // GameMenu.as // A simple title screen / logo screen and menu // that is displayed during the idle "attract mode" package { import flash.display.Bitmap; import flash.display3D.*; import flash.geom.Point; import flash.geom.Rectangle; public class GameMenu { // the sprite sheet image public var spriteSheet : LiteSpriteSheet; [Embed(source="../assets/titlescreen.png")] private var SourceImage : Class; // all the polygons that make up the scene public var batch : LiteSpriteBatch; // which menu item is active (0=none) public var menuState:int = 0; // pixel regions of the buttons public var menuWidth:int = 128; public var menuItemHeight:int = 32; public var menuY1:int = 0; public var menuY2:int = 0; public var menuY3:int = 0; // the sprites public var logoSprite:LiteSprite; // menu items when idle public var menuPlaySprite:LiteSprite; public var menuControlsSprite:LiteSprite; public var menuAboutSprite:LiteSprite; // menu items when active public var amenuPlaySprite:LiteSprite; public var amenuControlsSprite:LiteSprite; public var amenuAboutSprite:LiteSprite; // info screens public var aboutSprite:LiteSprite; public var controlsSprite:LiteSprite; public var showingAbout:Boolean = false; public var showingControls:Boolean = false; public var showingControlsUntil:Number = 0; public var showingAboutUntil:Number = 0; // where everything goes public var logoX:int = 0; public var logoY:int = 0; public var menuX:int = 0; public var menuY:int = 0;
Step 10: Init the Menu
Continuing with GameMenu.as
, implement the initializations. We want to ensure that the menu and logo are centered. In the function below where we create the geometry batch and "chop up" the spritesheet, we define rectangular regions for each of the sprites we will be using.
public function GameMenu(view:Rectangle) { trace("Init the game menu.."); setPosition(view); } public function setPosition(view:Rectangle):void { logoX = view.width / 2; logoY = view.height / 2 - 64; menuX = view.width / 2; menuY = view.height / 2 + 64; menuY1 = menuY - (menuItemHeight / 2); menuY2 = menuY - (menuItemHeight / 2) + menuItemHeight; menuY3 = menuY - (menuItemHeight / 2) + (menuItemHeight * 2); } public function createBatch(context3D:Context3D) : LiteSpriteBatch { var sourceBitmap:Bitmap = new SourceImage(); // create a spritesheet using the titlescreen image spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, 0, 0); // Create new render batch batch = new LiteSpriteBatch(context3D, spriteSheet); // set up all required sprites right now var logoID:uint = spriteSheet.defineSprite(0, 0, 512, 256); logoSprite = batch.createChild(logoID); logoSprite.position.x = logoX; logoSprite.position.y = logoY; var menuPlaySpriteID:uint = spriteSheet.defineSprite(0, 256, menuWidth, 48); menuPlaySprite = batch.createChild(menuPlaySpriteID); menuPlaySprite.position.x = menuX; menuPlaySprite.position.y = menuY; var amenuPlaySpriteID:uint = spriteSheet.defineSprite(0, 256+128, menuWidth, 48); amenuPlaySprite = batch.createChild(amenuPlaySpriteID); amenuPlaySprite.position.x = menuX; amenuPlaySprite.position.y = menuY; amenuPlaySprite.alpha = 0; var menuControlsSpriteID:uint = spriteSheet.defineSprite(0, 304, menuWidth, 32); menuControlsSprite = batch.createChild(menuControlsSpriteID); menuControlsSprite.position.x = menuX; menuControlsSprite.position.y = menuY + menuItemHeight; var amenuControlsSpriteID:uint = spriteSheet.defineSprite(0, 304+128, menuWidth, 32); amenuControlsSprite = batch.createChild(amenuControlsSpriteID); amenuControlsSprite.position.x = menuX; amenuControlsSprite.position.y = menuY + menuItemHeight; amenuControlsSprite.alpha = 0; var menuAboutSpriteID:uint = spriteSheet.defineSprite(0, 336, menuWidth, 48); menuAboutSprite = batch.createChild(menuAboutSpriteID); menuAboutSprite.position.x = menuX; menuAboutSprite.position.y = menuY + menuItemHeight + menuItemHeight; var amenuAboutSpriteID:uint = spriteSheet.defineSprite(0, 336+128, menuWidth, 48); amenuAboutSprite = batch.createChild(amenuAboutSpriteID); amenuAboutSprite.position.x = menuX; amenuAboutSprite.position.y = menuY + menuItemHeight + menuItemHeight; amenuAboutSprite.alpha = 0; var aboutSpriteID:uint = spriteSheet.defineSprite(128, 256, 384, 128); aboutSprite = batch.createChild(aboutSpriteID); aboutSprite.position.x = menuX; aboutSprite.position.y = menuY + 64; aboutSprite.alpha = 0; var controlsSpriteID:uint = spriteSheet.defineSprite(128, 384, 384, 128); controlsSprite = batch.createChild(controlsSpriteID); controlsSprite.position.x = menuX; controlsSprite.position.y = menuY + 64; controlsSprite.alpha = 0; return batch; }
Step 11: Handle Menu State
We want to define functions that will be called by our game based on user input. If the game has keyboard focus, the user can use the arrow keys to scroll up and down. Alternatively, the mouse can be used to hover over and click menu items. When we update the state of the menu, we turn off certain sprites and turn on others by simply changing the alpha transparency of the sprite.
// our game will call these based on user input public function nextMenuItem():void { menuState++; if (menuState > 3) menuState = 1; updateState(); } public function prevMenuItem():void { menuState--; if (menuState < 1) menuState = 3; updateState(); } public function mouseHighlight(x:int, y:int):void { //trace('mouseHighlight ' + x + ',' + y); // when mouse moves, assume it moved OFF all items menuState = 0; var menuLeft:int = menuX - (menuWidth / 2); var menuRight:int = menuX + (menuWidth / 2); if ((x >= menuLeft) && (x <= menuRight)) { //trace('inside ' + menuLeft + ',' + menuRight); if ((y >= menuY1) && (y <= (menuY1 + menuItemHeight))) { menuState = 1; } if ((y >= menuY2) && (y <= (menuY2 + menuItemHeight))) { menuState = 2; } if ((y >= menuY3) && (y <= (menuY3 + menuItemHeight))) { menuState = 3; } } updateState(); } // adjust the opacity of menu items public function updateState():void { // ignore if menu is not visible: if (showingAbout || showingControls) return; // user clicked or pressed fire on a menu item: switch (menuState) { case 0: // nothing selected menuAboutSprite.alpha = 1; menuControlsSprite.alpha = 1; menuPlaySprite.alpha = 1; amenuAboutSprite.alpha = 0; amenuControlsSprite.alpha = 0; amenuPlaySprite.alpha = 0; break; case 1: // play selected menuAboutSprite.alpha = 1; menuControlsSprite.alpha = 1; menuPlaySprite.alpha = 0; amenuAboutSprite.alpha = 0; amenuControlsSprite.alpha = 0; amenuPlaySprite.alpha = 1; break; case 2: // controls selected menuAboutSprite.alpha = 1; menuControlsSprite.alpha = 0; menuPlaySprite.alpha = 1; amenuAboutSprite.alpha = 0; amenuControlsSprite.alpha = 1; amenuPlaySprite.alpha = 0; break; case 3: // about selected menuAboutSprite.alpha = 0; menuControlsSprite.alpha = 1; menuPlaySprite.alpha = 1; amenuAboutSprite.alpha = 1; amenuControlsSprite.alpha = 0; amenuPlaySprite.alpha = 0; break; } } // activate the currently selected menu item // returns true if we should start the game public function activateCurrentMenuItem(currentTime:Number):Boolean { // ignore if menu is not visible: if (showingAbout || showingControls) return false; // activate the proper option: switch (menuState) { case 1: // play selected return true; break; case 2: // controls selected menuAboutSprite.alpha = 0; menuControlsSprite.alpha = 0; menuPlaySprite.alpha = 0; amenuAboutSprite.alpha = 0; amenuControlsSprite.alpha = 0; amenuPlaySprite.alpha = 0; controlsSprite.alpha = 1; showingControls = true; showingControlsUntil = currentTime + 5000; break; case 3: // about selected menuAboutSprite.alpha = 0; menuControlsSprite.alpha = 0; menuPlaySprite.alpha = 0; amenuAboutSprite.alpha = 0; amenuControlsSprite.alpha = 0; amenuPlaySprite.alpha = 0; aboutSprite.alpha = 1; showingAbout = true; showingAboutUntil = currentTime + 5000; break; } return false; }
In the activateCurrentMenuItem
function above, we either return true
if the play button has been pressed and it is time to start the game, or we turn off the menu entirely and display one of the two information screens for five seconds by telling the menu class to wait for 5000ms before reverting back to the idle menu state.
Step 12: Animate the Menu
To add a little polish to our menu system, we will pulse the logo in and out and rotate it slightly. This adds a little movement and pizazz to our game. We will also pulse the highlighted menu item to provide clear feedback to the user that this item has been selected. It is little touches like these that will make your game seems a bit more professional - and they're easy to implement too.
// called every frame: used to update the animation public function update(currentTime:Number) : void { logoSprite.position.x = logoX; logoSprite.position.y = logoY; var wobble:Number = (Math.cos(currentTime / 500) / Math.PI) * 0.2; logoSprite.scaleX = 1 + wobble; logoSprite.scaleY = 1 + wobble; wobble = (Math.cos(currentTime / 777) / Math.PI) * 0.1; logoSprite.rotation = wobble; // pulse the active menu item wobble = (Math.cos(currentTime / 150) / Math.PI) * 0.1; amenuAboutSprite.scaleX = amenuAboutSprite.scaleY = amenuControlsSprite.scaleX = amenuControlsSprite.scaleY = amenuPlaySprite.scaleX = amenuPlaySprite.scaleY = 1.1 + wobble; // show the about/controls for a while if (showingAbout) { if (showingAboutUntil > currentTime) { aboutSprite.alpha = 1; } else { aboutSprite.alpha = 0; showingAbout = false; updateState(); } } if (showingControls) { if (showingControlsUntil > currentTime) { controlsSprite.alpha = 1; } else { controlsSprite.alpha = 0; showingControls = false; updateState(); } } } } // end class } // end package
That's it for our simple animated title screen and main menu.
Step 13: Pump Up the Jam!
Music and sound effects add a lot to a game. Not only does it help to give the player an emotional connection to the action, it also sets the tone and feel. Sound is a valuable feedback mechanism as well, because it gives an auditory confirmation that what the player just did had some sort of effect. When you hear a sound effect like a laser blast, you know you did something.
Fire up your favorite sound editor (such as SoundForge, CoolEditPro, Reason, or even a microphone and your own voice) and design the music and sounds you want to use in your game. If you don't know how to make sound effects or are not a composer of music, don't worry. There are millions of free and legal sound effects and song at your diposal online.
For example, you can take advantage of the vast library of sound effects at FreeSound (just ensure that they are licensed for commercial use and be sure to give credit). You can also generate sound effects using Dr. Petter's SFXR (or its fantastic online port, BFXR), which automatically generates an infinite variety of retro beeps and bloops. Finally, for legal music you can search through giant catalogues with Jamendo and the amazing ccMixter, which are designed for indies as a resource of completely legal music.
In order to keep the size of our SWF as small as possible, once you've downloaded or created the sounds and music you want, re-encode them in the lowest tolerable sound quality. When explosions and gun sounds are firing, players will not notice if your music is in MONO and being played at a low bitrate. For the example game, all music and sounds fit in about 500k by saving the MP3s as mono 22kHz files.
Step 14: The Sound Class
For this example, we are going to create a barebones game sound system that includes four different MP3 files. The first is the music, and the other three are different gun shot sounds. Eventually, the plan is to have different sounds for the player's weapon that go with different weapon states.
Create a brand new file in your project called GameSound.as
and embed the MP3s as follows:
// Stage3D Shoot-em-up Tutorial Part 2 // by Christer Kaitila - www.mcfunkypants.com // GameSound.as // A simple sound and music system for our game package { import flash.media.Sound; import flash.media.SoundChannel; public class GameSound { // to reduce .swf size these are mono 11khz [Embed (source = "../assets/sfxmusic.mp3")] private var _musicMp3:Class; private var _musicSound:Sound = (new _musicMp3) as Sound; private var _musicChannel:SoundChannel; [Embed (source = "../assets/sfxgun1.mp3")] private var _gun1Mp3:Class; private var _gun1Sound:Sound = (new _gun1Mp3) as Sound; [Embed (source = "../assets/sfxgun2.mp3")] private var _gun2Mp3:Class; private var _gun2Sound:Sound = (new _gun2Mp3) as Sound; [Embed (source = "../assets/sfxgun3.mp3")] private var _gun3Mp3:Class; private var _gun3Sound:Sound = (new _gun3Mp3) as Sound;
Step 15: Trigger the Sounds
The final step in creating our sound manager is to implement the functions that our game will call when we want to trigger a new sound effect.
// the different phaser shooting sounds public function playGun(num:int):void { switch (num) { case 1 : _gun1Sound.play(); break; case 2 : _gun2Sound.play(); break; case 3 : _gun3Sound.play(); break; } } // the looping music channel public function playMusic():void { trace("Starting the music..."); // stop any previously playing music stopMusic(); // start the background music looping _musicChannel = _musicSound.play(0,9999); } public function stopMusic():void { if (_musicChannel) _musicChannel.stop(); } } // end class } // end package
As simple as it looks, this is enough to handle sound and music. Note the above we need to keep track of the sound channel used by the music so that we can turn it off. Gun sounds can overlap (and in fact this is the desired behavior) but we never want two copies of the music playing if a player dies and then starts another game. Therefore, we ensure that any previous instances of the music are turned off before starting playing it.
Step 16: The Background Layer
As one last bit of polish that we are going to put into our game this week, we're going to implement a simple background starfield that scrolls slower than the action in front. This parallax effect will give the game some depth and will nook much nicer than a plain black background.
We're going to use another geometry batch and spritesheet and draw it underneath everything else. Because it is so similar to the entity manager class we made last week, we're going to use class inheritance and simply extend that for our purposes. This way, we only need to implement the routines that are different.
For now, we're just going to layer copies of the following space tile, but in future versions of our game we might add more details like asteroids or nebulae to the mix.
Create a new file in your project called GameBackground.as
and start by extending the EntityManager
class as follows:
// Stage3D Shoot-em-up Tutorial Part 2 // by Christer Kaitila - www.mcfunkypants.com // GameBackground.as // A very simple batch of background stars that scroll 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; public function GameBackground(view:Rectangle) { // run the init functions of the EntityManager class super(view); }
Step 17: Init the Background
We are going to create a single giant sprite out of this "spritesheet" rather than chopping it up into many small parts. Since the image is 512x512 and the game screen is wider than that, we might need up to three sprites visible, depending on where they are at a given time as they cross over the edges of the screen.
override public function createBatch(context3D:Context3D) : LiteSpriteBatch { var bgsourceBitmap:Bitmap = new bgSourceImage(); // create a spritesheet with a 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; minX = -256; maxY = view.height; minY = view.y; } // for this test, create random entities that move // from right to left with random speeds and scales public function initBackground():void { trace("Init background..."); // we need three 512x512 sprites var anEntity1:Entity = respawn(0) anEntity1 = 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; }
Step 18: Animate the Background
The final step in getting our simple but attractive background layer rendering is to code the update
function, which will scroll the sprites and optionally "loop" them to the opposite edge for reuse. This way, the background can scroll infinitely. Continuing with GameBackground.as
implement our scrolling behavior as follows:
// 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; 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
That's it for our scrolling space background class. Now all we need to do is add it to our main game and test it out.
Step 19: Add the Player and Bullets
There's one tiny upgrade we need to make to our existing EntityManager
class from last week. We want a unique entity for the player's ship that doesn't follow the rules of all the other sprites that are flying by in our demo. We also want some bullets to shoot. First, ensure that your sprite sheet image includes these kinds of sprites, since last week there were no bullets in it.
The new spritesheet with bullets and explosions
First, edit Entity.as
and add one public variable that will be used to store a reference to an AI (artificial intelligence) function. In future versions of our game we might add special AI functions for different kinds of enemies, homing missiles, etc. Add this line of code alongside the other entity class variables like speed.
// if this is set, custom behaviors are run public var aiFunction : Function;
Now edit EntityManager.as
and add the following class variable to EntityManager.as
where we create all the other vars.
// the player entity - a special case public var thePlayer:Entity;
Next, implement two new functions that will handle the creation of this player entity and the spawning of new bullets.
// this entity is the PLAYER public function addPlayer(playerController:Function):Entity { thePlayer = respawn(10); // sprite #10 looks nice for now thePlayer.sprite.position.x = 32; thePlayer.sprite.position.y = maxY / 2; thePlayer.sprite.rotation = 180 * (Math.PI/180); // degrees to radians thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = 2; thePlayer.speedX = 0; thePlayer.speedY = 0; thePlayer.aiFunction = playerController; return thePlayer; } // shoot a bullet (from the player for now) public function shootBullet():Entity { var anEntity:Entity; anEntity = respawn(39); // bullet sprite is #39 anEntity.sprite.position.x = thePlayer.sprite.position.x + 8; anEntity.sprite.position.y = thePlayer.sprite.position.y + 4; anEntity.sprite.rotation = 180 * (Math.PI/180); anEntity.sprite.scaleX = anEntity.sprite.scaleY = 2; anEntity.speedX = 10; anEntity.speedY = 0; return anEntity; }
Finally, upgrade the entity manager's update function to skip over the player in its standard simulation step by checking to see if that entity has an ai function defined (for now, only the player will).
anEntity.sprite.position.x += anEntity.speedX; anEntity.sprite.position.y += anEntity.speedY; // the player follows different rules if (anEntity.aiFunction != null) { anEntity.aiFunction(anEntity); } else // all other entities use the "demo" logic { anEntity.sprite.rotation += 0.1; // ... and so on ...
We will now have a special player ship entity that moves around and can shoot bullets aplenty. Check out this lovely example of bullet hell (or would that be shoot-'em-up heaven?):
The bullets reflect when they reach the edge of the screen because of this snippet of code, which we added in the first part of the series:
if (anEntity.sprite.position.x > maxX) { anEntity.speedX *= -1; anEntity.sprite.position.x = maxX; }
Step 20: Upgrade the Game!
Now that we've implemented a sound system, a scrolling background, keyboard controls, a title screen and a main menu, we need to upgrade our game to incorporate them into the demo. This involves all sorts of tiny subtle changes to the existing Main.as
file that we made last week. To avoid confusion that might result from a dozen one-liners of code scattered around in a dozen different locations, all the changes are presented below in a linear fashion.
Step 21: New Class Variables
We first need to create new class variables used to refer to and control the new systems we made this week, as follows:
// Stage3D Shoot-em-up Tutorial Part2 // by Christer Kaitila - www.mcfunkypants.com // Created for active.tutsplus.com package { [SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] import flash.display3D.*; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageQuality; import flash.display.StageScaleMode; import flash.events.Event; import flash.events.ErrorEvent; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.utils.getTimer; public class Main extends Sprite { // 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; // player one's entity public var thePlayer:Entity; // 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 _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 22: Upgrade the Inits
Continuing with Main.as
, upgrade the game inits to spawn instances of this week's new classes.
// 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("Stage3D Shoot-em-up Tutorial Part 2"); addChild(_gui); stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); trace("Stage3D requested..."); _sfx = new GameSound(); } // this is called when the 3d card has been set up // and is ready for rendering using stage3d private function onContext3DCreate(e:Event):void { trace("Stage3D context created! Init sprite engine..."); context3D = stage.stage3Ds[0].context3D; initSpriteEngine(); } // this can be called when using an old version of flash // or if the html does not include wmode=direct private function errorHandler(e:ErrorEvent):void { trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); } protected function onResizeEvent(event:Event) : void { trace("resize event..."); // Set correct dimensions if we resize _width = stage.stageWidth; _height = stage.stageHeight; // Resize Stage3D to continue to fit screen var view:Rectangle = new Rectangle(0, 0, _width, _height); if ( _spriteStage != null ) { _spriteStage.position = view; } if(_entities != null) { _entities.setPosition(view); } if(_mainmenu != null) { _mainmenu.setPosition(view); } } private function initSpriteEngine():void { // 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 _bg = new GameBackground(stageRect); _bg.createBatch(context3D); _spriteStage.addBatch(_bg.batch); _bg.initBackground(); // create a single rendering batch // which will draw all sprites in one pass var view:Rectangle = new Rectangle(0,0,_width,_height) _entities = new EntityManager(stageRect); _entities.createBatch(context3D); _spriteStage.addBatch(_entities.batch); // 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); }
Step 23: Player Movement
We want the player to be able to control their spaceship, so we are going to implement the player logic routines. They take advantage of the new GameControls
class to check the state of the keyboard and change the ship's speed (or the highlighted menu item if we are at the title screen) depending on which directional keys are being pressed. We also listen for mouse events and update the state of the main menu if it is currently active.
public function playerLogic(me:Entity):void { me.speedY = me.speedX = 0; if (_controls.pressing.up) me.speedY = -4; if (_controls.pressing.down) me.speedY = 4; if (_controls.pressing.left) me.speedX = -4; if (_controls.pressing.right) me.speedX = 4; // keep on screen if (me.sprite.position.x < 0) me.sprite.position.x = 0; if (me.sprite.position.x > _width) me.sprite.position.x = _width; if (me.sprite.position.y < 0) me.sprite.position.y = 0; if (me.sprite.position.y > _height) me.sprite.position.y = _height; } private function mouseDown(e:MouseEvent):void { trace('mouseDown at '+e.stageX+','+e.stageY); if (_state == 0) // are we at the main menu? { if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer())) { // if the above returns true we should start the game startGame(); } } } private function mouseMove(e:MouseEvent):void { if (_state == 0) // are we at the main menu? { // select menu items via mouse if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY); } } // handle any player input private function processInput():void { if (_state == 0) // are we at the main menu? { // select menu items via keyboard if (_controls.pressing.down || _controls.pressing.right) { if (nothingPressedLastFrame) { _sfx.playGun(1); _mainmenu.nextMenuItem(); nothingPressedLastFrame = false; } } else if (_controls.pressing.up || _controls.pressing.left) { if (nothingPressedLastFrame) { _sfx.playGun(1); _mainmenu.prevMenuItem(); nothingPressedLastFrame = false; } } else if (_controls.pressing.fire) { if (_mainmenu.activateCurrentMenuItem(getTimer())) { // if the above returns true we should start the game startGame(); } } else { // this ensures the menu doesn't change too fast nothingPressedLastFrame = true; } } else { // we are NOT at the main menu: we are actually playing the game // in future versions we will add projectile // spawning functinality here to fire bullets if (_controls.pressing.fire) { _sfx.playGun(1); _entities.shootBullet(); } } }
Step 24: Start the Game!
The mouse and keyboard handlers above call the menu's activateCurrentMenuItem
function which returns true
if it is time to start the game. When it is, we remove the menu and logo from the sprite stage, fire up the game's music, and add a player sprite, ready to be controlled.
private function startGame():void { trace("Starting game!"); _state = 1; _spriteStage.removeBatch(_mainmenu.batch); _sfx.playMusic(); // add the player entity to the game! thePlayer = _entities.addPlayer(playerLogic); }
Step 25: Upgrade the Render Loop
The final step in upgrading our Stage3D game is to call the appropriate update functions each frame for all our new classes that we created above. Continuing with Main.as
, upgrade the onEnterFrame
function as follows:
// this function draws the scene every frame private function onEnterFrame(e:Event):void { try { // grab timestamp of current frame currentTime = getTimer(); // erase the previous frame context3D.clear(0, 0, 0, 1); // for debugging the input manager, update the gui _gui.titleText = _controls.textDescription(); // process any player input processInput(); // scroll the background _bg.update(currentTime); // update the main menu titlescreen if (_state == 0) _mainmenu.update(currentTime); // keep adding more sprites - FOREVER! // this is a test of the entity manager's // object reuse "pool" _entities.addEntity(); // move/animate all entities _entities.update(currentTime); // draw all entities _spriteStage.render(); // update the screen context3D.present(); } 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
Our super-optimized Flash 11 Stage3D Shoot-em-up game is really starting to take shape! 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 instand gratification of everything in one place, remember that you can download the full source code here. You should see something that looks like this:
Part Two Complete: Prepare for Level Three!
That's it for tutorial number two in this series. Tune in next week to watch the game slowly evolve into a great-looking, silky-smooth 60fps shoot-em-up.
What this project needs is a little destruction and mayhem to make it feel like a real game! In part three, we will have a lot of fun implementing all the eye candy: bullets, a particle system, and the collision detection logic that will tell our game when something needs to blow up.
I'd love to hear from you regarding this tutorial. I warmly welcome all readers to get in touch with me via twitter: @McFunkypants, my blog mcfunkypants.com or on google+ any time. In particular, I'd love to see the games you make using this code and I'm always looking for new topics to write future tutorials on.
Good luck and HAVE FUN!
Comments