A space shooter example.
Use wasd to move around, and move the mouse to rotate the ship.
On a touch device, the rotation controller will appear below the game.
You can get the fullscreen version and all source files from here
space shooter dog fight demo
(function() {
var exF;//ExplosionFactory.
var bullets =[];
var pBulletThrott = 300;
var eBulletThrott = 400;
var enemyPlane = null;//each levels main enemy, a RotatingShooter.
var enemyPath;
var star;//CanvasAnimation.
var level = 1;
var levelSpecs = {2:[50, new tabageos.Rectangle(960,656,96,96)], 3:[75, new tabageos.Rectangle(720,383,96,96)]};
var enemies = [];//holding the level 3 little ships.
function ShipShooter() {
var gameSpecs = {
gWidth:256, gHeight:320,cameraWidth:256, cameraHeight:320,
cameraFollowOffsetX:0, cameraFollowOffsetY:0, tileW:16, tileH:16,
spriteSheetImage: "spacesheet.png", containerDivId:"container", rootDivId:"root",
controllerDivId:"controller", gameScale:0,hudScale:8, useScreenOrganizer:true,startWidth:80, startHeight:32,
controllerHeight:144, initialLives:3, initPlayerPosition:new tabageos.MoverPoint(64,132), startLocations:new tabageos.MoverPoint(90,224),
gameLoop:this.loop,initializationSpecifics:this.setup, disableBackgroundAlpha:0,useSceneChanger:0,
addedResizeMethod:null, sceneResetSpecifics:null,fullResetSpecifics:null, additionalSceneResetSpecifics:null,
positionResetSpecifics:null, cameraType:2, backgroundColor:"#c8c8c8"
};
tabageos.GameSkeleton.call(this, gameSpecs);
this.establishKeyEventsForReset();
}
ShipShooter.constructor = ShipShooter;
ShipShooter.prototype = Object.create(tabageos.GameSkeleton.prototype);
ShipShooter.prototype.setup = function(sax,say) {
this.lives = 1;//the player will start with 200 health which is alot, so only 1 life, the game is quick and easy unless you do nothing.
this.title.floor.canvas.setAttribute("style", "image-rendering: -moz-crisp-edges;image-rendering: -webkit-crisp-edges;image-rendering: pixelated;image-rendering: crisp-edges;");
this.gameOverContainer.floor.canvas.setAttribute("style", "image-rendering: -moz-crisp-edges;image-rendering: -webkit-crisp-edges;image-rendering: pixelated;image-rendering: crisp-edges;");
this.backgroundLayer.canvas.setAttribute("style", "image-rendering: -moz-crisp-edges;image-rendering: -webkit-crisp-edges;image-rendering: pixelated;image-rendering: crisp-edges;");
//we copy the title image from the sprite sheet onto the title CanvasObject.
this.title.floor.copyPixels(this._image,new tabageos.Rectangle(336,608,256,320), new tabageos.MoverPoint());
//the title image has a start image that we will use, so we dont need the startButton to have any text.
this.startButton.innerHTML = "";
level = 1;
enemies = [];
//next we copy the background image onto the offscreen backgroundLayer, the camera will copy from backgroundLayer first then charLayer.
this.backgroundLayer.copyPixels(this._image, new tabageos.Rectangle(336,256,256,320), new tabageos.MoverPoint());
this.gameOverContainer.floor.copyPixels(this._image, new tabageos.Rectangle(608,608,256,320), new tabageos.MoverPoint());
//this.gameOverContainer.div is also available to put html in. floor is a CanvasObejct. gameOverContainer is a CanvasObjectContainer.
//to rotate each RotatingShooter/Traveler needs a rotationImage CanvasObejct that is the right facing image to be rotated in the middle of a space 3x its width/height.
//in this case the ship is 32x32 so the rotation image is 32 x 3, and we place the image in the middle of the 96x96 area and facing right.
var rotationImage = new tabageos.CanvasObject(null,96,96);
//a WayDeterminer lets us do pixel perfect collision detection, and RotatingShooters are set up by default to use one.
//but we will not really be using it, we set up a dummy WayDeterminer by passing null as the second param and setting its defaultReturn to true.
//to use the WayDeterminer for pixel checks, the html5 getImageData method is used, which requiers special permission, must be online or emulate a server.
var wd = new tabageos.WayDeterminer(0,null);
wd._defaultReturn = true;
//player is a rotating shooter, ready to rotate and shoot BlittedTraveler bullets.
//the first of the Rectangles we pass tells it where in the sprite sheet to get the rotation image that will be copied onto its offscreen rotationImage CanvasObject.
//again, that image must be of the image inside of 3x blank space all around.
//what happens is that that CanvasObject is rotated and what is shown on screen is the result, with a lesser area than 3x clipping would happen.
this.player = new tabageos.RotatingShooter(wd,this._image,this.charLayer,null,100,132,32,32, rotationImage, new tabageos.Rectangle(848,240,96,96),this.charLayer,new tabageos.Rectangle(272,64,16,16));
this.player._gravityLevel = .285;
this.player.health = 200;
exF = new tabageos.ExplosionFactory(16,16,9,160,16);//many quick animations, all horizontal in the sprite sheet, all from a default location if not specified otherwise.
this.setupMouseTouchHandle(1);//by calling this we get this.mouseIsDown and this.mousePoint in the loop.
var enC = new tabageos.CanvasObject(null,96,96); //the enemy plane is another RotatingShooter with its own offscreen rotationImage.
//if we want many different enemyPlanes that are going to rotate, ideally we'd want to create some routine where they would share the same rotatingImage CanvasObejct.
enemyPlane = new tabageos.RotatingShooter(wd,this._image,this.charLayer,null,100,32,32,32, enC, new tabageos.Rectangle(960,464,96,96),this.charLayer,new tabageos.Rectangle(272,64,16,16));
enemyPlane._veloc.y = 1;//sets it moving a little to get it to its path, because we are not directly altering velocity otherwise, and by default it is 0, and with no velocity it does not move.
//MapTravelers and MapMovers, in the platform and rpg examples, dont behave that way, their move method also changes their velocity.
//but BlittedTravelers which RotatingShooter inherits from only apply velocity during their move method.
enemyPlane.health = 25;
//we make a nice curvey path for the enemy plane to follow, a generally long pattern of curves, making for nice boss style repetative movement that seems randomish.
//a hermite curve path is a wave path, the second and last points define the wave length, the last param tells how many points we want to get.
enemyPath = tabageos.GeometricMath.getHermiteCurvePath(new tabageos.MoverPoint(64,16), new tabageos.MoverPoint(128,64), new tabageos.MoverPoint(8,256), new tabageos.MoverPoint(16,320),10);
//an arc curve path is a curve defined by three points, the start the middle and the end poit.
enemyPath = tabageos.GeometricMath.mergeArrays(enemyPath, tabageos.GeometricMath.getArcCurvePath(new tabageos.MoverPoint(8,256), new tabageos.MoverPoint(200,148), new tabageos.MoverPoint(8,16),10));
enemyPath = tabageos.GeometricMath.mergeArrays(enemyPath, tabageos.GeometricMath.getHermiteCurvePath(new tabageos.MoverPoint(164,256), new tabageos.MoverPoint(228,64), new tabageos.MoverPoint(164,16), new tabageos.MoverPoint(116,320),10));
//so enemyPath is an array of MoverPoints, ready to be used with the enemyPlane.followPath method which it inherits from TravelerSkeleton.
//the level complete star animation.
star = new tabageos.CanvasAnimation(this._image,this.cameraLayer,null,0,0,160,160);
star.animationSpecs = { "grow":[0,[0,63,16,63,32,63,48,63,64,63]] };
star.fromXOffset = -(160-16);
star.fromYOffset = -(160-16);
star.currentAnimation = "grow";
star.animate();
if(this.controller._style == 1) {//the controller does not get remade when the game resets.
this.controller._basicControllerButtonTakedown();
this.controller.rotationalControllerButtonSetup();
}
//the rotation controller needs centerRotationX and centerRotationY to be set.
//they define the center point of the rotation controllers circle in relation to the screen.
//in this example case the game is not full screen, it's in the middle of the page horizontally and at the top vertically.
//so we need its x position, which is going to be half window width plus half game width.
//and the y position is from 0 to the game height.
//both are added by about 72 which is the distance from the controller images top edge to the middle of the circle.
//after setting these you need to test it in dev tools, it should work in dev tools, test and adjust from 32-80 or as needed.
//to see this fullscreen, download the files from https://tadgames.itch.io/space-shooter-dog-fight-demo
//the fullscreen setup is different as scale is accounted for using _scaleRectRef.
var fif = (window.innerWidth/2);
this.controller.centerRotationX = ( fif - (256/2) ) + 32;
this.controller.centerRotationY = 320 + 72;
var i = 0; var l = 10;var e;
for(i;i < l;i++) {//the little ships of level 3.
e = new tabageos.BlittedTraveler(this._image,this.charLayer,null,0,0,16,16);
e.animationSpecs = {
"left":[6, [59,6,59,6,59,6,59,6] ],
"right":[6, [55,6,55,6,55,6,55,6] ],
"up":[6, [61,6,61,6,61,6,61,6] ],
"down":[6, [57,6,57,6,57,6,57,6] ]
};
e.currentAnimation = "down";
//the path is from -320 straight down then right and back up offscreen.
e.path = [new tabageos.MoverPoint(256/2,-320), new tabageos.MoverPoint(256/2,320/2), new tabageos.MoverPoint(256/2,400), new tabageos.MoverPoint(320, 400), new tabageos.MoverPoint(320,-320)];
//then we randomize the x position of the going down portion of the path.
var ranx = Math.floor(Math.random()*280);
if(!ranx) ranx = 256/2;
e.path[0].x = ranx;
e.path[1].x = ranx;
e.path[2].x = ranx;
e.easeProximity = 1;
e.setX(256/2);e.setY(-325);
e._veloc.y = Math.floor(Math.random()*5) + 3;
e.maxSpeed = Math.floor(Math.random()*8) + 3;
enemies.push(e);
}
};
ShipShooter.prototype.showWaveComplete = function(ts) {
//this methos takes over the loop.
//using helperPoint we only call this part once to draw the wave complete background and begin the star animation.
if(this._helperPoint.x != 0 || this._helperPoint.y != 0) {
this._helperPoint.x = 0;this._helperPoint.y = 0;
this._helperRect.x = 48; this._helperRect.y = 608;
this._helperRect.width = 256; this._helperRect.height = 320;
this.cameraLayer.copyPixels(this._image,this._helperRect,this._helperPoint);
star.resetCurrentAnimation();star.x = -12;star.y= 180;
}
//when the star animation is finished.
if(star.finishedCurrentAnimation()) {
//if the health of the player is more than 50 show another star, if it is more than 75 show 3 stars.
if((this.player.health > 50 && star.x == -12) || (this.player.health > 75 && star.x == 48)) {
star.x += 60;
if(star.y == 180) {star.y = 150;}
else if(star.y == 150) { star.y = 181;}
star.resetCurrentAnimation();
} else { //when everything is finished, we call endLevelComplete which shows a transition and then gives the loop back to the loop method.
this.endLevelComplete(0);
}
} else {
star.animate(.3);//the cameraLayer is not cleared.
//so each animation blit builds onto the cameraLayer.
star.blit();
}
};
ShipShooter.prototype.showCredits = function(ts) {
//this is a loop that hijacks the main loop.
//and we only want to execute this once, so we use _helperPoint for that, since we'll be setting it to 0 and know that in our main loop its set to not 0.
if(this._helperPoint.x != 0 || this._helperPoint.y != 0) {
this._helperPoint.x = this._helperPoint.y = 0;
this._helperRect.x = 48; this._helperRect.y = 256;
this._helperRect.width = 256; this._helperRect.height = 320;
this.cameraLayer.copyPixels(this._image,this._helperRect,this._helperPoint);
}
//because we have not given a time in the gameComplete call below.
//this method will perminantly replace loop.
//and just go on forever unless the user hits the x hud button to exit to title. or presses esc.
//or we could check for a button press in here and then call fullResetToTitle.
//or reset to level 1 or whatever.
//if we give a time during the gameComplete call, then after that time it will auto reset to title.
//to see this setup in a simple manner see the levelComplete example.
};
ShipShooter.prototype.loop = function() {
var cb = this.controller.buttons;
if(cb.left) { //moving the player is also different than with the MapTraveler/Mover classes, for BlittedTraveler we have to change veloc ourself.
this.player._veloc.x = -4;
}
if(cb.right) {
this.player._veloc.x = 4;
}
if(cb.up) {
this.player._veloc.y = -4;
}
if(cb.down) {
this.player._veloc.y = 4;
}
if(!cb.up && !cb.down) {
this.player._veloc.y = 0;
}
if(!cb.left && !cb.right) {
this.player._veloc.x = 0;
}
this.player.move();//BlittedTraveler move is only applying velocity, its like the update method of MapTravelers/Movers.
if(this.player.getRotation() != this.controller.rotation && this.device) {
this.player.setRotation(this.controller.rotation);
} else {
if(!this.device) {
this.player.rotateWithMoverPoint(this.mousePoint,32,32);
}
}
enemyPlane.circleDistance = 32;
enemyPlane.maxForce = 70;
enemyPlane.maxSpeed = 3;
//following the path looks nice but to make it seem really a.i.
//we'll tell it to circle the player if its real close, otherwise follow its path.
if(tabageos.GeometricMath.testForPointInCircle(this.player._pos,48,enemyPlane._pos)) {
enemyPlane.circle(this.player._pos);
} else {
enemyPlane.followPath(enemyPath,1);
}
enemyPlane.move();
this._helperPoint.x = enemyPlane.x - 32; this._helperPoint.y = enemyPlane.y - 32;//offset the drawing of the rotation.
enemyPlane.rotateWithMoverPoint(this.player._pos);//rotate the enemy plane towards the player, 2 planes can not share the same rotationImage, one would not rotate correctly.
//each enemy that you want to rotate would need to have its own off screen rotation CanvasObejct.
//and more than 10 or so of those would slow the game down on lower end devices.
enemyPlane.blit(0,0, this._helperPoint);
//if(cb.b || this.mouseIsDown) {//player shooting.
if(pBulletThrott < = 0) {
pBulletThrott = 400;
bullets.push(this.player.shoot(-8,-8));
} else { pBulletThrott -= 33.3; }
//} //autofire.
var b;
eBulletThrott -= 33.3;
if(eBulletThrott < = 0) {//enemy shooting.
b = enemyPlane.shoot(-8,-8);
b.health = 900;
bullets.push(b);
eBulletThrott = 500;
}
var i = 0;var j;var ob;
if(level == 3) {//on level 3 the little ships keep cycling their path, randomly restarting starting from different x positions.
for(i;i < enemies.length;i++) {//there are only 10 little ships.
obj = enemies[i];
obj.followPath(obj.path, 1);
obj.move();
obj.animate();
obj.blit();
if(tabageos.GeometricMath.testForPointInCircle(obj._pos,16,this.player._pos)) {
obj.maxSpeed = 20;
obj.easeTo(this.player._pos);//if its close to the player try to ram the player.
}
if(tabageos.GeometricMath.rectanglesOverlapAmount(obj.getRectangle(), this.player.getRectangle())/16 >= 1) {
obj.setY(-380);
obj._pathIndex = 4;obj.maxSpeed = Math.floor(Math.random()*8) + 3;
exF.addExplosion(this.player.x,this.player.y,1,656,16,32,32);
exF.addExplosion(this.player.x+16,this.player.y+16,1,656,16,32,32);
this.player.health -= 10;
this.camera.shake(900, this.container,1);//this example is a still camera, when the camera type is moving then shake happens with the camera. So we pass a 1 at the end to execute the shake manually.
this.showHealthBar(this.player.health,"#c8c8c8","#6495ed",3);
}
if(obj.y >=390) {
var ranx = Math.floor(Math.random()*280);
if(!ranx) ranx = 256/2;
obj.path[0].x = ranx;
obj.path[1].x = ranx;
obj.path[2].x = ranx;
}
}
}
i = 0;j = 0;
for(i;i < bullets.length;i++) {//bullet movement, both player and enemy bullets together.
b = bullets[i];//there is only ever so many bullets(151), they keep getting recycled, and both the enemy and player are using the same bullets.
b.move();//bullets are BlittedTravelers.
b.blit();
if(b.x < this.camera.v.x || b.x > this.camera.v.x + this.camera.v.width || b.y < 0 || b.y > this.camera.v.y + this.camera.v.height) {
tabageos.GeometricMath.splice(bullets, bullets.indexOf(b));
b.health = 0;
this.player.reclaimBullet(b);
break;
}
if((!b.health || b.health != 900) && tabageos.GeometricMath.testForPointInCircle(b._pos,32,enemyPlane._pos)) {
exF.addExplosion(b.x,b.y,1,656,16,32,32);//do a big explosion.
tabageos.GeometricMath.splice(bullets, bullets.indexOf(b));
this.player.reclaimBullet(b);
enemyPlane.health -= 1;
this._helperRect.x = enemyPlane.x + (enemyPlane.width * .5);
this._helperRect.y = enemyPlane.y - (16/2);
this._helperRect.width = Math.round(enemyPlane.health/5);
this._helperRect.height = 3;
this.charLayer.drawRect(this._helperRect, "#ff0000");//enemy health bar.
if(enemyPlane.health <= 0) {
exF.addExplosion(enemyPlane.x,enemyPlane.y,1,656,16,32,32);
exF.addExplosion(enemyPlane.x-32,enemyPlane.y+32,1,656,16,32,32);
enemyPlane.setY( -320 );level += 1;
if(level <= 3) {
enemyPlane.health = levelSpecs[level][0];
enemyPlane.rFromRect = levelSpecs[level][1];//changing what it looks like.
this.levelComplete(this.showWaveComplete,0);
} else {
this.gameComplete(this.showCredits);
}
}
break;
}
//b.health as 900 signifies an enemy bullet.
if(b.health === 900 && tabageos.GeometricMath.testForPointInCircle(b._pos,32,this.player._pos)) {
exF.addExplosion(b.x,b.y,1,656,16,32,32);//do a big explosion.
tabageos.GeometricMath.splice(bullets, bullets.indexOf(b));
enemyPlane.reclaimBullet(b);
this.player.health -= 1;
this.showHealthBar(this.player.health,"#c8c8c8","#6495ed",3);
if(this.player.health <= 0) {
this.gameOver();
}
break;
}
//for each little enemy if a player bullet hits it, show an explosion.
if(level == 3) {
j = 0;
for(j;j < enemies.length;j++) {
ob = enemies[j];
if(b.health != 900 && tabageos.GeometricMath.testForPointInCircle(b._pos,8,ob._pos)) {
exF.addExplosion(ob.x,ob.y,1,656,16,32,32);
ob.setY(-380);
ob._pathIndex = 4;ob.maxSpeed = Math.floor(Math.random()*8) + 3;
tabageos.GeometricMath.splice(bullets, bullets.indexOf(b));
this.player.reclaimBullet(b);
break; break;
}
}
}
//for each other bullet if it's the opposite and they hit, do a little explosion.
j = 0;
for(j;j < bullets.length;j++){
ob = bullets[j];
if(ob != b && b.health != ob.health) {
if(tabageos.GeometricMath.testForPointInCircle(b._pos,8,ob._pos)) {//even though BlittedTravelers together with a WayDeterminer can do pixel collision detection, thats only really needed in rare cases. GeometricMath is our friend.
exF.addExplosion(b.x,b.y);
tabageos.GeometricMath.splice(bullets, bullets.indexOf(ob));
enemyPlane.reclaimBullet(ob);
tabageos.GeometricMath.splice(bullets, bullets.indexOf(b));
b.health = 0;
this.player.reclaimBullet(b);
break; break;
}
}
}
}//end bullets.
exF.displayExplosions(this.charLayer, this._image);
this._helperPoint.x = this.player.x - 32; this._helperPoint.y = this.player.y - 32;//offset the drawing of the rotation.
this.player.blit(0,0, this._helperPoint);//notice that we know that _helperPoint is altered here at the end of our loop.
};
new ShipShooter();
})();
back to top