
Building a Simple Space Invaders-Style Game in HTML/JS/CSS (and Rebuilding in Phaser)
Table of Contents
Introduction
Hey there, I’m Nate Ross! Today, I'm absolutely thrilled to walk you through building a simple “Space Invaders”-style game in good old HTML, CSS, and JavaScript.
Then, in the second part, I’ll show you how quickly we can recreate (and further expand) that same game using the Phaser framework. Let’s dive right in!
Part 1: Building a Basic Space Invaders Game in HTML/JS/CSS
Game Design Essentials
Before we code, let’s outline what we want:
- A simple grid of enemy invaders at the top of the screen.
- A player-controlled ship at the bottom that can move left/right and shoot projectiles.
- If a projectile hits an invader, that invader disappears.
- If the invaders reach the bottom, the player loses.
Basic Setup
First, I'll create a basic HTML page. Let’s call it index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Space Invaders (Vanilla JS)</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<canvas id="gameCanvas" width="600" height="600"></canvas>
<script src="game.js"></script>
</body>
</html>
Styling with CSS
Next, I’ll add some simple styling in styles.css
. We just want to remove margins, ensure the canvas is centered, etc.
html, body {
margin: 0;
padding: 0;
background: #000;
font-family: sans-serif;
}
#gameCanvas {
display: block;
margin: 0 auto;
background: #111;
}
Core JavaScript Game Logic
Now the fun part: let’s handle our game logic. We’ll use the canvas
element, draw our player, move them around, create invaders, and detect collisions. In game.js
:
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
let rightPressed = false;
let leftPressed = false;
// Player
const player = {
x: canvasWidth / 2 - 20,
y: canvasHeight - 50,
width: 40,
height: 20,
speed: 5
};
// Projectiles
let projectiles = [];
// Invaders
const invaders = [];
const rows = 3;
const columns = 8;
const invaderWidth = 40;
const invaderHeight = 20;
const invaderPadding = 10;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < columns; c++) {
invaders.push({
x: c * (invaderWidth + invaderPadding) + 50,
y: r * (invaderHeight + invaderPadding) + 30,
width: invaderWidth,
height: invaderHeight,
alive: true
});
}
}
// Key Listeners
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
} else if (e.key === " " || e.key === "Spacebar") {
// Shoot
shoot();
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function shoot() {
projectiles.push({
x: player.x + player.width / 2 - 2,
y: player.y,
width: 4,
height: 10,
speed: 6
});
}
function update() {
// Move player
if (rightPressed && player.x < canvasWidth - player.width) {
player.x += player.speed;
} else if (leftPressed && player.x > 0) {
player.x -= player.speed;
}
// Move projectiles
projectiles.forEach((proj) => {
proj.y -= proj.speed;
});
// Remove off-screen projectiles
projectiles = projectiles.filter((proj) => proj.y > 0);
// Check collisions
for (let inv of invaders) {
if (!inv.alive) continue;
for (let proj of projectiles) {
if (
proj.x < inv.x + inv.width &&
proj.x + proj.width > inv.x &&
proj.y < inv.y + inv.height &&
proj.y + proj.height > inv.y
) {
inv.alive = false;
proj.y = -999; // move projectile off screen
}
}
}
}
function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw player
ctx.fillStyle = "lightgreen";
ctx.fillRect(player.x, player.y, player.width, player.height);
// Draw invaders
invaders.forEach((inv) => {
if (inv.alive) {
ctx.fillStyle = "red";
ctx.fillRect(inv.x, inv.y, inv.width, inv.height);
}
});
// Draw projectiles
projectiles.forEach((proj) => {
ctx.fillStyle = "yellow";
ctx.fillRect(proj.x, proj.y, proj.width, proj.height);
});
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
That’s the bare-bones version of the classic “Space Invaders.” We’re drawing the player, invaders, and projectiles on the canvas
each frame.
We handle keyboard presses to move the player and shoot projectiles. When a projectile collides with an invader, that invader is marked as not alive.
Play It Here (Vanilla Version)
Below is an embedded iframe demo of our vanilla HTML/CSS/JS “Space Invaders” in action. Use arrow keys to move left and right, and space bar to shoot! (Note: This is just a placeholder path—adapt it to your setup if you're replicating the project.)
Part 2: Rebuilding (and Expanding) the Same Game in Phaser
Why Phaser?
Phaser is a popular 2D game framework for JavaScript developers. It's perfect for quickly spinning up game prototypes. If coding from scratch is a bit involved, Phaser provides a lot of functionality—like sprite management, collision detection, scene handling—out of the box.
Setting Up Phaser
To get started, we can include Phaser by downloading the library or referencing a CDN. Then, we initialize a Phaser.Game instance and define our scene(s). Below, I start with a straightforward example for the same Space-Invaders style mechanics:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Space Invaders (Phaser)</title>
<script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
</head>
<body>
<div id="game"></div>
<script src="main.js"></script>
</body>
</html>
Then in main.js
, we define our preload, create, and update functions:
const config = {
type: Phaser.AUTO,
width: 600,
height: 600,
scene: {
preload,
create,
update
},
parent: 'game'
};
let player;
let cursors;
let bullets;
let invaders;
let lastFired = 0;
const game = new Phaser.Game(config);
function preload() {
// Load any assets if needed (e.g., images)
}
function create() {
// Create player
player = this.add.rectangle(300, 550, 40, 20, 0x00ff00);
this.physics.add.existing(player);
// Create bullet group
bullets = this.physics.add.group({
defaultKey: null,
maxSize: 10
});
// Create invaders
invaders = this.physics.add.group();
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 8; c++) {
let invader = this.add.rectangle(50 + c * 50, 30 + r * 30, 40, 20, 0xff0000);
this.physics.add.existing(invader);
invaders.add(invader);
}
}
cursors = this.input.keyboard.createCursorKeys();
// Collision detection
this.physics.add.overlap(bullets, invaders, handleHit, null, this);
}
function update(time) {
// Move player
if (cursors.left.isDown) {
player.x -= 3;
} else if (cursors.right.isDown) {
player.x += 3;
}
// Shoot
if (cursors.space.isDown && time > lastFired) {
fireBullet(this);
lastFired = time + 300; // 300ms delay
}
}
function fireBullet(scene) {
const bullet = scene.add.rectangle(player.x, player.y - 20, 4, 10, 0xffff00);
scene.physics.add.existing(bullet);
bullet.body.velocity.y = -300;
bullets.add(bullet);
}
function handleHit(bullet, invader) {
bullet.destroy();
invader.destroy();
}
That’s all there is to it! Phaser handles the physics, and we get a simple method to create objects. Collision detection is managed with this.physics.add.overlap()
for bullet-to-invader interactions.
Play It Here (Phaser Simplified Version)
Below is an embedded iframe with a simplified Phaser version of our game in action. Again, you can move with left/right arrow keys and shoot with space bar.
An Extended Example: Multiple Scenes, Score, and Lives (Phaser 3.60)
If you want to take it a step further and introduce more features—like multiple scenes (Title, Main, and Game Over), score counters, and multiple lives—here’s a more advanced version of our Cosmic Invaders game in Phaser 3.60.
We’ll use three scenes (TitleScene, MainScene, GameOverScene) and store persistent game data (score and lives) in a simple object.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cosmic Invaders (Phaser 3)</title>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
</head>
<body style="margin: 0; padding: 0; background: #000">
<script>
// A few global game settings:
const GAME_WIDTH = 400;
const GAME_HEIGHT = 300;
// We'll store shared data (score, lives) in a simple object:
let gameData = {
score: 0,
lives: 3,
};
class TitleScene extends Phaser.Scene {
constructor() {
super("TitleScene");
}
preload() {
// Generate star texture (a 2x2 white pixel).
const starGraphics = this.make.graphics({ x: 0, y: 0, add: false });
starGraphics.fillStyle(0xffffff, 1);
starGraphics.fillRect(0, 0, 2, 2);
starGraphics.generateTexture("star", 2, 2);
// Create a cosmic gradient-like background...
let bgGraphics = this.make.graphics({ x: 0, y: 0, add: false });
bgGraphics.fillStyle(0x07051c, 1);
bgGraphics.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
bgGraphics.fillStyle(0x291b51, 0.5);
bgGraphics.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
bgGraphics.generateTexture("cosmicBg", GAME_WIDTH, GAME_HEIGHT);
// Player texture: green rectangle with outline
const playerGraphics = this.make.graphics({ x: 0, y: 0, add: false });
playerGraphics.fillStyle(0x00ff00, 1);
playerGraphics.fillRect(-20, -10, 40, 20);
playerGraphics.lineStyle(2, 0x66ff66, 0.7);
playerGraphics.strokeRect(-20, -10, 40, 20);
playerGraphics.generateTexture("player", 40, 20);
// Enemy texture: red square with outline
const enemyGraphics = this.make.graphics({ x: 0, y: 0, add: false });
enemyGraphics.fillStyle(0xff0000, 1);
enemyGraphics.fillRect(-10, -10, 20, 20);
enemyGraphics.lineStyle(2, 0xff6666, 0.7);
enemyGraphics.strokeRect(-10, -10, 20, 20);
enemyGraphics.generateTexture("enemy", 20, 20);
// Bullet texture: narrow white rectangle
const bulletGraphics = this.make.graphics({ x: 0, y: 0, add: false });
bulletGraphics.fillStyle(0xffffff, 1);
bulletGraphics.fillRect(-2, -5, 4, 10);
bulletGraphics.generateTexture("bullet", 4, 10);
}
create() {
// Reset game data
gameData.score = 0;
gameData.lives = 3;
// Add cosmic background
this.add.image(0, 0, "cosmicBg").setOrigin(0);
// Create starfield group
this.stars = this.add.group();
for (let i = 0; i < 50; i++) {
let star = this.add.image(
Phaser.Math.Between(0, GAME_WIDTH),
Phaser.Math.Between(0, GAME_HEIGHT),
"star"
);
star.setScale(Phaser.Math.FloatBetween(0.5, 1.5));
star.speed = Phaser.Math.Between(20, 60);
this.stars.add(star);
}
// Title screen text
this.titleText = this.add
.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 20, "COSMIC INVADERS", {
fontFamily: "Arial",
fontSize: "20px",
fontStyle: "bold",
color: "#ffffff",
})
.setOrigin(0.5);
// Press SPACE prompt
this.startText = this.add
.text(
GAME_WIDTH / 2,
GAME_HEIGHT / 2 + 10,
"[Press SPACE to Start]",
{
fontFamily: "Arial",
fontSize: "14px",
color: "#ffffff",
}
)
.setOrigin(0.5);
this.spaceKey = this.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
}
update(time, delta) {
// Move stars downward
this.stars.children.each((star) => {
star.y += star.speed * (delta / 1000);
if (star.y > GAME_HEIGHT) {
star.y = 0;
star.x = Phaser.Math.Between(0, GAME_WIDTH);
}
});
// Start on SPACE
if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) {
this.scene.start("MainScene");
}
}
}
class MainScene extends Phaser.Scene {
constructor() {
super("MainScene");
}
create() {
// Background
this.bg = this.add.image(0, 0, "cosmicBg").setOrigin(0);
// Starfield
this.stars = this.add.group();
for (let i = 0; i < 50; i++) {
let star = this.add.image(
Phaser.Math.Between(0, GAME_WIDTH),
Phaser.Math.Between(0, GAME_HEIGHT),
"star"
);
star.setScale(Phaser.Math.FloatBetween(0.5, 1.5));
star.speed = Phaser.Math.Between(20, 60);
this.stars.add(star);
}
// Variables
this.playerSpeed = 200;
this.bulletSpeed = 300;
this.lastFired = 0;
this.fireRate = 400;
this.alienDirection = 1;
this.alienMoveX = 25;
this.alienMoveDown = 10;
// Player
this.player = this.physics.add.sprite(
GAME_WIDTH / 2,
GAME_HEIGHT - 30,
"player"
);
this.player.setCollideWorldBounds(true);
// Bullets
this.bullets = this.physics.add.group({
defaultKey: "bullet",
maxSize: 20,
});
// Enemies
this.enemies = this.physics.add.group();
this.createEnemyWave();
// Overlaps for bullet/enemy collisions
this.physics.add.overlap(
this.bullets,
this.enemies,
this.hitEnemy,
null,
this
);
// Input
this.cursors = this.input.keyboard.createCursorKeys();
this.spaceKey = this.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
// UI text
this.scoreText = this.add.text(10, 10, `Score: ${gameData.score}`, {
fontFamily: "Arial",
fontSize: "12px",
color: "#ffffff",
});
this.livesText = this.add
.text(320, 10, `Lives: ${gameData.lives}`, {
fontFamily: "Arial",
fontSize: "12px",
color: "#ffffff",
})
.setOrigin(1, 0);
}
update(time, delta) {
// Move starfield
this.stars.children.each((star) => {
star.y += star.speed * (delta / 1000);
if (star.y > GAME_HEIGHT) {
star.y = 0;
star.x = Phaser.Math.Between(0, GAME_WIDTH);
}
});
// Move player
this.player.setVelocityX(0);
if (this.cursors.left.isDown) {
this.player.setVelocityX(-this.playerSpeed);
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(this.playerSpeed);
}
// Fire bullet
if (this.spaceKey.isDown && time > this.lastFired) {
this.fireBullet();
this.lastFired = time + this.fireRate;
}
// Move enemies horizontally and bounce
let outOfBounds = false;
this.enemies.children.each((enemy) => {
enemy.x += this.alienMoveX * this.alienDirection * (delta / 1000);
if (enemy.x < 10 || enemy.x > GAME_WIDTH - 10) {
outOfBounds = true;
}
});
if (outOfBounds) {
this.alienDirection *= -1;
this.enemies.children.each((enemy) => {
enemy.y += this.alienMoveDown;
if (enemy.y > GAME_HEIGHT - 50) {
this.lostLife();
}
});
}
}
createEnemyWave() {
let rows = 3;
let cols = 8;
let offsetX = 40;
let offsetY = 30;
let spacingX = 30;
let spacingY = 25;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let x = offsetX + c * spacingX;
let y = offsetY + r * spacingY;
let enemy = this.enemies.create(x, y, "enemy");
enemy.setOrigin(0.5);
}
}
}
fireBullet() {
const bullet = this.bullets.get(this.player.x, this.player.y - 15);
if (bullet) {
bullet.setActive(true);
bullet.setVisible(true);
bullet.body.velocity.y = -this.bulletSpeed;
}
}
hitEnemy(bullet, enemy) {
bullet.destroy();
enemy.destroy();
gameData.score += 10;
this.scoreText.setText(`Score: ${gameData.score}`);
// If no active enemies remain, spawn new wave
if (this.enemies.countActive() === 0) {
this.createEnemyWave();
}
}
lostLife() {
gameData.lives--;
this.livesText.setText(`Lives: ${gameData.lives}`);
// Move enemies back up a bit
this.enemies.children.each((enemy) => {
enemy.y -= 40;
});
// Check for game over
if (gameData.lives <= 0) {
this.scene.start("GameOverScene");
}
}
}
class GameOverScene extends Phaser.Scene {
constructor() {
super("GameOverScene");
}
create() {
this.add.image(0, 0, "cosmicBg").setOrigin(0);
// Starfield
this.stars = this.add.group();
for (let i = 0; i < 50; i++) {
let star = this.add.image(
Phaser.Math.Between(0, GAME_WIDTH),
Phaser.Math.Between(0, GAME_HEIGHT),
"star"
);
star.setScale(Phaser.Math.FloatBetween(0.5, 1.5));
star.speed = Phaser.Math.Between(20, 60);
this.stars.add(star);
}
// Game over text
this.add
.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 20, "GAME OVER", {
fontFamily: "Arial",
fontSize: "20px",
color: "#ff4444",
})
.setOrigin(0.5);
// Final score
this.add
.text(
GAME_WIDTH / 2,
GAME_HEIGHT / 2 + 10,
`FINAL SCORE: ${gameData.score}`,
{ fontFamily: "Arial", fontSize: "14px", color: "#ffffff" }
)
.setOrigin(0.5);
// Play again
this.add
.text(
GAME_WIDTH / 2,
GAME_HEIGHT / 2 + 40,
"[Press SPACE to Restart]",
{ fontFamily: "Arial", fontSize: "12px", color: "#ffffff" }
)
.setOrigin(0.5);
this.spaceKey = this.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
}
update(time, delta) {
// Move stars
this.stars.children.each((star) => {
star.y += star.speed * (delta / 1000);
if (star.y > GAME_HEIGHT) {
star.y = 0;
star.x = Phaser.Math.Between(0, GAME_WIDTH);
}
});
// Restart on SPACE
if (Phaser.Input.Keyboard.JustDown(this.spaceKey)) {
this.scene.start("TitleScene");
}
}
}
// Game config
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH,
height: GAME_HEIGHT,
backgroundColor: "#000000",
physics: {
default: "arcade",
arcade: {
debug: false,
},
},
scene: [TitleScene, MainScene, GameOverScene],
};
// Create the game
const game = new Phaser.Game(config);
</script>
</body>
</html>
In this approach, we feature multiple scenes for the title screen, the main gameplay, and the game-over state. We also track a score and the number of lives.
Each time the enemies manage to get too low on the screen, you lose a life!
Play It Here (Extended Phaser Version)
Here’s the full expanded version of our game running in an iframe. It’s served from phaser-game-1.html
. Enjoy the starfield background, multiple scenes, score tracking, and more robust gameplay mechanics!
Conclusion
I hope this all-in-one guide showed you how to build a fun “Space Invaders”-style project from scratch and how a framework like Phaser can make your life so much easier—especially when you start adding features like multiple scenes, UI elements, physics, collisions, and more.
Whether you prefer the raw approach or the convenience of Phaser, remember that practice is key to mastering game development. So get out there, build something cool, and enjoy the ride!
Thank you for reading, and stay tuned for more fun dev adventures!