8bitrocket.com
6Aug/100

Space Gate Enemy Path Tutorial by Barnaby Byrne

Barnaby Byrne (Aka Badger Manufacture) has just released is latest retro inspired Flash Masterpiece, Space Gate, at Odo Games. I was some impressed with the games and the intricate enemy patterns within that I asked if he would be kind enough to explain to our readers exactly how it is implemented. Luckily he agreed.  We are very happy to now present to you his tutorial:

Here is what Barnaby has to say:

It's exciting that after such a break from game development in the engineering industry designing buildings, I am now really enjoying it again.  I've often been asked how I do the enemy paths for my shooters, and like Jeff I am a firm believer in sharing rather than hoarding knowledge.

This game of mine SpaceGate has been a long time coming.  I started coding Space Invaders clones way back in the 80s on the ZX Spectrum here in the UK.  The game is really just a natural progression of that game with a few elements of Blaster Mines, Galaga, Xenon II and bullet hell thrown into the mix.  I have studied every skill on this site and in the 8bitrocket book The Essential Guide to Flash Games and hopefully my game will demonstrate what is possible if you follow the tutorials with some useful insight into my shoot em up spawning technique too.  Where relevant I have provided links to tutorials on this site where I first learnt the blitting and memory optimisation techniques.

Supplied files in this tutorial

Download The Files Here

Main.as

The main class has functionality for loading the maps, moving the camera, and caching, updating and rendering all the graphics.

Ship.as

Provides an update function that moves the ships along targets[] using a targetlength[] to specify each targets length in frames.

Tile.as

Simple object with no update function, only stores each tiles xpos, ypos and ID.

Tilemap.txt

Exported from mappy, all the tiles positions and tile id (only 2 used) in the example level.

Spritemap.txt

Each enemy waves start position.

ship1.png

Enemy ship graphics

tiles.png

Tile strip

Overview

Enemies are drawn onto a sprite layer in an editor such as mappy.  When it comes time to export the map I tend to use csv maps instead of xml, but I appreciate the divide on this.  Multi-layer maps are still possible with csv although attaching meta data to objects is not so well catered for.  The resulting csv file can vary from editor to editor, and if you are using maps with a different format just rewrite the loadLevel() function.

The tilemaps, xml, and bitmapdata tutorial on this site for loading maps is also a good resourcet if you are unsure about this process.  If you want to match the format in this tutorial you may want to get an exporter script for Mappy or even learn lua, it's scripting language.  At the very least I'd suggest reading around the subject of csv vs xml.  Each enemy in the csv file represents an wave of enemies in the game, so when designing the level I only had to place about 15 or 20 enemy ships per level, each one spawns 8,10 or 12 enemy ships by the engine.  You are encouraged to open the supplied text files (Tilemap.txt and Spritemap.txt) if only to appreciate how little data is actually in the sprite map but also to appreciate how the map is loaded and functions with the rest of the code.

Preparing the main class

First of all the main canvas (backbufferdata) is initialised and added as a child to the stage.

[cc lang="javascript" width="550"]
//----------------------------------------------------------------------------------------
// Screen
//----------------------------------------------------------------------------------------
private var screenWidth:int=320;
private var screenHeight:int=300;
private var backBuffer:Bitmap;
private var backBufferData:BitmapData=new BitmapData(320,300, false , 0x111563);
private var blitRect:Rectangle=new Rectangle(0,0,0,0);
private var point:Point = new Point(0, 0);
private var backgroundRect:Rectangle;
private var backgroundBD:BitmapData = new BitmapData(320,300, false , 0x111563);
private var backgroundPoint:Point = new Point(0, 0);
private var gameTimer:Timer;
...
backBuffer = new Bitmap(backBufferData);
backBuffer.y=0;
addChild(backBuffer);
[/cc]

Before loading the map there are a few steps to take in order to embed the png graphics and store them frame by frame in arrays.

[cc lang="javascript" width="550"]
cacheBlitObject(ship1BmpData,ship1BmpDataArray,24);
cacheTileStrip(stripBmpData,stripBmpDataArray,16);
cacheTrigVelocities();
[/cc]

cacheBlitObject uses a single frame of bitmap data to rotate itself 90 times and populate it's second parameter a bitmap data array.  That is more than sufficient to achieve smooth snake like movement.

generateRotation provides this functionality by using a standard matrix rotate also explained elsewhere in more detail on this site.

cacheTileStrip uses a whole strip to populate it's second parameter, also a bitmap data array.

cacheTrigValues does all the cos and sin calculations in advance and stores them so we don't ever have to do real time trig for the snake movement.

[cc lang="javascript" width="550"]
public function cacheBlitObject(bitmapDataToCache:BitmapData,arrayToCacheTo:Array,widthOfEnemy:int):void
{
// caches 90 frames at 4 degrees each

var sourceX:int=0;
var sourceY:int=0;
var sourceRect:Rectangle=new Rectangle(sourceX,sourceY,widthOfEnemy,widthOfEnemy);
var destPoint:Point=new Point(0,0);

for(i=0; i<91; i++)
{
arrayToCacheTo[i] = new BitmapData(widthOfEnemy, widthOfEnemy, true, 0x00000000);
sourceX=0;
sourceY=0;
sourceRect=new Rectangle(sourceX,sourceY,widthOfEnemy,widthOfEnemy);
arrayToCacheTo[i].copyPixels(bitmapDataToCache,sourceRect,destPoint, null, null, true);
arrayToCacheTo[i]=generateRotation(arrayToCacheTo[i],i*4);
}
}

//----------------------------------------------------------------------------------------
// generateRotation
//----------------------------------------------------------------------------------------

private function generateRotation(DATA:BitmapData,ROTATETO:int):BitmapData
{
var degrees:int=ROTATETO;
var angle_in_radians:Number = Math.PI * 2 * (ROTATETO / 360);
var rotationMatrix:Matrix = new Matrix();
var width:int=DATA.width/2;
rotationMatrix.translate(-width,-width);
rotationMatrix.rotate(angle_in_radians);
rotationMatrix.translate(width,width);
var matrixImage:BitmapData = new BitmapData(width*2,width*2, true, 0x00000000);
matrixImage.draw(DATA, rotationMatrix);
return matrixImage;
}

//----------------------------------------------------------------------------------------
// cacheTileStrip
//
// Paramaters:
// bitmapDataToCache - the bitmap data to cache
// arrayToCacheTo - the array to store the bitmap data cache
//----------------------------------------------------------------------------------------

public function cacheTileStrip(bitmapDataToCache:BitmapData,arrayToCacheTo:Array,widthOfTile:int):void
{
var sourceX:int=0;
var sourceY:int=0;
var sourceRect:Rectangle=new Rectangle(sourceX,sourceY,widthOfTile,widthOfTile);
var destPoint:Point=new Point(0,0);

for(i=0; i<(bitmapDataToCache.width/widthOfTile); i++)
{
arrayToCacheTo[i] = new BitmapData(widthOfTile, widthOfTile, true, 0x00000000);
sourceX=i*widthOfTile;
sourceY=0;
sourceRect=new Rectangle(sourceX,sourceY,widthOfTile,widthOfTile);
arrayToCacheTo[i].copyPixels(bitmapDataToCache,sourceRect,destPoint, null, null, true);
}
}

//----------------------------------------------------------------------------------------
// cacheTrigVelocities
//
// caches velocities from 0 to 360 degrees
// to avoid any real time cos or sin calculations
//----------------------------------------------------------------------------------------

public function cacheTrigVelocities():void
{
for (var ctr:int=0;ctr<361;ctr++)
{
rotnCacheX.push(Math.cos(2.0*Math.PI*(ctr-90)/360.0));
rotnCacheY.push(Math.sin(2.0*Math.PI*(ctr-90)/360.0));
}
}

[/cc]

A timer system is employed to establish recurrence of the main loop.  The tile strip is cached frame by frame, whereas the single frame ship.png is cached then rotated 90 times (once for each 4 degrees).  You can find out more about caching bitmapdata and it's benefits elsewhere on this site.  It has become second nature for me now that I have studied and practised Jeff's tutorials to such an extent.

Next, I created 2 object pools.  The idea is to avoid using the 'new' operator in real time, and also to restrict the number ever created.  Since I know I'll never have more than 400 tiles and never have more than 100 enemies I can confidently restrict these object pools in advance.

[cc lang="javascript" width="550"]
prepareSpritePool();
prepareTilePool();

//----------------------------------------------------------------------------------------
// prepareSpritePool
//
// set up some blit objects
// the game will cycle through this capped pool
//----------------------------------------------------------------------------------------

public function prepareSpritePool():void
{
for(i=0;i<maxNoSpritesOnScreen;i++)
{
spritePool[i]=new Ship(rotnCacheX,rotnCacheY);
}
}

//----------------------------------------------------------------------------------------
// prepareTilePool
//
// set up some tile objects
// the game will cycle through this capped pool
//----------------------------------------------------------------------------------------

public function prepareTilePool():void
{
for(i=0;i<maxNoTilesOnScreen;i++)
{
tilePool[i]=new Tile();
}
}

[/cc]

Now that we have cached all our graphics and trigonometric calculations, it's time to load the map.

We load the map layer by layer, tiles first:

[cc lang="javascript" width="550"]
//----------------------------------------------------------------------------------------
// loadLevel
//
// loads the map
//----------------------------------------------------------------------------------------

public function loadLevel():void
{
noMapTiles=-1;

for(i=0;i<10000;i++)
{
tilesMapPositionsX[i]=-100;
tilesMapPositionsY[i]=0;
tilesMapIDs[i]=0;
}

var cols:Array=new Array();
var rows:Array = new Array();

var widthInTiles:int;
var heightInTiles:int;

rows=TilesMapData.split("\n");

heightInTiles = rows.length;

for(r = 0; r < heightInTiles; r++)
{
cols = rows[r].split(",");
widthInTiles = cols.length;

for(c = 0; c < widthInTiles; c++)
{
if((uint(cols[c]))>0) // found a tile
{
noMapTiles++;
tilesMapPositionsX[noMapTiles]=(c*16);
tilesMapPositionsY[noMapTiles]=r*16;
tilesMapIDs[noMapTiles]=int(cols[c]);
}
}
}
currentTileMapItem=noMapTiles;
...

[/cc]

Then the sprites:

[cc lang="javascript" width="550"]
...
noMapSprites=-1;

for(i=0;i<10000;i++)
{
spritesMapPositionsX[i]=-100;
spritesMapPositionsY[i]=0;
spritesMapIDs[i]=0;
}

cols=new Array();
rows = new Array();

rows=SpriteMapData.split("\n");

heightInTiles = rows.length;

for(r = 0; r < heightInTiles; r++)
{
cols = rows[r].split(",");
widthInTiles = cols.length;

for(c = 0; c < widthInTiles; c++) { if((int(cols[c]))>0) // found a sprite
{
noMapSprites++;
spritesMapPositionsX[noMapSprites]=(c*16);
spritesMapPositionsY[noMapSprites]=r*16;
spritesMapIDs[noMapSprites]=int(cols[c]);
}
}
}
currentSpriteMapItem=noMapSprites;
}

[/cc]

Now that we have all the tiles and sprite locations and ids stored in arrays we can examine how the main loop accesses them.  The currentTileMapItem and currentSpriteMapItem are now equal to the total number of tiles and sprites in the map.  We can lookup the current tile and sprite at the top of this stack and see if it's y coordinate is less than 2 pixels higher than the top of the screen (the camerYpos).  If so we spawn a wave of enemies, keeping track of the wave number so that we can do different spawning code for each.

[cc lang="javascript" width="550"]
//----------------------------------------------------------------------------------------
//
//
// UPDATE
//
// calls updateBlitObjects and renderBlitObjects
//
//
//----------------------------------------------------------------------------------------

public function update(e:Event):void
{
updateBlitObjects();
renderBlitObjects();
}

//----------------------------------------------------------------------------------------
// updateBlitObjects
//
// calls each blit objects update function
//----------------------------------------------------------------------------------------

public function updateBlitObjects():void
{
cameraYpos--;
checkMap();
for each(var ship:Ship in spritesOnScreen)
{
ship.update();
ship.update();
}
checkForSplice();
}

//----------------------------------------------------------------------------------------
// checkMap
//
// checks to see if the cameray is approaching a new tile or enemy
//----------------------------------------------------------------------------------------

public function checkMap():void
{
while(tilesMapPositionsY[currentTileMapItem]>=cameraYpos-16 && currentTileMapItem>1)
{
tilePool[currentTilePoolItem].xpos=tilesMapPositionsX[currentTileMapItem];
tilePool[currentTilePoolItem].ypos=tilesMapPositionsY[currentTileMapItem];
tilePool[currentTilePoolItem].id=tilesMapIDs[currentTileMapItem];

tilesOnScreen.push(tilePool[currentTilePoolItem]);

currentTilePoolItem++;
if(currentTilePoolItem>maxNoTilesOnScreen-1)currentTilePoolItem=0;
if(currentTileMapItem>1)currentTileMapItem--;
}

while(spritesMapPositionsY[currentSpriteMapItem]>=cameraYpos-16 && currentSpriteMapItem>1)
{
spritePool[currentSpritePoolItem].xpos=spritesMapPositionsX[currentSpriteMapItem];
spritePool[currentSpritePoolItem].ypos=spritesMapPositionsY[currentSpriteMapItem];
spritePool[currentSpritePoolItem].frameIndex=0;

currentWaveNumber++;

for(i=0;i<10;i++)
{
if(currentWaveNumber==1)
{
var spacing:int=35;
spritePool[currentSpritePoolItem].targets=[3,15,10,15,10,3];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
spritePool[currentSpritePoolItem].xpos=160-12;
spritePool[currentSpritePoolItem].ypos=(i*spacing)*-1;
}
else if(currentWaveNumber==2)
{
if(i<5)
{
spacing=25;
spritePool[currentSpritePoolItem].targets=[1,14,11,14,11,1];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
spritePool[currentSpritePoolItem].xpos=340+(i*spacing);
spritePool[currentSpritePoolItem].ypos=150;
}
else
{
spacing=25;
spritePool[currentSpritePoolItem].targets=[2,10,15,10,15,2];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
spritePool[currentSpritePoolItem].xpos=-40-(i*spacing);
spritePool[currentSpritePoolItem].ypos=50;
}
}
if(currentWaveNumber==3)
{
spacing=35;
spritePool[currentSpritePoolItem].targets=[3,11,13,11,13,3];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
spritePool[currentSpritePoolItem].xpos=160-12;
spritePool[currentSpritePoolItem].ypos=(i*spacing)*-1;
}

spritePool[currentSpritePoolItem].currentstep=0;
spritePool[currentSpritePoolItem].distancethisstep=0;

spritesOnScreen.push(spritePool[currentSpritePoolItem]);

currentSpritePoolItem++;
if(currentSpritePoolItem>maxNoSpritesOnScreen-1)currentSpritePoolItem=0;
}
if(currentSpriteMapItem>1)currentSpriteMapItem--;
}
}

//----------------------------------------------------------------------------------------
// checkForSplice
//
// see if there are any enemies to remove from the on screen array
//----------------------------------------------------------------------------------------

public function checkForSplice():void
{
i=-1;
for each(var ship:Ship in spritesOnScreen)
{
i++;
if(ship.currentstep>0 && (ship.ypos>300 || ship.ypos<-30 || ship.xpos<0 || ship.xpos>320))
{
spritesOnScreen.splice(i,1);
}
}
i=-1;
for each(var tile:Tile in tilesOnScreen)
{
i++;
if(tile.ypos>cameraYpos+300)
{
tilesOnScreen.splice(i,1);
}
}

}

[/cc]

Let's isolate one of these spawns as an example and see how the map position is translated into a screen position.

Check map is called once every frame tick, although if you wanted to optimise you could change this so it's called only once every two frame ticks.  I actually use duality in my main loops although in this example I have not.  In SpaceGate, I use a task number so if task==1 do collisions if task==2 do checkMap.  Duality is another topic though but who knows I may get the chance to blog on that sometime too.

checkMap is called by our update function and if it finds a ship in the map close enough to the camera it spawns a new wave.  The wave is assigned targets and targetlengths.  Each of the batch is given a fresh counter (distancethisstep) and has it's currentstep defaulted to 0.

[cc lang="javascript" width="550"]
public function checkMap():void
{
while(spritesMapPositionsY[currentSpriteMapItem]>=cameraYpos-16 && currentSpriteMapItem>1)
{
spritePool[currentSpritePoolItem].xpos=spritesMapPositionsX[currentSpriteMapItem];
spritePool[currentSpritePoolItem].ypos=spritesMapPositionsY[currentSpriteMapItem];
spritePool[currentSpritePoolItem].frameIndex=0;

currentWaveNumber++;

for(i=0;i<10;i++)
{
if(currentWaveNumber==1)
{
var spacing:int=35;
spritePool[currentSpritePoolItem].targets=[3,15,10,15,10,3];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
spritePool[currentSpritePoolItem].xpos=160-12;
spritePool[currentSpritePoolItem].ypos=(i*spacing)*-1;
}
spritePool[currentSpritePoolItem].currentstep=0;
spritePool[currentSpritePoolItem].distancethisstep=0;

spritesOnScreen.push(spritePool[currentSpritePoolItem]);

currentSpritePoolItem++;
if(currentSpritePoolItem>maxNoSpritesOnScreen-1)currentSpritePoolItem=0;
}
..
if(currentSpriteMapItem>1)currentSpriteMapItem--;
[/cc]

spritePool

is the array of type Ship that we created in advance. The spawn method

above assigns however many of these is needed

to the spriteOnScreen array.

The Ship pool members are updated twice per frame tick, so let's have a look at the Ship class, the only other class of any

considerable length in the example.

[cc lang="javascript" width="550"]
public class Ship
{
// GLOBAL VARIABLES

public var xvelocity:Number;
public var yvelocity:Number;
public var xpos:Number;
public var ypos:Number;
public var width:int;
public var height:int;
public var health:int;
public var frameIndex:int;
public var currentstep:int=0;
public var distancethisstep:int=0;
public var targets:Array;
public var rotFrameIndex:int=0;
public var mvctr:int=0;
public var targetlengths:Array;
public var radians:Number;
public var radius:Number=0.05;
private var rotnCacheX:Vector.<Number>=new Vector.<Number>();
private var rotnCacheY:Vector.<Number>=new Vector.<Number>();
public var angle:int=0;
public var rotateframes:int=360;
public var qucircle:int=rotateframes/4;
public var hacircle:int=rotateframes/2;
public var tqcircle:int=qucircle+hacircle;
public var fucircle:int=rotateframes;
public var step:int=0;
public var rotinc:int=1;
public var temp:int=1;
public var targetArray:Vector.<Array>=new Vector.<Array>();
[/cc]

Most of the work is done in the update function.

As we know, in the example wave, our objects have these characteristics:

[cc lang="javascript" width="550"]
for(i=0;i<10;i++)
{
if(currentWaveNumber==1)
{
var spacing:int=35;
spritePool[currentSpritePoolItem].targets=[3,15,10,15,10,3];
spritePool[currentSpritePoolItem].targetlengths=[50+(i*spacing),150,150,50,50,500];
}
}
spritePool[currentSpritePoolItem].currentstep=0;
spritePool[currentSpritePoolItem].distancethisstep=0;
[/cc]

Now we update them like this:

[cc lang="javascript" width="550"]
public function update():void
{
mvctr+=1;
distancethisstep++;
if(distancethisstep>=targetlengths[currentstep])
{

step=currentstep++;
mvctr=distancethisstep=0;
}
temp=((distancethisstep/targetlengths[currentstep])*qucircle);
if(targets[currentstep]==1)
{
xpos-=1;
angle=270;
}
else if(targets[currentstep]==2)
{
xpos+=1;
angle=90;
}
else if(targets[currentstep]==3)
{
ypos+=1;
angle=180;
}
else if(targets[currentstep]==4)
{
ypos-=1;
angle=270;
}
else if(targets[currentstep]==10)// q1 clockwise
{
temp+=hacircle;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp]*-1;
}
else if(targets[currentstep]==11) // q2 clockwise
{
temp+=tqcircle;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp]*-1;
}
else if(targets[currentstep]==12) // q3 clockwise
{
temp+=0;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp]*-1;
}
else if(targets[currentstep]==13) // q4 clockwise
{
temp+=qucircle;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp]*-1;
}
else if(targets[currentstep]==14) // q1 anticlockwise
{
temp+=0;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp];
}
else if(targets[currentstep]==15)// q2 anticlockwise
{
temp+=qucircle; //*qucircle for cached values;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp];
}
else if(targets[currentstep]==16) // q3 anticlockwise
{
temp+=hacircle;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp];
}
else if(targets[currentstep]==17)// q4 anticlockwise
{
temp+=tqcircle;
xpos+=rotnCacheY[temp]; ypos+=rotnCacheX[temp];
}

if ((targets[currentstep]>=10) && (targets[currentstep]<=13)) {angle=180+temp+qucircle;} if(angle>(fucircle-1)){angle-=(fucircle);}

if((targets[currentstep]>=14) && (targets[currentstep]<=17)) {angle=180+qucircle-temp;}

if(angle<0){angle+=(fucircle);} if(angle>(fucircle-1)){angle-=(fucircle-1);}

while(angle<0)angle+=360; while(angle>360)angle-=360;
}

[/cc]

The system makes use of the paradigm of quadrants, so that each 90° turn is actually a single target.

I used the codes 1-4 for straight lines, 10-13 for clockwise targets and 14-17 for anti-clockwise.

Each ship's current distance along it's current target is increased by 1 when

update is called. Ship's also keep track of which target they are on.

The distance that the ship has travelled along it's current target is

divided by the target length, and then multiplied by 90°, then used as

the objects current angle.

This angle is also used to move the ship using the rotation cache's we sent to the ship when instatiating:

[cc lang="javascript" width="550"]
public function Ship(ROTATIONSX:Vector.,ROTATIONSY:Vector.)
{
rotnCacheX=ROTATIONSX;
rotnCacheY=ROTATIONSY;
...
[/cc]

The system makes it fairly easy to create interesting looking enemy waves

and allows for rapid development. You can try changing the target[]

array and targetlengths[] for completely different patterns.
I hope that you find it useful and any questions will gladly be answered,

as long as you show me the awesome games you make with it.

BadgerManufactureInc

I want to thank Barnaby for taking the time to explain how this works. I have been planning to create a system like this and his advice and generous view on code sharing will certainly help me and others that want to create blit patterns such as these. Now, go play Space Gate. You will not be disappointed.

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.