Build Your Own Space Invaders++
A step-by-step guide to recreating the Space Invaders++ game from scratch: polygon aliens, grid movement, lives, rounds, and destructible barriers. All client-side with vanilla JavaScript and the Canvas 2D API.
The live version also includes custom key bindings, colour themes with localStorage persistence, particle effects, and high-score submission — this guide covers the core mechanics.
Step 1: The Canvas
Start with an HTML page containing a <canvas> element and a script block:
<canvas id="game" width="800" height="600"></canvas>
<script>
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var W = 800, H = 600;
</script>
The width and height attributes set the drawing buffer size. CSS can scale the display independently.
Step 2: Game Loop
Use requestAnimationFrame for smooth 60fps animation with delta-time capping:
var lastTime = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
var dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
The 0.05 cap prevents physics explosions if the tab is backgrounded and then refocused.
Step 3: Keyboard Input
Track which keys are pressed using a simple key map:
var keys = {};
window.addEventListener('keydown', function(e) {
keys[e.code] = true;
if (['ArrowLeft','ArrowRight','Space'].includes(e.code)) e.preventDefault();
});
window.addEventListener('keyup', function(e) { keys[e.code] = false; });
e.preventDefault() stops the page from scrolling when you press arrow keys.
Step 4: The Player Ship
The ship is a polygon triangle pointing up. Define it relative to its centre:
var SHIP_POLYGON = [
{ x: 0, y: -12 },
{ x: 14, y: 10 },
{ x: 12, y: 14 },
{ x: 0, y: 8 },
{ x: -12, y: 14 },
{ x: -14, y: 10 },
];
Ship state includes position, velocity, and lives:
var player = {
x: W / 2,
y: H - 40,
lives: 3,
invincible: 0,
};
Controls update movement in update():
var speed = 300;
if (keys['ArrowLeft']) player.x -= speed * dt;
if (keys['ArrowRight']) player.x += speed * dt;
player.x = Math.max(20, Math.min(W - 20, player.x));
Clamping keeps the ship within screen bounds (no wrap-around needed).
Step 5: Alien Polygons
Aliens are hand-crafted polygon shapes, not procedurally generated. Each template is a closed polygon with bilateral symmetry (left and right sides mirror). Three tiers:
Squid (top row, highest score):
var SQUID = [
{ x: 0, y: -15 }, { x: 3, y: -13 }, { x: 6, y: -11 },
{ x: 8, y: -7 }, { x: 7, y: -2 }, { x: 9, y: 3 },
{ x: 7, y: 8 }, { x: 4, y: 13 }, { x: 1, y: 11 },
{ x: 0, y: 13 }, { x: -1, y: 11 }, { x: -4, y: 13 },
{ x: -7, y: 8 }, { x: -9, y: 3 }, { x: -7, y: -2 },
{ x: -8, y: -7 }, { x: -6, y: -11 }, { x: -3, y: -13 },
];
Crab (middle row): Wider silhouette with upward claw points and side legs — see the full reference.
Humanoid (bottom row, lowest score): Taller shape with a helmet head, arms extended outward, and spread legs — see the full reference.
Store the templates in an array with colours and point values:
var ALIEN_TEMPLATES = [
{ base: SQUID, score: 100, colour: '#e74c3c' },
{ base: CRAB, score: 50, colour: '#3498db' },
{ base: HUMANOID, score: 20, colour: '#f1c40f' },
];
Step 6: Alien Grid
Aliens are arranged in a grid. Each round creates a new grid:
var ALIEN_COLS = 7, ALIEN_ROWS = 3;
function createAliens(cols, rows) {
var aliens = [];
var spacingX = 60, spacingY = 50;
var gridW = cols * spacingX, gridH = rows * spacingY;
var startX = (W - gridW) / 2 + spacingX / 2;
var startY = 60;
for (var r = 0; r < rows; r++) {
var templateIndex = Math.min(r, ALIEN_TEMPLATES.length - 1);
for (var c = 0; c < cols; c++) {
aliens.push({
x: startX + c * spacingX,
y: startY + r * spacingY,
templateIndex: templateIndex,
alive: true,
});
}
}
return aliens;
}
Row 0 gets the squid template, row 1 gets crab, rows 2+ get humanoid.
Step 7: Drawing Polygons
A reusable function to draw any polygon (ship, alien) with the game's outline style:
function drawPolygon(pts, cx, cy, color, scale) {
ctx.save();
ctx.translate(cx, cy);
if (scale) ctx.scale(scale, scale);
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (var i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.fillStyle = color;
ctx.globalAlpha = 0.12;
ctx.fill();
ctx.globalAlpha = 1;
ctx.stroke();
ctx.restore();
}
Scale lets you make aliens slightly different sizes per tier.
Step 8: Alien Group Movement
Aliens move as a group — they drift sideways, then descend when hitting a wall:
var alienDir = 1; // 1 = right, -1 = left
var alienSpeed = 60; // pixels per second
function updateAliens(dt) {
var hitWall = false;
// Move all aliens
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
aliens[i].x += alienSpeed * alienDir * dt;
if (aliens[i].x < 20 || aliens[i].x > W - 20) hitWall = true;
}
// If any alien hit a wall, reverse direction and descend
if (hitWall) {
alienDir *= -1;
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
aliens[i].y += 20; // descend
aliens[i].x += alienSpeed * alienDir * dt; // step away from wall
}
}
}
The descent amount (20px per wall hit) means aliens march downward as they zigzag.
Step 9: Bullets
Player bullets
Fire upward on Space key press:
var playerBullets = [];
function fireBullet() {
if (fireCooldown > 0) return;
playerBullets.push({
x: player.x, y: player.y - 20,
vy: -400, // negative = upward
life: 2.0, // seconds
w: 4, h: 12,
});
fireCooldown = 0.25;
}
Update bullet positions and remove those that leave the screen:
for (var i = playerBullets.length - 1; i >= 0; i--) {
var b = playerBullets[i];
b.y += b.vy * dt;
b.life -= dt;
if (b.life <= 0 || b.y < -20) playerBullets.splice(i, 1);
}
Alien bullets
Aliens fire randomly downward:
var alienBullets = [];
var alienFireTimer = 2.0;
function alienFire() {
var alive = aliens.filter(function(a) { return a.alive; });
if (alive.length === 0) return;
var shooter = alive[Math.floor(Math.random() * alive.length)];
alienBullets.push({
x: shooter.x, y: shooter.y + 14,
vy: 200, // positive = downward
w: 4, h: 8,
});
}
Fire on a timer:
alienFireTimer -= dt;
if (alienFireTimer <= 0) {
alienFire();
alienFireTimer = 1.5 + Math.random() * 0.5;
}
Step 10: Collision Detection
Player bullet vs alien
Use the ray-casting algorithm to check if a bullet point is inside an alien's polygon:
function pointInPolygon(px, py, poly) {
var inside = false, n = poly.length;
for (var i = 0, j = n - 1; i < n; j = i++) {
var xi = poly[i].x, yi = poly[i].y;
var xj = poly[j].x, yj = poly[j].y;
if ((yi > py) !== (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
inside = !inside;
}
return inside;
}
Transform the alien's polygon from local to world coordinates before checking:
function getWorldPoints(poly, cx, cy) {
var out = [];
for (var i = 0; i < poly.length; i++)
out.push({ x: poly[i].x + cx, y: poly[i].y + cy });
return out;
}
Note: aliens don't rotate, so no rotation transform is needed.
Collision check:
for (var bi = playerBullets.length - 1; bi >= 0; bi--) {
var bullet = playerBullets[bi], hit = false;
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
var wp = getWorldPoints(ALIEN_TEMPLATES[aliens[i].templateIndex].base, aliens[i].x, aliens[i].y);
if (pointInPolygon(bullet.x, bullet.y, wp)) {
aliens[i].alive = false;
score += ALIEN_TEMPLATES[aliens[i].templateIndex].score;
hit = true;
break;
}
}
if (hit) playerBullets.splice(bi, 1);
}
Alien bullet vs player
Simple AABB overlap check:
for (var i = 0; i < alienBullets.length; i++) {
var b = alienBullets[i];
if (b.x > player.x - 14 && b.x < player.x + 14 &&
b.y > player.y - 16 && b.y < player.y + 16) {
alienBullets.splice(i, 1);
playerDie();
break;
}
}
Step 11: Lives System
var lives = 3;
function playerDie() {
lives--;
if (lives <= 0) {
running = false; // game over
} else {
// Respawn with invincibility
player.x = W / 2;
player.invincible = 1.5;
}
}
During invincibility, make the ship blink by skipping draw on even blink frames:
if (player.invincible <= 0 || Math.floor(player.invincible * 10) % 2 === 0) {
drawPolygon(SHIP_POLYGON, player.x, player.y, shipColour);
}
Step 12: Rounds
When all aliens are cleared, start a new round:
var round = 0;
function startRound() {
round++;
aliens = createAliens(ALIEN_COLS + round - 1, ALIEN_ROWS);
playerBullets = [];
alienBullets = [];
alienDir = 1;
alienSpeed = 60 + (round - 1) * 10;
}
Each round adds one more column and increases alien speed slightly.
Step 13: Game Over
Set running = false. The draw loop renders a semi-transparent overlay and "Game Over" text:
if (!running) {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#eee';
ctx.font = 'bold 48px sans-serif';
ctx.fillText('Game Over', W / 2, H / 2 - 30);
ctx.font = '24px sans-serif';
ctx.fillText('Score: ' + score, W / 2, H / 2 + 20);
ctx.font = '16px sans-serif';
ctx.globalAlpha = 0.6;
ctx.fillText('Enter to play again', W / 2, H / 2 + 65);
ctx.globalAlpha = 1;
}
Also check if any alien reaches the bottom of the screen — instant game over:
for (var i = 0; i < aliens.length; i++) {
if (aliens[i].alive && aliens[i].y > H - 60) {
gameOver = true;
}
}
Complete Example
A self-contained HTML file combining every step above — a playable Space Invaders clone with polygon aliens, grid movement, lives, and rounds:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Space Invaders++</title>
<style>
body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { display: block; width: 800px; height: 600px; }
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<script>
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var W = 800, H = 600;
var keys = {};
var running = false;
var score = 0, round = 0;
var player, aliens = [], playerBullets = [], alienBullets = [];
var alienDir = 1, alienSpeed = 60, fireCooldown = 0, alienFireTimer = 2;
var lives = 3;
window.addEventListener('keydown', function(e) {
keys[e.code] = true;
if (['ArrowLeft','ArrowRight','Space'].includes(e.code)) e.preventDefault();
if (e.code === 'Enter' && !running) restart();
});
window.addEventListener('keyup', function(e) { keys[e.code] = false; });
function rand(min, max) { return Math.random() * (max - min) + min; }
var SHIP_POLYGON = [{ x: 0, y: -12 }, { x: 14, y: 10 }, { x: 12, y: 14 }, { x: 0, y: 8 }, { x: -12, y: 14 }, { x: -14, y: 10 }];
var SQUID = [
{ x: 0, y: -15 }, { x: 3, y: -13 }, { x: 6, y: -11 }, { x: 8, y: -7 },
{ x: 7, y: -2 }, { x: 9, y: 3 }, { x: 7, y: 8 }, { x: 4, y: 13 },
{ x: 1, y: 11 }, { x: 0, y: 13 }, { x: -1, y: 11 }, { x: -4, y: 13 },
{ x: -7, y: 8 }, { x: -9, y: 3 }, { x: -7, y: -2 }, { x: -8, y: -7 },
{ x: -6, y: -11 }, { x: -3, y: -13 },
];
var CRAB = [
{ x: 0, y: -11 }, { x: 4, y: -10 }, { x: 8, y: -8 }, { x: 12, y: -7 },
{ x: 9, y: -3 }, { x: 14, y: 1 }, { x: 10, y: 3 }, { x: 9, y: 7 },
{ x: 6, y: 11 }, { x: 3, y: 10 }, { x: 0, y: 12 }, { x: -3, y: 10 },
{ x: -6, y: 11 }, { x: -9, y: 7 }, { x: -10, y: 3 }, { x: -14, y: 1 },
{ x: -9, y: -3 }, { x: -12, y: -7 }, { x: -8, y: -8 }, { x: -4, y: -10 },
];
var HUMANOID = [
{ x: 0, y: -17 }, { x: 4, y: -15 }, { x: 7, y: -12 }, { x: 10, y: -8 },
{ x: 13, y: -5 }, { x: 11, y: -1 }, { x: 7, y: 1 }, { x: 9, y: 6 },
{ x: 7, y: 12 }, { x: 4, y: 10 }, { x: 0, y: 13 }, { x: -4, y: 10 },
{ x: -7, y: 12 }, { x: -9, y: 6 }, { x: -7, y: 1 }, { x: -11, y: -1 },
{ x: -13, y: -5 }, { x: -10, y: -8 }, { x: -7, y: -12 }, { x: -4, y: -15 },
];
var ALIEN_TEMPLATES = [
{ base: SQUID, score: 100, colour: '#e74c3c' },
{ base: CRAB, score: 50, colour: '#3498db' },
{ base: HUMANOID, score: 20, colour: '#f1c40f' },
];
function createAliens(cols, rows) {
var list = [];
var spacingX = 60, spacingY = 50;
var gridW = cols * spacingX, gridH = rows * spacingY;
var startX = (W - gridW) / 2 + spacingX / 2;
var startY = 60;
for (var r = 0; r < rows; r++) {
var ti = Math.min(r, ALIEN_TEMPLATES.length - 1);
for (var c = 0; c < cols; c++) {
list.push({ x: startX + c * spacingX, y: startY + r * spacingY, templateIndex: ti, alive: true });
}
}
return list;
}
function drawPolygon(pts, cx, cy, colour, scale) {
ctx.save();
ctx.translate(cx, cy);
if (scale) ctx.scale(scale, scale);
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (var i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
ctx.strokeStyle = colour;
ctx.lineWidth = 1;
ctx.fillStyle = colour;
ctx.globalAlpha = 0.12;
ctx.fill();
ctx.globalAlpha = 1;
ctx.stroke();
ctx.restore();
}
function getWorldPoints(poly, cx, cy) {
var out = [];
for (var i = 0; i < poly.length; i++) out.push({ x: poly[i].x + cx, y: poly[i].y + cy });
return out;
}
function pointInPolygon(px, py, poly) {
var inside = false, n = poly.length;
for (var i = 0, j = n - 1; i < n; j = i++) {
var xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
if ((yi > py) !== (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
function playerDie() {
lives--;
if (lives <= 0) { running = false; return; }
player.x = W / 2;
player.invincible = 1.5;
}
function fireBullet() {
if (fireCooldown > 0) return;
playerBullets.push({ x: player.x, y: player.y - 20, vy: -400, life: 2.0, w: 4, h: 12 });
fireCooldown = 0.25;
}
function alienFire() {
var alive = [];
for (var i = 0; i < aliens.length; i++) if (aliens[i].alive) alive.push(aliens[i]);
if (alive.length === 0) return;
var shooter = alive[randInt(0, alive.length - 1)];
alienBullets.push({ x: shooter.x, y: shooter.y + 14, vy: 200, w: 4, h: 8 });
}
function randInt(min, max) { return Math.floor(rand(min, max + 1)); }
function startRound() {
round++;
aliens = createAliens(7 + round - 1, 3);
playerBullets = [];
alienBullets = [];
alienDir = 1;
alienSpeed = 60 + (round - 1) * 10;
if (player) { player.x = W / 2; player.invincible = 1.5; }
}
function initGame() {
player = { x: W / 2, y: H - 40, invincible: 0 };
score = 0; round = 0; lives = 3; running = true;
fireCooldown = 0; alienFireTimer = 2;
startRound();
}
function update(dt) {
if (!running) return;
player.x += (keys['ArrowRight'] - keys['ArrowLeft']) * 300 * dt;
player.x = Math.max(20, Math.min(W - 20, player.x));
if (player.invincible > 0) player.invincible -= dt;
fireCooldown = Math.max(0, fireCooldown - dt);
if (keys['Space']) fireBullet();
// Player bullets
for (var i = playerBullets.length - 1; i >= 0; i--) {
var b = playerBullets[i];
b.y += b.vy * dt;
b.life -= dt;
if (b.life <= 0 || b.y < -20) playerBullets.splice(i, 1);
}
// Alien bullets
for (var i = alienBullets.length - 1; i >= 0; i--) {
var b = alienBullets[i];
b.y += b.vy * dt;
if (b.y > H + 20) alienBullets.splice(i, 1);
}
// Alien movement
var hitWall = false;
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
aliens[i].x += alienSpeed * alienDir * dt;
if (aliens[i].x < 20 || aliens[i].x > W - 20) hitWall = true;
}
if (hitWall) {
alienDir *= -1;
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
aliens[i].y += 20;
aliens[i].x += alienSpeed * alienDir * dt;
}
}
// Alien fire timer
alienFireTimer -= dt;
if (alienFireTimer <= 0) { alienFire(); alienFireTimer = 1.5 + rand(0, 1); }
// Alien reached bottom?
for (var i = 0; i < aliens.length; i++) {
if (aliens[i].alive && aliens[i].y > H - 60) { running = false; return; }
}
// Player bullet vs alien
for (var bi = playerBullets.length - 1; bi >= 0; bi--) {
var bullet = playerBullets[bi], hit = false;
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
var t = ALIEN_TEMPLATES[aliens[i].templateIndex];
var wp = getWorldPoints(t.base, aliens[i].x, aliens[i].y);
if (pointInPolygon(bullet.x, bullet.y, wp)) {
aliens[i].alive = false;
score += t.score;
hit = true;
break;
}
}
if (hit) playerBullets.splice(bi, 1);
}
// Alien bullet vs player
if (player.invincible <= 0) {
for (var i = 0; i < alienBullets.length; i++) {
var b = alienBullets[i];
if (b.x > player.x - 14 && b.x < player.x + 14 &&
b.y > player.y - 16 && b.y < player.y + 16) {
alienBullets.splice(i, 1);
playerDie();
return;
}
}
}
// Win check
var aliveCount = 0;
for (var i = 0; i < aliens.length; i++) if (aliens[i].alive) aliveCount++;
if (aliveCount === 0) startRound();
}
function draw() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
// Aliens
for (var i = 0; i < aliens.length; i++) {
if (!aliens[i].alive) continue;
var a = aliens[i];
var t = ALIEN_TEMPLATES[a.templateIndex];
drawPolygon(t.base, a.x, a.y, t.colour, 0.8 + a.templateIndex * 0.2);
}
// Player bullets
for (var i = 0; i < playerBullets.length; i++) {
var b = playerBullets[i];
ctx.fillStyle = '#ff4444';
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
}
// Alien bullets
for (var i = 0; i < alienBullets.length; i++) {
var b = alienBullets[i];
ctx.fillStyle = '#ffaa00';
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
}
// Player ship
if (player.invincible <= 0 || Math.floor(player.invincible * 10) % 2 === 0) {
drawPolygon(SHIP_POLYGON, player.x, player.y, '#4a9eff');
}
// HUD
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillStyle = '#eee';
ctx.font = '18px sans-serif';
ctx.fillText('Lives: ' + lives, 20, 10);
ctx.textAlign = 'right';
ctx.fillText('Score: ' + score, W - 20, 10);
// Game over
if (!running) {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#eee';
ctx.font = 'bold 48px sans-serif';
ctx.fillText('Game Over', W / 2, H / 2 - 30);
ctx.font = '24px sans-serif';
ctx.fillText('Score: ' + score, W / 2, H / 2 + 20);
ctx.font = '16px sans-serif';
ctx.globalAlpha = 0.6;
ctx.fillText('Enter to play again', W / 2, H / 2 + 65);
ctx.globalAlpha = 1;
}
}
var lastTime = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
var dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(gameLoop);
}
function restart() { initGame(); }
initGame();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
Open this file in a browser and you'll have a fully working Space Invaders clone built from the techniques in this guide. Left/Right to move, Space to fire, Enter to restart after game over.
Next Steps
- Add particle effects on alien destruction
- Add destructible barriers (shields) for the player to hide behind
- Add sound effects using the Web Audio API
- Implement power-ups (spread shot, shield, extra life)
- Add a high score system with localStorage
- Add mobile touch controls
Reference
- MDN Canvas API
- Ray casting algorithm (point in polygon)
- See the full implementation at
/invadersandstatic/js/invaders.js