To Play: Use the arrow keys to move and the space bar to shoot.
The Asteroid game is a development project that I created in the process of learning JavaScript.
//consts
const FPS = 30; // frames per second
const FRICTION = 0.7; // fiction coefficient of space (0 no friction, 1 lots of frictions)
const GAME_LIVES = 3; // number of lives you get
const LASER_DIST = 0.275; // max distance the laser can travel as fraction of screen width
const LASER_EXP_DUR = 0.1; // the duration of the lasers explosions in seconds
const LASER_MAX = 10; // maximum num of lasers on screen
const LASER_SPD = 500; // Speed of lasers in pixles per second
const ORBS_NUM = 3; // starting number of astroids also ORBS are Astroids it just shorter for me to write
const ORBS_SIZE = 100; // starting size of astroids in pixles
const ORBS_SPD = 50; // Max starting speed of astroids pixles per second
const ORBS_VERT = 10; // average number of vertices on the asteroids
const ORBS_JAG = 0.4; // Jaggedness of the astroids \
const ORBS_PTS_LRG = 20; // points scored fot the large astroids
const ORBS_PTS_MED = 50; // points scored fot the medium astroids
const ORBS_PTS_SM = 100; // points scored fot the small astroids
const SAVE_KEY_SCORE = "scorekey" // save key for local storage
const SHIP_SIZE = 30; // ship height in pixles
const SHIP_THRUST = 5; // thrust accelertation of the ship
const SHIP_EXP_DUR = 0.3; // duration of the ship explosion
const SHIP_INV_DUR = 3; // ships invincibility duration
const SHIP_BLINK_DUR = 0.1; // duration of the ships blink
const TURN_SPEED = 360; // turn speed in derees per second
const SHOW_RED_DOT = false; // shows of hides the center dot of the ship
const SHOW_BOUNDING = false; // show or hide collision detection
const TEXT_FADE_TIME = 2.5; // text fade time seconds
const TEXT_SIZE = 50; // text font size in pixles
const SOUND_ON = false; // this turns the sound off and on
const MUSIC_ON = false; // this turns the sound off and on
/** @type {HTMLCanvasElement} */
var canv = document.getElementById("GameCanvas");
var ctx = canv.getContext("2d");
function resize() {
canv.width = window.innerWidth -15;
canv.height = window.innerHeight;
}
// set up event handaler for canvas sizing
window.addEventListener('resize', resize, false); resize();
// set up sound effects
var fxExplode = new Sound("sounds/explode.m4a");
var fxLaser = new Sound("sounds/laser.m4a", 5, 0.5);
var fxHit = new Sound("sounds/hit.m4a", 5);
var fxThrust = new Sound("sounds/thrust.m4a");
// set up the music
var music = new Music("sounds/music-low.m4a", "sounds/music-high.m4a");
var orbsLeft, orbsTotal;
// set up the game peramiters
var level, lives, orbs, score, highScore, ship, text, textAlpha;
newGame();
// set up key handalers
// disable default key functions
window.addEventListener("keydown", function(e) {
// space and arrow keys
if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}, false);
// event listeners
document.addEventListener("keydown", keyDown);
document.addEventListener("keyup", keyUp);
// set up the game loop
setInterval(update, 1000 / FPS);
// creting astroids
function createAsteroidBelt() {
orbs = [];
orbsTotal = (ORBS_NUM + level) * 7;
orbsLeft = orbsTotal;
orbs = [];
var x, y;
for (var i = 0; i < ORBS_NUM + level; i++) {
do{
x = Math.floor(Math.random() * canv.width);
y = Math.floor(Math.random() * canv.height);
} while(distBetweenShip(ship.x, ship.y, x, y) < ORBS_SIZE * 2 + ship.r);
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 2)));
}
}
function destroyAsteroid(index) {
var x = orbs[index].x;
var y = orbs[index].y;
var r = orbs[index].r;
// split the asteroid in two
if (r == Math.ceil(ORBS_SIZE / 2)) {
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 4)));
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 4)));
score += ORBS_PTS_LRG;
} else if (r == Math.ceil(ORBS_SIZE / 4)) {
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
score += ORBS_PTS_MED;
} else{
score += ORBS_PTS_SM;
}
if (score > highScore) {
highScore = score;
localStorage.setItem(SAVE_KEY_SCORE, highScore);
}
// destroy the asteroid
orbs.splice(index, 1);
fxHit.play();
// calculate the ratio of remaining astroids to determin music tempo
orbsLeft--;
music.setAsteroidRatio(orbsLeft == 0 ? 1 : orbsLeft / orbsTotal);
//next level
if (orbs.length == 0) {
level++;
newLevel();
}
}
function distBetweenShip(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
function drawShip(x, y, a, color = "white") {
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = SHIP_SIZE / 20;
ctx.beginPath();
ctx.moveTo( // nose of the ship
x + 5 / 3 * ship.r * Math.cos(a),
y - 5 / 3 * ship.r * Math.sin(a)
);
ctx.lineTo( // rear left of the ship
x - ship.r * (Math.cos(a) + Math.sin(a)),
y + ship.r * (Math.sin(a) - Math.cos(a))
);
ctx.lineTo( // rear right of the ship
x - ship.r * (Math.cos(a) - Math.sin(a)),
y + ship.r * (Math.sin(a) + Math.cos(a))
);
ctx.closePath();
ctx.stroke();
}
function explodeShip() {
ship.explodeTime = Math.ceil(SHIP_EXP_DUR * FPS);
fxExplode.play();
}
function gameOver(){
ship.dead = true;
text = "Game Over";
textAlpha = 1.0;
}
// space ship movement
function keyDown(/** @type {keyboardevent} */ ev) {
if (ship.dead){
return;
}
switch(ev.keyCode) {
case 32: // Space bar shoots laser
shootLaser();
break;
case 37: // left arrow key rotate ship left
ship.rot = TURN_SPEED / 180 * Math.PI / FPS;
break;
case 38: // up arrow moves ship forward
ship.thrusting = true;
break;
case 39: // left arrow rotate ship right
ship.rot = -TURN_SPEED / 180 * Math.PI / FPS;
break;
}
}
function keyUp(/** @type {keyboardevent} */ ev){
if (ship.dead){
return;
}
switch(ev.keyCode) {
case 32: // Space bar allow shooting again
ship.canShoot = true;
break;
case 37: // left arrow key stop rotation left
ship.rot = 0;
break;
case 38: // stop forward movement
ship.thrusting = false;
break;
case 39: // left arrow stop rotating right
ship.rot = 0;
break;
}
}
function newAsteroid(x, y, r) {
var lvlMult = 1 + 0.1 * level;
var orbs = {
a: Math.random() * Math.PI * 2, // in radians
r: r,
vert: (Math.random() * (ORBS_VERT + 1) + ORBS_VERT / 2),
x: x,
y: y,
xv: Math.random() * ORBS_SPD * lvlMult/ FPS * (Math.random() < 0.5 ? 1 : -1),
yv: Math.random() * ORBS_SPD * lvlMult / FPS * (Math.random() < 0.5 ? 1 : -1),
offs: []
};
// crete the vertex offsets array
for (var i = 0; i < orbs.vert; i++) {
orbs.offs.push(Math.random() * ORBS_JAG * 2 + 1 - ORBS_JAG);
}
return orbs;
}
function newGame() {
lives = GAME_LIVES;
level = 0;
score = 0;
ship = newShip();
// get the score from local storage
var scoreStr = localStorage.getItem(SAVE_KEY_SCORE);
if (scoreStr == null) {
highScore = 0;
} else {
highScore = parseInt(scoreStr);
}
newLevel();
}
function newLevel() {
text = "Level " + (level + 1);
textAlpha = 1.0;
createAsteroidBelt();
}
// creates the ship
function newShip() {
return {
x: canv.width / 2,
y: canv.height / 2,
r: SHIP_SIZE / 3,
a: 90 / 180 * Math.PI, // convert radians
blinkNum: Math.ceil(SHIP_INV_DUR / SHIP_BLINK_DUR),
blinkTime: Math.ceil(SHIP_BLINK_DUR * FPS),
canShoot: true,
dead: false,
explodeTime: 0,
lasers: [],
rot: 0,
thrusting: false,
thrust: {
x: 0,
y: 0,
}
}
}
function shootLaser() {
// create the laser object
if (ship.canShoot && ship.lasers.length < LASER_MAX) {
ship.lasers.push({ // from the nose of the ship
x: ship.x + 4 / 3 * ship.r * Math.cos(ship.a),
y: ship.y - 4 / 3 * ship.r * Math.sin(ship.a),
xv: LASER_SPD * Math.cos(ship.a) / FPS,
yv: -LASER_SPD * Math.sin(ship.a) / FPS,
dist: 0,
explodeTime: 0,
});
fxLaser.play();
}
// prevent further shooting
ship.canShoot = false;
}
function Music(srcLow, srcHigh) {
this.soundLow = new Audio(srcLow);
this.soundHigh = new Audio(srcHigh);
this.low = true;
this.tempo = 1.0; // secounds per beat
this.beatTime = 0; // tempo of the beat
this.play = function() {
if (MUSIC_ON) {
if (this.low) {
this.soundLow.play();
} else {
this.soundHigh.play();
}
this.low = !this.low;
}
}
this.setAsteroidRatio = function(ratio) {
this.tempo = 1.0 - 0.75 * (1.0 - ratio);
}
this.tick = function() {
if (this.beatTime == 0) {
this.play();
this.beatTime = Math.ceil(this.tempo * FPS);
} else {
this.beatTime--;
}
}
}
function Sound(src, maxStreams = 1, vol = 1.0) {
this.streamNum = 0;
this.streams = [];
for (var i = 0; i < maxStreams; i++) {
this.streams.push(new Audio(src));
this.streams[i].volume = vol;
}
this.play = function() {
if (SOUND_ON) {
this.streamNum = (this.streamNum + 1) % maxStreams;
this.streams[this.streamNum].play();
}
}
this.stop = function() {
this.streams[this.streamNum].pause();
this.streams[this.streamNum].curentTime = 0;
}
}
function update() {
var blinkOn = ship.blinkNum % 2 == 0;
var exploding = ship.explodeTime > 0;
// tick the music
music.tick();
// draw space
ctx.fillStyle = "black";
ctx.fillRect(0,0, canv.width, canv.height);
// draw the asteroids
var x, y, r, a, vert, offs;
for (var i = 0; i < orbs.length; i++) {
ctx.strokeStyle = "slategray";
ctx.lineWidth = SHIP_SIZE / 20;
// get the asteroids properties
x = orbs[i].x;
y = orbs[i].y;
r = orbs[i].r;
a = orbs[i].a;
vert = orbs[i].vert;
offs = orbs[i].offs;
// draw a path
ctx.beginPath();
ctx.moveTo(
x + r * offs[0] * Math.cos(a),
y + r * offs[0] * Math.sin(a),
);
// draw a polygon
for(var j = 1; j < vert; j++) {
ctx.lineTo(
x + r * offs[j] * Math.cos(a + j * Math.PI * 2 / vert),
y + r * offs[j] * Math.sin(a + j * Math.PI * 2 / vert),
);
}
ctx.closePath();
ctx.stroke();
// show astroids collision circles
if (SHOW_BOUNDING) {
ctx.strokeStyle = "orange";
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, false);
ctx.stroke();
}
}
// Thrust the ship
if (ship.thrusting && !ship.dead) {
ship.thrust.x += SHIP_THRUST * Math.cos(ship.a) / FPS;
ship.thrust.y -= SHIP_THRUST * Math.sin(ship.a) / FPS;
fxThrust.play();
// draw the thrust
if (!exploding && blinkOn) {
ctx.fillStyle = "red";
ctx.strokeStyle = "orange";
ctx.lineWidth = SHIP_SIZE / 10;
ctx.beginPath();
ctx.moveTo( // rear left
ship.x - ship.r * (2 / 3 * Math.cos(ship.a) + 0.5 * Math.sin(ship.a)),
ship.y + ship.r * (2 / 3 * Math.sin(ship.a) - 0.5 * Math.cos(ship.a)),
);
ctx.lineTo( // rear center behind ship
ship.x - ship.r * 5 / 3 * Math.cos(ship.a),
ship.y + ship.r * 5 / 3 * Math.sin(ship.a),
);
ctx.lineTo( // rear right of the ship
ship.x - ship.r * (2 / 3 *Math.cos(ship.a) - 0.5 * Math.sin(ship.a)),
ship.y + ship.r * (2 / 3 *Math.sin(ship.a) + 0.5 * Math.cos(ship.a)),
);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
} else {
ship.thrust.x -= FRICTION * ship.thrust.x / FPS;
ship.thrust.y -= FRICTION * ship.thrust.y / FPS;
fxThrust.stop();
}
// draw ship
if(!exploding) {
if (blinkOn && !ship.dead) {
drawShip(ship.x, ship.y, ship.a);
}
// handle blinking
if (ship.blinkNum > 0){
// reduce blink time
ship.blinkTime--;
// reduce the blink num
if (ship.blinkTime == 0){
ship.blinkTime = Math.ceil(SHIP_BLINK_DUR * FPS);
ship.blinkNum--;
}
}
} else {
// draw the explosion
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(ship.x, ship.y, ship.r * 1.2, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "orange";
ctx.beginPath();
ctx.arc(ship.x, ship.y, ship.r * 1, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(ship.x, ship.y, ship.r * 0.5, 0, Math.PI * 2, false);
ctx.fill();
}
if (SHOW_BOUNDING) {
ctx.strokeStyle = "lime";
ctx.beginPath();
ctx.arc(ship.x, ship.y, ship.r, 0, Math.PI * 2, false);
ctx.stroke();
}
// ship center dot
if (SHOW_RED_DOT) {
ctx.fillStyle = "red";
ctx.fillRect(ship.x, ship.y, 2, 2)
}
// draw the lasers
for (var i = 0; i < ship.lasers.length; i++) {
if (ship.lasers[i].explodeTime == 0){
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(ship.lasers[i].x, ship.lasers[i].y, SHIP_SIZE / 15, 0, Math.PI * 2, false);
ctx.fill();
} else {
// draw the explosion
ctx.fillStyle = "orange";
ctx.beginPath();
ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.75, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "orangered";
ctx.beginPath();
ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.5, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.25, 0, Math.PI * 2, false);
ctx.fill();
}
}
// draw the game text
if (textAlpha >= 0) {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(255, 255, 255, " + textAlpha + ")";
// make sure to add in font
ctx.font = "small-caps " + TEXT_SIZE + "px good-times";
ctx.fillText(text, canv.width / 2, canv.height / 1.5);
textAlpha -= (1.0 / TEXT_FADE_TIME / FPS);
} else if (ship.dead) {
newGame();
}
// draw the lives
var lifeColor;
for (var i = 0; i < lives; i++) {
lifeColor = exploding && i == lives - 1 ? "red" : "white";
drawShip(SHIP_SIZE + i * SHIP_SIZE * 1.2, canv.height - SHIP_SIZE * 2.75, 0.5 * Math.PI, lifeColor);
ctx.fill();
}
// draw the score
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
// make sure to add in font
ctx.font = TEXT_SIZE * 0.75 + "px good-times";
ctx.fillText("Score " + score, canv.width / 2 + 50, 25);
// draw the high score
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
// make sure to add in font
ctx.font = TEXT_SIZE * 0.5 + "px good-times";
ctx.fillText("High Score " + highScore, SHIP_SIZE + 2 * SHIP_SIZE * 1.2 , canv.height - SHIP_SIZE * 1.5);
// detect when the laser hits asteroids
var ax, ay, ar, lx, ly;
for (var i = orbs.length - 1; i >=0; i--) {
// get the astroids properties
ax = orbs[i].x;
ay = orbs[i].y;
ar = orbs[i].r;
// loop over the lasers
for (var j = ship.lasers.length - 1; j >= 0; j--) {
// get the laser properties
lx = ship.lasers[j].x;
ly = ship.lasers[j].y;
// detect hits
if (ship.lasers[j].explodeTime == 0 && distBetweenShip(ax, ay, lx, ly) < ar) {
// destroy the astroid and activate the laser explosion
destroyAsteroid(i);
ship.lasers[j].explodeTime = Math.ceil(LASER_EXP_DUR * FPS);
break;
}
}
}
// cheak for collisions
if (!exploding) {
if (ship.blinkNum == 0 && !ship.dead) {
for (var i = 0; i < orbs.length; i++){
if(distBetweenShip(ship.x, ship.y, orbs[i].x, orbs[i].y,) < ship.r + orbs[i].r) {
explodeShip();
destroyAsteroid(i);
break;
}
}
}
// rotate ship
ship.a += ship.rot;
// move ship
ship.x += ship.thrust.x;
ship.y += ship.thrust.y;
} else {
ship.explodeTime--;
if (ship.explodeTime == 0) {
lives--;
if (lives == 0){
gameOver();
} else {
ship = newShip();
}
}
}
// handle edge of screen
if (ship.x < 0 - ship.r) {
ship.x = canv.width + ship.r;
} else if (ship.x > canv.width + ship.r) {
ship.x = 0 - ship.r;
}
if (ship.y < 0 - ship.r) {
ship.y = canv.height + ship.r;
} else if (ship.y > canv.height + ship.r) {
ship.y = 0 - ship.r;
}
for (var i = ship.lasers.length - 1; i >= 0; i--) {
// cheak the distance the laser has traveled
if(ship.lasers[i].dist > LASER_DIST * canv.width) {
ship.lasers.splice(i, 1);
continue;
}
// handel the laser explosion
if (ship.lasers[i].explodeTime > 0) {
ship.lasers[i].explodeTime--;
// destroy the laser after the duration is up
if (ship.lasers[i].explodeTime == 0) {
ship.lasers.splice(i, 1);
continue;
}
} else {
// move the lasers
ship.lasers[i].x += ship.lasers[i].xv;
ship.lasers[i].y += ship.lasers[i].yv;
// calculate the distance travlled
ship.lasers[i].dist += Math.sqrt(Math.pow(ship.lasers[i].xv, 2) + Math.pow(ship.lasers[i].yv, 2));
}
// handle edge of screen
if (ship.lasers[i].x < 0) {
ship.lasers[i].x = canv.width;
} else if (ship.lasers[i].x > canv.width) {
ship.lasers[i].x = 0;
}
if (ship.lasers[i].y < 0) {
ship.lasers[i].y = canv.height;
} else if (ship.lasers[i].y > canv.height) {
ship.lasers[i].y = 0;
}
}
// move the asteroid
for (var i = 0; i < orbs.length; i++){
orbs[i].x += orbs[i].xv;
orbs[i].y += orbs[i].yv;
// handle edge of screen
if(orbs[i].x < 0 - orbs[i].r) {
orbs[i].x = canv.width + orbs[i].r;
} else if (orbs[i].x > canv.width + orbs[i].r) {
orbs[i].x = 0 - orbs[i].r;
}
if(orbs[i].y < 0 - orbs[i].r) {
orbs[i].y = canv.height + orbs[i].r;
} else if (orbs[i].y > canv.height + orbs[i].r) {
orbs[i].y = 0 - orbs[i].r;
}
}
}