Architecture & Core Concept
The screen represents a vertical starfield (960x540). Star particles drift downwards at variable velocities. Enemies spawn off-screen and glide into an organized grid formation. While in formation, enemies execute periodic parametric sinusoidal swaying. Periodically, a random enemy detaches into 'dive' mode, curving toward player.x with downward acceleration. Collisions check laser bounding boxes against enemy coordinates, spawning collectible powerup diamonds.
Key Game Mechanics Taught:
- Multi-layer starfield parallax with velocity randomization
- Grid-to-orbit enemy fleet formation and sinusoidal swaying math
- Autonomous dive-bombing enemy flight trajectories
- Rapid-fire weapon powerups with bullet cooldown timers
- Dual collision loops: player lasers vs aliens & alien fire vs player
Step-by-Step Implementation
Generate Parallax Starfield
Create an array of 120 star objects with randomized X, Y, speed, and brightness. In each frame, advance their Y coordinate and wrap them back to the top when they exit the canvas.
const stars = [];
for (let i = 0; i < 120; i++) {
stars.push({
x: Math.random() * 960,
y: Math.random() * 540,
s: Math.random() * 2 + 1, // Size
v: Math.random() * 35 + 15 // Speed (parallax depth)
});
}
function updateStars(dt) {
stars.forEach(s => {
s.y += s.v * dt;
if (s.y > 540) { s.y = 0; s.x = Math.random() * 960; }
});
}
Fleet Formation & Sinusoidal Swaying
Each alien ship has a home coordinate (homeX, homeY). In 'enter' mode, it lerps into position. In 'form' mode, it sways using Math.sin and Math.cos offsets.
function updateAlien(e, dt) {
if (e.mode === 'enter') {
e.x += (e.homeX - e.x) * dt * 2.4;
e.y += (e.homeY - e.y) * dt * 2.4;
if (Math.abs(e.y - e.homeY) < 3) e.mode = 'form';
} else if (e.mode === 'form') {
// Elegant swaying formation
e.x = e.homeX + Math.sin(performance.now() / 700 + e.phase) * 11;
e.y = e.homeY + Math.cos(performance.now() / 900 + e.phase) * 5;
} else if (e.mode === 'dive') {
e.x += e.vx * dt;
e.y += e.vy * dt;
e.vy += 115 * dt; // Dive acceleration
if (e.y > 560) {
e.mode = 'enter'; // Loop back from top
e.y = -50;
e.x = e.homeX;
}
}
}
Trigger Dive Attacks
Every 1-2 seconds, pick a random enemy that is currently waiting in formation and send it diving toward the player's current X position.
let diveTimer = 2.0;
function checkDivingEnemies(dt) {
diveTimer -= dt;
if (diveTimer <= 0) {
const formationEnemies = enemies.filter(e => e.hp > 0 && e.mode === 'form');
if (formationEnemies.length > 0) {
const chosen = formationEnemies[Math.floor(Math.random() * formationEnemies.length)];
chosen.mode = 'dive';
chosen.vx = (player.x - chosen.x) * 0.32;
chosen.vy = 160;
}
diveTimer = 1.5;
}
}
Dual Laser and Powerup Mechanics
When the player collects a powerup, grant an 8-second rapid triple-fire buff. Clean up off-screen lasers to maintain 60 FPS performance.
if (keys.fire && fireCooldown <= 0) {
lasers.push({ x: player.x, y: player.y - 28, w: 5, h: 18 });
if (player.powerTimer > 0) {
// Triple shot upgrade
lasers.push({ x: player.x - 18, y: player.y - 20, w: 5, h: 18 });
lasers.push({ x: player.x + 18, y: player.y - 20, w: 5, h: 18 });
fireCooldown = 0.12;
} else {
fireCooldown = 0.25;
}
playLaserSound();
}
Complete Standalone Source Code
Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Space Shooter Demo</title>
<style>body{margin:0;background:#050716;display:flex;justify-content:center;align-items:center;height:100vh;}canvas{background:#050716;}</style>
</head><body><canvas id="c" width="960" height="540"></canvas>
<script>
const c = document.querySelector('#c'), x = c.getContext('2d');
const K = { left:0, right:0, fire:0 };
const P = { x:480, y:480, lives:3, score:0 };
let stars = [], shots = [], aliens = [], last = performance.now();
for(let i=0;i<80;i++) stars.push({x:Math.random()*960,y:Math.random()*540,v:Math.random()*40+20});
for(let r=0;r<3;r++) for(let col=0;col<8;col++) {
aliens.push({ homeX:200+col*75, homeY:80+r*60, x:200+col*75, y:80+r*60, hp:1, mode:'form', phase:col*0.5 });
}
window.onkeydown = e => {
if(['ArrowLeft','a'].includes(e.key)) K.left = 1;
if(['ArrowRight','d'].includes(e.key)) K.right = 1;
if(['ArrowUp',' '].includes(e.key)) K.fire = 1;
};
window.onkeyup = e => {
if(['ArrowLeft','a'].includes(e.key)) K.left = 0;
if(['ArrowRight','d'].includes(e.key)) K.right = 0;
if(['ArrowUp',' '].includes(e.key)) K.fire = 0;
};
let fireWait = 0;
function loop(t) {
const dt = Math.min((t-last)/1000, 0.03); last = t;
stars.forEach(s => { s.y += s.v*dt; if(s.y>540) s.y=0; });
P.x += (K.right - K.left) * 400 * dt;
P.x = Math.max(30, Math.min(930, P.x));
fireWait -= dt;
if(K.fire && fireWait <= 0) {
shots.push({ x:P.x, y:P.y-20 }); fireWait = 0.2;
}
shots.forEach(s => s.y -= 600 * dt);
shots = shots.filter(s => s.y > -20);
aliens.forEach(e => {
e.x = e.homeX + Math.sin(performance.now()/600 + e.phase) * 12;
});
shots.forEach(s => {
aliens.forEach(e => {
if(e.hp > 0 && Math.hypot(s.x - e.x, s.y - e.y) < 24) {
e.hp = 0; s.y = -99; P.score += 100;
}
});
});
x.fillStyle = '#050716'; x.fillRect(0,0,960,540);
x.fillStyle = '#fff'; stars.forEach(s => x.fillRect(s.x, s.y, 2, 2));
aliens.filter(e => e.hp > 0).forEach(e => {
x.fillStyle = '#ee4d78'; x.fillRect(e.x - 16, e.y - 12, 32, 24);
x.fillStyle = '#38e1d2'; x.fillRect(e.x - 6, e.y - 4, 12, 8);
});
x.fillStyle = '#42f5e6'; shots.forEach(s => x.fillRect(s.x - 2, s.y, 4, 14));
x.fillStyle = '#f6f4ff';
x.beginPath(); x.moveTo(P.x, P.y - 20); x.lineTo(P.x + 20, P.y + 15); x.lineTo(P.x - 20, P.y + 15); x.fill();
x.fillStyle = '#fff'; x.font = 'bold 16px sans-serif';
x.fillText('SCORE: ' + P.score, 20, 30);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body></html>
Ideas to Expand Your Game
Now that you have the core mechanics running, consider adding:
- Local High Scores: Use
localStorage.setItem()andlocalStorage.getItem()to persist personal bests. - Visual Polish: Add screen shake on impacts by displacing
ctx.translate(shakeX, shakeY). - Mobile Touch Controls: Bind on-screen directional buttons using
onpointerdownandonpointerup.