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