Write an HTML and JavaScript page implementing space invaders
<think>
Okay, the user wants me to create a Space Invaders game using HTML and JavaScript. Let's see... I need to start by setting up the basic structure with an HTML5 canvas element. The canvas will be where all the game graphics are drawn.
First, I'll include a script tag in the HTML to handle the JavaScript. Then, in the JS part, I'll get the canvas context for 2D drawing. The game loop will probably use requestAnimationFrame to keep it smooth.
Next, the player's ship needs to be controlled. Let me think: the player should move left and right with arrow keys or A/D, and shoot bullets. I'll need to set up event listeners for keyboard input. Maybe an object to track key states.
For the enemies, they should form a grid that moves back and forth. Each enemy will need to be represented somehow—maybe an array of objects or just positions. I'll handle their movement, making them reverse direction when they hit the edges of the canvas.
Bullets from the player will be another array. They'll move upwards, and I need to check for collisions between bullets and enemies. When a collision happens, the enemy should be removed or marked as destroyed.
Collision detection: for each bullet and each enemy, check if their rectangles overlap. If yes, remove both from their arrays.
Enemies should also shoot bullets occasionally. Maybe a timer that randomly selects an enemy to fire. That adds some challenge.
Game over conditions: if enemies reach the bottom of the canvas, or if the player is hit. Although in classic Space Invaders, the player doesn't get hit unless they are under an enemy when it drops. Hmm, maybe implement that as a game over condition.
For the game loop steps: clear the canvas each frame, update positions (enemies moving left/right with a slight downward drift), check for collisions, draw all elements.
Let me outline the steps:
- Set up canvas and context.
- Define player object with x position, width, height.
- Enemies: grid of enemies. Maybe a 2D array or list. Each has x, y position.
- Bullets: two types—player bullets and enemy bullets.
- Keyboard controls for left/right movement (arrow keys or A/D).
- Shooting mechanics: spacebar to shoot, with a cooldown.
- Enemy movement logic: move left/right, reverse direction when hitting edges or walls.
- Collision detection between bullets and enemies (and maybe player if needed).
- Game over logic when enemies reach the bottom.
- Scoring system (optional, but user didn't specify).
Potential issues to consider:
- Performance: with many enemies and bullets, need efficient loops.
- Edge cases: when all enemies are destroyed (win condition?).
- Bullet cleanup to prevent memory leaks.
Let me start coding step by step.
First, HTML structure. The canvas element with id "game" and set some width/height. Then the script section.
In JavaScript, variables for canvas, context, player's x position. Enemies array, bullets arrays. Also variables for key presses.
Player movement: on keydown and keyup events to track left/right keys. In the game loop, update player position based on keys pressed.
Shooting: when spacebar is pressed, add a bullet to the bullets array. Each bullet has x and y position, moving upwards.
Enemies: create a grid, maybe 5 rows by 8 columns. Each enemy's position is calculated based on their row and column. They move left or right, and when they reach the edge, they switch direction and drop down.
Drawing functions: clear screen, draw player as a rectangle (maybe later replace with images), enemies as rectangles or shapes. Bullets as small lines.
Collision detection: for each bullet, check against enemies. If collision, remove enemy and bullet.
Game loop function, called recursively with requestAnimationFrame.
Let me think about the initial setup. Let's set canvas size to 480x640, for example.
Testing: need to make sure enemies move correctly and bullets hit them. Also, enemy shooting logic—maybe a random chance each frame for enemies to shoot.
For the player's bullets, set their speed higher than enemy bullets. Also, when a bullet goes off-screen, remove it.
Now code structure:
-
Variables: canvas, ctx, player, enemies, bullets (player and enemy?), keys, etc.
-
InitializeEnemies function to create the grid.
-
drawPlayer(), drawEnemies(), drawBullets() functions.
-
update() function to handle movement, collisions.
Let me start writing the code.
Wait, maybe separate player bullets and enemy bullets. Each bullet has a direction (up or down). So have two arrays, or one array with a type or direction.
Probably better to have a single bullets array with properties like x, y, speed (could be negative or positive for direction). But maybe separate playerBullets and enemyBullets arrays for simplicity.
In the game loop, update each bullet's position. For player bullets, moving up (y decreases). Enemy bullets move down (y increases).
Collision between enemy bullets and player? Maybe add that. But original Space Invaders has no such thing, so maybe not unless the user asks for it.
For now, just handle player bullets hitting enemies. Game over when enemies reach bottom.
Testing collision: for each enemy, check if any player bullet is within its bounds. If so, remove both.
Enemies should also move left/right and drop down when they hit the screen edges. Their movement direction can be tracked with a variable, say dx (direction x), and when they hit the edge, reverse dx and increase dy (downward step).
Let me code that.
Potential problem: when enemies move left or right, check if they are about to go beyond the canvas. For example, if moving right and their x + width exceeds canvas.width minus padding.
Now, let's code step by step.
First HTML:
<title>Space Invaders</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } canvas { border: 1px solid black; background: #000; } </style> <script> // JavaScript code here </script>Then in the script:
const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d');
Player setup: let player = { x: canvas.width / 2 - 15, y: canvas.height - 30, width: 30, height: 10, dx: 0 };
Keyboard input: document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') { keys.left = true; player.dx = -5; } if (e.key === 'ArrowRight' || e.key === 'd') { keys.right = true; player.dx = 5; } if (e.key === ' ') { shootBullet(); } }); document.addEventListener('keyup', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') { keys.left = false; if (keys.right) player.dx = 5; else player.dx = 0; } if (e.key === 'ArrowRight' || e.key === 'd') { keys.right = false; if (keys.left) player.dx = -5; else player.dx = 0; } }); Need a keys object to track pressed keys. Maybe define let keys = { left: false, right: false };
Shooting function shootBullet() adds a bullet to playerBullets array. But need to prevent rapid fire. Add a cooldown.
Let's have a lastShot timestamp. Only allow shooting every 300ms or so.
Now, enemies:
Initialize a grid of enemies. Let's say 5 rows and 8 columns.
let enemies = []; function initEnemies() { for (let row = 0; row < 5; row++) { for (let col = 0; col < 8; col++) { enemies.push({ x: 50 + col * 50, y: 50 + row * 40, width: 30, height: 20, dx: 2 }); } } }
Each enemy has x, y, size, and movement direction.
But maybe have a single dx for all enemies. So in the code, track enemyDirection (1 for right, -1 for left).
In updateEnemyPosition() function:
Check the farthest enemies. For example, when moving right, check if any enemy's x + width exceeds canvas.width. If so, reverse direction and move down.
Similarly for left: check if x is less than 0.
So in the game loop:
for each enemy, move them by dx. But need to check boundaries.
But handling this for all enemies may be redundant. Maybe track the edges of the enemy group.
Alternatively, in each frame:
Calculate new x positions for all enemies. Check if any enemy's x + width > canvas.width or x < 0.
If yes, reverse dx direction and move down.
Let me think of variables:
let enemyDirection = 1; // 1 for right, -1 for left
In the game loop:
enemies.forEach(enemy => { enemy.x += enemyDirection * enemy.dx; });
Then check if any enemy is out of bounds. If so, reverse direction and move all enemies down.
But how to check if any enemy is at the edge?
Maybe find the max x of all enemies and see if it exceeds canvas.width - enemy.width.
Alternatively, track the leftmost and rightmost positions of enemies. Maybe a bit complex.
For simplicity, perhaps check if any enemy's x + width > canvas.width or x < 0.
But in code, during each frame:
let rightMost = enemies.reduce((max, e) => Math.max(max, e.x + e.width), 0); let leftMost = enemies.reduce((min, e) => Math.min(min, e.x), 0);
if (rightMost > canvas.width || leftMost < 0) { enemyDirection *= -1; // move down enemies.forEach(e => e.y += 10); }
This would reverse direction and move down when any enemy hits the edge.
That's manageable.
Now, bullets:
let playerBullets = [];
function shootBullet() { if (canShoot) { playerBullets.push({ x: player.x + player.width / 2, y: player.y, width: 2, height: 10, speed: -5 }); canShoot = false; setTimeout(() => canShoot = true, 300); // cooldown } }
In the game loop:
update bullets:
playerBullets.forEach(bullet => { bullet.y += bullet.speed; // moves up });
Then remove bullets that go off screen:
playerBullets = playerBullets.filter(b => b.y > 0);
Drawing bullets as rectangles.
Collision detection between bullets and enemies:
Loop through all enemies and playerBullets. For each enemy, check if any bullet hits it.
But this is O(n*m) complexity, which is acceptable for small numbers.
So:
for (let i = enemies.length - 1; i >= 0; i--) { let enemy = enemies[i]; for (let j = playerBullets.length - 1; j >=0 ; j--) { let bullet = playerBullets[j]; if (bullet.x < enemy.x + enemy.width && bullet.x + bullet.width > enemy.x && bullet.y < enemy.y + enemy.height && bullet.y + bullet.height > enemy.y) { enemies.splice(i, 1); playerBullets.splice(j, 1); break; } } }
This needs to be done in the game loop.
Now, game over condition: check if any enemy reaches bottom of canvas.
enemies.some(enemy => enemy.y + enemy.height >= canvas.height) --> game over.
Now, putting it all together.
Also need to draw everything:
function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw player
ctx.fillStyle = 'white';
ctx.fillRect(player.x, player.y, player.width, player.height);
// draw enemies
ctx.fillStyle = 'lime';
enemies.forEach(enemy => {
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// draw bullets
ctx.fillStyle = 'red';
playerBullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
}
Then the game loop:
function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }
In the update function:
Handle player movement (player.x += dx, but clamped to canvas edges).
Check for game over.
Update bullets positions.
Handle enemy movement and direction changes.
Now, putting all together with code.
Potential issues: enemies moving down when they hit the edge. Also, player movement needs to be clamped so it doesn't go out of bounds.
Player.x should be between 0 and canvas.width - player.width.
So in the update:
player.x += player.dx;
if (player.x < 0) player.x = 0; if (player.x + player.width > canvas.width) { player.x = canvas.width - player.width; }
Now, let me code all that.
Also need to initialize enemies on load. So call initEnemies() when starting.
Putting it all together, the JavaScript code would be something like:
const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d');
let player = { x: canvas.width / 2 - 15, y: canvas.height - 30, width: 30, height: 10, dx: 0 };
let enemies = []; let playerBullets = []; let enemyDirection = 1; // 1 for right, -1 for left let canShoot = true; let keys = { left: false, right: false };
function initEnemies() { enemies = []; for (let row = 0; row < 5; row++) { for (let col = 0; col < 8; col++) { enemies.push({ x: 50 + col * 50, y: 50 + row * 40, width: 30, height: 20 }); } } }
function shootBullet() { if (canShoot) { playerBullets.push({ x: player.x + player.width / 2 - 1, y: player.y, width: 2, height: 10, speed: -5 }); canShoot = false; setTimeout(() => canShoot = true, 300); } }
document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') { keys.left = true; player.dx = -5; } if (e.key === 'ArrowRight' || e.key === 'd') { keys.right = true; player.dx = 5; } if (e.key === ' ') { shootBullet(); } }); document.addEventListener('keyup', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') { keys.left = false; if (keys.right) player.dx = 5; else player.dx = 0; } if (e.key === 'ArrowRight' || e.key === 'd') { keys.right = false; if (keys.left) player.dx = -5; else player.dx = 0; } });
function update() { // Player movement player.x += player.dx; if (player.x < 0) player.x = 0; else if (player.x + player.width > canvas.width) { player.x = canvas.width - player.width; }
// Update bullets
playerBullets.forEach(bullet => {
bullet.y += bullet.speed;
});
playerBullets = playerBullets.filter(bullet => bullet.y > 0);
// Enemy movement
let rightMost = enemies.reduce((max, e) => Math.max(max, e.x + e.width), 0);
let leftMost = enemies.reduce((min, e) => Math.min(min, e.x), 0);
if (rightMost > canvas.width - 10 || leftMost < 10) {
enemyDirection *= -1;
enemies.forEach(e => e.y += 20);
}
enemies.forEach(enemy => {
enemy.x += enemyDirection * 2; // moving 2 units per frame
});
// Collision detection
for (let i = enemies.length - 1; i >= 0; i--) {
let enemy = enemies[i];
for (let j = playerBullets.length - 1; j >= 0; j--) {
let bullet = playerBullets[j];
if (bullet.x < enemy.x + enemy.width &&
bullet.x + bullet.width > enemy.x &&
bullet.y < enemy.y + enemy.height &&
bullet.y + bullet.height > enemy.y) {
enemies.splice(i, 1);
playerBullets.splice(j, 1);
break;
}
}
}
// Game over check
if (enemies.some(enemy => enemy.y + enemy.height >= player.y)) {
alert('Game Over!');
initEnemies();
}
if (enemies.length === 0) {
alert('You Win!');
initEnemies();
}
}
function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw player
ctx.fillStyle = 'white';
ctx.fillRect(player.x, player.y, player.width, player.height);
// Draw enemies
ctx.fillStyle = 'lime';
enemies.forEach(enemy => {
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// Draw bullets
ctx.fillStyle = 'red';
playerBullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
}
initEnemies(); function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }
gameLoop();
Wait, but in the enemy movement code: enemies.forEach(enemy => { enemy.x += ... }), but I have set dx to 2. Maybe that's okay.
Testing this code: when the enemies hit the edge, they reverse direction and move down. The player can shoot bullets that hit enemies.
But in the collision check, maybe need to loop from end to start so when splicing, indexes are not messed up.
Also, the game over condition checks if any enemy's y + height >= player.y. That is when enemies reach the bottom where the player is.
But in original Space Invaders, it's when they touch the bottom of the screen. So maybe check enemy.y + height >= canvas.height.
So in code:
if (enemies.some(enemy => enemy.y + enemy.height >= canvas.height)) { alert('Game Over!'); initEnemies(); }
That's better.
Also, when all enemies are destroyed, show a win message.
Now, possible bugs:
- If the enemies move down and then off-screen before being destroyed. Also, in code above, after collision detection, check game over.
Another thing: when the enemies move down, their y increases. So in update(), after moving them left/right and possibly changing direction and moving down, check if any is at the bottom.
Now, putting all together, this code should work. But need to test for edge cases.
Another thing: enemy movement speed is 2 per frame. Maybe that's too fast or slow, but can adjust.
Testing in code:
Enemies start at x=50 + col50. With 8 columns, last column is 50+750=400. Canvas width is 480, so that's okay.
Another thing: in the enemy movement code, after checking for collision with walls, enemies are moved down by 20. Then their x is updated by enemyDirection * 2.
But the code as written:
if (rightMost > ... || leftMost < ...) { enemyDirection *= -1; enemies.forEach(e => e.y += 20); } enemies.forEach(enemy => { enemy.x += enemyDirection * 2; });
So enemies move down when changing direction.
Yes, that's correct. So each time they hit the wall, they drop down and reverse direction.
Now, this code should create a basic Space Invaders game. The player can move left/right and shoot. Enemies move back and forth, dropping down when hitting walls. Bullets destroy enemies. Game over if enemies reach the bottom.
Potential improvements:
- Add enemy shooting.
- Add scoring.
- Use images instead of rectangles.
- Sound effects.
But the user hasn't asked for these, so this basic version should be okay.
Here's a complete implementation of a simple Space Invaders game using HTML5 Canvas and JavaScript. This version includes:
- Player movement (left/right)
- Shooting bullets
- Enemies moving in a grid pattern and dropping down when they hit the canvas edge
- Collision detection between bullets and enemies
- Game over condition (if enemies reach the bottom)
- Win condition (when all enemies are destroyed)
- Player Movement – Controlled using arrow keys or 'A' and 'D'.
- Shooting Mechanism – Spacebar to shoot bullets (with a cooldown).
- Enemy Movement & Behavior – Enemies move left/right and drop down when they hit the canvas edge.
- Collision Detection – Bullet-enemy collisions remove both entities.
- Game Over and Win Conditions
- Game over: Any enemy reaches the bottom of the canvas.
- Win: All enemies are destroyed.
- The player is represented by a white rectangle.
- Enemies are stored in an array and move as a group (bouncing at the edges).
- Bullets are shot upward by the player and removed when they leave the canvas or hit an enemy.
- Collision is detected using basic bounding box intersection.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Space Invaders</title>
<style>
html, body {
margin: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
}
canvas {
border: 1px solid white;
background-color: black;
}
</style>
</head>
<body>
<canvas id="game" width="480" height="640"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let player = {
x: canvas.width / 2 - 15,
y: canvas.height - 30,
width: 30,
height: 10,
dx: 0
};
let enemies = [];
let playerBullets = [];
let canShoot = true;
let enemyDirection = 1; // 1 for right, -1 for left
let keys = { left: false, right: false };
function initEnemies() {
enemies = [];
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 8; col++) {
enemies.push({
x: 50 + col * 50,
y: 60 + row * 40,
width: 30,
height: 20
});
}
}
}
function shootBullet() {
if (canShoot) {
playerBullets.push({
x: player.x + player.width / 2 - 1,
y: player.y,
width: 2,
height: 10,
speed: -5
});
canShoot = false;
setTimeout(() => {
canShoot = true;
}, 300);
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'a') {
keys.left = true;
player.dx = -5;
}
if (e.key === 'ArrowRight' || e.key === 'd') {
keys.right = true;
player.dx = 5;
}
if (e.key === ' ') {
shootBullet();
}
});
document.addEventListener('keyup', (e) => {
if ((e.key === 'ArrowLeft' || e.key === 'a') && !keys.right) {
player.dx = 0;
}
if ((e.key === 'ArrowRight' || e.key === 'd') && !keys.left) {
player.dx = 0;
}
});
function update() {
// Player movement
player.x += player.dx;
if (player.x < 0) player.x = 0;
else if (player.x + player.width > canvas.width) {
player.x = canvas.width - player.width;
}
// Update bullets
for (let i = 0; i < playerBullets.length; i++) {
playerBullets[i].y += playerBullets[i].speed;
}
playerBullets = playerBullets.filter(b => b.y > 0);
// Enemy movement
let rightMost = Math.max(...enemies.map(e => e.x + e.width));
let leftMost = Math.min(...enemies.map(e => e.x));
if (rightMost > canvas.width - 10 || leftMost < 10) {
enemyDirection *= -1;
enemies.forEach(e => e.y += 20); // drop down
}
enemies.forEach(enemy => {
enemy.x += enemyDirection * 2;
});
// Bullet-Enemy collision
for (let i = enemies.length - 1; i >= 0; i--) {
let enemy = enemies[i];
for (let j = playerBullets.length - 1; j >= 0; j--) {
let bullet = playerBullets[j];
if (
bullet.x < enemy.x + enemy.width &&
bullet.x + bullet.width > enemy.x &&
bullet.y < enemy.y + enemy.height &&
bullet.y + bullet.height > enemy.y
) {
enemies.splice(i, 1);
playerBullets.splice(j, 1);
}
}
}
// Game over or win
if (enemies.some(e => e.y + e.height >= canvas.height)) {
alert("Game Over! Better luck next time.");
initEnemies();
}
if (enemies.length === 0) {
alert("You Win! All enemies destroyed!");
initEnemies();
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw player
ctx.fillStyle = 'white';
ctx.fillRect(player.x, player.y, player.width, player.height);
// Draw enemies
ctx.fillStyle = 'lime';
enemies.forEach(enemy => {
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// Draw bullets
ctx.fillStyle = 'red';
playerBullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
}
initEnemies();
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>
- Open the HTML file in a browser.
- Use the left/right arrow keys or 'A'/'D' to move.
- Press the spacebar to shoot bullets at enemies.
- Add enemy shooting.
- Add scoring system and timer.
- Play sound effects on shoot, hit, or game over.
- Introduce different types of enemies with unique behaviors.
Let me know if you'd like to expand this game further!