8bitrocket.com
25Mar/080

Tutorial: Using Flash CS3 and Actionscript 3 to create Atari 7800 Asteroids Part 4

Tutorial: Using Flash CS3 and Actionscript 3 to create Atari 7800 Asteroids Part 4

In this 4th and final part we will cover 2 main topics. First, we will use pixel perfect BitmapData collision detection to detect missile and ship collisions with the rocks. And second, we will create a very simple particle engine and use a pre-created farm/pool of particles to draw from. This will allow us to have some nice particle effects but also keep optimization in mind.

In Part 3 we covered firing missiles from the player ship. We used a png sprite sheet for the missile animation and demonstrated how to blit the missiles to the BitmapData canvas.

In Part 2 we covered adding the asteroids to the screen. The asteroid animation was cached into an array in a different manner then the ship rotation. With the asteroids, we took the frames of a timeline animation and drew each one into a BitmapData image that was placed into an array.

In Part 1 we covered creating an array of pre-calculated vector values for animating our player ship. We also covered caching the rotated frames of the ship in an array of BitmapData objects. We also added the ship to the screen and move it with the keyboard.

First, here is the the final game we will create. It isn't fully fleshed out game-wise, but it contains all of the basic elements necessary for you to apply to your own games. There is only 1 level, and the ship will not die. It will collide with rocks and provide a particle explosion though. In the upper part of the screen you will see Active Graph showing the game's current memory usage. When you click to start a new game, I don't explicitly null out all of the created objects, so you will see a slight increase in memory use on 2nd and subsequent plays. If this were a real game, I would set every object to null and call dispose() on all BitmapData objects. You will also see a frameRate counter next to the Active Graph. It is included in the zip file below.

Arrow keys rotate left and right and the up thrusts forward. The [z] key fires. The the window is embeded with a wmode=window. It will work at about 5 more FPS if I could use use the transparent or opaque settings. I don't because some browsers will not work with with it yet.. The below example starts with 20 rocks and each use up 40 particles for each explosion. Even using the BitmapData collision detection (which uses more resources than math-based) we can still keep a pretty constant 30 FPS frame rate. The .fla and all class files are provide so you can play with it and change things around if you like.

The source files are here: 7800 Asteroids Tutorial Part 4 source files

In the first 3 parts of this tutorial I have exhaustively gone through all of the code for each section of the game. I am not going to do that in part 4. My reasoning is simple. I don't think the tutorials layout has been as effective as they can be up to this point. The code box has been easy to read, but the explanations have not. I am currently fiddling with the layout and content of this an future tutorials, so I will do this one a little different. I will explain in detail as much as I feel is sufficient, but I will focus on a little more of the details behind these two theories. I definitely will show the code here, but you will need to unzip the files and play with them to see absolutely everything in action. I have added in some optimizations in this version that I will also explain at the end.

BitmapData pixel perfect collision detection
The theory about collision detection up to this point (especially in Flash) has been that math based detection is the best manner in which to detect. I agree with this, but I have seen some pretty un optimized math based collisions in my time. This is especially true if you are going to use the new Point() class and check the radius of each object against the other objects to see if they overlap. While this is an excellent way to do collisions, most new AS3 programmers make the mistake of in-line creating new Point objects on every frame tick. This wastes a lot of execution time because Object creation is very time consuming in Actionscript. Now, I'm not saying that checking each non-transparent pixel on a BitmapData object is faster, but as you will see it works fine and maintains a decent frame rate. So, since I wanted to try and make a game with pixel perfect collision detection I went ahead with this later route. What I am saying here really is no matter what type of collision detection you choose, if you write un-optimized code, it can perform worse than you might have imagined.

BitmapData objects are basically a collection of colored and transparent pixels. When two BitmapData objects overlap, if any non-transparent pixels are in the overlapped portion, a hit will be detected. This is very different from the standard Bounding Box to Bounding Box and Bounding Box to single screen point that is available with standard display objects. Now, you can also test pixel perfect collisions between a BitmapData object and a Sprite or MovieClip, but we will keep it simple as check to BitmapData objects against one another.

References: A Paradox with BitmapData.hitTest?
Keep in mid that when you create a BitmapData object from another BitmapData object through the assignment operation (=), you have just created a reference to the original BitmapData object. You don't physically have two different pieces of BitmapData, but two references to the same object. This is important because our game is made up of a lot of objects that all share the same array of BitmapData objects for display. If we were to modify one of the frames of animation for an asteroid on the fly somehow (by setting a pixel color for instance), all of the asteroids would get the change because they all share the same original BitmapData objects for each animation frame. Since all of the asteroids share the same array of BitmapData objects, detecting collisions between objects based on the BitmapData of each object would seem impossible, but it isn't. You see, if you remember from earlier parts of this tutorial, on each frame tick, an object might animate by changing to the next frame of BitmapData in the shared array of a animation frames. Each asteroid holds just a index int that represents the location in the array of asteroid frames to copyPixels from for display to the canvas. There is no way we can use the same index and reference for collision detection. So, we simply need to make sure that each asteroid also contains another variable called bitmapData. This variable will hold a reference to the current BitmapData object that is displayed by the asteroid object. It doesn't sound logical, but it works. If you have any experience with the BitmapData object, you might think that you need to use the clone() method to create a new object for the hitTest, but you do not. I was surprised by this myself, and will keep exploring how this might be beneficial in the future.

This new checkCollisions() method is below. I will not be discussing each line of the code but the most significant functionality will be discussed in detail.

[cc lang="javascript" width="550"]
private function checkCollisions():void {
missileLen=aMissile.length-1;
rockLen=aRock.length-1;

rocks: for (rockCtr=rockLen;rockCtr>=0;rockCtr--) {
tempRock=aRock[rockCtr];
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
missiles: for (missileCtr=missileLen;missileCtr>=0;missileCtr--) {
tempMissile=aMissile[missileCtr];
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
//trace("1");
try{
if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255)) {
// trace("hit!");
createExplode(tempRock.x+18,tempRock.y+18);
tempMissile=null;
tempRock=null;
aMissile.splice(missileCtr,1);
aRock.splice(rockCtr,1);
break rocks;
break missiles;

}
}catch(e:Error) {
trace("error in missle test");
}
}

//trace("2");
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
}

//trace("3");
}
[/cc]

Labels
We need to loop through all of the rocks and all of the missiles to see if they hit another. We don't care if the missiles hit other missiles, and we don't care if rocks hit other rocks, but we do care of rocks hit the playerShip. For that reason, we loop through the rocks in our outer loop and the missiles in our inner loop. If we looped through the missiles as our outer loop, then for each missile we would need to check each rock AND also check the ship against each rock. That would be a waste of time, so we loop through each rock in the outer loop - first looking to see if a rock has hit a missile, and if it does NOT, then check the rock against the ship. We do this be setting up two labels called:

rocks:
missiles:

You can use labels in nested loops to help discern the loop you want to break out of. We do that here when a rock and a missile collide. The break rocks; and break missiles; lines tell the run-time to stop looking at this rock and this missile and start with the next in each loop.

Looping Backward
Why the heck does Jeff loop backward through his arrays? Is he one of the THOSE people who can use ++i and i++ correctly? Sadly, I cannot, but that is beside the point. We loop backward through the rocks and the missiles because we need to splice them out of their respective arrays when a collision is detected. If we looped forward through the array, and we had to splice say the 10th element in the missile array, something unexpected would happen. We would actually SKIP checking the 11th element in our array. That is because if the missileCtr is on 10 and we splice the element at index 10 in our array, right away, the 11th element shifts into the 10th spot (and all other elements after 11 shift one spot also). The missileCtr would then increase from 10 to 11, and the former element at position 11 (now at 10) is never checked. By looping backward through the array, when we splice, we don't cause this same problem. If we splice the 10th element in the array, 11 certainly moves in to fill its place, but the next one we check is the element in position 9 (we are looping backward), so we never skip an element.

The BitmpData.hittest method.
[cc lang="javascript" width="550"]if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255))
[/cc]
The hitTest() method of the BitmapData object is a powerful one. When called on a BitmpData object, it looks at all of the pixels on the object to see if they are overlapping with pixels on the other object. If those pixels are not transparent (or don't fall over the opacity threshold value) and hit is detected. As an optimization, I created generic point objects called rockPoint and missilePoint as global class variables. By doing so, I use a little extra memory up front, but don't have to instantiate the point objects on each hitTest. The missilePoint and rockPoint must be the upper left-hand corner of each object on the scene in global coordinates. The 255 basically tells the hitTest that NO alpha areas are to be considered opaque for the rock. Setting this number lower we change areas with alpha values above it to be considered opaque for this test.

The tempMissle.bitmapData and tempRock.bitmpaData hold the current objects referenced to the bitmapData needed for the hitTest. It is essential that this be changed each time the BitmapData of the object changes.

For example, in the code below, I have modified the previous drawMissiles methods from Part 3 to include one more line at the bottom. (see the //added in part 4 section).

[cc lang="javascript" width="550"]
private function drawMissiles():void {
missileLen=aMissile.length-1;

for each (tempMissile in aMissile) {

//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;

canvasBD.copyPixels(aMissileAnimation[tempMissile.animationIndex],missileRect, missilePoint);

tempMissile.animationIndex++;
if (tempMissile.animationIndex > missileArrayLength-1) {
tempMissile.animationIndex = 0;
}
//added in part 4
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];

}

}[/cc]

//added in part 4

tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex

As we update the missile on the screen we also change the piece of BitmapData that used to display it on the screen. (it animates from red to yellow). When this change is made, we store a reference to the new BitmapData object representing the animation in our tempMissile.bitmapData variable.

We have added similar lines to the the drawRocks() method (see the bottom).

 

[cc lang="javascript" width="550"]
private function drawRocks() {
rockLen=aRock.length-1;

for each (tempRock in aRock) {

//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;

canvasBD.copyPixels(aAsteroidAnimation[tempRock.animationIndex],rockRect, rockPoint);

tempRock.frameCount++;
if (tempRock.frameCount > tempRock.frameDelay){

tempRock.animationIndex++;
if (tempRock.animationIndex > asteroidFrames-1) {
tempRock.animationIndex = 0;
}
tempRock.frameCount=0;
//added in part 4
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
}
}

}
[/cc]

For the playerObject. we needed to add the code only when the frame of animation changed by turning the ship:

[cc lang="javascript" width="550"]
private function checkKeys():void {
if (aKeyPress[38]){
//trace("up pressed");
playerObject.dx=aRotation[playerObject.arrayIndex].dx;
playerObject.dy=aRotation[playerObject.arrayIndex].dy;

var mxn:Number=playerObject.movex+playerObject.acceleration*(playerObject.dx);
var myn:Number=playerObject.movey+playerObject.acceleration*(playerObject.dy);

var currentSpeed:Number = Math.sqrt ((mxn*mxn) + (myn*myn));
if (currentSpeed < playerObject.maxVelocity) { playerObject.movex=mxn; playerObject.movey=myn; } // end speed check } if (aKeyPress[37]){ playerObject.arrayIndex--; if (playerObject.arrayIndex <0) playerObject.arrayIndex=shipAnimationArrayLength-1; //added in part 4 playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]; } if (aKeyPress[39]){ playerObject.arrayIndex++; if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0; //added in part 4 playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]; } //*** added for part 3 if (aKeyPress[90]){ fireMissile(); } } [/cc]

You can se in the above code we simply added
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]


when a change is made to the animationIndex.

So, that's basically pixel perfect collision detection between bitmapData objects in a nut shell. You need 4 pieces of data:
1. A reference to the bitmapData object for the first object you want to check.
2. A reference to the bitmapData object for the second object you want to check.
3. A point object representing the current upper left corner x and y values of the first object.
4. A point object representing the current upper left corner x and y values for the second object.

I usually leave the Alpha threshold values at 255, but I can see a need for them with more complicated objects. So, here is what the code looks like for the hitTest between a rock and the playerObject:

[cc lang="javascript" width="550"]

playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}

[/cc]

This is very similar to the check between the rocks and the missiles. When a rock is hit by either the ship or a missile, we set the tempRock to be null, splice it from our array of rocks (we do this with the missile and the missile array when a rock - missile collision is detected also), and then we create an explosion.

createExplode(tempRock.x+18,tempRock.y+18);

The new createExplode function takes two parameter and x and y value. Here, since the x and y values for the rock are the upper left hand corner, and the rock is a 36x36 bitmap, we add 18 to the x and y before we pass them in. That will put the explosion roughly in the middle of the rock.

The Particle Explosion and Particle Pool/Farm
Since the particles that make up our explosion are merely for decoration, I have decided to limit the number available for display. I have done this through the implementation of a farm or pool or particles. This is a set of pre-created particle objects in an array called aFarmParticle. A the beginning of the game, I pre-create 500 particles objects and place them in this array. The code is below:

[cc lang="javascript" width="550"]
private function createFarmParticles() {
var particleCtr:int;
for (particleCtr=0;particleCtr<maxParticles;particleCtr++) {
var tempPart:Object={};
tempPart.lifeCount=0;
tempPart.life=0;
tempPart.x=0;
tempPart.y=0;
tempPart.dx=0;
tempPart.dy=0;
tempPart.speed=0;
tempPart.bitmapData=null;
aFarmParticle.push(tempPart);
}
}
[/cc]

I have a maxParticles variable set to 500 in my variable definition section. This code merely loops 500 times, creates some dummy particles and places them in the array for later use. This way, I don't have to create particles on fly, thus saving valuable execution time.

When a particle explosion is needed, we call the createExplode function. This function will start a loop and try to move maxPartsPerExplode (currently set to 40) from the aFarmParticle to the aActiveParticle array.

[cc lang="javascript" width="550"]
private function createExplode(xval:Number,yval:Number):void {

//trace("aFarmParticle.length=" + aFarmParticle.length);
//trace("aActiveParticle.length=" + aActiveParticle.length);

for (explodeCtr=0;explodeCtr<maxPartsPerExplode;explodeCtr++) {
farmLen=aFarmParticle.length-1;
if (farmLen >0){
tempPart=aFarmParticle[farmLen];
aFarmParticle.splice(farmLen,1);
tempPart.lifeCount=0;
tempPart.life=int(Math.random()*partMaxLife)+partMinLife;;
tempPart.x=xval;
tempPart.y=yval;
tempPart.speed=(Math.random()*partMaxSpeed)+1;
randIntFrame=int(Math.random()*10);
tempPart.bitmapData=aMissileAnimation[randIntFrame];
randIntVector=int(Math.random()*36);
tempPart.dx=aRotation[randIntVector].dx;
tempPart.dy=aRotation[randIntVector].dy;
aActiveParticle.push(tempPart);
}
}
}
[/cc]

On each iteration through the the loop we first check to make sure our farm/pool length is not 0. If it is above 0, there are particles that can be moved from the farm to the active array. We do this by creating a tempPart (temporary particle) from the last element in the aFarmParticle array. We then splice that particle from the farm, set random properties for the tempPart and add it to the aActiveParticle array.

We set random properties for the movement vector (dx and dy) values by randomly picking a number between 0-36 and them using that as the index for our pre-calculated aRotation array. We also set a random speed, and a random frame from our missile tile sheet (array of BitmapData) aMissileAnimation - because we have those already and they are easy to use. We also set a random life between partMaxLife (currently 50) and partMinLife (currently 10). We do all of this to make the particles have some minor differences and seem more organic. There are many more things we could do, but this is our simple particle emitter.

Now, once we have active particles, we need to update them on each frame. This is very similar to updating the rocks or the missiles. We loop through them, and copy the current bitmapData of the particle to the canvasBD.

[cc lang="javascript" width="550"]
private function drawParts():void {

activeLen=aActiveParticle.length-1;

for (partCtr=activeLen;partCtr>=0;partCtr--) {
removePart=false;
tempPart=aActiveParticle[partCtr];
tempPart.x+=tempPart.dx*tempPart.speed;;
tempPart.y+=tempPart.dy*tempPart.speed;

if ((tempPart.x > stage.width) || (tempPart.x < 0)) { removePart=true; } if ((tempPart.y > stage.height) || (tempPart.y < 0)){ removePart=true; } tempPart.lifeCount++; if (tempPart.lifeCount > tempPart.life) {

}

if (removePart) {
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);

}else{
//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
partPoint.x=tempPart.x;
partPoint.y=tempPart.y;

canvasBD.copyPixels(tempPart.bitmapData,partRect, partPoint);

}

}

}
[/cc]

A particle is removed from the screen is its life count is greater than its life value or it leaves the screen boundaries. We have 500 particles and only a hand full of asteroids on the screen. We will never use all 500 at one time, but this just sets us up for higher level of complex space battles and explosions later in the game. When a particle is removed, it is added back to the aFarmParticle array and spliced from the the aActiveParticle array:

aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);

So, we have now completed all of the features for this Asteroids game. Our new game loop looks like this:

[cc lang="javascript" width="550"]
private function runGame(e:TimerEvent) {

checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
frameTimer.countFrames();
frameTimer.render();
if (aRock.length ==0 && aActiveParticle.length==0) stopRunningGame();
}
[/cc]

I have added a simple frameRate counter and the class is in the .zip above. It is easy to use, you just need to make sure that you call its countFrames() and render() methods on each frame tick.

Below is the entire code listing, including the Main class, StartBox class, and FrameTimer class.

 

[cc lang="javascript" width="550"]

/**
* ...
* @author Default
* @version 0.1
*/

package {

import flash.display.Bitmap;
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Timer;
import flash.display.BitmapData;
import flash.geom.*;
import flash.events.*;
import ActiveGraph.*;
//** added for part 3
import flash.utils.getTimer;
//*** added for part 4
import FrameTimer;

public class Main extends MovieClip {

var aRotation:Array=[];
var aShipAnimation:Array=[];
var shipAnimationArrayLength:int;
var ship:Ship;
var shipHolder:MovieClip;
var animationTimer:Timer;
var animationCounter:int;
var playerObject:Object;
var canvasBD:BitmapData;
var canvasBitmap:Bitmap;
var backgroundBD:BitmapData;
var backgroundSource:Background;
var gameTimer:Timer;
var backgroundRect:Rectangle;
var backgroundPoint:Point;
var playerRect:Rectangle;
var playerPoint:Point;
var aKeyPress:Array=[];
var spriteHeight:int=20;
var spriteWidth:int=20;

//** part 2 variables
var aAsteroidAnimation:Array=[];
var asteroidAnimationTimer:Timer;
var asteroidHolder:RockToCache;
var asteroidFrames:int=12;
var aRock:Array=[];
var level:int=1;
var rockPoint:Point=new Point(0,0);
var rockRect:Rectangle=new Rectangle(0,0,36,36);
var levelRocksCreated:Boolean=false;

//*** part 3 variables
var missleTileSheet:BitmapData;
var aMissileAnimation:Array;
var aMissile:Array; //holds missile objects fires
var missileSpeed:int=4;
var missileWidth:int=4;
var missileHeight:int=4;
var missileArrayLength=10;
var missilePoint:Point=new Point(0,0);
var missileRect:Rectangle=new Rectangle(0,0,4,4);
var missileMaxLife:int=50;
var missileFireDelay:Number=100;
var lastMissileShot:Number=getTimer();

//*** part 4 variables
var missileLen:int;
var missileCtr:int;
var tempMissile:Object;
var tempRock:Object;
var rockLen:int;
var rockCtr:int;
var aFarmParticle:Array=[];
var aActiveParticle:Array=[];
var maxParticles:int=500;
var maxPartsPerExplode:int=40;
var explodeCtr:int;
var tempPart:Object;
var farmLen:int;
var randIntVector:int;
var randIntFrame:int;
var activeLen:int;
var partCtr:int;
var partMaxSpeed:int=2;
var partMaxLife:int=50;
var partMinLife:int=10;
var removePart:Boolean=false;
var partPoint:Point=new Point(0,0);
var partRect:Rectangle=new Rectangle(0,0,4,4);

//part 4 optimizations
var randInt:int;
var randInt2:int;
var ctr:int;
var frameTimer:FrameTimer;
var startBox:StartBox;
var ag:ActiveGraph;

public function Main() {
trace("main");
createObjects();
createRotationArray();
createShipAnimation();
startBox=new StartBox(this);
ag=new ActiveGraph(0,false,true,1);

}

private function startBoxOn():void{
startBox.startit();

}

private function createObjects():void {
//create generic object for the player
playerObject=new Object();
playerObject.arrayIndex=0;
playerObject.x=200;
playerObject.y=200;
playerObject.dx=0;
playerObject.dy=0;
playerObject.movex=0;
playerObject.movey=0;
playerObject.acceleration=.3;
playerObject.maxVelocity=8;
playerObject.friction=.01;
playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight;
playerRect=new Rectangle(0,0,spriteWidth*2,spriteHeight*2);
playerPoint=new Point(playerObject.x,playerObject.y);

//init canvas and display bitmap for canvas
canvasBD=new BitmapData(400,400,false,0x000000);
canvasBitmap=new Bitmap(canvasBD);

//init background
backgroundSource=new Background();
backgroundBD=new BitmapData(400,400,false,0x000000);
backgroundBD.draw(backgroundSource,new Matrix());
backgroundRect=new Rectangle(0,0,400,400);
backgroundPoint=new Point(0,0);

//part 3 init tilesheet for missiles
aMissile=[];
aMissileAnimation=[];
missleTileSheet=new missile_sheet(40,4);
var tilesPerRow:int=10;
var tileSize:int=4;
for (var tileNum=0;tileNum<10;tileNum++) {
var sourceX:int=(tileNum % tilesPerRow)*tileSize;
var sourceY:int=(int(tileNum/tilesPerRow))*tileSize;
var tileBitmapData:BitmapData=new BitmapData(tileSize,tileSize,true,0x00000000);
tileBitmapData.copyPixels(missleTileSheet,
new Rectangle(sourceX,sourceY,tileSize,tileSize),new Point(0,0));
aMissileAnimation.push(tileBitmapData);
}

//added in part 4
createFarmParticles();
frameTimer=new FrameTimer(this,140,0,canvasBD);

}

private function createFarmParticles() {
var particleCtr:int;
for (particleCtr=0;particleCtr< playerObject.maxVelocity) { playerObject.movex=mxn; playerObject.movey=myn; } // end speed check } if (aKeyPress[37]){ playerObject.arrayIndex--; if (playerObject.arrayIndex <0) playerObject.arrayIndex=shipAnimationArrayLength-1; //added in part 4 playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]; } if (aKeyPress[39]){ playerObject.arrayIndex++; if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0; //added in part 4 playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]; } //*** added for part 3 if (aKeyPress[90]){ fireMissile(); } } private function updatePlayer():void { //add friction if (playerObject.movex > 0) {
playerObject.movex-=playerObject.friction;
}else if (playerObject.movex < 0) { playerObject.movex+=playerObject.friction; } if (playerObject.movey > 0) {
playerObject.movey-=playerObject.friction;
}else if (playerObject.movey < 0) { playerObject.movey+=playerObject.friction; } playerObject.x+=(playerObject.movex); playerObject.y+=(playerObject.movey); playerObject.centerx=playerObject.x+spriteWidth; playerObject.centery=playerObject.y+spriteHeight; if (playerObject.centerx > stage.width) {
playerObject.x=-spriteWidth;
playerObject.centerx=playerObject.x+spriteWidth;
}else if (playerObject.centerx < 0) { playerObject.x=stage.width - spriteWidth; playerObject.centerx=playerObject.x+spriteWidth; } if (playerObject.centery > stage.height) {
playerObject.y=-spriteHeight;
playerObject.centery=playerObject.y+spriteHeight;
}else if (playerObject.centery < 0) { playerObject.y=stage.height-spriteHeight; playerObject.centery=playerObject.y+spriteHeight; } //trace("centerx=" + playerObject.centerx); } private function updateRocks():void { if (!levelRocksCreated) { for (ctr=0;ctr<level+5;ctr++) { //create a rock; tempRock=new Object(); randInt=int(Math.random()*36); randInt2=int(Math.random()*asteroidFrames); tempRock.dx=aRotation[randInt].dx; tempRock.dy=aRotation[randInt].dy; tempRock.x=20; tempRock.y=20; tempRock.animationIndex=randInt2; //added for part 4 tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex]; tempRock.frameDelay=3; tempRock.frameCount=0; tempRock.speed = (Math.random()*level)+1; //trace("tempRock.speed=" + tempRock.speed); aRock.push(tempRock); } levelRocksCreated=true; } for each (tempRock in aRock) { tempRock.x+=tempRock.dx*tempRock.speed;; tempRock.y+=tempRock.dy*tempRock.speed; if (tempRock.x > stage.width) {
tempRock.x=0;
}else if (tempRock.x < 0) { tempRock.x=stage.width; } if (tempRock.y > stage.height) {
tempRock.y=0;
}else if (tempRock.y < 0) { tempRock.y=stage.height } } } private function drawBackground():void { canvasBD.copyPixels(backgroundBD,backgroundRect, backgroundPoint); } private function drawPlayer():void { playerPoint.x=playerObject.x; playerPoint.y=playerObject.y; canvasBD.copyPixels(aShipAnimation[playerObject.arrayIndex], playerRect, playerPoint); } private function drawRocks() { rockLen=aRock.length-1; for each (tempRock in aRock) { rockPoint.x=tempRock.x; rockPoint.y=tempRock.y; canvasBD.copyPixels(aAsteroidAnimation[tempRock.animationIndex], rockRect, rockPoint); tempRock.frameCount++; if (tempRock.frameCount > tempRock.frameDelay){

tempRock.animationIndex++;
if (tempRock.animationIndex > asteroidFrames-1) {
tempRock.animationIndex = 0;
}
tempRock.frameCount=0;
//added in part 4
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
}
}

}

private function fireMissile():void {

if (getTimer() > lastMissileShot + missileFireDelay) {
tempMissile=new Object();
tempMissile.x=playerObject.centerx;
tempMissile.y=playerObject.centery;
tempMissile.dx=aRotation[playerObject.arrayIndex].dx;
tempMissile.dy=aRotation[playerObject.arrayIndex].dy;
tempMissile.speed=missileSpeed;
tempMissile.life=50;
tempMissile.lifeCount=0;
tempMissile.animationIndex=0;
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];
aMissile.push(tempMissile);
lastMissileShot=getTimer();
}
}

private function updateMissiles():void {
missileLen=aMissile.length-1;
for (ctr=missileLen;ctr>=0;ctr--) {
tempMissile=aMissile[ctr];
tempMissile.x+=tempMissile.dx*tempMissile.speed;;
tempMissile.y+=tempMissile.dy*tempMissile.speed;

if (tempMissile.x > stage.width) {
tempMissile.x=0;
}else if (tempMissile.x < 0) { tempMissile.x=stage.width; } if (tempMissile.y > stage.height) {
tempMissile.y=0;
}else if (tempMissile.y < 0) { tempMissile.y=stage.height } tempMissile.lifeCount++; if (tempMissile.lifeCount > tempMissile.life) {
aMissile.splice(ctr,1);
tempMissile=null;
}

}
}

private function drawMissiles():void {
missileLen=aMissile.length-1;

for each (tempMissile in aMissile) {

missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;

canvasBD.copyPixels(aMissileAnimation[tempMissile.animationIndex],
missileRect, missilePoint);

tempMissile.animationIndex++;
if (tempMissile.animationIndex > missileArrayLength-1) {
tempMissile.animationIndex = 0;
}
//added in part 4
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];

}

}
//added in part 4
private function checkCollisions():void {
missileLen=aMissile.length-1;
rockLen=aRock.length-1;

rocks: for (rockCtr=rockLen;rockCtr>=0;rockCtr--) {
tempRock=aRock[rockCtr];
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
missiles: for (missileCtr=missileLen;missileCtr>=0;missileCtr--) {
tempMissile=aMissile[missileCtr];
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
//trace("1");
try{
if (tempMissile.bitmapData.hitTest(missilePoint,255,
tempRock.bitmapData,rockPoint,255)) {
// trace("hit!");
createExplode(tempRock.x+18,tempRock.y+18);
tempMissile=null;
tempRock=null;
aMissile.splice(missileCtr,1);
aRock.splice(rockCtr,1);
break rocks;
break missiles;

}
}catch(e:Error) {
trace("error in missle test");
}
}

//trace("2");
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,
playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
}

//trace("3");
}

private function createExplode(xval:Number,yval:Number):void {

//trace("aFarmParticle.length=" + aFarmParticle.length);
//trace("aActiveParticle.length=" + aActiveParticle.length);

for (explodeCtr=0;explodeCtr<maxPartsPerExplode;explodeCtr++) {
farmLen=aFarmParticle.length-1;
if (farmLen >0){
tempPart=aFarmParticle[farmLen];
aFarmParticle.splice(farmLen,1);
tempPart.lifeCount=0;
tempPart.life=int(Math.random()*partMaxLife)+partMinLife;;
tempPart.x=xval;
tempPart.y=yval;
tempPart.speed=(Math.random()*partMaxSpeed)+1;
randIntFrame=int(Math.random()*10);
tempPart.bitmapData=aMissileAnimation[randIntFrame];
randIntVector=int(Math.random()*36);
tempPart.dx=aRotation[randIntVector].dx;
tempPart.dy=aRotation[randIntVector].dy;
aActiveParticle.push(tempPart);
}
}
}

private function drawParts():void {

activeLen=aActiveParticle.length-1;

for (partCtr=activeLen;partCtr>=0;partCtr--) {
removePart=false;
tempPart=aActiveParticle[partCtr];
tempPart.x+=tempPart.dx*tempPart.speed;;
tempPart.y+=tempPart.dy*tempPart.speed;

if ((tempPart.x > stage.width) || (tempPart.x < 0)) { removePart=true; } if ((tempPart.y > stage.height) || (tempPart.y < 0)){ removePart=true; } tempPart.lifeCount++; if (tempPart.lifeCount > tempPart.life) {

}

if (removePart) {
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);

}else{

partPoint.x=tempPart.x;
partPoint.y=tempPart.y;

canvasBD.copyPixels(tempPart.bitmapData,partRect, partPoint);

}

}

}

}

}
[/cc]

 

[cc lang="javascript" width="550"]
package {
import flash.display.MovieClip;
import flash.events.*;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.Timer;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.display.BitmapData;

public class FrameTimer {
private var format:TextFormat=new TextFormat();
private var messageText:String;
private var messageBitmapData:BitmapData;
private var messageTextField:TextField = new TextField();
public var frameTimer:Timer;
public var framesCounted:int=0;
public var parent:MovieClip;
public var x:int;
public var y:int;
public var canvasBD:BitmapData;
public var messagePoint:Point;
public var messageRect:Rectangle;

public function FrameTimer(parentVal:MovieClip,xval:int,yval:int,canvasval:BitmapData):void {
x=xval;
y=yval;
canvasBD=canvasval;
format.size=12;
format.font="Arial";
format.color="0xffffff";
format.bold=true;
messageText="0";
messageTextField.text=messageText;
messageTextField.setTextFormat(format);
//messageTextField.width=(messageText.length+2)*int(format.size);
messageTextField.width=30;
//messageTextField.height=int(format.size)*2;
messageTextField.height=20;
messageBitmapData=new BitmapData(messageTextField.width,
messageTextField.height,true,0xffff0000);
parent=parentVal;
frameTimer= new Timer(1000,0);
frameTimer.addEventListener(TimerEvent.TIMER,frameCounter,false,0,true);
frameTimer.start();
messagePoint=new Point(x,y);
messageRect=new Rectangle(0,0,messageTextField.width,messageTextField.height);

}

function frameCounter(e:TimerEvent):void {
messageText=framesCounted.toString();
messageTextField.text=messageText;
//trace("frameRate:" + framesCounted.toString());
framesCounted=0;
}

function countFrames():void {
framesCounted++;
}

function render():void {
format.size=12;
format.font="Arial";
format.color="0xffffff";
format.bold=true;
messageTextField.setTextFormat(format);
messageBitmapData.fillRect(messageRect,0xffff0000);
//trace("messageTextField.text=" + messageTextField.text);
messageBitmapData.draw(messageTextField);
canvasBD.copyPixels(messageBitmapData,messageRect, messagePoint);
}

} // end class

} // end package

 



package {
import flash.display.MovieClip;
import flash.events.*;

public class StartBox extends MovieClip {
var parentClass:MovieClip;

public function StartBox(parentVal:MovieClip) {
parentClass=parentVal;

}

public function startit():void {
parentClass.addChild(this);
trace("starting startBox");
x=80;
y=100;
start_mc.addEventListener(MouseEvent.CLICK, startButtonListener,false,0,true);
}

public function stopit():void {
start_mc.removeEventListener(MouseEvent.CLICK, startButtonListener);
trace("stopping startBox");
parentClass.removeChild(this);
}

private function startButtonListener(e:Event) {
stopit();
parentClass.startGame();
}
}

}
[/cc]

Final notes and optimizations

If you have read through all 4 parts of this tutorial, you will note that I didn't go as deep into making a finalized game as I initially intended. I decided to focus more on optimizations than adding in Mochi Ads and Leader Boards because those might not be relevant to all readers. If you compare the code provided for the Main class in part 3 with the code above for part 4, you notice quite a few changes. All have been noted in the variable definition section and inside the code where needed. Mostly, I moved many variables that were being locally defined over and over in loops and made them global variables. This gives the game a larger initial memory foot print, but will save much execution time while the game is processing.

That's it, if you have any questions or comments, please email info[at]8bitrocket[dot]com, leave a message below, or visit the forums and post.

Jeff Fulton (8bitjeff).

If you enjoyed this post, please consider leaving a comment or subscribing to the RSS feed to have future articles delivered to your feed reader.
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.

This site is protected by Comment SPAM Wiper.