Skip to content

Instantly share code, notes, and snippets.

@manila
Last active April 27, 2019 05:40
Show Gist options
  • Save manila/10e3a7baa1b61ba25ef48e8eb270bd4f to your computer and use it in GitHub Desktop.
Save manila/10e3a7baa1b61ba25ef48e8eb270bd4f to your computer and use it in GitHub Desktop.
Space invaders in JavaScript
<!--
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