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:

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:

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!
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.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post