Advertisement
  1. Code
  2. Games

Build a Stage3D Shoot-'Em-Up: Interaction

Scroll to top
Read Time: 36 min
This post is part of a series called Shoot-'Em-Up.
Build a Stage3D Shoot-'Em-Up: Sprite Test
Quick Tip: Add a Blurry Trail Effect to Your Bullets

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:

1
2
// Stage3D Shoot-em-up Tutorial Part 2
3
// by Christer Kaitila - www.mcfunkypants.com
4
// Created for active.tutsplus.com
5
6
// GameControls.as
7
// A simple keyboard input class
8
package
9
{
10
11
import flash.display.Stage;
12
import flash.ui.Keyboard;
13
import flash.events.*;
14
15
public class GameControls
16
{
17
  // the current state of the keyboard controls
18
  public var pressing:Object = 
19
  { up:0, down:0, left:0, right:0, fire:0, hasfocus:0 };
20
21
  // the game's main stage
22
  public var stage:Stage;
23
  
24
  // class constructor
25
  public function GameControls(theStage:Stage)  
26
  {
27
    stage = theStage;
28
    // get keypresses and detect the game losing focus
29
    stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
30
    stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased);
31
    stage.addEventListener(Event.DEACTIVATE, lostFocus);
32
    stage.addEventListener(Event.ACTIVATE, gainFocus);
33
  }

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.Keyboard controls that work on QWERTY, AZERTY and DVORAK.Keyboard controls that work on QWERTY, AZERTY and DVORAK.
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:

1
2
  private function keyPressed(event:KeyboardEvent):void 
3
  {
4
    keyHandler(event, true);
5
  }
6
7
  private function keyReleased(event:KeyboardEvent):void 
8
  {
9
    keyHandler(event, false);
10
  }
11
12
  // if the game loses focus, don't keep keys held down
13
  // we could optionally pause the game here
14
  private function lostFocus(event:Event):void 
15
  {
16
    trace("Game lost keyboard focus.");
17
    pressing.up = false;
18
    pressing.down = false;
19
    pressing.left = false;
20
    pressing.right = false;
21
    pressing.fire = false;
22
    pressing.hasfocus = false;
23
  }
24
25
  // we could optionally unpause the game here
26
  private function gainFocus(event:Event):void 
27
  {
28
    trace("Game received keyboard focus.");
29
    pressing.hasfocus = true;
30
  }

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:

1
2
  // used only for debugging
3
  public function textDescription():String
4
  {
5
    return ("Controls: " + 
6
      (pressing.up?"up ":"") + 
7
      (pressing.down?"down ":"") + 
8
      (pressing.left?"left ":"") + 
9
      (pressing.right?"right ":"") + 
10
      (pressing.fire?"fire":""));
11
  }
12
  
13
  private function keyHandler(event:KeyboardEvent, isDown:Boolean):void 
14
  {
15
    //trace('Key code: ' + event.keyCode);
16
    
17
    // alternate "fire" buttons
18
    if (event.ctrlKey || 
19
      event.altKey || 
20
      event.shiftKey)
21
      pressing.fire = isDown;
22
23
    // key codes that support international keyboards:
24
    // QWERTY = W A S D
25
    // AZERTY = Z Q S D
26
    // DVORAK = , A O E
27
      
28
    switch(event.keyCode)
29
    {
30
      case Keyboard.UP:
31
      case 87: // W
32
      case 90: // Z
33
      case 188:// ,
34
        pressing.up = isDown;
35
      break;
36
      
37
      case Keyboard.DOWN:
38
      case 83: // S
39
      case 79: // O
40
        pressing.down = isDown;
41
      break;
42
      
43
      case Keyboard.LEFT:
44
      case 65: // A
45
      case 81: // Q
46
        pressing.left = isDown;
47
      break;
48
      
49
      case Keyboard.RIGHT:
50
      case 68: // D
51
      case 69: // E
52
        pressing.right = isDown;
53
      break;
54
      
55
      case Keyboard.SPACE:
56
      case Keyboard.SHIFT:
57
      case Keyboard.CONTROL:
58
      case Keyboard.ENTER:
59
      case 88: // x
60
      case 67: // c
61
        pressing.fire = isDown;
62
      break;
63
      
64
    }
65
  }
66
67
} // end class
68
} // 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.

1
2
  public class Main extends Sprite 
3
  {
4
    // the keyboard control system
5
    private var _controls : GameControls;
6
    // don't update the menu too fast
7
    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.

1
2
    private function init(e:Event = null):void 
3
    {
4
      _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:

1
2
    // this function draws the scene every frame
3
    private function onEnterFrame(e:Event):void 
4
    {
5
      try 
6
      {
7
        // for debugging the input manager, update the gui
8
        _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.Debug display showing controls in use.Debug display showing controls in use.
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:

The titlescreen spritesheetThe titlescreen spritesheetThe titlescreen spritesheet

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:

1
2
// Stage3D Shoot-em-up Tutorial Part 2
3
// by Christer Kaitila - www.mcfunkypants.com
4
5
// GameMenu.as
6
// A simple title screen / logo screen and menu
7
// that is displayed during the idle "attract mode"
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 GameMenu
17
  {
18
    // the sprite sheet image
19
    public var spriteSheet : LiteSpriteSheet;
20
    [Embed(source="../assets/titlescreen.png")]
21
    private var SourceImage : Class;
22
    
23
    // all the polygons that make up the scene
24
    public var batch : LiteSpriteBatch;
25
    
26
    // which menu item is active (0=none)
27
    public var menuState:int = 0;
28
    
29
    // pixel regions of the buttons
30
    public var menuWidth:int = 128;
31
    public var menuItemHeight:int = 32;
32
    public var menuY1:int = 0;
33
    public var menuY2:int = 0;
34
    public var menuY3:int = 0;
35
    
36
    // the sprites
37
    public var logoSprite:LiteSprite;
38
    // menu items when idle
39
    public var menuPlaySprite:LiteSprite;
40
    public var menuControlsSprite:LiteSprite;
41
    public var menuAboutSprite:LiteSprite;
42
    // menu items when active
43
    public var amenuPlaySprite:LiteSprite;
44
    public var amenuControlsSprite:LiteSprite;
45
    public var amenuAboutSprite:LiteSprite;
46
    // info screens
47
    public var aboutSprite:LiteSprite;
48
    public var controlsSprite:LiteSprite;
49
    
50
    public var showingAbout:Boolean = false;
51
    public var showingControls:Boolean = false;
52
    public var showingControlsUntil:Number = 0;
53
    public var showingAboutUntil:Number = 0;
54
      
55
    // where everything goes
56
    public var logoX:int = 0;
57
    public var logoY:int = 0;
58
    public var menuX:int = 0;
59
    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.

1
2
    public function GameMenu(view:Rectangle)
3
    {
4
      trace("Init the game menu..");
5
      setPosition(view);  
6
    }
7
    
8
    public function setPosition(view:Rectangle):void 
9
    {
10
      logoX = view.width / 2;
11
      logoY = view.height / 2 - 64;
12
      menuX = view.width / 2;
13
      menuY = view.height / 2 + 64;
14
      menuY1 = menuY - (menuItemHeight / 2);
15
      menuY2 = menuY - (menuItemHeight / 2) + menuItemHeight;
16
      menuY3 = menuY - (menuItemHeight / 2) + (menuItemHeight * 2);
17
    }
18
    
19
    public function createBatch(context3D:Context3D) : LiteSpriteBatch 
20
    {
21
      var sourceBitmap:Bitmap = new SourceImage();
22
23
      // create a spritesheet using the titlescreen image
24
      spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, 0, 0);
25
      
26
      // Create new render batch 
27
      batch = new LiteSpriteBatch(context3D, spriteSheet);
28
29
      // set up all required sprites right now
30
      var logoID:uint = spriteSheet.defineSprite(0, 0, 512, 256);
31
      logoSprite = batch.createChild(logoID);
32
      logoSprite.position.x = logoX;
33
      logoSprite.position.y = logoY;
34
35
      var menuPlaySpriteID:uint = spriteSheet.defineSprite(0, 256, menuWidth, 48);
36
      menuPlaySprite = batch.createChild(menuPlaySpriteID);
37
      menuPlaySprite.position.x = menuX;
38
      menuPlaySprite.position.y = menuY;
39
40
      var amenuPlaySpriteID:uint = spriteSheet.defineSprite(0, 256+128, menuWidth, 48);
41
      amenuPlaySprite = batch.createChild(amenuPlaySpriteID);
42
      amenuPlaySprite.position.x = menuX;
43
      amenuPlaySprite.position.y = menuY;
44
      amenuPlaySprite.alpha = 0;
45
46
      var menuControlsSpriteID:uint = spriteSheet.defineSprite(0, 304, menuWidth, 32);
47
      menuControlsSprite = batch.createChild(menuControlsSpriteID);
48
      menuControlsSprite.position.x = menuX;
49
      menuControlsSprite.position.y = menuY + menuItemHeight;
50
51
      var amenuControlsSpriteID:uint = spriteSheet.defineSprite(0, 304+128, menuWidth, 32);
52
      amenuControlsSprite = batch.createChild(amenuControlsSpriteID);
53
      amenuControlsSprite.position.x = menuX;
54
      amenuControlsSprite.position.y = menuY + menuItemHeight;
55
      amenuControlsSprite.alpha = 0;
56
57
      var menuAboutSpriteID:uint = spriteSheet.defineSprite(0, 336, menuWidth, 48);
58
      menuAboutSprite = batch.createChild(menuAboutSpriteID);
59
      menuAboutSprite.position.x = menuX;
60
      menuAboutSprite.position.y = menuY + menuItemHeight + menuItemHeight;
61
62
      var amenuAboutSpriteID:uint = spriteSheet.defineSprite(0, 336+128, menuWidth, 48);
63
      amenuAboutSprite = batch.createChild(amenuAboutSpriteID);
64
      amenuAboutSprite.position.x = menuX;
65
      amenuAboutSprite.position.y = menuY + menuItemHeight + menuItemHeight;
66
      amenuAboutSprite.alpha = 0;
67
68
      var aboutSpriteID:uint = spriteSheet.defineSprite(128, 256, 384, 128);
69
      aboutSprite = batch.createChild(aboutSpriteID);
70
      aboutSprite.position.x = menuX;
71
      aboutSprite.position.y = menuY + 64;
72
      aboutSprite.alpha = 0;
73
74
      var controlsSpriteID:uint = spriteSheet.defineSprite(128, 384, 384, 128);
75
      controlsSprite = batch.createChild(controlsSpriteID);
76
      controlsSprite.position.x = menuX;
77
      controlsSprite.position.y = menuY + 64;
78
      controlsSprite.alpha = 0;
79
80
      return batch;
81
    }

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.

1
2
    // our game will call these based on user input
3
    public function nextMenuItem():void
4
    {
5
      menuState++;
6
      if (menuState > 3) menuState = 1;
7
      updateState();
8
    }
9
    public function prevMenuItem():void
10
    {
11
      menuState--;
12
      if (menuState < 1) menuState = 3;
13
      updateState();
14
    }
15
    
16
    public function mouseHighlight(x:int, y:int):void
17
    {
18
      //trace('mouseHighlight ' + x + ',' + y);
19
      
20
      // when mouse moves, assume it moved OFF all items
21
      menuState = 0;
22
      
23
      var menuLeft:int = menuX - (menuWidth / 2);
24
      var menuRight:int = menuX + (menuWidth / 2);
25
      if ((x >= menuLeft) && (x <= menuRight))
26
      {
27
        //trace('inside ' + menuLeft + ',' + menuRight);
28
        if ((y >= menuY1) && (y <= (menuY1 + menuItemHeight)))
29
        {
30
          menuState = 1;
31
        }
32
        if ((y >= menuY2) && (y <= (menuY2 + menuItemHeight)))
33
        {
34
          menuState = 2;
35
        }
36
        if ((y >= menuY3) && (y <= (menuY3 + menuItemHeight)))
37
        {
38
          menuState = 3;
39
        }
40
      }
41
      updateState();
42
    }
43
    
44
    // adjust the opacity of menu items
45
    public function updateState():void
46
    {
47
      // ignore if menu is not visible:
48
      if (showingAbout || showingControls) return;
49
      // user clicked or pressed fire on a menu item:
50
      switch (menuState)
51
      {
52
        case 0: // nothing selected
53
          menuAboutSprite.alpha = 1;
54
          menuControlsSprite.alpha = 1;
55
          menuPlaySprite.alpha = 1;
56
          amenuAboutSprite.alpha = 0;
57
          amenuControlsSprite.alpha = 0;
58
          amenuPlaySprite.alpha = 0;
59
          break;
60
        case 1: // play selected
61
          menuAboutSprite.alpha = 1;
62
          menuControlsSprite.alpha = 1;
63
          menuPlaySprite.alpha = 0;
64
          amenuAboutSprite.alpha = 0;
65
          amenuControlsSprite.alpha = 0;
66
          amenuPlaySprite.alpha = 1;
67
          break;
68
        case 2: // controls selected
69
          menuAboutSprite.alpha = 1;
70
          menuControlsSprite.alpha = 0;
71
          menuPlaySprite.alpha = 1;
72
          amenuAboutSprite.alpha = 0;
73
          amenuControlsSprite.alpha = 1;
74
          amenuPlaySprite.alpha = 0;
75
          break;
76
        case 3: // about selected
77
          menuAboutSprite.alpha = 0;
78
          menuControlsSprite.alpha = 1;
79
          menuPlaySprite.alpha = 1;
80
          amenuAboutSprite.alpha = 1;
81
          amenuControlsSprite.alpha = 0;
82
          amenuPlaySprite.alpha = 0;
83
          break;
84
      }
85
    }
86
  
87
    // activate the currently selected menu item
88
    // returns true if we should start the game
89
    public function activateCurrentMenuItem(currentTime:Number):Boolean
90
    {
91
      // ignore if menu is not visible:
92
      if (showingAbout || showingControls) return false;
93
      // activate the proper option:
94
      switch (menuState)
95
      {
96
        case 1: // play selected
97
          return true;
98
          break;
99
        case 2: // controls selected
100
          menuAboutSprite.alpha = 0;
101
          menuControlsSprite.alpha = 0;
102
          menuPlaySprite.alpha = 0;
103
          amenuAboutSprite.alpha = 0;
104
          amenuControlsSprite.alpha = 0;
105
          amenuPlaySprite.alpha = 0;
106
          controlsSprite.alpha = 1;
107
          showingControls = true;
108
          showingControlsUntil = currentTime + 5000;
109
          break;
110
        case 3: // about selected
111
          menuAboutSprite.alpha = 0;
112
          menuControlsSprite.alpha = 0;
113
          menuPlaySprite.alpha = 0;
114
          amenuAboutSprite.alpha = 0;
115
          amenuControlsSprite.alpha = 0;
116
          amenuPlaySprite.alpha = 0;
117
          aboutSprite.alpha = 1;
118
          showingAbout = true;
119
          showingAboutUntil = currentTime + 5000;
120
          break;
121
      }
122
      return false;
123
    }

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.

1
2
    // called every frame: used to update the animation
3
    public function update(currentTime:Number) : void
4
    {   
5
      logoSprite.position.x = logoX;
6
      logoSprite.position.y = logoY;
7
      var wobble:Number = (Math.cos(currentTime / 500) / Math.PI) * 0.2;
8
      logoSprite.scaleX = 1 + wobble;
9
      logoSprite.scaleY = 1 + wobble;
10
      wobble = (Math.cos(currentTime / 777) / Math.PI) * 0.1;
11
      logoSprite.rotation = wobble;
12
      
13
      // pulse the active menu item
14
      wobble = (Math.cos(currentTime / 150) / Math.PI) * 0.1;
15
      amenuAboutSprite.scaleX = 
16
      amenuAboutSprite.scaleY = 
17
      amenuControlsSprite.scaleX = 
18
      amenuControlsSprite.scaleY = 
19
      amenuPlaySprite.scaleX = 
20
      amenuPlaySprite.scaleY = 
21
        1.1 + wobble;
22
      
23
      // show the about/controls for a while
24
      if (showingAbout)
25
      {
26
        if (showingAboutUntil > currentTime)
27
        {
28
          aboutSprite.alpha = 1;
29
        }
30
        else
31
        {
32
          aboutSprite.alpha = 0;
33
          showingAbout = false;
34
          updateState();
35
        }
36
      }
37
38
      if (showingControls)
39
      {
40
        if (showingControlsUntil > currentTime)
41
        {
42
          controlsSprite.alpha = 1;
43
        }
44
        else
45
        {
46
          controlsSprite.alpha = 0;
47
          showingControls = false;
48
          updateState();
49
        }
50
      }
51
    }
52
  } // end class
53
} // 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:

1
2
// Stage3D Shoot-em-up Tutorial Part 2
3
// by Christer Kaitila - www.mcfunkypants.com
4
5
// GameSound.as
6
// A simple sound and music system for our game
7
8
package
9
{
10
11
import flash.media.Sound;
12
import flash.media.SoundChannel;
13
14
  public class GameSound
15
  {
16
    
17
    // to reduce .swf size these are mono 11khz
18
    [Embed (source = "../assets/sfxmusic.mp3")]
19
    private var _musicMp3:Class;
20
    private var _musicSound:Sound = (new _musicMp3) as Sound;
21
    private var _musicChannel:SoundChannel;
22
23
    [Embed (source = "../assets/sfxgun1.mp3")]
24
    private var _gun1Mp3:Class;
25
    private var _gun1Sound:Sound = (new _gun1Mp3) as Sound;
26
    
27
    [Embed (source = "../assets/sfxgun2.mp3")]
28
    private var _gun2Mp3:Class;
29
    private var _gun2Sound:Sound = (new _gun2Mp3) as Sound;
30
    
31
    [Embed (source = "../assets/sfxgun3.mp3")]
32
    private var _gun3Mp3:Class;
33
    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.

1
2
    // the different phaser shooting sounds
3
    public function playGun(num:int):void
4
    {
5
      switch (num)
6
      {
7
        case 1 : _gun1Sound.play(); break;
8
        case 2 : _gun2Sound.play(); break;
9
        case 3 : _gun3Sound.play(); break;
10
      }
11
    }
12
    
13
    // the looping music channel
14
    public function playMusic():void
15
    {
16
      trace("Starting the music...");
17
      // stop any previously playing music
18
      stopMusic();
19
      // start the background music looping
20
      _musicChannel = _musicSound.play(0,9999); 
21
    }
22
    
23
    public function stopMusic():void
24
    {
25
      if (_musicChannel) _musicChannel.stop();
26
    }
27
28
  } // end class
29
} // 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.

The game background starfield tileThe game background starfield tileThe game background starfield tile

Create a new file in your project called GameBackground.as and start by extending the EntityManager class as follows:

1
2
3
// Stage3D Shoot-em-up Tutorial Part 2
4
// by Christer Kaitila - www.mcfunkypants.com
5
6
// GameBackground.as
7
// A very simple batch of background stars that scroll
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
    public function GameBackground(view:Rectangle)
27
    {
28
      // run the init functions of the EntityManager class
29
      super(view);
30
    }

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.

1
2
    override public function createBatch(context3D:Context3D) : LiteSpriteBatch 
3
    {
4
      var bgsourceBitmap:Bitmap = new bgSourceImage();
5
6
      // create a spritesheet with a single giant sprite
7
      spriteSheet = new LiteSpriteSheet(bgsourceBitmap.bitmapData, bgSpritesPerRow, bgSpritesPerCol);
8
      
9
      // Create new render batch 
10
      batch = new LiteSpriteBatch(context3D, spriteSheet);
11
      
12
      return batch;
13
    }
14
15
    override public function setPosition(view:Rectangle):void 
16
    {
17
      // allow moving fully offscreen before looping around
18
      maxX = 256+512+512;
19
      minX = -256;
20
      maxY = view.height;
21
      minY = view.y;
22
    }
23
    
24
    // for this test, create random entities that move 
25
    // from right to left with random speeds and scales
26
    public function initBackground():void 
27
    {
28
      trace("Init background...");
29
      // we need three 512x512 sprites
30
      var anEntity1:Entity = respawn(0)
31
      anEntity1 = respawn(0);
32
      anEntity1.sprite.position.x = 256;
33
      anEntity1.sprite.position.y = maxY / 2;
34
      anEntity1.speedX = bgSpeed;
35
      var anEntity2:Entity = respawn(0)
36
      anEntity2.sprite.position.x = 256+512;
37
      anEntity2.sprite.position.y = maxY / 2;
38
      anEntity2.speedX = bgSpeed;
39
      var anEntity3:Entity = respawn(0)
40
      anEntity3.sprite.position.x = 256+512+512;
41
      anEntity3.sprite.position.y = maxY / 2;
42
      anEntity3.speedX = bgSpeed;
43
    }

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:

1
2
    
3
    // called every frame: used to update the scrolling background
4
    override public function update(currentTime:Number) : void
5
    {   
6
      var anEntity:Entity;
7
      
8
      // handle all other entities
9
      for(var i:int=0; i<entityPool.length;i++)
10
      {
11
        anEntity = entityPool[i];
12
        if (anEntity.active)
13
        {
14
          anEntity.sprite.position.x += anEntity.speedX;
15
16
          if (anEntity.sprite.position.x >= maxX)
17
          {
18
            anEntity.sprite.position.x = minX;
19
          }
20
          else if (anEntity.sprite.position.x <= minX)
21
          {
22
            anEntity.sprite.position.x = maxX;
23
          }
24
        }
25
      }
26
    }
27
  } // end class
28
} // 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
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.

1
2
    // if this is set, custom behaviors are run
3
    public var aiFunction : Function;

Now edit EntityManager.as and add the following class variable to EntityManager.as where we create all the other vars.

1
2
    // the player entity - a special case
3
    public var thePlayer:Entity;

Next, implement two new functions that will handle the creation of this player entity and the spawning of new bullets.

1
2
    // this entity is the PLAYER
3
    public function addPlayer(playerController:Function):Entity 
4
    {
5
      thePlayer = respawn(10); // sprite #10 looks nice for now
6
      thePlayer.sprite.position.x = 32;
7
      thePlayer.sprite.position.y = maxY / 2;
8
      thePlayer.sprite.rotation = 180 * (Math.PI/180); // degrees to radians
9
      thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = 2; 
10
      thePlayer.speedX = 0;
11
      thePlayer.speedY = 0;
12
      thePlayer.aiFunction = playerController;
13
      return thePlayer;
14
    }   
15
    
16
    // shoot a bullet (from the player for now)
17
    public function shootBullet():Entity 
18
    {
19
      var anEntity:Entity;
20
      anEntity = respawn(39); // bullet sprite is #39
21
      anEntity.sprite.position.x = thePlayer.sprite.position.x + 8;
22
      anEntity.sprite.position.y = thePlayer.sprite.position.y + 4;
23
      anEntity.sprite.rotation = 180 * (Math.PI/180);
24
      anEntity.sprite.scaleX = anEntity.sprite.scaleY = 2; 
25
      anEntity.speedX = 10;
26
      anEntity.speedY = 0;
27
      return anEntity;
28
    }

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

1
2
          anEntity.sprite.position.x += anEntity.speedX;
3
          anEntity.sprite.position.y += anEntity.speedY;
4
          
5
          // the player follows different rules
6
          if (anEntity.aiFunction != null)
7
          {
8
            anEntity.aiFunction(anEntity);
9
          }
10
          else // all other entities use the "demo" logic
11
          {
12
          
13
            anEntity.sprite.rotation += 0.1;
14
            // ... 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?):

Bullet hell!Bullet hell!Bullet hell!

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:

1
2
                    if (anEntity.sprite.position.x > maxX)
3
                    {
4
                        anEntity.speedX *= -1;
5
                        anEntity.sprite.position.x = maxX;
6
                    }

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:

1
2
3
// Stage3D Shoot-em-up Tutorial Part2
4
// by Christer Kaitila - www.mcfunkypants.com
5
// Created for active.tutsplus.com
6
7
package 
8
{
9
  [SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")]
10
11
  import flash.display3D.*;
12
  import flash.display.Sprite;
13
  import flash.display.StageAlign;
14
  import flash.display.StageQuality;
15
  import flash.display.StageScaleMode;
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
    
22
  public class Main extends Sprite 
23
  {
24
    // the keyboard control system
25
    private var _controls : GameControls;
26
    // don't update the menu too fast
27
    private var nothingPressedLastFrame:Boolean = false;
28
    // timestamp of the current frame
29
    public var currentTime: int;
30
    // player one's entity
31
    public var thePlayer:Entity;
32
    // main menu = 0 or current level number
33
    private var _state : int = 0;
34
    // the title screen batch
35
    private var _mainmenu : GameMenu;
36
    // the sound system
37
    private var _sfx : GameSound; 
38
    // the background stars
39
    private var _bg : GameBackground; 
40
    
41
    private var _entities : EntityManager;
42
    private var _spriteStage : LiteSpriteStage;
43
    private var _gui : GameGUI;
44
    private var _width : Number = 600;
45
    private var _height : Number = 400;
46
    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.

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("Stage3D Shoot-em-up Tutorial Part 2");
20
      addChild(_gui);
21
      stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
22
      stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler);
23
      stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO);
24
      trace("Stage3D requested...");    
25
      _sfx = new GameSound();
26
    }
27
        
28
    // this is called when the 3d card has been set up
29
    // and is ready for rendering using stage3d
30
    private function onContext3DCreate(e:Event):void 
31
    {
32
      trace("Stage3D context created! Init sprite engine...");
33
      context3D = stage.stage3Ds[0].context3D;
34
      initSpriteEngine();
35
    }
36
    
37
    // this can be called when using an old version of flash
38
    // or if the html does not include wmode=direct
39
    private function errorHandler(e:ErrorEvent):void 
40
    {
41
      trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text);
42
    }
43
44
    protected function onResizeEvent(event:Event) : void
45
    {
46
      trace("resize event...");
47
      
48
      // Set correct dimensions if we resize
49
      _width = stage.stageWidth;
50
      _height = stage.stageHeight;
51
      
52
      // Resize Stage3D to continue to fit screen
53
      var view:Rectangle = new Rectangle(0, 0, _width, _height);
54
      if ( _spriteStage != null ) {
55
        _spriteStage.position = view;
56
      }
57
      if(_entities != null) {
58
        _entities.setPosition(view);
59
      }
60
      if(_mainmenu != null) {
61
        _mainmenu.setPosition(view);
62
      }
63
    }
64
65
    private function initSpriteEngine():void 
66
    {
67
      // init a gpu sprite system
68
      var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); 
69
      _spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect);
70
      _spriteStage.configureBackBuffer(_width,_height);
71
      
72
      // create the background stars
73
      _bg = new GameBackground(stageRect);
74
      _bg.createBatch(context3D);
75
      _spriteStage.addBatch(_bg.batch);
76
      _bg.initBackground();
77
      
78
      // create a single rendering batch
79
      // which will draw all sprites in one pass
80
      var view:Rectangle = new Rectangle(0,0,_width,_height)
81
      _entities = new EntityManager(stageRect);
82
      _entities.createBatch(context3D);
83
      _spriteStage.addBatch(_entities.batch);
84
      
85
      // create the logo/titlescreen main menu
86
      _mainmenu = new GameMenu(stageRect);
87
      _mainmenu.createBatch(context3D);
88
      _spriteStage.addBatch(_mainmenu.batch);
89
      
90
      // tell the gui where to grab statistics from
91
      _gui.statsTarget = _entities; 
92
      
93
      // start the render loop
94
      stage.addEventListener(Event.ENTER_FRAME,onEnterFrame);
95
96
      // only used for the menu
97
      stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);   
98
      stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove); 
99
    }

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.

1
2
3
    public function playerLogic(me:Entity):void
4
    {
5
      me.speedY = me.speedX = 0;
6
      if (_controls.pressing.up)
7
        me.speedY = -4;
8
      if (_controls.pressing.down)
9
        me.speedY = 4;
10
      if (_controls.pressing.left)
11
        me.speedX = -4;
12
      if (_controls.pressing.right)
13
        me.speedX = 4;
14
        
15
      // keep on screen
16
      if (me.sprite.position.x < 0)
17
        me.sprite.position.x = 0;
18
      if (me.sprite.position.x > _width)
19
        me.sprite.position.x = _width;
20
      if (me.sprite.position.y < 0)
21
        me.sprite.position.y = 0;
22
      if (me.sprite.position.y > _height)
23
        me.sprite.position.y = _height;
24
    }
25
    
26
    private function mouseDown(e:MouseEvent):void   
27
    {   
28
      trace('mouseDown at '+e.stageX+','+e.stageY);
29
      if (_state == 0) // are we at the main menu?
30
      {
31
        if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer()))
32
        { // if the above returns true we should start the game
33
          startGame();
34
        }
35
      }
36
    }
37
38
    private function mouseMove(e:MouseEvent):void   
39
    {
40
      if (_state == 0) // are we at the main menu?
41
      {
42
        // select menu items via mouse
43
        if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY);
44
      }
45
    }
46
47
    // handle any player input
48
    private function processInput():void
49
    {
50
      if (_state == 0) // are we at the main menu?
51
      {
52
        // select menu items via keyboard
53
        if (_controls.pressing.down || _controls.pressing.right)
54
        {
55
          if (nothingPressedLastFrame) 
56
          {
57
            _sfx.playGun(1);
58
            _mainmenu.nextMenuItem();
59
            nothingPressedLastFrame = false;
60
          }
61
        }
62
        else if (_controls.pressing.up || _controls.pressing.left)
63
        {
64
          if (nothingPressedLastFrame) 
65
          {
66
            _sfx.playGun(1);
67
            _mainmenu.prevMenuItem();
68
            nothingPressedLastFrame = false;
69
          }
70
        }
71
        else if (_controls.pressing.fire)
72
        {
73
          if (_mainmenu.activateCurrentMenuItem(getTimer()))
74
          { // if the above returns true we should start the game
75
            startGame();
76
          }
77
        }
78
        else
79
        {
80
          // this ensures the menu doesn't change too fast
81
          nothingPressedLastFrame = true;
82
        }
83
      }
84
      else 
85
      {
86
        // we are NOT at the main menu: we are actually playing the game
87
        // in future versions we will add projectile
88
        // spawning functinality here to fire bullets
89
        if (_controls.pressing.fire)
90
        {
91
          _sfx.playGun(1);
92
          _entities.shootBullet();
93
        }
94
      }
95
    }

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.

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

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:

1
2
    
3
    // this function draws the scene every frame
4
    private function onEnterFrame(e:Event):void 
5
    {
6
      try 
7
      {
8
        // grab timestamp of current frame
9
        currentTime = getTimer();
10
11
        // erase the previous frame
12
        context3D.clear(0, 0, 0, 1);
13
        
14
        // for debugging the input manager, update the gui
15
        _gui.titleText = _controls.textDescription();
16
        
17
        // process any player input
18
        processInput();
19
20
        // scroll the background
21
        _bg.update(currentTime);
22
        
23
        // update the main menu titlescreen
24
        if (_state == 0)
25
          _mainmenu.update(currentTime);
26
        
27
        // keep adding more sprites - FOREVER!
28
        // this is a test of the entity manager's
29
        // object reuse "pool"
30
        _entities.addEntity();
31
        
32
        // move/animate all entities
33
        _entities.update(currentTime);
34
        
35
        // draw all entities
36
        _spriteStage.render();
37
38
        // update the screen
39
        context3D.present();
40
      }
41
      catch (e:Error) 
42
      {
43
        // this can happen if the computer goes to sleep and
44
        // then re-awakens, requiring reinitialization of stage3D
45
        // (the onContext3DCreate will fire again)
46
      }
47
    }
48
  } // end class
49
} // 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:

Screenshot of this week's upgrades in actionScreenshot of this week's upgrades in actionScreenshot of this week's upgrades in action

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!

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