8bitrocket.com
13Feb/080

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

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 then added the ship to the screen and move it with the keyboard.

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 this lesson, we will cover shooting the missiles from the player ship. We will use a third type of BitmapData animation caching by implementing a sprite sheet. We will cache the frames from the sprite sheet into an array and play them back when the missiles are in flight. We will also add code to capture the [z] key for firing missiles, and implement a method to limit the number of missiles the player can have in flight at any one time.

Creating the missile sprite sheet.

To create the missile sprite sheet I first fired up Fireworks (my graphic editor of choice). In Fireworks, I create a new image with a width of 40 and a height of 4. I plan to have 10 frames of animation and a 4x4 missile. Using the limited creativity at my disposal, I created 10 missile frames by changing the color of the missile on each of the 10 frames. I saved it as a PNG file and imported it into the library of my 7800asteroids3.fla. If you want to use the Flex SDK to make this game, I have provided the .png sprite sheet in the the .zip of for this tutorial.

The missile sprite sheet in Fireworks looks like this (at 800% zoom):

In the library in my .fla file I changed the linkage id of this file to be missile_sheet. That is all the setup needed inside the .fla file. Now, on to the code needed to get our missiles on the screen. Like part 2, I will just be showing and explaining the changes I have made to the code since part 1 and 2. The entire new .fla and .as files available at the end of this tutorial.

In our Main.as class, I had added a class import:

[cc lang="javascript" width="550"]
import flash.display.Bitmap;
import flash.display.MovieClip;
import flash.events.TimerEvent;
import flash.utils.Timer;
import flash.display.BitmapData;
import flash.geom.*;
import flash.events.KeyboardEvent;
//** added for part 3
import flash.utils.getTimer;
[/cc]

We added the getTimer class. It will be used for figuring out when the ship is allowed to fire another missile. This delay can be changed to add massive amounts of missiles to the screen. If all of these optimizations work correctly, we have the horse power to make some awesome effects.

In the variable definition section I have added in the new game variables needed for animating the missiles on the screen.
[cc lang="javascript" width="550"]
//*** 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();[/cc]

The missileTileSheet variable is a BitmapData object that will hold an instance of the missile_sheet (exported name) in our library.
The aMissileAnimation array will hold the 10 frames of animation for our missile. These will be BitmapData objects.
The aMissile array will hold all of the missile the ship fires that are currently in flight.
Our missileSpeed will be 4 pixels per frame tick. As well, our missileWidth and height will also be 4 pixels. There is no reason why the speed and the height and width are the same, the speed can be anything you want, but the faster you make it, the more of a chance there will be that our missile will go through a rock and collisions will not be detected.

Next we pre-calculate missileArrayLength and set it at 10. Pre-storing this will help speed up our loops, and save execution time. We also pre-create missilePoint, and missileRect. The missilePoint will change for the current x and y coordinates of the missile, but by pre-creating the Point object, we will save execution time during our game loop. The missileRect will always be the same, and we pre-create it here to save execution time later in the update and draw cycles.

The last three things we do effect the life of the missile (the number of frame ticks it will be on the screen), and the amount of missiles that can be on the screen at any one time. By changing the missileMaxLife variable, you will change the number of frames each missile remains on the screen. By changing the missileFireDelay to a lower number, you will allow more missiles to be fired in a shorter amount of time. Conversely, making his number higher will limit the amount of missiles the player ship can have on the screen at the same time. The final new variable is a getTimer object that holds the last time a missile was fired. We will explain more on this later.

In the createObjects() function, we have added lines to create the tile sheet for our missile sprite, and cache the 10 frames into an array of BitmapData objects.

[cc lang="javascript" width="550"]
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);
}

}
[/cc]

First, we in initialize the aMissile array. This will hold all of the missiles that the player fires so we can updated them on the screen and eventually allow us to do collision detection between the missiles and the rocks.
Next, we initialize the aMissileAnimation variable. This will hold the 10 frames of BitmapData representing the animation frames for our missile.
We then create an instance of our missile_sheet library png. Since a .png file imported into the library is essentially a Bitmap object (pixel BitmapData), our missileTileSheet BitmapData object is assigned the new instance of the missile_sheet. We pass in the width and height of the missile_sheet when we create an instance of it.

Our next task is to actually loop through the 10 tiles in the tile sheet and create a BitmapData object out of each tile. First we set our tilesPerRow variable to be the number of tiles in the row (40/4=10). We then set our tileSize to be 4. Our tile is a square of 4x4. If the width and height were not the same, we would have to create a tilewidth and tileheight set of separate variables. I specifically created square tiles to eliminate this need. We then create a loop with 10 steps in it.

Within each step, we first need to find the upper left hand corner x position of our current tile to cache and put that in the sourceX variable. This is done by taking the remainder from out tileNum/tilesPerRow calculation and multiplying it by our tile size. So, one the first pass, our source x should be 0. The first tileNum is 0 also. 0%0*4=0. Our sourceX=0; Our sourceY=0/0*4 or just 0 also. On the next line, create a new BitmapData object called tileBitmapData, and give it a a size of 4x4 with a transparent background and no fill color. We then copyPixels from our missileTileSheet into that new tileBitmapData object. We need to supply a rectangle bounding the area we want to copy and a starting point to copy into. The starting point will always be 0,0 or the top left-hand corner of our tileBitmapData object. The rectangle will start at our sourceX and sourceY and extend 4 pixels in each direction. This allows us to cut out the slice of the BitmapData we want and then we place it in the aMissileAnimation array with a push() function call.

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

private function runGame(e:TimerEvent) {
checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
}
[/cc]

Our runGame() method has been updated. We have now added updateMissiles() and drawMissiles() methods to each game timer tick. As you can see below, we also added some code to the checkKeys() function. This allows us to capture the [z] key press and fire off a missile when it is pressed.

 

[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; } if (aKeyPress[39]){ playerObject.arrayIndex++; if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0; } //*** added for part 3 if (aKeyPress[90]){ fireMissile(); } } [/cc]

The fireMissile() method is called above when the [z] key is pressed.

[cc lang="javascript" width="550"]
private function fireMissile():void {
if (getTimer() > lastMissileShot + missileFireDelay) {
var tempMissile:Object=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;
aMissile.push(tempMissile);
lastMissileShot=getTimer();
}
}
[/cc]

When the [z] key is pressed, we first check the value of the lastMissileShot variable. Remember, in the variable initialization section of code for the class, we set this to be the value of getTimer(). Before a missile is shot for the first time, it holds the number of milliseconds that have elapsed since that initialization. We check to see if the current value of the getTimer() is greater than this lastMissileShot value + our missileFireDelay (100 milliseconds). If so, we can fire off a new missile.

We do this by first creating a generic object called tempMissile. We set the current x value of the missile to be the playerObject's centerx value, and its y to be the playerObject's centery value. This way, it will appear that the missiles start from the center of the player's ship. We next set the dx and dy values to match the direction the player is currently facing. We do this by finding the current arrayIndex of the playerObject and using it in the pre-calculated delta x and delta y movement vector array.

We set the life of the missile to be 50 frame ticks, and set its starting life count to be 0. When it counts up to 50, it will be removed from the screen. We set the animationIndex value of the missile to be 0. That will put the missile on the first (red) graphic. The index will be increased on each frame tick. When it reaches 10, it will be set back to 0 and start over.

The final two things we do are push the new tempMissile object into the aMissile array, and make sure to reset of lastMissileShot value to be the current value of a getTimer() call. This will start over the 100 millisecond delay count and won't let another missile be fire until that delay has been reached.

 

[cc lang="javascript" width="550"]
private function updateMissiles():void {
var missileLen:int=aMissile.length-1;
for (var ctr:int=missileLen;ctr>=0;ctr ) {
var tempMissile:Object=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;
}

}
}
[/cc]

The updateMissile function is called on each frame tick by the game loop. It is virtually identical to the updateRocks() method with one big exception. The missiles will expire when 50 frame ticks have passed. For that reason, we cannot use a FOR EACH statement to loop through the array of missiles. We much use an iterator (for loop) that has a variable as an incrementor. Above, you see that we have a for statement that starts with the current length of the aMissile array (minus 1). We loop backward through the array of missiles.

First we set the new x and y values for the missile by multiplying the dx and dy values (respectively) by the speed of the missile. After that, we check the new x and y values against the stage bounds to apply the WRAP AROUND warp to the missiles. If they leave one side of the screen (or top or bottom) they will wrap around to the other side.

The final thing we do is increment the lifeCount variable and then remove the missile if the count is greater than the life value of the missile. If it is, we splice the missile from the aMissile array and set it to be null so the garbage collector will sweep it up on a future pass. The reason we loop backward through the array is evident here. If we looped forward, when we spiced a value from the array, it would change all of the future array values for this frame tick and the next value in the array (after the splice) would suddenly become the current value. Because we increase our counter loop counter (ctr) right away, we would actually skip checking the missile object that comes after the spliced object. It might sound crazy, but this small item has chased away hours and hours of my debug time.The way around this of course is to loop backward through the array. When we splice a value while looping backward we don't effect the missile objects that are lower than the spliced object.

WHEW!

Now on to the final new section of code for this lesson.

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

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

for each (var tempMissile:Object 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;
}

}

}
[/cc]

The drawMissiles() method is actually ALMOST exactly like the drawRocks version also. We are simply looping through all of the missiles and calling a copyPixels() method on each on. We copy the current BitmapData object from the aMissileAnimation array to the canvasBD. We first need to set the missilePoint.x and y values to be the current x and y for the missile. Remember, the missileRect was created earlier and doesn't change. It is a Rectangle(0,0,4,4) rectangle that doesn't change, and can be used for all of the missiles.

The only difference between the missiles and the rocks is that we are not going to have a frameCount for the missiles. The missiles will update the animationmIndex on each frame and not wait 3 frames like the rocks do.

Any, my few friends who have made it this far, we are done with part 3.

Use the left and right arrow keys to rotate the ship. The up will thrust, and the [z] key will fire missiles.

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

Read Part 4

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.