Last active
April 27, 2019 05:40
-
-
Save manila/10e3a7baa1b61ba25ef48e8eb270bd4f to your computer and use it in GitHub Desktop.
Space invaders in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
Space Invaders without the ability to shoot | |
by Manuel Nila April 2019 | |
live version https://manila.me/invaders | |
--> | |
<html lang="en"> | |
<head> | |
<style type="text/css"> | |
body { | |
padding: 0px; | |
margin: 0px; | |
background: #000; | |
} | |
canvas#game { | |
background: inherit; | |
display: block; | |
margin: 0em auto 0em auto; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="game" width="448" height="512"></canvas> | |
<script> | |
/* | |
Bitmaps | |
Each Alien is assigned two arrays of data. These are crude bitmap sprites | |
*/ | |
const ALIEN_SMALL = [[0,0,0,1,1,0,0,0, | |
0,0,1,1,1,1,0,0, | |
0,1,1,1,1,1,1,0, | |
1,1,0,1,1,0,1,1, | |
1,1,1,1,1,1,1,1, | |
0,1,0,1,1,0,1,0, | |
1,0,0,0,0,0,0,1, | |
0,1,0,0,0,0,1,0], | |
[0,0,0,1,1,0,0,0, | |
0,0,1,1,1,1,0,0, | |
0,1,1,1,1,1,1,0, | |
1,1,0,1,1,0,1,1, | |
1,1,1,1,1,1,1,1, | |
0,0,1,0,0,1,0,0, | |
0,1,0,1,1,0,1,0, | |
1,0,1,0,0,1,0,1]]; | |
const ALIEN_MEDIUM = [[0,0,1,0,0,0,0,0,1,0,0, | |
0,0,0,1,0,0,0,1,0,0,0, | |
0,0,1,1,1,1,1,1,1,0,0, | |
0,1,1,0,1,1,1,0,1,1,0, | |
1,1,1,1,1,1,1,1,1,1,1, | |
1,0,1,1,1,1,1,1,1,0,1, | |
1,0,1,0,0,0,0,0,1,0,1, | |
0,0,0,1,1,0,1,1,0,0,0], | |
[0,0,1,0,0,0,0,0,1,0,0, | |
1,0,0,1,0,0,0,1,0,0,1, | |
1,0,1,1,1,1,1,1,1,0,1, | |
1,1,1,0,1,1,1,0,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1, | |
0,1,1,1,1,1,1,1,1,1,0, | |
0,0,1,0,0,0,0,0,1,0,0, | |
0,1,0,0,0,0,0,0,0,1,0]]; | |
const ALIEN_LARGE = [[0,0,0,0,1,1,1,1,0,0,0,0, | |
0,1,1,1,1,1,1,1,1,1,1,0, | |
1,1,1,1,1,1,1,1,1,1,1,1, | |
1,1,1,0,0,1,1,0,0,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1,1, | |
0,0,0,1,1,0,0,1,1,0,0,0, | |
0,0,1,1,0,1,1,0,1,1,0,0, | |
1,1,0,0,0,0,0,0,0,0,1,1], | |
[0,0,0,0,1,1,1,1,0,0,0,0, | |
0,1,1,1,1,1,1,1,1,1,1,0, | |
1,1,1,1,1,1,1,1,1,1,1,1, | |
1,1,1,0,0,1,1,0,0,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1,1, | |
0,0,1,1,1,0,0,1,1,1,0,0, | |
0,1,1,0,0,1,1,0,0,1,1,0, | |
0,0,1,1,0,0,0,0,1,1,0,0]]; | |
/* Player bitmap AKA lasercannon */ | |
const PLAYER = [0,0,0,0,0,1,0,0,0,0,0, | |
0,0,0,0,1,1,1,0,0,0,0, | |
0,0,0,0,1,1,1,0,0,0,0, | |
0,1,1,1,1,1,1,1,1,1,0, | |
1,1,1,1,1,1,1,1,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1, | |
1,1,1,1,1,1,1,1,1,1,1]; | |
/* | |
This is the base Alien Class, we extend these below for each size of alien | |
*/ | |
class Alien { | |
constructor(x, y, width, height, bitmaps) { | |
this.x = x; | |
this.y = y; | |
this.frame = 0; //Use this to track sprite animation | |
this.width = width; | |
this.height = height; | |
this.bitmaps = bitmaps; | |
} | |
/* | |
Access bitmap like it's a property, return the current sprite of the alien. This is a hacky way to animate between two sprites. | |
*/ | |
get bitmap() { | |
return this.bitmaps[Math.floor(this.frame % 2)]; | |
} | |
/* | |
Paint the Alien on the Game canvas | |
*/ | |
draw() { | |
drawBitmap(this.x, | |
this.y, | |
this.width, | |
this.height, | |
this.bitmap, | |
Game.ctx, | |
Game.scale); | |
} | |
/* | |
Change the X and Y coordinate | |
*/ | |
move(directionX, directionY) { | |
this.x += directionX; | |
this.y += directionY; | |
this.frame++; | |
} | |
} | |
/* | |
This is the smallest alien, it sits on the top row | |
*/ | |
class AlienSmall extends Alien { | |
constructor (x, y) { | |
super(x, y, 8, 8, ALIEN_SMALL); | |
} | |
} | |
/* | |
This is the medium sized alien, this is the one I usually picture when I think of space invaders. It occupys two rows after the first row | |
*/ | |
class AlienMedium extends Alien { | |
constructor (x, y) { | |
super(x, y, 11, 8, ALIEN_MEDIUM); | |
} | |
} | |
/* | |
The largest alien, it occupys the last two rows | |
*/ | |
class AlienLarge extends Alien { | |
constructor (x, y) { | |
super(x, y, 12, 8, ALIEN_LARGE); | |
} | |
} | |
/* | |
This is the player or lasercannon. | |
*/ | |
var Player = { | |
x: 106, | |
y: 248, | |
width: 11, | |
height: 8, | |
speed: 0, | |
bitmap: PLAYER, | |
/* | |
Since we can only move on the X-axis this is the only parameter accepted in the move function | |
*/ | |
move () { | |
/* Check to see if any keys are being pressed */ | |
if (Game.keysPressed.length > 0) { | |
/* Check the last key action being pressed */ | |
if (Game.keysPressed[Game.keysPressed.length - 1] == 'left') { | |
this.speed = -2; | |
} else if (Game.keysPressed[Game.keysPressed.length - 1] == 'right') { | |
this.speed = 2; | |
} else { | |
this.speed = 0; | |
} | |
} else { | |
this.speed = 0; | |
} | |
/* Don't go outside the game area */ | |
if (this.x + this.speed > 0 && | |
this.x + this.width + this.speed < Game.width) { | |
this.x += this.speed; | |
} | |
}, | |
draw() { | |
drawBitmap(this.x, | |
this.y, | |
this.width, | |
this.height, | |
this.bitmap, | |
Game.ctx, | |
Game.scale) | |
} | |
} | |
/* | |
The alien swarm is made up of all three types of aliens in 5 rows | |
The swarm moves side to side lowering each time it reaches the wall. | |
The swarm is a large entity with propertys to track it position, each alien also has their own propertys for tracking position | |
*/ | |
var Swarm = { | |
x: 24, //Current X position of whole swarm | |
y: 16, | |
width: 0, //furthest occupied pixel on the right of the screen | |
height: 0, //lowest occupied pixel on the bottom of the screen | |
direction: { //direction of the swarm | |
x: 2, | |
y: 0 | |
}, | |
aliens: [], //Individual alien objects stored here | |
draw() { | |
for (let i=0; i<this.aliens.length; i++) { | |
this.aliens[i].draw(); | |
} | |
}, | |
move() { | |
this.update(); //Update the size and direction (if hitting a wall) before moving each alien in the swarm | |
for (let i=0; i<this.aliens.length; i++) { | |
this.aliens[i].move(this.direction.x, this.direction.y); | |
} | |
}, | |
/* | |
This function tracks the size of the swarm and updates the direction that the swarm will move if there is an alien on the end hitting a wall | |
*/ | |
update() { | |
/* Find the alien that exends the farthest of the right and bottom edge of the swarm */ | |
for (let i=0; i<this.aliens.length; i++) { | |
this.width = 0; //Reset width and height of swarm | |
this.height = 0; | |
/* Calculate width of swarm */ | |
if (this.aliens[i].x + this.aliens[i].width > this.width) { | |
this.width = this.aliens[i].x + this.aliens[i].width; | |
} | |
/* Calculate height of swarm */ | |
if (this.aliens[i].y + this.aliens[i].height > this.height) { | |
this.height = this.aliens[i].y + this.aliens[i].height; | |
} | |
} | |
/* End game if swarm hits same y coordinate as player */ | |
if (this.height >= 248) { | |
Game.end(); | |
} | |
/* Check if alien is going to hit the right side */ | |
if (this.width + this.direction.x > Game.width && Math.floor(this.y % 2) == 0) { | |
this.direction.y = 3; | |
this.direction.x = 0; | |
this.y += this.direction.y; | |
/* Check if alien is going to hit left side */ | |
} else if (this.x <= 0 && Math.floor(this.y % 2) !== 0) { | |
this.direction.y = 3; | |
this.direction.x = 0; | |
this.y += this.direction.y; | |
/* Move aliens left or right depending on if they are in an Odd or Even Y position */ | |
} else { | |
this.direction.y = 0; | |
this.direction.x = Math.floor(this.y % 2) == 0 ? 2 : -2; | |
this.x += this.direction.x; | |
} | |
}, | |
/* | |
Populate the swarm with 5 rows of 11 aliens | |
*/ | |
populate() { | |
/* First Row */ | |
for(let i=0; i<11; i++) { | |
this.aliens.push(new AlienSmall((i * 16) + 2 + this.x, this.y)); | |
} | |
/* Second and Third Row */ | |
for(let i=0; i<22; i++) { | |
this.aliens.push(new AlienMedium(Math.floor(i % 11) * 16 + 1 + this.x, | |
this.y + 16 + Math.floor(i / 11) * 16)); | |
} | |
/* Fourth and Fifth Row */ | |
for(let i=0; i<22; i++) { | |
this.aliens.push(new AlienLarge(Math.floor(i % 11) * 16 + this.x, | |
this.y + 48 + Math.floor(i / 11) * 16)); | |
} | |
} | |
} | |
var Game = { | |
canvas: null, //Canvas element where game is drawn | |
ctx: null, //Context of canvas element | |
paintDelta: 0, | |
alienDelta: 0, | |
paintLast: 0, | |
alienLastAnimFrame: 0, | |
animationRequest: null, | |
scale: 2, | |
width: 224, //Width of game "pixels", not physical pixels may be different than DOM pixels | |
height: 256, //Height of game "pixels", not physical pixels may be different than DOM pixels | |
now: 0, | |
/* Map key codes to an action */ | |
keymap: { | |
'37' : 'left', | |
'65' : 'left', | |
'39' : 'right', | |
'68' : 'right' | |
}, | |
keysPressed: [], //Store keys that have been pressed in order of when they are pressed | |
paintInterval: 1000/60, //How often should things be drawn to canvas (FPS) | |
alienMoveInterval: 600, //How often should aliens move in milliseconds | |
canvasWidth: 448, //Width of canvas in pixels, these are physical pixels as they relate directly to the DOM | |
canvasHeight: 512, //Height of canvas in pixels, physical in relation to the DOM | |
clear() { | |
this.ctx.clearRect(0, 0, this.canvasWidth * this.scale, this.canvasHeight * this.scale); | |
}, | |
/* Draw all pieces of the Game */ | |
draw() { | |
Game.clear(); | |
Swarm.draw(); | |
Player.draw(); | |
}, | |
/* Resize the canvas element based on window.innerHeight */ | |
resize() { | |
var windowWidth = window.innerWidth; | |
var windowHeight = window.innerHeight; | |
var scaleWidth = Math.floor(windowWidth / this.width) || 1; | |
var scaleHeight = Math.floor(windowHeight / this.height) || 1; | |
if (scaleWidth < scaleHeight) { | |
this.scale = scaleWidth; | |
} else { | |
this.scale = scaleHeight; | |
} | |
//this.scale = Math.floor(windowHeight / this.height) || 1; | |
this.canvas.width = this.width * this.scale; | |
this.canvas.height = this.height * this.scale; | |
this.ctx = this.canvas.getContext("2d"); | |
}, | |
/* Setup canvas, populate swarm, and listen for keystrokes */ | |
setup(canvasElement) { | |
this.canvas = canvasElement; | |
this.resize(); | |
Swarm.populate(); | |
window.addEventListener("keydown", function (event) { | |
/* Make sure the key being pressed is mapped to somthing */ | |
if (Game.keymap[event.keyCode]) { | |
/* Add the action of the key to the list of keys pressed */ | |
Game.keysPressed.push(Game.keymap[event.keyCode]); | |
} | |
console.log(event.keyCode); | |
}); | |
window.addEventListener("keyup", function (event) { | |
/* Make sure key being released is mapped to something */ | |
if (Game.keymap[event.keyCode]) { | |
/* Look for and remove the key action from everywhere in the list of pressed keys */ | |
while (Game.keysPressed.indexOf(Game.keymap[event.keyCode]) !== -1) { | |
Game.keysPressed.splice(Game.keysPressed.indexOf(Game.keymap[event.keyCode]),1); | |
} | |
} | |
}); | |
window.addEventListener("resize", function () { | |
Game.resize(); | |
}); | |
}, | |
/* Call loop with requestAnimationFrame, draw Game close to 60FPS, also use this to time the movement of the aliens */ | |
loop() { | |
Game.now = Date.now(); | |
Game.paintDelta = Game.now - Game.paintLast; | |
Game.alienDelta = Game.now - Game.alienLastAnimFrame; | |
if (Game.paintDelta > Game.paintInterval) { | |
Player.move(); | |
Game.draw(); | |
Game.paintLast = Game.now - (Game.paintDelta % Game.paintDelta); | |
} | |
if (Game.alienDelta > Game.alienMoveInterval) { | |
Swarm.move(); | |
Game.alienLastAnimFrame = Game.now - (Game.alienDelta % Game.alienDelta); | |
} | |
Game.animationRequest = window.requestAnimationFrame(Game.loop); | |
}, | |
start() { | |
Game.animationRequest = window.requestAnimationFrame(Game.loop); | |
}, | |
end() { | |
window.cancelAnimationFrame(Game.animationRequest); | |
alert("Game Over"); | |
Game.reset(); | |
}, | |
reset() { | |
Swarm.x = 0; | |
Swarm.y = 0; | |
Swarm.aliens = []; | |
Swarm.populate(); | |
Game.start(); | |
} | |
} | |
/* Draw an individual pixel at a specified coordinate */ | |
function putPixel(x, y, context, scale) { | |
if (y >= 248) { | |
context.fillStyle = '#00FF00'; | |
} else { | |
context.fillStyle = '#FFFFFF'; | |
} | |
context.fillRect(x * scale, y * scale, scale, scale); | |
} | |
/* Draw an array of pixels at a specified coordinate */ | |
function drawBitmap(x, y, width, height, bitmap, context, scale) { | |
for (let i=0; i<width*height; i++) { | |
if (bitmap[i] == 1) { | |
putPixel(Math.floor(i % width) + x, | |
Math.floor(i / width) + y, | |
context, | |
scale); | |
} | |
} | |
} | |
Game.setup(document.getElementById("game")); | |
Game.start(); | |
</script> | |
</body> | |
<html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment