Tank Combat
Hello, welcome to the (somewhat overdue) second part of my “Making HTML5 games by someone who knows pretty much nothing about making games” series. If you’ve not read the first part ( see here ) then it’s probably a good idea to check that out first as the plan is to try and continue from where I left off. If you want to jump right in you can grab a copy of the code produced previously here.
So, just as a recap, the “game” if it can be called that at this stage is being build using the Sprite.js framework and currently consists of a black screen and an ugly looking triangle, which can be controlled using both the arrow and wasd keys. In order to try and bring the “game” closer to something worthy of that description, the next logical step is to start adding a few more features to our game. Shooting, bad guys, stuff like that.
To do this, its probably a good idea to figure out exactly what kind of game we are planning to make, and thus which features its going to need. In the previous tutorial, I’d kinda been thinking of creating a space style shooter, although, to be honest, I don’t really think the movement method fits to well with that. Additionally, I feel like trying something a bit different.
So what’s the new plan?
Tank Combat
Use a tank to fight off the evil alien invaders.
The main aim of this tutorial is to create the above game, with the following four key features;
- The ability to drive the tank with the arrow and wasd keys.
- The ability to aim and shoot using the mouse.
- To have some sort of enemy to fight.
- To have some graphics that actually fit the new game idea.
Given this, I think the first step is to start bringing our existing “game” into line with the new vision.
To start, I’ve created a new “tank” sprite (just a really basic pixel tank).
The tank sprite consist’s of three panels. The first two display the tank body at different stages of movement (basically the tracks move slightly) and the third shows the tanks gun turret, which needs to be separate from the hull so we can position and rotate it separately.
To add the sprite into the game, we will need to update our “ship” sprite definition. I will also rename “ship” to “tank” while here, just to avoid confusion (remember in addition to changing the sprite file, its also important to update the sprite definition with the new sprites width and height).
tank = this.mytank = this.scene.Sprite("tank.png",{
"layer": this.layer, //Layer tanks will be displayed in.
"x": 100, //X position of sprite
"y": 100, //Y position of sprite
"w": 36, //width of sprite.
"h": 59 //height of sprite.
});
To go with the new sprite, you may also want to change the game’s background colour from black to green (like grass) so it fits a little better with the new theme.
Although the tank sprite is now in the game, it’s movement still feels a little wrong, since it was originally set up on the premise that we were going to be controlling a space ship. Because of this I think its worth spending a little time tweaking the way our sprite moves. To start friction plays a much bigger role when not in space, so to replicate this I’ve added an else, to slow the tank down when not accelerating and, in addition I also lowered the acceleration speed from 0.5 to +0.2.
if(this.inputs.keyboard.up){
if(speed<5) speed = speed+0.2;
}else{
//Apply friction
if(speed>0) speed = speed-0.1;
}
While we are playing with movement, it’s also a good time to add some code to stop the tank from constantly flying of the screen as you drive around. To do this, in its simplest form, we just need to add some code that will check whether or not the x & y coordinates the tank is moving too are within the current scene (on screen) and if not, stop that movement from being applied.
To do this add something along the lines of this just above where the new tank position and angle are applied to the sprite (above the comment “//Tell the tank”).
//Ensure new X position is inside playable area
if(nx < 0 || (nx + this.mytank.w) > this.layer.w){
nx = this.mytank.x;
}
//Ensure new Y position is inside playable area
if(ny < 0 || (ny + this.mytank.h) > this.layer.h){
ny = this.mytank.y;
}
With a little luck you should find that you are no longer able to drive your tank outside of the game area. While this gets the job done, it (for me at least) feels a little clunky. My solution to this was simply to swap the outright stop effect for a simple bounce effect instead. The new code looks as follows:
//Ensure new X position is inside playable area
if(nx < 0 || (nx + this.mytank.w) > this.layer.w){
speed = -(speed*0.4);
nx = this.mytank.x;
}
//Ensure new Y position is inside playable area
if(ny < 0 || (ny + this.mytank.h) > this.layer.h){
speed = -(speed*0.4);
ny = this.mytank.y;
}
It works in a very similar way to the simple stop, except rather than simply stopping the ship it also reverses the ship’s momentum at the same time.
Now that it is possible for the tank to have negative momentum, we need to ensure we still apply friction as the current system is only doing this for positive speed. To achieve this, add a check for “is speed is less than zero” and if so add to our speed until it reaches zero again.
if(this.inputs.keyboard.up){
if(speed>0) speed = speed-0.1;
if(speed<0) speed = speed+0.1;
}
The conversion of our existing “space game” into a “tank” game is now nearly complete. To finish off the conversion, we just need to give our tank its turret.
Adding the turret in a basic way should be fairly simple, so for now, let’s just create a new sprite object to be the turret (ensuring we have the correct offsets so that the turret part of our sprite image is all that is visible).
turret = this.myturret = this.scene.Sprite("tank.png",{
"layer": this.layer, //Layer tanks will be displayed in.
"x": 100, //X position of sprite
"y": 100, //Y position of sprite
"w": 36, //width of sprite.
"h": 59, //height of sprite.
"xoffset":75,
"yoffset":0,
});
Once that is done, we then need to add some code to keep our turret’s position in sync with the main tank. Under the “mytank.update();”, add the following code.
this.myturret.position(nx,ny);
this.myturret.setAngle(ang);
this.myturret.update();
With a little luck you should now have something along the lines of this:
The next step is to give our tank the ability to aim it’s turret, so when bad guys finally do get added, we can shoot them.
As with the keyboard inputs, getting the mouse position in Sprite.JS is fairly simple and is somthing we can just ask spirte.js’s input object to give us. From this and the tank’s own X,Y coordinates, figuring out which way our turret needs to be angled to fire where we point shouldn’t actually be all that difficult (thanks to a handy little math function called atan2).
//Get center point for the tank
tank_cent_y = this.mytank.y+(this.mytank.h/2);
tank_cent_x = this.mytank.x+(this.mytank.w/2);
//Work out angle that the cursor is from tank.
cursorAngle = Math.atan2(
(this.inputs.mouse.position.y - tank_cent_y),
(this.inputs.mouse.position.x - tank_cent_x)
)+1.571;//Add roughly 90 degrees.
Now we have the angle, we can pass it to the “myturrent.SetAngle” method, so that it’s aim will now follow the cursor (instead of just picking up the its angle from the tank hull as it is currently).
this.myturret.setAngle(cursorAngle);
Now we can aim our tank, it makes sense to give it the ability to fire. As with everything else so far, the first thing is to create the missile sprite, for laziness I just went with another quick “paint.exe” job:
Getting it into the game though, will be a little different to how the tank body/turret were added, as unlike them, chances are we are going to want lots of missiles, not just the one. Thus rather than storing each missile individually, we’ll need to keep track of them in an array.
First, define a new array to hold all our missiles at the top of the engine object.
this.missiles = [];
Then create a new function we can call every time the tank fires, in order to create us a new missile. For simplicity’s sake I just called mine “fireMissile”.
this.fireMissile = function(){
//Code here
}
Within this method, we then want to add the code to generate a new sprite and to store it in to our missiles array.
missile = this.scene.Sprite("missile.png",{
"layer": this.layer,
"w":4,
"h":11,
"angle": 0,
"x": 0,
"y": 0
});
this.missiles.push(missile);
The complex bit here is now going to be working out where the missile needs to be placed, in order to fire from our tanks turret. Getting the angle is the simplest task as we can just grab it from our turret.
var a = this.myturret.angle;
While the x and y coordinates are a little more complex, as although we could just grab the tanks x and y and use that, a missile suddenly appearing in the middle of the tank and flying out doesn’t really make much scene. Instead, we need to figure out the coordinates of the point thirty or so pixels in front of our tank in the direction that the turret is facing.
To start, we are still going to need to get the centre point.
x = this.mytank.x+(this.mytank.w/2);
y = this.mytank.y+(this.mytank.h/2);
Given that, we can then use the same bit of math we use to calculate tank momentum, in order to figure out where our missile is going to be thirty pixels away from the centre of the tank (travelling in the correct direction). I ended up tweaking this to twenty eight pixels as it seemed to fit a little better.
My resulting “fireMissile” function ended up looking pretty much like this:
this.fireMissile = function(){
var a = this.myturret.angle;
var x = (this.mytank.x+(this.mytank.w/2))+ (28 * Math.sin(a));
var y = (this.mytank.y+(this.mytank.h/2))+ (28 * Math.cos(a))*-1;
missile = this.scene.Sprite("missile.png",{
"layer": this.layer,
"w":4,
"h":11,
"angle": a,
"x": x,
"y": y
});
this.missiles.push(missile);
}
In order to make clicking actually call the function, you then need to add the following line under the rest of the movement/input code in the run function:
if(this.inputs.mouse.click) this.fireMissile();
At this point you will probably notice that none of the missile sprites added to the game are actually showing up yet, this is because we haven’t yet added a mechanism to update (and therfore draw) the new sprites.
To do this the easiest option is to again create an additioal function, then ensure its called ever time the game loop runs, by adding code such as the following to the bottom of the run function.
this.updateMissiles();
Next you need to create the updateMissiles function in order to give the aforementioned code something to call. Effectively the purpose of the updateMissiles function will be to loop through all missiles contained in the “missiles” array, and apply any updates needed (movement, redrawing etc).
To implement movement, we can use the same technique we used to calculate our missiles initial location (and to move the tank itself around) in order to calculate the missiles next position. This can then be applied to the sprite before “update()” is called to redraw the sprite in the new location.
this.updateMissiles = function(){
this.missiles.forEach(function(m){
var x = m.x+ (6 * Math.sin(m.angle));
var y = m.y+ (6 * Math.cos(m.angle))*-1
m.position(x,y);
m.update();
});
}
Although this works fine, it’s probably a good idea to add a little bit of functionality to clean up sprites we no longer need, as once a missile leaves the game area there’s not a lot of point keeping it updated, or indeed, in the game at all.
To do this we will need to be able to remove sprites from the “this.missiles” array in engine, meaning we need to ensure the engine is avaible outside the scope of the main game object. (As the .forEach() method will put us in its own scope). To do this simply define a variable called engine above of the engine function.
In order to calculate whether or not we now want to remove a missile, we will need to get it’s x and y position and check they are both above zero and not beyond the width or height of our game area. If it is, we will then want to call “remove()” on the sprite itself and then remove it from our missiles array.
if(!(
(m.x>0 && m.x < m.layer.w) &&
(m.y>0 && m.y < m.layer.h)
)){
m.remove();
engine.missiles.splice(engine.missiles.indexOf(m),1);
}
Assuming everything has gone to plan, you should now have a game that looks like this:
So we have a tank, we can drive around and can shoot missiles. I guess the next thing on the agenda is to create something to fight. To start I quickly created a new sprite to use as our “bad guy aliens”.
Then i added a additional array to our engine object so we have somewhere to keep track of them (essentially they are going to work in much the same way that missiles do now).
Under our “this.missiles” array, add a:
this.monsters = [];
Then create the outline of your new spawnSlug (which is my monster of choice) and updateMonster methods.
this.spawnSlug = function(x,y){
//stuff here
}
this.updateMonsters = function(){
//stuff here
}
Creating a new slug is going to be almost identical to how missiles are created. We just setup our monster sprite and add the result to our” monsters” array.
The angle itself doesn’t really matter so I’ve just set it as one, and the x and y coordinates to spawn our new slug are passed in as parameters.
this.spawnSlug = function(x,y){
slug = this.scene.Sprite("slug.png",{
"layer": this.layer,
"w":16,
"h":39,
"angle": 1,
"x": x,
"y": y
});
this.monsters.push(slug);
}
Since the slug sprite has a few steps of animation, it’s also a good time to make use of Sprite.js’s Cycle method, which essentially allows us to easily animate our sprite using the sprite panel we created. This is done by providing an array of steps, each consisting of the x and y offset we should apply to our Sprite panel and the duration each panel should display for. Once setup the Cycle can then be attached to the slug sprite.
In order to keep track of the new Cycle object, I’ve chosen to store it within the Sprite object itself in an attribute called animation.
//Create slug sprite
this.spawnSlug = function(x,y){
slug = this.scene.Sprite("slug.png",{
"layer": this.layer,
"w":16,
"h":39,
"angle": 1,
"x": x,
"y": y
});
//setup animation.
slug.animation = scene.Cycle([
[0,0,5],
[16,0,5],
[32,0,5],
[48,0,5],
[32,0,5],
[16,0,5]
]).addSprite(slug);
this.monsters.push(slug);
}
Now we can create slug sprites, the next step is to do something with them once they are in the game. As with the missiles this is done by iterating through the list of monsters and applying certain actions to each, before finally updating the sprite. It is useful to note that if you call update on a cycle (as stored in the m.animation) you no longer need to call update on the Sprite itself in order to apply any movement/rotation.
this.updateMonsters = function(){
//Loop through each monster sprite
this.monsters.forEach(function(m){
m.animation.next().update();
});
}
Now we can start adding some behaviours to our monster, for example making them slowly advance towards the player. Using “atan2” again, we can figure out which direction our sprite needs to travel in in order to find our tank (in the same way that we did when working out which direction to point our turret.)
var a = Math.atan2(
(engine.mytank.y-m.y),
(engine.mytank.x-m.x)
)+1.571;//90 degrees
Using this angle we can then point our sprite towards the tank and, then using the movement code now powering both the tank itself and it’s missiles, give our monsters the ability to move. The full code may look something like:
this.updateMonsters = function(){
//Loop through each monster sprite
this.monsters.forEach(function(m){
var a = Math.atan2(
(engine.mytank.y-m.y),
(engine.mytank.x-m.x)
)+1.571;
//Work out new location
var x = m.x+ (1 * Math.sin(a));
var y = m.y+ (1 * Math.cos(a))*-1;
//Apply changes
m.position(x,y);
m.setAngle(a)
m.animation.next().update();
});
}
In order to test that everything is now working as we expect, we just need to add a little code to our game to spawn the new slug monsters at certain intervals.
if((engine.ticker.currentTick % 200)==0){
this.spawnSlug(400,-50);
}
The above code will simply add a new slug monster, every 200 ticks of the game loop. Having monsters that want to eat our tank is all well and good, but not a lot of fun if we can’t shoot them. To start, lets add a hp variable to your tank sprite.
this.mytank.hp = 5;
And the same to your monsters, within the spawnSlug method. (I gave mine 2 hp)
slug.hp = 2;
Now comes the hard part, collision detection, fortunately for us, we don’t need to worry to much about this. Sprite.js helpfully comes packaged with its own collision detection system, stored in “lib/collison.js” . Adding this to our game, suddenly adds a host of useful “collidesWith” style methods for detecting collisions (no Math required).
<script type="text/javascript" src='../vendor/lib/collision.js'></script>
To make use of this, navigate back in to your “updateMonsters” method and under the code to apply sprite changes, add:
//Detect collisions
col = m.collidesWithArray(engine.missiles);
if(col !== false){ //false means there were no collisions
//Take 1 hp away from this monster.
m.hp--;
//If monster has less than 1 hp (0), remove it from the game.
if(m.hp < 1){
//Remove sprite
m.remove();
//remove fictional sprite from our array
engine.monsters.splice(engine.monsters.indexOf(m),1);
}
//remove missile
col.remove();
//remove missile from our array
engine.missiles.splice(engine.missiles.indexOf(col),1);
}
Essentally the above code checks to see whether the current monster has collided with any of the missiles stored within the missiles array. If no collisions were detected then the “m.collidesWithArray()” will return false and nothing will happen.
Alternately, if there was a collision between the current monster and a missile, 1 hp is removed from the monster and a check is done to ensure the monsters isn’t dead (if its hp is below 1 (as in 0)), if the monster turns out to be dead, it is then removed from the game. Once this is complete, the missile that hit the monster is then also removed from the game, regardless of whether the monster was killed.
If you play the game now, you should find you are able to shoot the slug monsters as they come towards you. Fun as this is, being invulnerable takes away from the satisfaction, so the next step will be to add a little danger.
Under the above collision code, add:
//Has a monster got to your tank?
col_me = m.collidesWith(engine.mytank);
if(col_me !== false){
//If so, remove the monster, and take 1hp away from your tank
m.remove();
engine.monsters.splice(engine.monsters.indexOf(m),1);
engine.mytank.hp--;
//Check to see if the tank is dead?
if(engine.mytank.hp < 1){
engine.ticker.pause();
alert("game over");
}
}
This code does the same thing as the previous collision code, except it is checking for collisions between the current monster and your tank, taking away your hp and removing the monster whenever one gets to you. If your tank runs out of hp, “game over” is then triggered by pausing the game loop and displaying a “game over” warning.
Because spawning is currently quite predictable, I think it’s worth adding a slightly better spawning mechanism, just to make the direction the monsters come from a little less predictable, and to additionally add a slowly increasing level of difficulty to the game.
Create a new function called spawnMonster, with code that looks something like;
this.spawnMonster = function(){
//50% chance of spawning top/bottom or left/right
if(Math.round(Math.random())==1){
//If left or right, randomly choose a height to attach
//our new monster at
h = Math.floor(Math.random()*this.layer.h);
if(Math.round(Math.random())==1){
//attach right
this.spawnSlug(this.layer.w+50,h);
}else{
//attach left
this.spawnSlug(-50,h);
}
}else{
//if top/bottom, figure out what width to add our sprite
//at
w = Math.floor(Math.random()*this.layer.w);
if(Math.round(Math.random())==1){
//at bottom
this.spawnSlug(w,this.layer.h+50);
}else{
//at top
this.spawnSlug(w,-50);
}
}
}
The above code essentially flips a coin on whether the monster should spawn at the top/bottom or left/right of the game area. Then depending on which is picked, pick a random position on either the x or y axis (just off screen) to add a sprite. The side the sprite will spawn on is then selected by another coin flip (so to speak).
Once this is done, go to the “run” function and just above where the speed variable is being defined, define a second one and call it spawnrate. Set it to 200. Now within the run method itself, replace the previous monster spawning code with:
//Spawn monster every time tick is a multiple of spawnrate
if((this.ticker.currentTick % spawnrate)==0){
//If spawn rate is higher than 20, decrese it by 2.
if(spawnrate>20) spawnrate=spawnrate-2;
//Spawn a monster somewhere
this.spawnMonster();
}
The new code will call the this.spawnMonster to create a new slug in a random location, every time the ticker equals a multiple of “spawnrate”. Additionally, each spawn, the time before the next spawn decreases slightly, meaning the monsters will begin to spawn at a faster and faster rate (until the maximum of one every 20 ticks is reached).
To finish off, make sure to add all the new sprite images to your scene.loadImages back in your bootstrap function, just to avoid the game starting before all sprites have been loaded in to memory.
scene.loadImages([
'tank.png',
'slug.png',
'missile.png',
], function() {
Hopefully, if everything went to plan your game should now look and play something like this:
Now the game is more a less playable and pretty much all our original goals have been completed. I just wanted to add a few final tweaks, that should hopefully make the game that little bit more enjoyable to play. 90% of this change just being the addition of a Score and the visibility of your hp.
To add the HP & score box, I added the following html in to the page (under the game_map div).
<div id='score_board'>
<div id='score'>0</div>
<div >Health: <span id='hp'>5</span>/5</div>
</div>
Combined with the following CSS
#score_board {position:absolute;width:100px;right:0;top:0;color:#fff;z-index:9999;}
#score_board div {text-align:center;}
#score{font-size:5em;font-weight:bold;}
This just adds a div on the top right, with a big number to indicate the score and a smaller line of text to tell you your hp. To make both the HP & score counter work, add a score attribute to the main game object (below “this.inputs”).
this.score = 0;
Then in the updateMonsters method, just below where we removed destroyed monsters from the game, add the following code.
engine.score++;
document.getElementById('score').innerHTML = engine.score;
Then, a little further down where hp is being deducted from your own tank (just under “engine.mytank.hp–;”) add
document.getElementById('hp').innerHTML = engine.mytank.hp;
to keep your HP in sync with the UI.
And last but not least, to make the loosing screen a little less annoying, replace the “game over” alert with:
document.body.innerHTML ='<div class="end"><h1>Game Over!</h1><div>Your Score: '+engine.score+'</div></div>';
And add the following bit of additional css to your styles:
.end {
background-color:#fff;
color:#000;
width:400px;
text-align:center;
margin-top:50px;
padding:10px;
}
With that complete, for the purposes of this tutorial. Your done! Congratulation, you should now have a simple tank game that looks more or less like this:
Keep in mind, at this point the game is still really basic, so I encourage you to have a go at developing it further. Just as a few ideas of things you may want to consider adding:
- Sounds for when firing the cannon, driving and blowing up monsters.
- More monsters types, and maybe some smarter monster behaviours.
- High scores?
- Some terrain, to drive around.
- Mobile support (Sprite.js) adds some basic stuff for mobile, but the game would benefit a lot from a properly mobile oriented control system.
All code seen in this tutorial and comprising the demos is released as Open Source under the MIT Licence. The source for all demos can be found here.
Thanks for reading,