G GAMESRICH
Retro Games · Intermediate

How to Build an Arcade Space Shooter: Kapal Angkasa

Create a classic vertical shmup with parallax starfields, formation math, dive-bombing aliens, and laser upgrades.

By GamesRich Engineering 8 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME ↗

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:

Step-by-Step Implementation

1

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.

Step 1 Implementation
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; }
  });
}
2

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.

Step 2 Implementation
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;
    }
  }
}
3

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.

Step 3 Implementation
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;
  }
}
4

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.

Step 4 Implementation
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:

Complete Runnable Game (index.html)
<!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: