Advertisement

Build a Stage3D Shoot-'Em-Up: Explosions, Parallax, and Collisions

by
This post is part of a series called Shoot-'Em-Up.
Starling Particle Effects for Stage3D Shooter Games
Quick Tip: A Simple Score Display for Flash Games

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're adding eye candy with particle systems, a parallax effect, framerate-independent game loop timers, and collision detection.


Final Result Preview

Let's take a look at the final result we will be working towards: a hardware-accelerated shoot-em-up demo that includes everything from parts one and two of this series, plus an efficient particle system for loads of eye-candy, framerate-independent timers for consistent movement, a subtle background parallax effect, the ability for entities to orbit one another, and a collision detection system capable of handling tons of entities.

Check it out: every explosion is slightly different!


Introduction: Welcome to Level Three!

Let's continue to make a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius in actionscript.

In the first part of this series, we implemented a basic 2D sprite engine that achieves great performance through the use of Stage3D hardware rendering as well as several optimizations.

In (the second part, we implemented a title screen, the main menu, sound effects and music, and an input system so that the player could control their spaceship using the keyboard.

In this part, we are going to add all the eye-candy: a particle system, complete with sparks, flying debris, shockwaves, engine fire trails and tons of explosions.

In previous versions, our game was framerate-locked and ran slower on old computers. To ensure the same timings for everything no matter what the framerate, we are going to change all movement and animation simulation units to account for the exact number of milliseconds that have passed since the previous frame. This way, whether you are running at 60fps on a modern gaming rig or your grandma's old netbook, the game experience itself will be identical.

Finally, we're going to program collision detection, which is required in nearly any game you can imagine. In order to trigger explosions and we need to be able to detect when a bullet has hit an enemy. While we're at it, we are going to throw in a little bit of additional pizazz, just for fun, including a vertical parallax effect to the starfield background and an R-Type inspired orbiting "power orb" companion that circles the player's ship.


Step 1: Open Your Existing Project

If you don't already have it, be sure to download the source code from part two. Open the project file in FlashDevelop and get ready to upgrade your game!

This source code will work in any other AS3 compiler, from CS6 to Flash Builder. If you do use FB, be sure to include "-default-frame-rate 60" in your compiler options to ensure you get the best performance.


Step 2: Get the Party Started!

We are going to take advantage of the well-optimized internals of your entity manager class from last time by adding a simple particle system to it that still uses all the same basic entity and spritesheet functionality.

This way, we are still rendering the entire game's sprites (ships, bullets and all) in a single geometry batch using a single texture. Therefore, much of the simulation of particles will be handled the same way as we currently handle the movement of the enemies. Most importantly, we are going to keep the number of draw calls to a minimum by inserting particles into our existing sprite batch.

The fist thing we need to do is define a few interesting effects. We're going to have a little fun and create some cool-looking effects such as an expanding ring of blueish energy (a "shockwave"), a bunch of different fireballs that spin and fade out, some fast moving sparks that stay nice and bright and some metallic spaceship hull debris.

Create a new file in your project called GameParticles.as and implement the basic particle and explosion helper functions.

// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com

// GameParticles.as
// A simple particle system class that is
// used by the EntityManager for explosions, etc.

package
{
  import flash.geom.Point;

  public class GameParticles
  { 
    public var allParticles : Vector.<Entity>;
    public var gfx:EntityManager;
  
    public function GameParticles(entityMan:EntityManager)
    {
      allParticles = new Vector.<Entity>();
      gfx = entityMan;
    }
    
    // a cool looking explosion effect with a big fireball,
    // a blue fast shockwave, smaller bursts of fire,
    // a bunch of small sparks and pieces of hull debris
    public function addExplosion(pos:Point):void
    {
      addShockwave(pos);
      addDebris(pos,6,12);
      addFireball(pos);
      addBursts(pos,10,20);
      addSparks(pos,8,16);
    }

In the code above, we created a new class that requires a reference to our existing EntityManager. In the class constructor, we initialize a list of all known particles, which might be useful in a later tutorial to save having to iterate through all known entities if all we want are the particles.

The addExplosion() function will be called in response to a collision detection between a bullet and an enemy during gameplay. It will spawn a shockwave, some debris, a huge fireball, some smaller spinnng balls of fire and a bunch of flying sparks. Let's define these different effects next - but before we can, we need a generic particle creation function.


Step 3: Define a Basic Particle

Continue to add to GameParticles.as by implementing the initialization function for a generic particle. It will use our entity manager to spawn (or respawn from the list of inactive entities) a sprite with some appropriate properties.

Some of the default values can't be inserted into the function declaration itself since they will take advantage of some randomness, so we simply use NaN ("not a number") as the optional function parameter defaults and execute some code if no value was defined when this function gets run. This way, we don't need to specifiy everything about a particular particle if the defaults will do. If we used zero as the default, then we couldn't force zero to be the actual value used.

    
    public function addParticle(
      spr:uint,         // sprite ID
      x:int, y:int,         // starting location
      startScale:Number = 0.01, // initial scale
      spdX:Number = 0,      // horizontal speed in px/sec
      spdY:Number = 0,      // vertical speed in px/sec
      startAlpha:Number = 1,    // initial transparency (1=opaque)
      rot:Number = NaN,     // starting rotation in degrees/sec
      rotSpd:Number = NaN,    // rotational speed in degrees/sec
      fadeSpd:Number = NaN,     // fade in/out speed per second
      zoomSpd:Number = NaN    // growth speed per second
      ):Entity 
    {
      // Defaults tell us to to randomize some properties
      // Why NaN? Can't put fastRandom() inside a function declaration
      if (isNaN(rot)) rot = gfx.fastRandom() * 360; 
      if (isNaN(rotSpd)) rotSpd = gfx.fastRandom() * 360 - 180;
      if (isNaN(fadeSpd)) fadeSpd = -1 * (gfx.fastRandom() * 1 + 1);
      if (isNaN(zoomSpd)) zoomSpd = gfx.fastRandom() * 2 + 1;
      
      var anEntity:Entity;
      anEntity = gfx.respawn(spr);
      anEntity.sprite.position.x = x;
      anEntity.sprite.position.y = y;
      anEntity.speedX = spdX;
      anEntity.speedY = spdY;
      anEntity.sprite.rotation = rot * gfx.DEGREES_TO_RADIANS;
      anEntity.rotationSpeed = rotSpd * gfx.DEGREES_TO_RADIANS;
      anEntity.collidemode = 0;
      anEntity.fadeAnim = fadeSpd;
      anEntity.zoomAnim = zoomSpd;
      anEntity.sprite.scaleX = startScale;
      anEntity.sprite.scaleY = startScale;
      anEntity.sprite.alpha = startAlpha;
      if (!anEntity.recycled)
        allParticles.push(anEntity);
      return anEntity;
    }

Step 4: Eye Candy!

The final step in the creation of our awesome new particle system class is to create the functions for various special effects used by our explosions in the game as listed above.

    // one big spinning ball of fire
    public function addFireball(pos:Point):void
    {
      addParticle(gfx.spritenumFireball, pos.x, pos.y, 0.01, 0, 0, 1, NaN, NaN, NaN, 4);
    }
    
    // a shockwave ring that expands quickly
    public function addShockwave(pos:Point):void
    {
      addParticle(gfx.spritenumShockwave, pos.x, pos.y, 0.01, 0, 0, 1, NaN, NaN, -3, 20);
    }

    // several small fireballs that move and spin
    public function addBursts(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        addParticle(gfx.spritenumFireburst, 
          pos.x + gfx.fastRandom() * 16 - 8, 
          pos.y + + gfx.fastRandom() * 16 - 8,
          0.02, 
          gfx.fastRandom() * 200 - 100,
          gfx.fastRandom() * 200 - 100, 
          0.75);
      }
    }

    // several small bright glowing sparks that move quickly
    public function addSparks(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        // small sparks that stay bright but get smaller
        addParticle(gfx.spritenumSpark, pos.x, pos.y, 1, 
          gfx.fastRandom() * 320 - 160, 
          gfx.fastRandom() * 320 - 160, 
          1, NaN, NaN, 0, -1.5);
      }
    }
      
    // small pieces of destroyed spaceship debris, moving on average slightly forward
    public function addDebris(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        addParticle(gfx.spritenumDebris, pos.x, pos.y, 1, 
        gfx.fastRandom() * 180 - 120, 
        gfx.fastRandom() * 180 - 90, 
        1, NaN, NaN, -1, 0);
      }
    }

  } // end class
} // end package

That's it for our simplistic particle system. As you can see, we only define the behaviors of each type of visual effect in this class: the work of animating each particle is done at the same time as animating all the other entities, by our entity manager class which we will upgrade next. First, however, we need to add a bunch of new properties to our basic entity class to support these new behaviors.


Step 5: Upgrade the Entity Class

We need to add a few new properties to our basic entity class. Since we are going to make the player's ship output a steady stream of fire from the engines, for example, we want to be able to store this new information for every entity. Additionally, some new properties that pertain to particle simulation and collision detection need to be defined here. Open your existing Entity.as and make a few changes as follows.

// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com

// Entity.as
// The Entity class will eventually hold all game-specific entity stats
// for the spaceships, bullets and effects in our game. For now,
// it simply holds a reference to a gpu sprite and a few demo properties.
// This is where you would add hit points, weapons, ability scores, etc.

package
{
  import flash.geom.Point;
  import flash.geom.Rectangle;
  
  public class Entity
  {
    private var _speedX : Number;
    private var _speedY : Number;
    private var _sprite : LiteSprite;
    public var active : Boolean = true;

    // if this is set, custom behaviors are run
    public var aiFunction : Function;
    
    // collision detection
    public var isBullet:Boolean = false; // only these check collisions
    public var leavesTrail:Boolean = false; // creates particles as it moves
    public var collidemode:uint = 0; // 0=none, 1=sphere, 2=box, etc.
    public var collideradius:uint = 32; // used for sphere collision    
    // box collision is not implemented (yet)
    public var collidebox:Rectangle = new Rectangle(-16, -16, 32, 32);
    public var collidepoints:uint = 25; // score earned if destroyed
    public var touching:Entity; // what entity just hit us?
    public var owner:Entity; // so your own bullets don't hit you
    public var orbiting:Entity; // entities can orbit (circle) others
    public var orbitingDistance:Number; // how far in px from the orbit center
    
    // used for particle animation (in units per second)
    public var fadeAnim:Number = 0;
    public var zoomAnim:Number = 0;
    public var rotationSpeed:Number = 0;

    // used to mark whether or not this entity was 
    // freshly created or reused from an inactive one
    public var recycled:Boolean = false;
    
    public function Entity(gs:LiteSprite = null)
    {
      _sprite = gs;
      _speedX = 0.0;
      _speedY = 0.0;
    }
    
    public function die() : void
    {
      // allow this entity to be reused by the entitymanager
      active = false;
      // skip all drawing and updating
      sprite.visible = false;
      // reset some things that might affect future reuses:
      leavesTrail = false;
      isBullet = false;
      touching = null;
      owner = null;
      collidemode = 0;
    }

    public function get speedX() : Number 
    {
      return _speedX;
    }
    public function set speedX(sx:Number) : void 
    {
      _speedX = sx;
    }
    public function get speedY() : Number 
    {
      return _speedY;
    }
    public function set speedY(sy:Number) : void 
    {
      _speedY = sy;
    }
    public function get sprite():LiteSprite 
    { 
      return _sprite;
    }
    public function set sprite(gs:LiteSprite):void 
    {
      _sprite = gs;
    }

As you can see, much of the inits are the same as in the previous tutorial. When an entity "dies" (that is to say, is made invisible and available for reuse by our optimized entity reuse pool) we turn off a few of these new values so that the next entity to reuse this sprite doesn't take on unwanted behaviors.


Step 6: Implement Collision Detection

This is the most important part of this tutorial. We are going to take our tech demo from a mere graphics demonstraton to something that feels like an actual game by implementing our collision detection routine.

To keep things simple (and fast) for now we are not going to implement bounding-box collision or box-to-sphere or ray collisions, which are often used in complex physics engines. We are going to focus on just what is needed by our game, which is a simple way to detect if something is "close enough" to something else to trigger a collision (and resulting explosion).

Sphere collision detection simply checks to see if one circle is inside the radius of another. This way, we can give different entities a "radius" size and check how far their centerpoints are to determine if these two circles are overlapping. To make our game run even faster, we are only going to do this math if both entities are set to be collide-able.

As a further optimization, instead of using the built-in Point.distance function, we are going to do the trigonometry math manually, since this has been shown in benchmarks to run approximately six times faster. It looks like more code, but all we're really doing is Pythagoras's theorem without any square roots.

By avoiding bothering with the "proper" distance and instead comparing "squared" distances using only multiplication, we actually are checking the distance to the power of two. None of this matters, however. The end result is a very fast and simple way of checking to see if two circles overlap that doesn't need to use sin, cos, divisions, power-of or square root calculations. Sleazy, but effective!


    // used for collision callback performed in GameActorpool
    public function colliding(checkme:Entity):Entity
    {
      if (collidemode == 0)
      {
        return null;
      }
      else if (collidemode == 1) // sphere
      {
        if (isCollidingSphere(checkme))
          return checkme;
        else
          return null;
      }
    }
  
  // simple sphere to sphere collision
  public function isCollidingSphere(checkme:Entity):Boolean
  {
    // never collide with yourself
    if (this == checkme) return false;
    // only check if these shapes are collidable
    if (!collidemode || !checkme.collidemode) return false;
    // don't check your own bullets
    if (checkme.owner == this) return false;
    // don't check things on the same "team"
    if (checkme.owner == owner) return false;
    // don't check if no radius
    if (collideradius == 0 || checkme.collideradius == 0) return false;
    
    // this is the simpler way to do it, but it runs really slow
    // var dist:Number = Point.distance(sprite.position, checkme.sprite.position);
    // if (dist <= (collideradius+checkme.collideradius))
    
    // this looks weird but is 6x faster than the above
    // see: http://www.emanueleferonato.com/2010/10/13/as3-geom-point-vs-trigonometry/
    if (((sprite.position.x - checkme.sprite.position.x) * 
      (sprite.position.x - checkme.sprite.position.x) +
      (sprite.position.y - checkme.sprite.position.y) *
      (sprite.position.y - checkme.sprite.position.y))
      <= 
      (collideradius+checkme.collideradius)*(collideradius+checkme.collideradius))
    {
      touching = checkme; // remember who hit us
      return true;
    }
    
    // default: too far away
    // trace("No collision. Dist = "+dist);
    return false;
    
  }

  } // end class
} // end package

Tht's it for our newly upgraded Entity.as class. We now have entities in our game that store the stats needed for this tutorial and can calculate collisions.


Step 7: Upgrade the Entity Manager

There are many new additions to the entity manager which add the ability to request collision detection, trigger sound effects, add particles and much more. Open your existing EntityManager.as and make a few changes as follows. There are so many small changes since last time that the entire file is listed here to avoid confusion, so you might want to simply replace the entire class with this new third version.


// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com

// EntityManager.as
// The entity manager handles a list of all known game entities.
// This object pool will allow for reuse (respawning) of
// sprites: for example, when enemy ships are destroyed,
// they will be re-spawned when needed as an optimization 
// that increases fps and decreases ram use.

package
{
  import flash.display.Bitmap;
  import flash.display3D.*;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  
  public class EntityManager
  {
    // a particle system class that updates our sprites
    public var particles:GameParticles;
    
    // so that explosions can be played
    public var sfx:GameSound;
    
    // the sprite sheet image
    public var spriteSheet : LiteSpriteSheet;
    private const SpritesPerRow:int = 8;
    private const SpritesPerCol:int = 8;
    [Embed(source="../assets/sprites.png")]
    private var SourceImage : Class;

    // the general size of the player and enemies
    private const shipScale:Number = 1.5;
    // how fast player bullets go per second
    public var bulletSpeed:Number = 250;

    // for framerate-independent timings
    public var currentFrameSeconds:Number = 0;
    
    // sprite IDs (indexing the spritesheet)
    public const spritenumFireball:uint = 63;
    public const spritenumFireburst:uint = 62;
    public const spritenumShockwave:uint = 61;
    public const spritenumDebris:uint = 60;
    public const spritenumSpark:uint = 59;
    public const spritenumBullet3:uint = 58;
    public const spritenumBullet2:uint = 57;
    public const spritenumBullet1:uint = 56;
    public const spritenumPlayer:uint = 10;
    public const spritenumOrb:uint = 17;

    // reused for calculation speed
    public const DEGREES_TO_RADIANS:Number = Math.PI / 180;
    public const RADIANS_TO_DEGREES:Number = 180 / Math.PI;
    
    // the player entity - a special case
    public var thePlayer:Entity;
    // a "power orb" that orbits the player
    public var theOrb:Entity;
    
    // a reusable pool of entities
    // this contains every known Entity
    // including the contents of the lists below
    public var entityPool : Vector.<Entity>;
    // these pools contain only certain types
    // of entity as an optimization for smaller loops 
    public var allBullets : Vector.<Entity>;
    public var allEnemies : Vector.<Entity>;
    
    // all the polygons that make up the scene
    public var batch : LiteSpriteBatch;
    
    // for statistics
    public var numCreated : int = 0;
    public var numReused : int = 0;
    
    public var maxX:int;
    public var minX:int;
    public var maxY:int;
    public var minY:int;

In the code above, we are defining a large number of class variables, many of which are new to this tutorial. Of note are those that pertain to timing and speed. Instead of the relative speeds of each moving ship being tied directly to the framerate, we want the game to run at the same speed no matter what kind of machine the player is using.

By keeping track of the elapsed time since the previous frame, we can multiply various "speed per second" constants by the number of milliseconds that the current frame took in order to achieve smooth movement even if the FPS fluctuates.


Step 8: Upgrade the Spritesheet

In the code above we are storing constant values for the positions of various sprites in our spritesheet image. We're added a few new kinds of sprite that pertain to particles, and have reorganized the spritesheet to ensure that our randly-spawning enemies only come from the first few rows. These changes have neccessitated a few minor changes to our spritesheet:

Our upgraded spritesheet.
Right-click to download.

As you can see, we now have some extra sprites for explosions, shockwaves, sparks and debris.


Step 9: Upgrade the Inits

Continuing with EntityManager.as, upgrade the basic init routines to create lists for each type of entity. Each of these lists keeps track of a specific kind of entity/sprite. We can use these lists to improve performance when we need to loop through all of only a particular kind of entity, saving the time it would take to look through all known entities of any kind. The particles, however, are going to be stored in their own class instance using the new GameParticles class we implemented above.

    
    public function EntityManager(view:Rectangle)
    {
      entityPool = new Vector.<Entity>();
      allBullets = new Vector.<Entity>();
      allEnemies = new Vector.<Entity>();
      particles = new GameParticles(this);
      setPosition(view);  
    }
    
    public function setPosition(view:Rectangle):void 
    {
      // allow moving fully offscreen before
      // automatically being culled (and reused)
      maxX = view.width + 64;
      minX = view.x - 64;
      maxY = view.height + 64;
      minY = view.y - 64;
    }

    public function createBatch(context3D:Context3D) : LiteSpriteBatch 
    {
      var sourceBitmap:Bitmap = new SourceImage();

      // create a spritesheet with 8x8 (64) sprites on it
      spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, SpritesPerRow, SpritesPerCol);
      
      // Create new render batch 
      batch = new LiteSpriteBatch(context3D, spriteSheet);
      
      return batch;
    }
    
    // search the entity pool for unused entities and reuse one
    // if they are all in use, create a brand new one
    public function respawn(sprID:uint=0):Entity
    {
      var currentEntityCount:int = entityPool.length;
      var anEntity:Entity;
      var i:int = 0;
      // search for an inactive entity
      for (i = 0; i < currentEntityCount; i++ ) 
      {
        anEntity = entityPool[i];
        if (!anEntity.active && (anEntity.sprite.spriteId == sprID))
        {
          //trace('Reusing Entity #' + i);
          anEntity.active = true;
          anEntity.sprite.visible = true;
          anEntity.recycled = true;
          numReused++;
          return anEntity;
        }
      }
      // none were found so we need to make a new one
      //trace('Need to create a new Entity #' + i);
      var sprite:LiteSprite;
      sprite = batch.createChild(sprID);
      anEntity = new Entity(sprite);
      entityPool.push(anEntity);
      numCreated++;
      return anEntity;
    }

Above, we've also upgraded the setPosition function to allow entities to move beyond any of the four edges of the screen before being respawned, since things are moving in all directions now. In last week's tutorial, entities were only destroyed if they moved off the left edge and bounced of the other three edges of the screen. The createBatch and respawn functions are virtually unchanged since last time.

To avoid entities being put into the various sub-lists we have implemented in our class constructor more than once, we add a new flag to the entity, enEntity.recycled which informs the entity manager whether or not the sprite that it returns is brand new or not. This way, only a single reference to each sprite is stored in our lists.


Step 10: Fast Random

As a further small optimization, instead of using Math.random() over and over during the game, we are going to implement a slightly faster, XOR-based pseudo-random function.

This function has a secondary benefit, apart from the fact that it runs four times faster than the built-in random function. It can optionally be seeded with a constant value as the starting fastrandomseed in order to produce the exact same set of random numbers in sequence each time.

This could be handy in future versions of your game for storing replays or for savegames. For now, however, the only reason we're doing things this way is to eke out a tiny bit more performance.

    
    // this XOR based fast random number generator runs 4x faster
    // than Math.random() and also returns a number from 0 to 1
    // see http://www.calypso88.com/?cat=7
    private const FASTRANDOMTOFLOAT:Number = 1 / uint.MAX_VALUE;
    private var fastrandomseed:uint = Math.random() * uint.MAX_VALUE;
    public function fastRandom():Number
    {
      fastrandomseed ^= (fastrandomseed << 21);
      fastrandomseed ^= (fastrandomseed >>> 35);
      fastrandomseed ^= (fastrandomseed << 4);
      return (fastrandomseed * FASTRANDOMTOFLOAT);
    }

Step 11: Upgrade the Player

Continuing with EntityManager.as, implement the following minor upgrades to the player inits. In particular, we are going to set the boolean flag thePlayer.leavesTrail to true so that the player's engines emit a steady stream of fireballs that quickly shrink and fade out. This will give a nice effect.

Additionally, just for fun let's implement a "power orb" that orbits the player's ship. This "companion" is something that is heavily inspired by retro shooters like R-Type and will give our game a little more pizazz. The "orb", as we'll call it, will spin around the player, emitting a smaller trail of its own, and will be able to destroy incoming enemies.

In future versions of our game, it might be interesting to make "orb kills" give the player more points than those achieved by shooting bullets. A sort of "skill shot", so to speak. You could even implement a special achievement award for an entire level completed without ever firing a shot - by using the orb as the sole means for defending yourself.

    // this entity is the PLAYER
    public function addPlayer(playerController:Function):Entity 
    {
      thePlayer = respawn(spritenumPlayer);
      thePlayer.sprite.position.x = 32;
      thePlayer.sprite.position.y = maxY / 2;
      thePlayer.sprite.rotation = 180 * DEGREES_TO_RADIANS;
      thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = shipScale; 
      thePlayer.speedX = 0;
      thePlayer.speedY = 0;
      thePlayer.active = true;
      thePlayer.aiFunction = playerController;
      thePlayer.leavesTrail = true;
      
      // just for fun, spawn an orbiting "power orb"
      theOrb = respawn(spritenumOrb);
      theOrb.rotationSpeed = 720 * DEGREES_TO_RADIANS;
      theOrb.leavesTrail = true;
      theOrb.collidemode = 1;
      theOrb.collideradius = 12;
      theOrb.isBullet = true;
      theOrb.owner = thePlayer;
      theOrb.orbiting = thePlayer;
      theOrb.orbitingDistance = 180;
      
      return thePlayer;
    }

Step 12: Bullets and Enemies

The shootBullet and addEntity functions from last time remain virtually the same, but are included here to make your life easier. Note that we are now using the new entity properties for collisions, and the random entities are now being set to the proper rotation to be facing whatever direction they are flying.

    // shoot a bullet (from the player for now)
    public function shootBullet(powa:uint=1):Entity 
    {
      var anEntity:Entity;
      // three possible bullets, progressively larger
      if (powa == 1) 
        anEntity = respawn(spritenumBullet1);
      else if (powa == 2) 
        anEntity = respawn(spritenumBullet2);
      else 
        anEntity = respawn(spritenumBullet3);
      anEntity.sprite.position.x = thePlayer.sprite.position.x + 8;
      anEntity.sprite.position.y = thePlayer.sprite.position.y + 2;
      anEntity.sprite.rotation = 180 * DEGREES_TO_RADIANS;
      anEntity.sprite.scaleX = anEntity.sprite.scaleY = 1; 
      anEntity.speedX = bulletSpeed;
      anEntity.speedY = 0;
      anEntity.owner = thePlayer;
      anEntity.collideradius = 10;
      anEntity.collidemode = 1;
      anEntity.isBullet = true;
      if (!anEntity.recycled)
        allBullets.push(anEntity);
      return anEntity;
    }   

    // for this test, create random entities that move 
    // from right to left with random speeds and scales
    public function addEntity():void 
    {
      var anEntity:Entity;
      var randomSpriteID:uint = Math.floor(fastRandom() * 55);
      // try to reuse an inactive entity (or create a new one)
      anEntity = respawn(randomSpriteID);
      // give it a new position and velocity
      anEntity.sprite.position.x = maxX;
      anEntity.sprite.position.y = fastRandom() * maxY;
      anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2);
      anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5);
      anEntity.sprite.scaleX = shipScale;
      anEntity.sprite.scaleY = shipScale;
      anEntity.sprite.rotation = pointAtRad(anEntity.speedX,anEntity.speedY) 
        - (90*DEGREES_TO_RADIANS);
      anEntity.collidemode = 1;
      anEntity.collideradius = 16;
      if (!anEntity.recycled)
        allEnemies.push(anEntity);
    }

Step 13: Handy Math Utilities

Next, we need to implement some of the helper math functions used by the spawning routines above. These are very handy and can be reused in all sorts of ways in the future. Because Flash (and most game engines, at least in the low level routines) store an object's rotation using radians (instead of degrees) we've defined a constant above that speeds up these calculations.

    
    // returns the angle in radians of two points
    public function pointAngle(point1:Point, point2:Point):Number
    {
      var dx:Number = point2.x - point1.x;
      var dy:Number = point2.y - point1.y;
      return -Math.atan2(dx,dy);
    }   
    
    // returns the angle in degrees of 0,0 to x,y
    public function pointAtDeg(x:Number, y:Number):Number
    {
      return -Math.atan2(x,y) * RADIANS_TO_DEGREES;
    }   

    // returns the angle in radians of 0,0 to x,y
    public function pointAtRad(x:Number, y:Number):Number
    {
      return -Math.atan2(x,y);
    }

Step 14: Collision Detection

Each frame, as each entity moves to a new location, those that have the entity property collidemode set to a non-zero value will be sent to the checkCollisions routine below.

Instead of having every single entity loop though every other known entity and check for collisions, we can optimize the vast majority of these checks out. This is because only bullets need to check for collisions, and in our demo game they can only collide with enemy ships (not other bullets, particles or the player).

Therefore, we can now take advantage of one of the "sub-lists" we filled above to search through only the entities that might require collision detection, the allEnemies list. The collision detection function that we wrote earlier (in the entity class) will check to ensure that the bullet and enemy are close enough to each other (and don't have the same "owner" which will be helpful for future versions where enemy bullets need not collide with friends).

    public function checkCollisions(checkMe:Entity):Entity
    {
      var anEntity:Entity;
      for(var i:int=0; i< allEnemies.length;i++)
      {
        //anEntity = entityPool[i];
        anEntity = allEnemies[i];
        if (anEntity.active && anEntity.collidemode)
        {
          if (checkMe.colliding(anEntity)) 
          {
            if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5));

            particles.addExplosion(checkMe.sprite.position);
            if ((checkMe != theOrb) && (checkMe != thePlayer)) 
              checkMe.die(); // the bullet
            if ((anEntity != theOrb) && ((anEntity != thePlayer))) 
              anEntity.die(); // the victim
            return anEntity;
          }
        }
      }
      return null;
    }

Step 15: Update the Simulation

The final step in upgrading our speedy EntityManager.as is to upgrade the entire simulation update loop. This function goes through the entire list of active entities (enemies, bullets and particles) and updates their positions, transparency, size, rotation and more.

We need to ensure that everything animates at the same speed no matter what the current framerate of the game is, so in this new version we are keeping track of the elapsed time since the previous frame and multiplying all speeds by this value.

This way, if the framerate is a silky smooth 60FPS, an enemy might move just one pixel in a particular direction, but if the player was using an old computer with poor graphic performance and the framerate was only 15FPS the same sprite would be moved 4 pixels.

Doing things this way ensures a smooth playing experience no matter what kind of machine you are using; plus, during gameplay, even on a fast machine, the FPS will fluctuate, and we don't want the player's flying speed to fluctuate along with it.

    
    // called every frame: used to update the simulation
    // this is where you would perform AI, physics, etc.
    // in this version, currentTime is seconds since the previous frame
    public function update(currentTime:Number) : void
    {   
      var anEntity:Entity;
      var i:int;
      var max:int;
      
      // what portion of a full second has passed since the previous update?
      currentFrameSeconds = currentTime / 1000;
      
      // handle all other entities
      max = entityPool.length;
      for (i = 0; i < max; i++)
      {
        anEntity = entityPool[i];
        if (anEntity.active)
        {
          anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds;
          anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds;
          
          // the player follows different rules
          if (anEntity.aiFunction != null)
          {
            anEntity.aiFunction(anEntity);
          }
          else // all other entities use the "demo" logic
          {
          
            // collision detection
            if (anEntity.isBullet && anEntity.collidemode)
            {
              checkCollisions(anEntity);
            }
            
            // entities can orbit other entities 
            // (uses their rotation as the position)
            if (anEntity.orbiting != null)
            {
              anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x + 
                ((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * 
                anEntity.orbitingDistance);
              anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y - 
                ((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * 
                anEntity.orbitingDistance);
            }

            // entities can leave an engine emitter trail
            if (anEntity.leavesTrail)
            {
              // leave a trail of particles
              if (anEntity == theOrb)
                particles.addParticle(63, 
                  anEntity.sprite.position.x, anEntity.sprite.position.y, 
                  0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1);
              else // player
                particles.addParticle(63, 
                  anEntity.sprite.position.x + 12, anEntity.sprite.position.y + 2, 
                  0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1);
              
            }
            
            if ((anEntity.sprite.position.x > maxX) ||
              (anEntity.sprite.position.x < minX) ||
              (anEntity.sprite.position.y > maxY) ||
              (anEntity.sprite.position.y < minY))             
            {
              // if we go past any edge, become inactive
              // so the sprite can be respawned
              if ((anEntity != thePlayer) && (anEntity != theOrb)) 
                anEntity.die();
            }
          }
          
          if (anEntity.rotationSpeed != 0)
            anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds;
            
          if (anEntity.fadeAnim != 0)
          {
            anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds;
            if (anEntity.sprite.alpha <= 0.001)
            {
              anEntity.die();
            }
            else if (anEntity.sprite.alpha > 1)
            {
              anEntity.sprite.alpha = 1;
            }
          }
          if (anEntity.zoomAnim != 0)
          {
            anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds;
            anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds;
            if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0)
              anEntity.die();
          }
        }
      }
    }
  } // end class
} // end package

In the code above, not only do we update the positions and rotations of each sprite, but we also optionally check for collisions, orbit other entities, and "die" (become available for reuse in our object pool) when we move off-screen, fade all the way to 100% invisible, or shrink in scale to nothingness.

That's it for the entity manager upgrades. All that remains in the eye-candy-filled third version of our game is to add a few little extras and to upgrade our main game class.


Step 16: Upgrade the Background

Just for fun, let's implement a simple and subtle vertical scrolling parallax effect to our background. We will change our background class to keep track of the player's current vertical position as a percentage of the height of the screen.

When the ship moves, we will scroll the background sprites just a little bit in the opposite direction, which will give the game a little more of a three dimensional feeling. Open your existing GameBackground.as and make a few changes as follows.

To avoid confusion, the entire class is presented here. This first section with all the inits remains unchanged since last time:


// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com

// GameBackground.as
// A very simple batch of background stars that scroll
// with a subtle vertical parallax effect

package
{
  import flash.display.Bitmap;
  import flash.display3D.*;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  
  public class GameBackground extends EntityManager
  {
    // how fast the stars move
    public var bgSpeed:int = -1;
    // the sprite sheet image
    public const bgSpritesPerRow:int = 1;
    public const bgSpritesPerCol:int = 1;
    [Embed(source="../assets/stars.gif")]
    public var bgSourceImage : Class;
    
    // since the image is larger thanthe screen we have some extra pixels to play with
    public var yParallaxAmount:Number = (512 - 400);
    public var yOffset:Number = 0;

    public function GameBackground(view:Rectangle)
    {
      // run the init functions of the EntityManager class
      super(view);
    }
    
    override public function createBatch(context3D:Context3D) : LiteSpriteBatch 
    {
      var bgsourceBitmap:Bitmap = new bgSourceImage();

      // create a spritesheet with single giant sprite
      spriteSheet = new LiteSpriteSheet(bgsourceBitmap.bitmapData, 
        bgSpritesPerRow, bgSpritesPerCol);
      
      // Create new render batch 
      batch = new LiteSpriteBatch(context3D, spriteSheet);
      
      return batch;
    }

    override public function setPosition(view:Rectangle):void 
    {
      // allow moving fully offscreen before looping around
      maxX = 256+512+512;
      minX = -256;
      maxY = view.height;
      minY = view.y;
      yParallaxAmount = (512 - maxY) / 2;
      yOffset = maxY / 2;
    }
    
    // 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 17: Background Parallax

Parallax is a word that is used to describe the way that things seem to move less the farther away you are viewing them. In this case, our background is going to scroll vertically just a little so that it gives a subtle effect in response the the player's movements. Add a new function, yParallax and upgrade the existing update routine as follows:

    
    // scroll slightly up or down to give more parallax
    public function yParallax(OffsetPercent:Number = 0) : void
    {
      yOffset = (maxY / 2) + (-1 * yParallaxAmount * OffsetPercent);
    }
    
    // called every frame: used to update the scrolling background
    override public function update(currentTime:Number) : void
    {   
      var anEntity:Entity;
      
      // handle all other entities
      for(var i:int=0; i<entityPool.length;i++)
      {
        anEntity = entityPool[i];
        if (anEntity.active)
        {
          anEntity.sprite.position.x += anEntity.speedX;
          anEntity.sprite.position.y = yOffset;

          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 all we need to do to the background class. Next up, a few new sound effects and the main game class and we're done for this week!


Step 18: Boom Time!

We definitely want to trigger some explosion sound effects. We are going to add three explosion sound effects to our game and randomly cycle through them during gameplay.

Whenever our entity manager is told that a collision was detected, it will choose one of these three new explosion sounds to play. Open your existing GameSound.as and make a few changes as follows.


// Stage3D Shoot-em-up Tutorial Part 3
// 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;
    
    [Embed (source = "../assets/sfxexplosion1.mp3")]
    private var _explode1Mp3:Class;
    private var _explode1Sound:Sound = (new _explode1Mp3) as Sound;
    [Embed (source = "../assets/sfxexplosion2.mp3")]
    private var _explode2Mp3:Class;
    private var _explode2Sound:Sound = (new _explode2Mp3) as Sound;
    [Embed (source = "../assets/sfxexplosion3.mp3")]
    private var _explode3Mp3:Class;
    private var _explode3Sound:Sound = (new _explode3Mp3) as Sound;

    // 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();
    }

    public function playExplosion(num:int):void
    {
      switch (num)
      {
        case 1 : _explode1Sound.play(); break;
        case 2 : _explode2Sound.play(); break;
        case 3 : _explode3Sound.play(); break;
      }
    }
    
  } // end class
} // end package

Using three different sounds for the same event will help to keep things less repetitive during gameplay. Now that we've implemented particles, new sounds, collision detection above, all that remains is to make sure that all this new functionality appears in our game.


Step 19: Upgrade the Main Game Class

The cool new upgrades we implemented above to our game also require some small changes to the main game document class. Open your existing Game.as and make a few changes as follows.

(As before, to avoid confusion, the entire class is presented here. All the inits remain the same as last week, except for a couple lines of code where we add some new properties for keeping track of the current timestamp and how many milliseconds have elapsed since the previous frame. We also send a reference to the sound system to our entity manager so that it has access to our new explosion sound effects.)


// Stage3D Shoot-em-up Tutorial Part 3
// 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;
    // for framerate independent speeds
    public var currentFrameMs:int;
    public var previousFrameTime:int;
    
    // player one's entity
    public var thePlayer:Entity;
    // movement speed in pixels per second
    public var playerSpeed:Number = 128;
    // timestamp when next shot can be fired
    private var nextFireTime:uint = 0;
    // how many ms between shots
    private var fireDelay:uint = 200;
    
    // main menu = 0 or current level number
    private var _state : int = 0;
    // the title screen batch
    private var _mainmenu : GameMenu;
    // the sound system
    private var _sfx : GameSound; 
    // the background stars
    private var _bg : GameBackground; 
    
    private var _entities : EntityManager;
    private var _spriteStage : LiteSpriteStage;
    private var _gui : GameGUI;
    private var _width : Number = 600;
    private var _height : Number = 400;
    public var context3D : Context3D;
    
    // 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 3");
      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);
      _entities.sfx = _sfx;
      _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 20: FPS Independence

Instead of moving a set amount each frame no matter what the framerate, we are now keeping track of elapsed time and multiplaying the player's speed by however much time elapsed since th previous frame.

We are also going to spawn a new particle every frame just behind the player's ship, as a sort of "vapor trail" effect.

Finally, in our input routines, we will keep track of the last time a bullet was fired and wait a short amount of time in between shots rather than allowing a new bullet to be spawned every single frame.

Therefore, we need to make a few very minor upgrades to our existing player logic routines. Continue editing Main.as as follows:

    
    public function playerLogic(me:Entity):void
    {
      me.speedY = me.speedX = 0;
      if (_controls.pressing.up)
        me.speedY = -playerSpeed;
      if (_controls.pressing.down)
        me.speedY = playerSpeed;
      if (_controls.pressing.left)
        me.speedX = -playerSpeed;
      if (_controls.pressing.right)
        me.speedX = playerSpeed;
        
      // 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;
        
      // leave a trail of particles
      _entities.particles.addParticle(63, 
        me.sprite.position.x - 12, 
        me.sprite.position.y + 2, 
        0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5);
    }

    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!
        // if enough time has passed, fire some bullets:
        if (_controls.pressing.fire)
        {
          // is it time to fire again?
          if (currentTime >= nextFireTime)
          {
            //trace("Fire!");
            nextFireTime = currentTime + fireDelay;
            _sfx.playGun(1);
            _entities.shootBullet(3);
          }
        }
      }
    }
    
    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 21: Upgrade the Render Loop

The final set of upgrades we need to make to our game is to the "render loop" which is run every single frame in response to an ENTER_FRAME event.

We first measure the amount of time that has elapsed since the previous frame, and store the number of milliseconds by which we will be multiplying all sorts of movement and animation values, both here and in the particle animation, where we slowly fade out or scale up various entities that are part of our explosions over time.

Next, we tell the game background where the player is in relation to the screen height so that it can have the subtle parallax vertical scrolling effect.

Finally, instead of spawning a new enemy every single frame, we randomly spawn a new enemy 10% of the time so that there is a little more breathing room. We then tell the entity manager to update the game simulation and render everything.

    
    // this function draws the scene every frame
    private function onEnterFrame(e:Event):void 
    {
      try 
      {
        // grab timestamp of current frame
        currentTime = getTimer();
        currentFrameMs = currentTime - previousFrameTime;
        previousFrameTime = currentTime;
        
        // 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
        if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height);
        _bg.update(currentTime);
        
        // update the main menu titlescreen
        if (_state == 0)
          _mainmenu.update(currentTime);
        
        // keep adding more sprites - FOREVER!
        // this is a test of the entity manager's
        // object reuse "pool"
        if (Math.random() > 0.9)
          _entities.addEntity();
        
        // move/animate all entities
        _entities.update(currentFrameMs);
        
        // 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

We're done! Compile your project, fix any typos, and run the game. If you're having trouble with the code you typed in or just want the instant gratification of everything in one place, remember that you can download the full source code here. You should see something that looks like this:

Screenshot of this week's upgrades in action

Part Three Complete: Prepare for Level Four!

That's it for tutorial number three in this series. Our super-optimized Flash 11 Stage3D Shoot-em-up game is finally starting to almost feel fun! Tune in next week for the first of three Premium tutorials to watch the game slowly evolve into an eminently-playable, silky-smooth 60fps shoot-em-up.

In the next tutorial we will implement enemy AI (by creating a simple artificial intelligence class) so that bad guys no longer move in a straight line. They will also shoot at you and present more of a challenge. We will also add more visual interest to the game by programming a background "terrain" system. Imagine blasting aliens amongst asteroid fields, huge space stations, planets and galaxies. This will help add variety to the game compared to the simple, neverending starfield we currently use as our backdrop.

In future versions of our game (parts five and six) we will program health, a score, and a nice-looking HUD (heads-up-display) GUI overlay to hold these counters. We will implement game over and winning conditions, difficulty and game balance (so that some enemy ships take more than one shot to destroy). We will be adding different weapon upgrades and powerups that change the kinds of bullets you shoot and what companion "orb" your ship has following it, and finally a BOSS BATTLE!

By the end of this six part tutorial series you will have a complete, playable, high-performance shoot-em-up game that feels polished and complete, and has a beginning, middle and end. I hope you'll join me all the way.

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. Get in touch with me any time.

If you have enjoyed these tutorials thus far, perhaps you'd like to learn more about Stage3D? If so, why not buy my Stage3d book! =)

Good luck and HAVE FUN!

Advertisement