HTML5 Canvas Game: The Enemy Ships
In the third installment of the Galaxian Style HTML5 game series, we’ll be learning about how to add enemies to our game using the same techniques we learned about in the previous article.
Difficulty: Medium
Languages: HTML5, JavaScript
Code: https://github.com/straker/galaxian-canvas-game/tree/master/part3
Part 3
We’ll begin by seeing the final result of this tutorial. Make sure you click inside the game if the controls aren’t working.
Controls: Move – Arrow keys (←↑↓→)
Shoot – Spacebar
As you can see, we haven’t implemented collision detection yet, we’ll get to that in the next tutorial.
The imageRepository
There is nothing to change in the HTML (just make sure you’re loading the correct javascript file), so we’ll move right into adding the enemy ships by adding their images to the imageRepository
.
var imageRepository = new function() {
// Define images
this.background = new Image();
this.spaceship = new Image();
this.bullet = new Image();
this.enemy = new Image();
this.enemyBullet = new Image();
// Ensure all images have loaded before starting the game
var numImages = 5;
var numLoaded = 0;
function imageLoaded() {
numLoaded++;
if (numLoaded === numImages) {
window.init();
}
}
this.background.onload = function() {
imageLoaded();
}
this.spaceship.onload = function() {
imageLoaded();
}
this.bullet.onload = function() {
imageLoaded();
}
this.enemy.onload = function() {
imageLoaded();
}
this.enemyBullet.onload = function() {
imageLoaded();
}
// Set images src
this.background.src = "bg.png";
this.spaceship.src = "ship.png";
this.bullet.src = "bullet.png";
this.enemy.src = "enemy.png";
this.enemyBullet.src = "bullet_enemy.png";
}
Nothing new here. We are just loading the enemy ship and the enemy bullets to our game and using the imageLoaded()
function to ensure they have all loaded before the game starts (make sure you change numImages
to 5
).
The Bullet and Pool objects
The next thing we need to do to add enemies to our game is update a the Bullet
and Pool
objects. The reason for this is twofold.
First, the enemies fire a different bullet than the player, and these bullets also have different behaviors than the player bullet. Second, the enemy ship will need it’s own bullet pool to handle all the bullets it will fire, so we need to update the Pool
object to handle enemy bullets. The object pool will also hold the enemy ships so we can continue to spawn new waves when the old wave is destroyed (to be implemented in a future tutorial, but it’s good to do it now in preparation).
function Bullet(object) {
this.alive = false; // Is true if the bullet is currently in use
var self = object;
/*
* Sets the bullet values
*/
this.spawn = function(x, y, speed) {
this.x = x;
this.y = y;
this.speed = speed;
this.alive = true;
};
/*
* Uses a "drity rectangle" to erase the bullet and moves it.
* Returns true if the bullet moved of the screen, indicating that
* the bullet is ready to be cleared by the pool, otherwise draws
* the bullet.
*/
this.draw = function() {
this.context.clearRect(this.x-1, this.y-1, this.width+1, this.height+1);
this.y -= this.speed;
if (self === "bullet" && this.y <= 0 - this.height) {
return true;
}
else if (self === "enemyBullet" && this.y >= this.canvasHeight) {
return true;
}
else {
if (self === "bullet") {
this.context.drawImage(imageRepository.bullet, this.x, this.y);
}
else if (self === "enemyBullet") {
this.context.drawImage(imageRepository.enemyBullet, this.x, this.y);
}
return false;
}
};
/*
* Resets the bullet values
*/
this.clear = function() {
this.x = 0;
this.y = 0;
this.speed = 0;
this.alive = false;
};
}
Bullet.prototype = new Drawable();
The Bullet
object first receives a string which indicates which type of bullet it will be: a player “bullet” or an “enemyBullet.” In the draw()
function, we determine if the bullet has gone off the screen based on which bullet it is (player bullets go off the screen at the top, enemyBullets go off the screen at the bottom). If the bullet hasn’t gone off the screen we draw the proper image to the canvas.
/*
* Populates the pool array with the given object
*/
this.init = function(object) {
if (object == "bullet") {
for (var i = 0; i < size; i++) {
// Initalize the object
var bullet = new Bullet("bullet");
bullet.init(0,0, imageRepository.bullet.width, imageRepository.bullet.height);
pool[i] = bullet;
}
}
else if (object == "enemy") {
for (var i = 0; i < size; i++) {
var enemy = new Enemy();
enemy.init(0,0, imageRepository.enemy.width, imageRepository.enemy.height);
pool[i] = enemy;
}
}
else if (object == "enemyBullet") {
for (var i = 0; i < size; i++) {
var bullet = new Bullet("enemyBullet");
bullet.init(0,0, imageRepository.enemyBullet.width, imageRepository.enemyBullet.height);
pool[i] = bullet;
}
}
};
We only have to change the init()
function of the Pool
object. The function receives a string which indicates what type of pool this should be: a “bullet” pool, an “enemy” pool, or an “enemyBullet” pool and then populates the array with the designated object.
Make sure you change the Ship
‘s bullet pool variable to reflect the change:
this.bulletPool.init("bullet");
The Enemy Ship
The enemies in our game will imitate the movement pattern of the enemies in Space Invaders, moving back and forth across the screen. The difference will be that they do not move down a row each time they hit the edge of the screen, but instead stay on their current row the entire time. We will also make them spawn off screen and then move down the screen to get to their rows.
/**
* Create the Enemy ship object.
*/
function Enemy() {
var percentFire = .01;
var chance = 0;
this.alive = false;
/*
* Sets the Enemy values
*/
this.spawn = function(x, y, speed) {
this.x = x;
this.y = y;
this.speed = speed;
this.speedX = 0;
this.speedY = speed;
this.alive = true;
this.leftEdge = this.x - 90;
this.rightEdge = this.x + 90;
this.bottomEdge = this.y + 140;
};
/*
* Move the enemy
*/
this.draw = function() {
this.context.clearRect(this.x-1, this.y, this.width+1, this.height);
this.x += this.speedX;
this.y += this.speedY;
if (this.x <= this.leftEdge) {
this.speedX = this.speed;
}
else if (this.x >= this.rightEdge + this.width) {
this.speedX = -this.speed;
}
else if (this.y >= this.bottomEdge) {
this.speed = 1.5;
this.speedY = 0;
this.y -= 5;
this.speedX = -this.speed;
}
this.context.drawImage(imageRepository.enemy, this.x, this.y);
// Enemy has a chance to shoot every movement
chance = Math.floor(Math.random()*101);
if (chance/100 < percentFire) {
this.fire();
}
};
/*
* Fires a bullet
*/
this.fire = function() {
game.enemyBulletPool.get(this.x+this.width/2, this.y+this.height, -2.5);
}
/*
* Resets the enemy values
*/
this.clear = function() {
this.x = 0;
this.y = 0;
this.speed = 0;
this.speedX = 0;
this.speedY = 0;
this.alive = false;
};
}
Enemy.prototype = new Drawable();
The enemies will have a small chance to fire a bullet each frame. Doing this helps the game to feel different each time and not a fixed pattern that the player could memorize. .01
may seem like a very small number, but when you consider that they move 60 times every second, the odds are that they will fire at least once every two or three seconds. And when we are planning to have 18 enemies on the screen, each one firing every two or three seconds is still a lot of bullets.
Because the enemies will be moving in a group and moving back and forth, each one needs to know how far to the left and right it can move before it changes direction. This change is relative to each individual ship, so we can’t just set a fixed boundary for all of the ship and have it work. Thus, the spawn()
function takes the start x
and y
position of the ship and then gives it three boundarys: a left edge, a right edge, and a bottom edge (since they will be moving down to start, we want them to end in their proper rows). So each ship will move back and forth, having a 180px range.
The draw()
function is straightforward, telling the ship to change direction when it hits either the left or right edge, and stop moving down and start moving left when it hits the bottom edge. Hitting the bottom edge will also slow the ship down a bit from it’s initial speed. We will use the Math.random()
function to generate a random number between 1 and 100 to see if the enemy will shoot this frame.
You may be wondering why the Enemy
object doesn’t have a bullet pool like the Ship
object does. The reason for this is again forward thinking. If each ship had it’s own bullet pool, then we would only animate those bullets so long as the ship was alive. If the ship died (due to being hit by a player bullet), then the enemy object pool would remove that enemy from the pool, clear it, then move it to the back of the pool. This would cause all the bullets the enemy had fired and were still on the screen to just stop moving and stay there. This is known as tight coupling.
The bullets moving rely heavily on the enemy ship being alive, so losing the enemy also affects the bullets. What we want instead is a loosely coupled system where if a ship dies the bullets will still function properly. To do this, we will have an enemyBullet
pool object that handles all the bullets for all the enemy ships and is controlled by the Game
object.
The Final Step
The last step is again to update the game and the animate functions to implement the enemy ships and bullets.
function Game() {
/*
* Gets canvas information and context and sets up all game
* objects.
* Returns true if the canvas is supported and false if it
* is not. This is to stop the animation script from constantly
* running on browsers that do not support the canvas.
*/
this.init = function() {
// Get the canvas elements
this.bgCanvas = document.getElementById('background');
this.shipCanvas = document.getElementById('ship');
this.mainCanvas = document.getElementById('main');
// Test to see if canvas is supported. Only need to
// check one canvas
if (this.bgCanvas.getContext) {
this.bgContext = this.bgCanvas.getContext('2d');
this.shipContext = this.shipCanvas.getContext('2d');
this.mainContext = this.mainCanvas.getContext('2d');
// Initialize objects to contain their context and canvas
// information
Background.prototype.context = this.bgContext;
Background.prototype.canvasWidth = this.bgCanvas.width;
Background.prototype.canvasHeight = this.bgCanvas.height;
Ship.prototype.context = this.shipContext;
Ship.prototype.canvasWidth = this.shipCanvas.width;
Ship.prototype.canvasHeight = this.shipCanvas.height;
Bullet.prototype.context = this.mainContext;
Bullet.prototype.canvasWidth = this.mainCanvas.width;
Bullet.prototype.canvasHeight = this.mainCanvas.height;
Enemy.prototype.context = this.mainContext;
Enemy.prototype.canvasWidth = this.mainCanvas.width;
Enemy.prototype.canvasHeight = this.mainCanvas.height;
// Initialize the background object
this.background = new Background();
this.background.init(0,0); // Set draw point to 0,0
// Initialize the ship object
this.ship = new Ship();
// Set the ship to start near the bottom middle of the canvas
var shipStartX = this.shipCanvas.width/2 - imageRepository.spaceship.width;
var shipStartY = this.shipCanvas.height/4*3 + imageRepository.spaceship.height*2;
this.ship.init(shipStartX, shipStartY, imageRepository.spaceship.width,
imageRepository.spaceship.height);
// Initialize the enemy pool object
this.enemyPool = new Pool(30);
this.enemyPool.init("enemy");
var height = imageRepository.enemy.height;
var width = imageRepository.enemy.width;
var x = 100;
var y = -height;
var spacer = y * 1.5;
for (var i = 1; i <= 18; i++) {
this.enemyPool.get(x,y,2);
x += width + 25;
if (i % 6 == 0) {
x = 100;
y += spacer
}
}
this.enemyBulletPool = new Pool(50);
this.enemyBulletPool.init("enemyBullet");
return true;
} else {
return false;
}
};
// Start the animation loop
this.start = function() {
this.ship.draw();
animate();
};
}
function animate() {
requestAnimFrame( animate );
game.background.draw();
game.ship.move();
game.ship.bulletPool.animate();
game.enemyPool.animate();
game.enemyBulletPool.animate();
}
Again, nothing too new here. The biggest change is implementing the enemy ships. We’ll have 18 enemy ships: 3 rows of 6 ships. Because we want them to start off screen, we’ll start the bottom row just above the top edge (y = -height
) and the leftmost enemy at 100px. The spacer
variable determines how far apart each row will be. Inside the loop, we create 18 enemies, each one being placed 25pxs apart from the previous one in the row. When we get six enemies on a row, we go up one row and restart the enemies spawn at 100px.
Lastly, we animate both the enemy pool and the enemy bullet pool in the animate()
function, and we’re done!
Conclusion
Adding enemies to our game was relatively simple by using the same techniques we used in the previous tutorial to add the player ship. We also did some forward thinking in this tutorial by implementing an enemy bullet pool outside the Enemy
object to have a loosely coupled system.
In the next tutorial, we’re going to implement collision detection and introduce a new data object to help us handle collision checks: the quadtree.