Aspera

Project Demo

Intro:

I didn’t actually know what I wanted to make, except that it would be a game. Originally I wanted to make a cross between a platformer and a shooter, but I couldn’t think of how to spawn the enemies in a way that would be fun and yet easy to make. After I made the sprite for the player I realised that it kinda looked like diep.io, so I switched and decided to make that instead. I actually have some experience making games in js but it wasn’t entirely on my own, it was like an online course where they guided me through the developing process. So I just borrowed some main concepts from there to make my game.

The aim of the game is to survive as long as possible. Early-game is pretty easy, only a handful of enemies spawn. Once the score goes above 100, the game starts to get more intense. To help the player, every 50 points, the health of the bullet will increase by 1, allowing it to bounce off walls and pierce through enemies. The game will also not spawn enemies near the player. However, there is recoil and the player also bounces off the walls. This also prevents abuse of the game’s not spawning enemies near the player.

Code environment:

I used mainly js for the game, and a little of HTML and CSS for the canvas. I chose to use js because it’s pretty easy to implement and test on different devices. Also it’s integrated with HTML and CSS so display-wise it isn’t that hard. To be honest, I never learnt any HTML or CSS but I barely used these two except to display the canvas, so it was pretty easy to just Google everything. For the IDE, I went with Repl.it because, well, I was kinda lazy to install an IDE on my computer so I just used a web-based one, and also it’s pretty easy to share the link to test on different devices. ***

Program Structure:

  • Player
    • Represented by a circle and a rectangle
    • Only object controlled by the user
    • Shoots bullets on mouse click and handles reloading
    • Direction of bullet is based on rotation of player
    • Health of bullet is also handled here
    • Contains draw() and move()
  • Bullet
    • Represented by a small grey circle
    • Bounces off walls
    • Stores health
    • Not much in here, most of it is handled either by World() or Player()
    • Contains draw() and move()
  • Enemy
    • Represented by circles, in red, yellow or purple, which give 1 point, 5 and 10 respectively
    • Contains the overlap function that checks for overlaps with bullets and the player
    • Randomises the colour and duration of colour change for each enemy
    • Contains draw(), move() and isOverlap()
  • Particle
    • Represented by a small square 2px wide
    • Randomises dx, dy and lifespan
    • Contains draw() and move()
  • World
    • Manages global variables and updates all objects as well as canvas
    • Renders each object on the “cleared” canvas
    • Handles spawning of enemies and particles and collisions between bullets, enemies and the player
    • In summary it controls what the user sees and is the main interface
    • Contains init(), update(), spawn(), draw(), move() and kill()
  • Detection of keydown and keyup, mousedown, and the running of the animation loop.

Code Runthrough

Canvas

First, we call the canvas and set canvas.width and canvas.height to window.innerWidth and window.innerHeight. This allows for full screen gameplay. Next, we set var w and h to .width and .height so it is more convenient for us to refer to it later on in the game, such as checking for enemies hitting the walls and bouncing off.

//Canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth - 20;
canvas.height = window.innerHeight - 20;
var w = ctx.canvas.width;
var h = ctx.canvas.height;

Mouse

We declare a new object, mouse, and set its x and y coordinates to 0. If the mouse moves on the canvas, we set the x-y coordinates in the array to the mouse’s x-y coordinates. We also check if the mouse is clicked or not.

//Mouse
var mouse = { x: 0, y: 0 };
ctx.canvas.onmousemove = function (evt) {
	mouse.x = evt.clientX;
	mouse.y = evt.clientY;
}

//Keeps track of whether the mouse is pressed down
var mouseDown = false;
ctx.canvas.onmousedown = function () {
	mouseDown = true;
}
ctx.canvas.onmouseup = function () {
	mouseDown = false;
}

Start page

If startGame is false, we make the background black and render the text as shown below.

//Start game
if (!startGame) {
	ctx.save();
	ctx.fillRect(0, 0, w, h);
	ctx.textAlign = "center";
	ctx.textBaseline = 'middle';
	ctx.fillStyle = 'white';
	ctx.font = "15pt sans-serif";
	ctx.fillText("Click to shoot", w / 2, h / 2 - 150);
	ctx.fillText("WASD to move", w / 2, h / 2 - 125);
	ctx.fillText("Avoid enemies", w / 2, h / 2 - 100);
	ctx.fillText("Press P to pause", w / 2, h / 2 - 75);
	ctx.fillText("and k to kill yourself", w / 2, h / 2 - 50);
	ctx.fillText("Red enemies give one point, ", w / 2, h / 2 - 25);
	ctx.fillText("yellow, five and purple, ten", w / 2, h / 2);
	ctx.font = "25pt sans-serif";
	ctx.fillText("Click and press B to begin", w / 2, h / 2 + 50);
	ctx.restore();
}

restart()

This function, when called, sets gameOver to false and resets the player’s health and score to 5 and 0. The player goes back to the center and loses its velocity. The bullet’s health is reset to 0.

//Restart game
gameOver = false;
world.player.explode = false;
world.player.health = 5;
world.player.score = 0;
world.player.x = w / 2;
world.player.y = h / 2;
world.player.vx = 0;
world.player.vy = 0;
bHealth = 0;

Animation loop

A function called updateAll() handles the entire game’s animation. First, we check if the game isn’t paused and if the game has started, or if the game has ended. If the first two or the last one is true, then we update the world, and set a timeout that calls updateAll() again after 20 milliseconds.

if (!pauseGame && startGame || gameOver) {
	world.update();
}
cmTID = setTimeout(updateAll, world.timeStep);

If gameOver is true, then we also draw the game over page, as shown below.

if (gameOver) {
	ctx.save();
	ctx.font = '48pt sans-serif';
	ctx.textAlign = 'center';
	ctx.fillStyle = 'red';
	ctx.textBaseline = 'middle';
	ctx.fillText('GAME OVER', w / 2, h / 2);
	ctx.font = '15pt sans-serif';
	ctx.fillText('Highscore: ' + localStorage.getItem("highscore"), w / 2, h / 2 + 45);
	ctx.font = "12pt sans-serif";
	if (!k) {
		ctx.fillText("Died February 25, 2020. Google that.", w / 2, h / 2 + 70);
	} else {
		ctx.fillText("RIP Kazuhisa Hashimoto, 1958-2020.", w / 2, h / 2 + 70);
	}
	ctx.font = '24pt sans-serif';
	ctx.fillText('Press R to restart', w / 2, h / 2 + 100);
	ctx.restore();
}

World

Initialising the world:

In the initialisation of the world, a new player is declared and assigned to the Player() object. Other global variables such as arrays for bullets, enemies and particles (basically anything that exists simultaneously in one frame) are also initialised in this phase. These variables represent every object present in a frame at any point of time. We don’t need to declare new variables for these as they are already called somewhere else in the code (e.g. bullets in player.move, enemies in world.spawn etc). They store the object instances which we can make use of for other functions discussed later.

this.init = function () {
	this.player = new  Player();
	this.bullets = [];
	this.enemies = [];
	this.particles = [];
}

Updating the world:

This method runs through each of the four main methods in World, spawn(), draw(), move() and kill(). spawn() handles the spawning of enemies, draw() handles the clearing and drawing of canvas, move() handles the change in x-y coordinates for all objects and kill() handles bullet-enemy collisions, enemy-player collisions and player’s death, on which your score is saved as the new high score (if it is higher than the previous high score). The restart page is also included in kill() because it’s much more convenient for us.

this.spawn();
this.draw();
this.move();
this.kill();

spawn():

This method spawns enemies with randomized x-y coordinates and dx and dy values (can be negative), which x and y increment by. The spawning function is called depending on the number of enemies on-screen and the health of the player.

this.spawn = function () {
//Chance of enemy spawning
var chance = this.spawnChance;

//Difficulty of game
var difficulty = world.player.score / 700;
chance /= this.enemies.length;

The spawn location of the enemy is decided by four boolean values (0, 0), (0, 1), (1, 0) and (1, 1), with each value representing one side. Next, the dx and dy of the new enemy is randomised, which represents the direction the enemy moves in. This value is stored in the enemy as long as it exists. Lastly, if the enemy is not too close to the player, spawning will be successful and we will render a new enemy along the aforementioned coordinates with the corresponding dx and dy, which will enable the enemy to move around.

//Determines spawn location
var c = Math.random();
var exey = Math.random();
if (c < 0.5) {
	if (Math.random() * 3 < chance + difficulty && this.player.health != 0 || this.enemies.length <= 3 && this.player.health != 0) {
		var x = Math.round(exey) * w;
		var y = Math.round(Math.random() * h)
		var dx = (difficulty + 10) * (Math.random() - 0.5) * (w / this.frames / 7);
		var dy = (difficulty + 10) * (Math.random() - 0.5) * (w / this.frames / 7);
		if (Math.abs(y - world.player.y) > 20) {
			this.enemies.push(new  Enemy(x, y, dx, dy));
		}
	}
} else  if (c >= 0.5) {
	if (Math.random() < chance + difficulty && this.player.health != 0 || this.enemies.length < difficulty * 100 && this.player.health != 0) {
	var x = Math.floor(Math.random() * w);
	var y = Math.round(exey) * h;
	var dx = (difficulty + 10) * (Math.random() - 0.5) * (w / this.frames / 7);
	var dy = (difficulty + 10) * (Math.random() - 0.5) * (w / this.frames / 7);
		if (Math.abs(x - world.player.x) > 20) {
			this.enemies.push(new  Enemy(x, y, dx, dy));
		}
	}
}

draw():

This method resets the canvas by rendering a black screen over the previous frame. Next, it re-renders each of the objects at its new x-y coordinates. The order of rendering here is extremely important as the last one to be rendered will overlap the rest. Thus, we render the player last, as it should cover the bullets and particles. The enemies and bullets should also cover the particles, so we render them first. Bullets are smaller and faster than enemies so they are harder to notice, so we render them before the enemies. Lastly, the user’s focus should mainly be on the player, so it has to overlap the enemy.

//Clear the canvas
ctx.fillRect(0, 0, w, h);

//Draw particles
var pl = this.particles.length;
for (let i = 0; i < pl; i++) {
	var p = this.particles[i];
	p.draw();
}

//Draw bullets
var bl = this.bullets.length;
for (let i = 0; i < bl; i++) {
	var b = this.bullets[i];
	b.draw();
}

//Draw enemies
var el = this.enemies.length;
for (let i = 0; i < el; i++) {
	var e = this.enemies[i];
	e.draw();
}

//Draw player
this.player.draw();

move():

This method runs through each object’s move() method and updates their x-y coordinates. We don’t redraw the objects here, we redraw them next frame in draw().

//Move player
this.player.move();
var bl = this.bullets.length;

//Move bullet
for (let i = 0; i < bl; i++) {
	var b = this.bullets[i];
	b.move();
}
var el = this.enemies.length;

//Move enemy
for (let i = 0; i < el; i++) {
	var e = this.enemies[i];
	e.move();
}

//Move particles
var pl = this.particles.length;
for (let i = 0; i < pl; i++) {
	var p = this.particles[i];
	p.move();
}

kill():

This section of the code checks for collisions between bullets and enemies. It runs through each instance of the enemy and bullet, and calls Enemy.isOverlap(Bullet), and if the function returns true then it kills both the enemy and the bullet. It then does the same for the player.

l = this.enemies.length;
//Checks for bullet-enemy collision
for (let i = 0; i < l; i++) {
	var e = this.enemies[i];
	var bl = this.bullets.length;
	for (let j = 0; j < bl; j++) {
		var b = this.bullets[j];
		if (e.isOverlap(b)) {
			b.health -= 1;
			if (b.health == 0) {
			b.dead = true;
			}
		e.health -= b.dmg;
		}
		if (e.health <= 0) {
			e.dead = true;
		}
	}
	//Checks for player-enemy collision
	if (e.isOverlap(this.player)) {
		//Bounce off the enemy
		this.player.vx = e.dx;
		this.player.vy = e.dy;
		//Kill enemy when it hits player
		e.dead = true;
		//Decrease health by one
		this.player.health -= 1;
		//Release some particles
		var pn = Math.random() * 3 + 1;
		var x = this.player.x;
		var y = this.player.y;
		for (let i = 0; i < pn; i++) {
			this.particles.push(new  Particle(x, y, "green"));
		}
	}
}

If the player’s health is less than 0, we make it equal to 0. Usually more than one enemy hits the player on death, so this can solve that problem. If the player is dead, it should explode.

//If health is less than 0, set it to 0, useful when more than one enemy hits player on death
if (this.player.health < 0) {
	this.player.health = 0;
}
//Make player explode
if (this.player.explode && this.player.opacity != 0) {
	var pn = Math.random() * 40 + 10;
	var x = this.player.x;
	var y = this.player.y;
	for (let i = 0; i < pn; i++) {
		this.particles.push(new  Particle(x, y, "green"));
	}
	this.player.explode = false;
	this.player.opacity = 0;
}
if (killPlayer) {
	this.player.health = 0;
}

If the player has run out of health, we kill all enemies and bullets, and run the above code to make the player explode. Next, we save the score locally if the score is higher than the current high score. Lastly, we check for restart, and set killPlayer to false.

//Kill player and end game
if (this.player.health == 0) {
	//Make all objects explode
	var l = this.enemies.length;
	for (let i = 0; i < l; i++) {
		e.dead = true;
	}
	l = this.bullets.length;
	for (let i = 0; i < l; i++) {
		var b = this.bullets[i];
		b.dead = true;
	}
	//End the game
	this.player.explode = true;
	gameOver = true;
	//Save highscore
	if (this.player.score > window.localStorage.getItem('highscore') && !k) {
		window.localStorage.setItem('highscore', this.player.score);
	}
	if (this.player.restart && this.enemies.length == 0) {
	this.player.restart = false;
	if (!k) {
		restart(10, w / 100, w / world.frames, 1, 0.3 * this.frames, false, 1, true);
	} else {
		konami();
	}
}
killPlayer = false;

This runs through each instance of the enemy and checks for dead enemies. If there is a dead enemy, we first check the colour of the enemy and add to the score accordingly. Then we explode the enemy and splice it from the array, absolutely obliterating it.

//Kill enemy
var l = this.enemies.length;
for (let i = l - 1; i >= 0; i--) {
	var e = this.enemies[i];
	if (e.dead) {
		if (e.colour == "red") {
			world.player.score += 1;
		} else  if (e.colour == "rgba(131, 123, 6, 1)") {
			world.player.score += 5;
		} else  if (e.colour == "rgba(67, 0, 96, 1)") {
			world.player.score += 10;
		}
		//Make enemy explode
		var pn = Math.random() * 40 + 10;
		var x = e.x;
		var y = e.y;
		for (let j = 0; j < pn; j++) {
			this.particles.push(new  Particle(x, y, e.colour));
		}
		//Remove enemy from array
		this.enemies.splice(i, 1);
	}
}

This runs through each instance of the bullet, checking if it has died. If it has died, we splice it from the array and make it explode, completely destroying it.

l = this.bullets.length;
for (let i = l - 1; i >= 0; i--) {
	var b = this.bullets[i];
	//Kill bullet
	if (b.dead) {
		//Make bullet explode
		var x = b.x;
		var y = b.y;
		for (let j = 0; j < 4; j++) {
			this.particles.push(new  Particle(x, y, "grey"));
		}
		//Remove bullet from array
		this.bullets.splice(i, 1);
	}
}

This runs through each instance of the particle, checking if it has died. If it has died, we splice it from the array, totally devastating it.

//Kill particle
l = this.particles.length;
for (let i = l - 1; i >= 0; i--) {
	var p = this.particles[i];
	if (p.dead) {
		//Remove particle from array
		this.particles.splice(i, 1);
	}
}

Player

draw():

First we check if the player is dead. If it is dead, we need to destroy the player, so we simply set the opacity to 0, rendering the player invisible.

//Opacity
if (this.health != 0) {
	this.opacity = 1;
} else {
	this.opacity = 0;
}

If it isn’t dead, we render a circle as well as a rectangle that sticks out of the front of the player and rotate the whole object. For aesthetic purposes, we fill both green.

//Player is just a circle with a rectangle sticking out
ctx.save();
ctx.translate(this.x, this.y);

//Rotates the player
ctx.rotate(-this.angle * Math.PI / 180);

//Renders the outline invisible
ctx.strokeStyle = "rgba(0, 0, 0, 0)";

//Draws the circle
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "rgba(50, 150, 50, " + this.opacity;
ctx.fill();

//Draws the rectangle
ctx.fillRect(this.size * -0.5, this.size * -0.5, this.size * 2.5, this.size);
ctx.restore();
Lastly, we draw the score and health at the top right corner of the canvas.

//Displays score and health
ctx.save();
ctx.fillStyle = "white";
ctx.font = '10pt Monaco';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("Score = " + this.score, w * (9 / 10), h / 16);
ctx.fillText("Health = " + Math.ceil(this.health), w * (9 / 10), h / 16 + 20)
ctx.restore();

move():

First, we rotate the player. We find the direction of the player by performing a bunch of mathematical equations which I found online and adapted for my player using trial and error. I actually don’t know why this works because firstly, radians to degrees is x / 180 * pi, and secondly, we already do that in this.draw(). I tried removing either one and swapping them but none of that worked, so :p.

//Rotation
var diffX = mouse.x - this.x;
var diffY = mouse.y - this.y;
var a = -Math.atan2(diffY, diffX);
this.angle = a / Math.PI * 180;

Second, we add this.vx to this.x and this.vy to this.y, and multiply this.vx and this.vy by friction. Then, we check which direction to move in and move accordingly.

//Movement
this.x += this.vx;
this.y += this.vy;
this.vx *= this.friction;
this.vy *= this.friction;
//Moving right
if (this.right) {
	this.vx += this.moveSpeed;
	this.vx = Math.min(this.maxMove, this.vx);
}
//Moving left
if (this.left) {
	this.vx -= this.moveSpeed;
	this.vx = Math.max(-this.maxMove, this.vx);
}
//Moving up
if (this.up) {
	this.vy -= this.moveSpeed;
	this.vy = Math.max(-this.maxMove, this.vy);
}
//Moving down
if (this.down) {
	this.vy += this.moveSpeed;
	this.vy = Math.min(this.maxMove, this.vy);
}

We also check if the player has hit a wall, and stop movement and bounce off accordingly.

//Don't go further than the walls
if (this.x > w - this.size) {
	this.x = w - this.size;
	this.vx -= pBounce;
}
if (this.x < this.size) {
	this.x = this.size;
	this.vx += pBounce;
}
if (this.y < this.size) {
	this.y = this.size;
	this.vy += pBounce;
}
if (this.y > h - this.size) {
	this.y = h - this.size;
	this.vy -= pBounce;
}

Lastly, we enable shooting if this.reload is 0. We obtain the direction of the bullet by running some calculations. Next, we set the health of the bullet. This value will allow the bullet to hit more enemies and bounce off walls.

//Shooting
if (mouseDown && !this.reload && this.health != 0) {
	//Direction of bullet
	a = this.angle / 180 * Math.PI;
	var x = this.x;
	var y = this.y;
	var vx = Math.cos(a) * this.bulletSpeed;
	var vy = -Math.sin(a) * this.bulletSpeed;
	bHealth = Math.floor(this.score / 50) + oBHealth;
	if (Math.ceil(this.score / 50) >= 5) {
		bHealth = 5 + oBHealth;
	}
	//Shoot the bullet
	var bullet = new  Bullet(x, y, vx, vy);
	world.bullets.push(bullet);
	//Recoil
	if (pRecoil) {
		this.vx += -vx / (this.size / 2);
		this.vy += -vy / (this.size / 2);
	}
	//Start reload timer
	this.reload = this.reloadDelay;
}
if (this.reload) {
	this.reload--;
}

Bullet

this.draw:

This method draws a grey circle.

ctx.save();
ctx.translate(this.x, this.y);
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "grey";
ctx.fill();
ctx.restore();

this.move:

This method updates the x-y coordinates of the bullet. Then, it checks if the bullet has hit the wall, and makes it bounce off, while subtracting one health. If its health is 0 or less, we kill it.

this.x += this.vx;
this.y += this.vy;
if (this.x - 1 <= this.size) {
	this.vx = -this.vx;
	this.health -= 1;
}
if (this.x + this.size + 1 >= w) {
	this.vx = -this.vx;
	this.health -= 1;
}
if (this.y - 1 <= this.size) {
	this.vy = -this.vy;
	this.health -= 1;
}
if (this.y + this.size + 1 >= h) {
	this.vy = -this.vy;
	this.health -= 1;
}
if (this.health <= 0) {
	this.dead = true;
}

Enemy

this.draw:

When the enemy spawns, we draw a red circle.

//Enemy starts as a red circle
ctx.save();
ctx.translate(this.x, this.y);
ctx.strokeStyle = colour;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = colour;
ctx.fill();
ctx.restore();

this.move:

We increment the position of the enemy by the velocity.

this.x += this.dx;
this.y += this.dy;

Next, we change the colour of the enemy. We make a counter that starts with a random number and counts down. When it reaches 0, we change the colour of the enemy to red, yellow or purple.

if (colourTime <= 0) {
	colour = Math.random();
	if (colour <= 0.5) {
		colourTime = 1 * world.frames;
		colourTime *= Math.random() * 10 + 1;
		colour = "red";
		this.colour = colour;
	} else  if (colour <= 0.9) {
		colourTime = 1 * world.frames;
		colourTime *= Math.random() * 10 + 1;
		colour = "rgba(131, 123, 6, 1)";
		this.colour = colour;
	} else {
		colourTime = 1 * world.frames;
		colourTime *= Math.random() * 10 + 1;
		colour = "rgba(67, 0, 96, 1)";
		this.colour = colour;
	}
} else {
	colourTime--;
}

Here, we check if the enemy has hit a wall. If it has, we reverse the direction of the velocity that made the enemy hit the wall, instead of reversing both. This way, the enemy will reflect off the wall, rather than just bouncing off.

//Bounce off the walls
if (this.x <= this.size) {
	this.dx = -this.dx;
	this.x = this.size;
}
if (this.x + this.size >= w) {
	this.dx = -this.dx;
	this.x = w - this.size;
}
if (this.y <= this.size) {
	this.dy = -this.dy;
	this.y = this.size;
}
if (this.y + this.size >= h) {
	this.dy = -this.dy;
	this.y = h - this.size;
}

this.isOverlap(o2):

We can assume that the parameter o2 has x-y coordinates and size. Then we do some calculations to check if o1, which is the enemy, has overlapped the object o2. If it has, we return true. If it hasn’t, we return false.

var o1 = this;
var dx = o1.x - o2.x;
var dy = o1.y - o2.y;
var s = (o1.size + o2.size);
var d = Math.sqrt(dx * dx + dy * dy);
return (d <= s);

Particles

this.draw:

We draw a small square so it looks like a dot. For aesthetic purposes, we color it the same colour as the object that spawned it.

//A particle is a dot with the object's colour
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = this.colour;
ctx.fillRect(0, 0, this.size, this.size);
ctx.restore();

this.move:

We increment the x-y coordinates by the direction, and bounce off the walls. If the lifespan of the particle is 0, we kill it.

this.x += this.dx;
this.y += this.dy;
if (this.x + this.size >= w || this.x - this.size <= 0) {
	this.dx = -this.dx;
}
if (this.y + this.size >= h || this.y - this.size <= 0) {
	this.dy = -this.dy;
}
if (this.life > 0) {
	this.life--;
} else {
	this.dead = true;
}

Bugs

During the process of making the game, especially nearer to the start, I faced quite a few bugs that I didn’t know what caused, but nothing too buggy that a few tweaks didn’t fix.

Firstly, when I was making the player rotate, it was rotating the wrong way (back facing front and front facing back). I solved this temporarily by subtracting 300 radians from the angle of the player. This solution wasn’t optimal though, as the player wasn’t really facing the mouse. When I was looking through my code for this write-up, I realised that I forgot to make the angle negative.

Secondly, when I was writing the code for the enemies spawning, River noticed that the enemies spawned concentrated at the top left corner of the canvas. So, I made the enemies spawn randomly along the sides of the canvas, but I couldn’t figure out how to make them spawn at the top and bottom too. A few days later, River suggested that I first let the code decide whether to spawn at the top and bottom or at the sides, and then use the same spawn code for both, which was pretty clever actually.

Thirdly, when the player and enemy collide, I made it so they would both bounce off each other. However, my code was a little simple, and only worked if the player and enemy collide head-on. Thus, River suggested that I kill the enemy when it hits the player, which made things a lot simpler as I just needed to handle the player’s direction and not the enemy’s. Right now, the enemy’s velocity will add to the player’s velocity, but it still feels weird if the player runs into the enemy from behind. I may attempt to fix that if I think of a way to do so.

Fourthly, when I was adding the code for the player to explode when it dies, it would explode incessantly even after it had died. It was so bad that it caused my browser to crash :( I had to fix it immediately or my game would be unplayable. In the end, it proved pretty easy to fix. I just set it such that once it explodes, it would set the opacity to 0, and when it is at 0, it will not explode anymore.

Lastly, I wanted to include some way for the player to regenerate health. First, I tried making some enemies have a heal tag so when they die the player regenerates health. However, it was perhaps too effective, especially late-game, with many many enemies spawning at the same time. The player was basically invincible. When I tried lowering the chance for an enemy to carry the heal tag, it became so negligible that it wasn’t really that useful anymore. Thus, I got rid of that idea, and tried spawning in a healer that heals the player every 20 points or so. It actually sounded pretty cool, but was really difficult. I may consider adding that some day. Lastly, I made it such that the player heals every 100 points, similar to the healer. However, like before, it was too overpowered late-game, and the player became invincible. Thus, I tried making the player shoot multiple bullets per shot. At first, I made the bullets shoot horizontally, sort of like a shotgun but the bullets remain parallel. It was pretty easy to implement, but I realised that the player could just stay in a corner and become practically untouchable. Thus, I changed the power up to a burst shot, where the bullets lined up horizontally, and made the player lose health when touching the walls. Sometime after making this write-up, I thought “hey, wouldn’t it be cool if the bullets bounced off the walls?” So I added a health value to the bullet, and when bouncing off walls or hitting enemies, the health would go down by one, with this value increasing every 50 points. At the same time, I got rid of the losing-health-when-touching-walls mechanism, and replaced it with a bounce-off-the-walls mechanism, which complemented my bullets bouncing off the walls.

Conclusion

In conclusion, this game has been pretty enjoyable to make, and I also learnt a lot from it. For example, I learnt how to track the mouse’s location in the canvas, how to track keydown and keyup, the purpose of setTimeout() and why it is superior to just using a while loop, how to store data locally in the browser’s cache and even something as simple as just making the canvas fit the size of the window, and something as trivial as how Math.random obtains a random value which helped entertain me while I was stuck at something. I intend to keep updating the game with more game modes, maybe one where the player controls the enemies and the tank is controlled by the computer. Also, I am working on another game now, but it will probably take a much longer time to make. And you might want to try using the Konami code at the game over screen :)