Create Asteroid Game using HTML, CSS, and JavaScript

Faraz

By Faraz -

Learn how to create an Asteroid game using HTML, CSS, and JavaScript. Follow this step-by-step guide to build a fun space shooter game.


create-asteroid-game-using-html-css-and-javascript.webp

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Conclusion
  6. Preview

Are you interested in creating a simple Asteroid game using HTML, CSS, and JavaScript? In this step-by-step guide, you will learn how to build a fun space shooter game with moving asteroids and player controls.

Prerequisites

Before we begin, make sure you have:

  • Basic knowledge of HTML, CSS, and JavaScript
  • A text editor like VS Code or Sublime Text
  • A web browser for testing

Source Code

Step 1 (HTML Code):

The first thing we need to do is create our HTML File for Asteroid game. We'll start with well-organized markup. After creating the files just paste the following codes into your file. Remember that you must save a file with the .html extension. Let’s break it down:

1. HTML Structure

The document follows a standard HTML5 structure.

Head Section (<head>)

  • <!DOCTYPE html>: Declares the document as an HTML5 file.
  • <html lang="en">: Sets the language to English.
  • <meta charset="UTF-8">: Defines the character encoding.
  • <meta name="viewport" content="width=device-width, initial-scale=1.0">: Ensures proper scaling on mobile devices.
  • <title>Asteroids Game</title>: Sets the page title.
  • <link> tags:
    • Google Fonts (Press Start 2P): Used for a retro game-like font.
    • External CSS (styles.css): Adds styles to the game.

2. Body Content (<body>)

The main content consists of:

Game Information (<div id="gameInfoContainer">)

  • Displays score, lives, power-up status, and thrust level.

Game Canvas (<canvas id="gameCanvas">)

  • The actual game is drawn on this HTML5 canvas using JavaScript.

Game Controls (<div id="controls">)

  • Contains buttons for player actions:
    • Left (id="leftBtn"): Turn left.
    • Thrust (id="thrustBtn"): Move forward.
    • Right (id="rightBtn"): Turn right.
    • Fire (id="fireBtn"): Shoot bullets.
    • Restart (id="restartBtn"): Restart the game (hidden initially).

Message Area (<div id="messageArea">)

  • Displays messages (e.g., "Game Over", "Level Up") in <p id="messageText">.

JavaScript File (<script src="script.js">)

  • The game logic is handled by script.js.

3. Functionality Overview

  • The canvas (<canvas>) is where the game is displayed.
  • The buttons allow player interaction.
  • The JavaScript (script.js) controls:
    • Player movement
    • Shooting
    • Collision detection
    • Score updates
    • Game restarts

Step 2 (CSS Code):

Next, we need to style our Asteroid game by adding our CSS. This will give our game an upgraded presentation. Create a CSS file with the name of styles.css and paste the given codes into your CSS file. Let’s break it down

1. Styling the <body>

body {
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #050515;
    font-family: 'Press Start 2P', cursive;
    color: #eee;
    overflow: hidden;
}
  • margin: 0; padding: 0; → Removes default spacing.
  • display: flex; flex-direction: column; → Arranges content in a vertical column.
  • justify-content: center; align-items: center; → Centers everything on the screen.
  • min-height: 100vh; → Makes sure the body covers the full height of the viewport.
  • background-color: #050515; → Dark background for a space-like theme.
  • font-family: 'Press Start 2P', cursive; → Uses a pixel-style font for a retro effect.
  • color: #eee; → Sets the default text color to light gray.
  • overflow: hidden; → Prevents scrollbars.

2. Styling the Game Canvas (<canvas>)

#gameCanvas {
    border: 2px solid #00ffff;
    display: block;
    max-width: 95vw;
    max-height: 75vh;
    box-shadow: 0 0 15px #00ffff;
    background-color: #080810;
}
  • border: 2px solid #00ffff; → Adds a cyan border around the game area.
  • display: block; → Removes unwanted space around the <canvas>.
  • max-width: 95vw; max-height: 75vh; → Limits the canvas size to fit within the viewport.
  • box-shadow: 0 0 15px #00ffff; → Adds a glowing effect around the canvas.
  • background-color: #080810; → Darker shade of blue for the game background.

3. Styling the Control Buttons

#controls {
    margin-top: 20px;
    text-align: center;
}
  • margin-top: 20px; → Adds space between the game and the controls.
  • text-align: center; → Centers the buttons.
#controls button {
    font-family: 'Press Start 2P', cursive;
    background-color: #301050;
    color: #00ffff;
    border: 2px solid #00ffff;
    padding: 10px 15px;
    margin: 5px;
    cursor: pointer;
    border-radius: 5px;
    box-shadow: 0 0 5px #00ffff inset, 0 0 5px #00ffff;
    transition: background-color 0.2s, box-shadow 0.2s;
}
  • background-color: #301050; → Purple button background.
  • color: #00ffff; → Cyan text color.
  • border: 2px solid #00ffff; → Cyan border.
  • padding: 10px 15px; margin: 5px; → Spacing for better appearance.
  • cursor: pointer; → Changes cursor to a pointer when hovering.
  • border-radius: 5px; → Slightly rounded corners.
  • box-shadow: 0 0 5px #00ffff inset, 0 0 5px #00ffff; → Neon glow effect.
  • transition: background-color 0.2s, box-shadow 0.2s; → Smooth effect when hovered or clicked.
#controls button:active, #controls button.active {
    background-color: #503070;
    box-shadow: 0 0 8px #00ffff inset, 0 0 8px #00ffff;
}
  • Active state (:active, .active)
    • background-color: #503070; → Darker purple when clicked.
    • box-shadow: 0 0 8px #00ffff inset, 0 0 8px #00ffff; → Stronger glow effect.

4. Styling the Game Info (Score, Lives, Power-ups)

#gameInfoContainer {
    position: absolute;
    top: 10px;
    left: 10px;
    font-size: 14px;
    color: #00ffff;
    text-shadow: 0 0 3px #00ffff;
    line-height: 1.4;
    z-index: 5;
}
  • position: absolute; top: 10px; left: 10px; → Positions the game info in the top-left corner.
  • font-size: 14px; → Small, readable font.
  • color: #00ffff; → Cyan text.
  • text-shadow: 0 0 3px #00ffff; → Glowing effect.
  • line-height: 1.4; → Adjusts spacing between text lines.
  • z-index: 5; → Ensures it appears above other elements.
#powerUpInfo, #thrustInfo { 
    font-size: 12px;
    color: #ffff00;
    text-shadow: 0 0 3px #ffff00;
}
  • font-size: 12px; → Slightly smaller than score/lives info.
  • color: #ffff00; → Yellow text for power-up/thrust info.
  • text-shadow: 0 0 3px #ffff00; → Adds a glowing yellow effect.

5. Styling the Message Area

#messageArea {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
    color: #ffffff;
    text-align: center;
    display: none;
    background-color: rgba(10, 10, 30, 0.8);
    padding: 20px;
    border: 2px solid #ffffff;
    border-radius: 10px;
    z-index: 10;
    text-shadow: 0 0 5px #ffffff;
}
  • position: absolute; → Positions the message area at the center.
  • top: 50%; left: 50%; transform: translate(-50%, -50%); → Ensures perfect centering.
  • font-size: 24px; → Large text for visibility.
  • color: #ffffff; text-shadow: 0 0 5px #ffffff; → White text with a glow.
  • text-align: center; → Centers the message.
  • display: none; → Hidden by default (shown during events like game over).
  • background-color: rgba(10, 10, 30, 0.8); → Semi-transparent dark blue background.
  • padding: 20px; border: 2px solid #ffffff; border-radius: 10px; → Styling for a rounded, bordered box.
  • z-index: 10; → Ensures it appears on top of other elements.
body {
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column; 
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #050515;
    font-family: 'Press Start 2P', cursive; 
    color: #eee; 
    overflow: hidden;
}

#gameCanvas {
    border: 2px solid #00ffff; 
    display: block;
    max-width: 95vw;
    max-height: 75vh; 
    box-shadow: 0 0 15px #00ffff; 
    background-color: #080810; 
}

#controls {
    margin-top: 20px; 
    text-align: center;
}

#controls button {
    font-family: 'Press Start 2P', cursive;
    background-color: #301050; 
    color: #00ffff; 
    border: 2px solid #00ffff;
    padding: 10px 15px;
    margin: 5px;
    cursor: pointer;
    border-radius: 5px; 
    box-shadow: 0 0 5px #00ffff inset, 0 0 5px #00ffff; 
    transition: background-color 0.2s, box-shadow 0.2s;
}

#controls button:active, #controls button.active {
    background-color: #503070; 
    box-shadow: 0 0 8px #00ffff inset, 0 0 8px #00ffff; 
}

#gameInfoContainer {
     position: absolute;
     top: 10px;
     left: 10px;
     font-size: 14px;
     color: #00ffff; 
     text-shadow: 0 0 3px #00ffff;
     line-height: 1.4; 
     z-index: 5; 
 }

#powerUpInfo, #thrustInfo { 
    font-size: 12px;
    color: #ffff00; 
    text-shadow: 0 0 3px #ffff00;
}


#messageArea {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
    color: #ffffff; 
    text-align: center;
    display: none; 
    background-color: rgba(10, 10, 30, 0.8); 
    padding: 20px;
    border: 2px solid #ffffff; 
    border-radius: 10px;
    z-index: 10;
    text-shadow: 0 0 5px #ffffff;
} 

Step 3 (JavaScript Code):

Finally, we need to create a function in JavaScript. Below is a detailed explanation of its structure and functionality:

1. Game Setup

The game initializes by setting up references to the HTML elements (e.g., canvas, score, lives, buttons) and defining constants for gameplay mechanics.

Key Elements:

  • Canvas Context (ctx): Used for rendering graphics.
  • Game Constants:
    • SHIP_SIZE, SHIP_TURN_SPEED, BASE_SHIP_THRUST: Define ship behavior.
    • BULLET_SPEED, BULLET_LIFETIME: Control bullet properties.
    • ASTEROID_NUM_START, ASTEROID_SPEED, ASTEROID_SIZE_*: Define asteroid behavior and sizes.
    • POWERUP_CHANCE, POWERUP_EFFECT_DURATION: Control power-up spawn rates and durations.
  • Game State Variables:
    • ship, bullets, asteroids, powerUps: Arrays or objects representing game entities.
    • score, lives, keys: Track player progress, remaining lives, and input states.
    • isGameOver, shipInvincibleFrames, activePowerUp: Manage game flow and special conditions.

2. Classes

The game uses object-oriented programming with classes to represent game entities.

Ship Class

  • Represents the player's spaceship.
  • Properties:
    • Position (x, y), velocity (vel), angle, thrust state, invincibility frames, etc.
  • Methods:
    • rotate(direction): Rotates the ship left or right.
    • thrust(): Applies thrust based on the current direction.
    • update(): Updates position, velocity, and timers.
    • draw(): Renders the ship, booster flame, and shield (if active).
    • tryShoot(): Fires bullets if the cooldown allows.
    • activatePowerUp(type): Activates a power-up effect (shield, rapid fire, spread shot).
    • deactivatePowerUp(): Removes the active power-up.

Bullet Class

  • Represents projectiles fired by the ship.
  • Properties:
    • Position (x, y), velocity (vel), lifetime.
  • Methods:
    • update(): Updates position and reduces lifetime.
    • draw(): Renders the bullet with a radial gradient for a glowing effect.

Asteroid Class

  • Represents space rocks that the player must destroy.
  • Properties:
    • Position (x, y), size, velocity (vel), rotation speed, and vertices (for irregular shapes).
  • Methods:
    • createVertices(): Generates random vertices for a jagged appearance.
    • update(): Updates position and handles screen wrapping.
    • draw(): Renders the asteroid with a radial gradient for texture.

PowerUp Class

  • Represents collectible items that grant temporary abilities.
  • Properties:
    • Position (x, y), type (shield, rapid fire, spread shot), lifetime, pulse phase (for animation).
  • Methods:
    • update(): Decreases lifetime and animates the pulsing effect.
    • draw(): Renders the power-up orb with gradients and a symbol indicating its type.

3. Game Functions

These functions handle the core game logic, including initialization, updates, collision detection, and rendering.

Initialization

  • resizeCanvas(): Adjusts the canvas size to fit the window and recreates stars.
  • initGame(): Resets the game state, spawns initial asteroids, and starts the game loop.

Game Loop

  • gameLoop(): The main game loop that:
    • Handles player input.
    • Updates game entities (ship, bullets, asteroids, power-ups).
    • Checks for collisions.
    • Renders the game scene.
    • Repeats at a fixed interval (FPS = 60).

Collision Detection

  • checkCollisions():
    • Detects collisions between bullets and asteroids.
    • Handles ship-asteroid collisions (applies shield or loses life).
    • Detects ship-power-up collisions to activate effects.

Asteroid Splitting

  • splitAsteroid(asteroid, index):
    • Destroys an asteroid and spawns smaller ones if applicable.
    • Awards points based on asteroid size.
    • Spawns a power-up with a chance (POWERUP_CHANCE).

Level Progression

  • levelUp(): Advances to the next level by increasing asteroid count and resetting the game state.

UI Updates

  • updateUI(): Updates the score, lives, and thrust multiplier display.

4. Event Listeners

The game listens for keyboard and button inputs to control the ship and interact with the UI.

Keyboard Input

  • Handles arrow keys (ArrowLeft, ArrowRight, ArrowUp) and spacebar for movement and shooting.
  • Allows adjusting the thrust multiplier with - and = keys.

Button Input

  • Touch-friendly buttons (leftBtn, rightBtn, thrustBtn, fireBtn) mimic keyboard controls.

5. Visual Enhancements

The code includes several visual effects to make the game more engaging:

  • Starfield Background: Randomly generated stars with varying sizes and brightness.
  • Booster Flame: Animated flames behind the ship when thrusting, with radial gradients for depth.
  • Shield Effect: A glowing circle around the ship when the shield is active.
  • Power-Up Animation: Pulsing orbs with gradients and symbols for different types.

6. Game Flow

1. Start:

  • Initialize the canvas, spawn asteroids, and start the game loop.

2. Play:

  • Player controls the ship to shoot asteroids while avoiding collisions.
  • Power-ups provide temporary advantages.

3. Level Up:

  • When all asteroids are destroyed, the game advances to the next level with more asteroids.

4. Game Over:

  • If the player loses all lives, the game ends, and a restart option is displayed.

7. Key Features

  • Responsive Design: The canvas adjusts to fit the browser window.
  • Physics-Based Movement: Ships and asteroids move with inertia and friction.
  • Power-Ups: Add variety and strategic depth to gameplay.
  • Polished Graphics: Gradients, animations, and particle effects enhance the visual experience.

8. Performance Considerations

  • Efficient Rendering: The game uses requestAnimationFrame indirectly via setTimeout for smooth animations.
  • Collision Optimization: Only checks relevant pairs of objects (e.g., bullets vs. asteroids).

9. How to Play

  • Use the arrow keys or on-screen buttons to rotate, thrust, and shoot.
  • Avoid asteroids or use the shield to survive collisions.
  • Collect power-ups to gain advantages like rapid fire or spread shots.
  • Destroy all asteroids to advance to the next level.
// --- Game Setup ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const messageArea = document.getElementById('messageArea');
const messageText = document.getElementById('messageText');
const restartBtn = document.getElementById('restartBtn');
const powerUpStatusEl = document.getElementById('powerUpStatus');
const thrustLevelEl = document.getElementById('thrustLevel');

// Buttons
const leftBtn = document.getElementById('leftBtn');
const rightBtn = document.getElementById('rightBtn');
const thrustBtn = document.getElementById('thrustBtn');
const fireBtn = document.getElementById('fireBtn');

let canvasWidth, canvasHeight;
let ASTEROID_NUM_START = 4;
// Game Constants
const SHIP_SIZE = 20;
const SHIP_TURN_SPEED = 0.1;
const BASE_SHIP_THRUST = 0.1;
const FRICTION = 0.99;
const BULLET_SPEED = 5;
const BULLET_LIFETIME = 60;
const ASTEROID_SPEED = 1;
const ASTEROID_SIZE_LARGE = 40;
const ASTEROID_SIZE_MEDIUM = 20;
const ASTEROID_SIZE_SMALL = 10;
const ASTEROID_POINTS_LARGE = 20;
const ASTEROID_POINTS_MEDIUM = 50;
const ASTEROID_POINTS_SMALL = 100;
const SHIP_INVINCIBILITY_FRAMES = 180;
const FPS = 60;
const POWERUP_CHANCE = 0.15;
const POWERUP_LIFETIME = 480;
const POWERUP_EFFECT_DURATION = 600;
const BASE_FIRE_DELAY = 15;
const RAPID_FIRE_DELAY = 5;
const MIN_THRUST_MULTIPLIER = 0.5;
const MAX_THRUST_MULTIPLIER = 2.0;
const THRUST_ADJUST_STEP = 0.1;
const NUM_STARS = 150; // Number of stars for the background

const PowerUpType = {
  SHIELD: 'shield',
  RAPID_FIRE: 'rapid_fire',
  SPREAD_SHOT: 'spread_shot',
};

// Game State Variables
let ship;
let bullets = [];
let asteroids = [];
let powerUps = [];
let stars = []; // Array for starfield background
let score = 0;
let lives = 3;
let keys = {
  left: false,
  right: false,
  up: false,
  space: false,
  minus: false,
  equal: false,
};
let isGameOver = false;
let shipInvincibleFrames = 0;
let gameLoopTimeout;
let timeSinceLastShot = 0;
let currentFireDelay = BASE_FIRE_DELAY;
let activePowerUp = null;
let powerUpTimer = 0;
let thrustMultiplier = 1.0;

// --- Classes ---

class Ship {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.angle = -Math.PI / 2;
    this.vel = { x: 0, y: 0 };
    this.radius = SHIP_SIZE / 2;
    this.isThrusting = false;
    this.shieldActive = false;
    this.boosterFlameLength = 0;
  }
  rotate(direction) {
    this.angle += SHIP_TURN_SPEED * direction;
  }
  thrust() {
    const currentThrust = BASE_SHIP_THRUST * thrustMultiplier;
    this.vel.x += currentThrust * Math.cos(this.angle);
    this.vel.y += currentThrust * Math.sin(this.angle);
    this.isThrusting = true;
  }
  update() {
    this.vel.x *= FRICTION;
    this.vel.y *= FRICTION;
    this.x += this.vel.x;
    this.y += this.vel.y;
    // Screen wrapping
    if (this.x < -this.radius) this.x = canvasWidth + this.radius;
    if (this.x > canvasWidth + this.radius) this.x = -this.radius;
    if (this.y < -this.radius) this.y = canvasHeight + this.radius;
    if (this.y > canvasHeight + this.radius) this.y = -this.radius;
    // Update booster flame animation
    if (this.isThrusting)
      this.boosterFlameLength = Math.min(
        this.boosterFlameLength + 2,
        SHIP_SIZE * 1.0
      ); // Slightly longer max flame
    else this.boosterFlameLength = Math.max(this.boosterFlameLength - 1, 0);
    this.isThrusting = false;
    // Timers
    if (shipInvincibleFrames > 0) shipInvincibleFrames--;
    if (powerUpTimer > 0) {
      powerUpTimer--;
      if (powerUpTimer <= 0) this.deactivatePowerUp();
    }
    if (timeSinceLastShot < currentFireDelay) timeSinceLastShot++;
  }
  draw() {
    if (shipInvincibleFrames > 0 && Math.floor(Date.now() / 100) % 2 === 0)
      return;
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);

    // Draw enhanced booster flame if length > 0
    if (this.boosterFlameLength > 0) {
      const flicker = Math.random() * 0.2 + 0.9; // 0.9 to 1.1
      const flameLen = this.boosterFlameLength * flicker;

      // Use radial gradients for a hotter core effect
      const gradOuter = ctx.createRadialGradient(
        -SHIP_SIZE / 2,
        0,
        flameLen * 0.1,
        -SHIP_SIZE / 2,
        0,
        flameLen * 0.7
      );
      gradOuter.addColorStop(0, 'rgba(255, 150, 0, 0.8)'); // Orange center
      gradOuter.addColorStop(1, 'rgba(255, 0, 0, 0.3)'); // Red outer, more transparent

      const gradInner = ctx.createRadialGradient(
        -SHIP_SIZE / 2,
        0,
        flameLen * 0.05,
        -SHIP_SIZE / 2,
        0,
        flameLen * 0.4
      );
      gradInner.addColorStop(0, 'rgba(255, 255, 200, 1)'); // Bright yellow/white core
      gradInner.addColorStop(1, 'rgba(255, 200, 0, 0.7)'); // Yellow outer

      // Outer flame shape
      ctx.beginPath();
      ctx.moveTo(-SHIP_SIZE / 2.5, SHIP_SIZE / 4);
      ctx.lineTo(-SHIP_SIZE / 2 - flameLen, 0);
      ctx.lineTo(-SHIP_SIZE / 2.5, -SHIP_SIZE / 4);
      ctx.closePath();
      ctx.fillStyle = gradOuter;
      ctx.fill();

      // Inner flame shape
      ctx.beginPath();
      ctx.moveTo(-SHIP_SIZE / 2.5, SHIP_SIZE / 6);
      ctx.lineTo(-SHIP_SIZE / 2 - flameLen * 0.8, 0); // Slightly shorter inner
      ctx.lineTo(-SHIP_SIZE / 2.5, -SHIP_SIZE / 6);
      ctx.closePath();
      ctx.fillStyle = gradInner;
      ctx.fill();
    }

    // Draw main ship body with gradient for depth
    const shipGrad = ctx.createLinearGradient(
      -SHIP_SIZE / 2,
      -SHIP_SIZE / 3,
      SHIP_SIZE / 2,
      SHIP_SIZE / 3
    );
    shipGrad.addColorStop(0, '#50a0ff'); // Lighter cyan/blue highlight
    shipGrad.addColorStop(1, '#0050cc'); // Darker cyan/blue shadow

    ctx.beginPath();
    ctx.moveTo(SHIP_SIZE / 2, 0); // Nose
    ctx.lineTo(-SHIP_SIZE / 2, -SHIP_SIZE / 3); // Rear left
    ctx.lineTo(-SHIP_SIZE / 2, SHIP_SIZE / 3); // Rear right
    ctx.closePath();
    ctx.fillStyle = shipGrad; // Apply gradient fill
    ctx.strokeStyle = '#00ffff'; // Keep cyan outline
    ctx.lineWidth = 1.5; // Slightly thinner outline
    ctx.fill();
    ctx.stroke();

    ctx.restore();

    // Draw shield if active
    if (this.shieldActive) {
      const shieldGrad = ctx.createRadialGradient(
        this.x,
        this.y,
        this.radius,
        this.x,
        this.y,
        this.radius + 5
      );
      shieldGrad.addColorStop(0, 'rgba(0, 180, 255, 0.1)');
      shieldGrad.addColorStop(1, 'rgba(0, 220, 255, 0.7)');
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.radius + 5, 0, Math.PI * 2);
      ctx.strokeStyle = shieldGrad; // Use gradient for stroke
      ctx.lineWidth = 3; // Thicker shield
      ctx.stroke();
    }
  }
  tryShoot() {
    if (timeSinceLastShot >= currentFireDelay) {
      this.shoot();
      timeSinceLastShot = 0;
    }
  }
  shoot() {
    const noseX = this.x + (SHIP_SIZE / 2) * Math.cos(this.angle);
    const noseY = this.y + (SHIP_SIZE / 2) * Math.sin(this.angle);
    if (activePowerUp === PowerUpType.SPREAD_SHOT) {
      const spreadAngle = 0.25;
      bullets.push(
        new Bullet(
          noseX,
          noseY,
          BULLET_SPEED * Math.cos(this.angle),
          BULLET_SPEED * Math.sin(this.angle)
        )
      );
      bullets.push(
        new Bullet(
          noseX,
          noseY,
          BULLET_SPEED * Math.cos(this.angle - spreadAngle),
          BULLET_SPEED * Math.sin(this.angle - spreadAngle)
        )
      );
      bullets.push(
        new Bullet(
          noseX,
          noseY,
          BULLET_SPEED * Math.cos(this.angle + spreadAngle),
          BULLET_SPEED * Math.sin(this.angle + spreadAngle)
        )
      );
    } else {
      bullets.push(
        new Bullet(
          noseX,
          noseY,
          BULLET_SPEED * Math.cos(this.angle),
          BULLET_SPEED * Math.sin(this.angle)
        )
      );
    }
  }
  reset() {
    this.x = canvasWidth / 2;
    this.y = canvasHeight / 2;
    this.vel = { x: 0, y: 0 };
    this.angle = -Math.PI / 2;
    shipInvincibleFrames = SHIP_INVINCIBILITY_FRAMES;
    this.deactivatePowerUp();
  }
  activatePowerUp(type) {
    this.deactivatePowerUp();
    activePowerUp = type;
    powerUpTimer = POWERUP_EFFECT_DURATION;
    switch (type) {
      case PowerUpType.SHIELD:
        this.shieldActive = true;
        powerUpStatusEl.textContent = 'Shield Active!';
        break;
      case PowerUpType.RAPID_FIRE:
        currentFireDelay = RAPID_FIRE_DELAY;
        powerUpStatusEl.textContent = 'Rapid Fire!';
        break;
      case PowerUpType.SPREAD_SHOT:
        powerUpStatusEl.textContent = 'Spread Shot!';
        break;
    }
  }
  deactivatePowerUp() {
    if (!activePowerUp) return;
    switch (activePowerUp) {
      case PowerUpType.SHIELD:
        this.shieldActive = false;
        break;
      case PowerUpType.RAPID_FIRE:
        currentFireDelay = BASE_FIRE_DELAY;
        break;
      case PowerUpType.SPREAD_SHOT:
        break;
    }
    activePowerUp = null;
    powerUpTimer = 0;
    powerUpStatusEl.textContent = 'None';
  }
}

class Bullet {
  constructor(x, y, velX, velY) {
    this.x = x;
    this.y = y;
    this.vel = { x: velX, y: velY };
    this.radius = 3;
    this.lifetime = BULLET_LIFETIME;
  }
  update() {
    this.x += this.vel.x;
    this.y += this.vel.y;
    this.lifetime--;
  }
  draw() {
    // Use radial gradient for hotter core
    const grad = ctx.createRadialGradient(
      this.x,
      this.y,
      0,
      this.x,
      this.y,
      this.radius
    );
    grad.addColorStop(0, 'rgba(255, 255, 255, 1)'); // White hot center
    grad.addColorStop(0.5, 'rgba(255, 255, 0, 1)'); // Yellow mid
    grad.addColorStop(1, 'rgba(255, 200, 0, 0.5)'); // Orange transparent edge

    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius * 1.2, 0, Math.PI * 2); // Slightly larger arc for gradient effect
    ctx.fill();

    // Optional: Add a subtle glow effect (can impact performance)
    // ctx.shadowColor = '#ffff00';
    // ctx.shadowBlur = 6;
    // ctx.fill();
    // ctx.shadowColor = 'transparent';
    // ctx.shadowBlur = 0;
  }
}

class Asteroid {
  constructor(x, y, size, velX, velY) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.radius = size / 2;
    this.vel = { x: velX, y: velY };
    this.angle = Math.random() * Math.PI * 2;
    this.rotationSpeed = (Math.random() - 0.5) * 0.02;
    this.vertices = this.createVertices();
    // Store gradient offset for varied lighting
    this.gradientOffset = {
      x: (Math.random() - 0.5) * this.radius * 0.5,
      y: (Math.random() - 0.5) * this.radius * 0.5,
    };
  }
  createVertices() {
    const numVertices = 8 + Math.floor(Math.random() * 5);
    const vertices = [];
    for (let i = 0; i < numVertices; i++) {
      const angle = (i / numVertices) * Math.PI * 2;
      const radius = this.radius * (0.7 + Math.random() * 0.6); // More variation for rougher look
      vertices.push({
        x: radius * Math.cos(angle),
        y: radius * Math.sin(angle),
      });
    }
    return vertices;
  }
  update() {
    this.x += this.vel.x;
    this.y += this.vel.y;
    this.angle += this.rotationSpeed;
    // Screen wrapping
    if (this.x < -this.radius) this.x = canvasWidth + this.radius;
    if (this.x > canvasWidth + this.radius) this.x = -this.radius;
    if (this.y < -this.radius) this.y = canvasHeight + this.radius;
    if (this.y > canvasHeight + this.radius) this.y = -this.radius;
  }
  draw() {
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);

    // Create radial gradient for rocky texture/shading
    // Offset the gradient center slightly for a non-uniform lighting effect
    const gradX = this.gradientOffset.x;
    const gradY = this.gradientOffset.y;
    const grad = ctx.createRadialGradient(
      gradX,
      gradY,
      this.radius * 0.1,
      gradX,
      gradY,
      this.radius * 1.2
    );
    grad.addColorStop(0, '#A0A0A0'); // Lighter grey highlight
    grad.addColorStop(0.7, '#707070'); // Mid grey
    grad.addColorStop(1, '#404040'); // Darker grey shadow

    ctx.beginPath();
    ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
    for (let i = 1; i < this.vertices.length; i++) {
      ctx.lineTo(this.vertices[i].x, this.vertices[i].y);
    }
    ctx.closePath();

    ctx.fillStyle = grad; // Fill with gradient
    ctx.strokeStyle = '#303030'; // Darker outline
    ctx.lineWidth = 1.5;
    ctx.fill(); // Fill first
    ctx.stroke(); // Then draw outline

    ctx.restore();
  }
}

class PowerUp {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type;
    this.radius = 8;
    this.lifetime = POWERUP_LIFETIME;
    this.pulsePhase = 0;
  }
  update() {
    this.lifetime--;
    this.pulsePhase += 0.1;
  }
  draw() {
    ctx.save();
    ctx.translate(this.x, this.y);
    const scale = 1 + Math.sin(this.pulsePhase) * 0.15;
    ctx.scale(scale, scale);

    let baseColor, lightColor, darkColor, symbol;
    switch (this.type) {
      case PowerUpType.SHIELD:
        baseColor = '0, 180, 255';
        lightColor = '150, 220, 255';
        darkColor = '0, 100, 180';
        symbol = 'S';
        break;
      case PowerUpType.RAPID_FIRE:
        baseColor = '0, 255, 100';
        lightColor = '150, 255, 180';
        darkColor = '0, 180, 80';
        symbol = 'R';
        break;
      case PowerUpType.SPREAD_SHOT:
        baseColor = '255, 150, 0';
        lightColor = '255, 200, 100';
        darkColor = '200, 100, 0';
        symbol = 'W';
        break;
      default:
        baseColor = '200, 200, 200';
        lightColor = '255, 255, 255';
        darkColor = '150, 150, 150';
        symbol = '?';
    }

    // Gradient fill for powerup orb
    const grad = ctx.createRadialGradient(
      0,
      -this.radius * 0.3,
      this.radius * 0.1,
      0,
      0,
      this.radius
    );
    grad.addColorStop(0, `rgba(${lightColor}, 0.9)`); // Highlight
    grad.addColorStop(0.7, `rgba(${baseColor}, 0.8)`); // Base color
    grad.addColorStop(1, `rgba(${darkColor}, 0.7)`); // Shadow

    ctx.beginPath();
    ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = grad;
    ctx.fill();
    // Optional subtle outline
    // ctx.strokeStyle = `rgba(${darkColor}, 0.9)`;
    // ctx.lineWidth = 1;
    // ctx.stroke();

    // Draw letter inside
    ctx.fillStyle = 'white';
    ctx.shadowColor = 'black'; // Shadow for better readability
    ctx.shadowBlur = 3;
    ctx.font = `${this.radius * 1.2}px 'Press Start 2P'`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(symbol, 0, 1);

    ctx.restore();
  }
}

// --- Game Functions ---

// Create the starfield background
function createStars() {
  stars = []; // Clear existing stars
  for (let i = 0; i < NUM_STARS; i++) {
    stars.push({
      x: Math.random() * canvasWidth,
      y: Math.random() * canvasHeight,
      radius: Math.random() * 1.2 + 0.3, // Varying star sizes (0.3 to 1.5)
      alpha: Math.random() * 0.5 + 0.3, // Varying brightness (0.3 to 0.8)
    });
  }
}

// Draw the starfield background
function drawStars() {
  ctx.save();
  ctx.fillStyle = '#080810'; // Very dark background color
  ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  stars.forEach((star) => {
    ctx.beginPath();
    ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
    ctx.fillStyle = `rgba(255, 255, 255, ${star.alpha})`; // White stars with varying alpha
    ctx.fill();
  });
  ctx.restore();
}

function resizeCanvas() {
  const maxWidth = window.innerWidth * 0.95;
  const maxHeight = window.innerHeight * 0.75;
  let width = maxWidth;
  let height = width * (3 / 4);
  if (height > maxHeight) {
    height = maxHeight;
    width = height * (4 / 3);
  }
  canvas.width = width;
  canvas.height = height;
  canvasWidth = canvas.width;
  canvasHeight = canvas.height;
  createStars(); // Recreate stars when canvas resizes
}

function initGame() {
  isGameOver = false;
  score = 0;
  lives = 3;
  shipInvincibleFrames = 0;
  timeSinceLastShot = BASE_FIRE_DELAY;
  thrustMultiplier = 1.0;
  ship = new Ship(canvasWidth / 2, canvasHeight / 2);
  ship.deactivatePowerUp();
  createStars(); // Create stars on initial load
  updateUI();
  messageArea.style.display = 'none';
  restartBtn.style.display = 'none';
  bullets = [];
  asteroids = [];
  powerUps = [];
  createAsteroids(ASTEROID_NUM_START, null, ASTEROID_SIZE_LARGE);
  clearTimeout(gameLoopTimeout);
  gameLoop();
}

function createAsteroids(
  count,
  sourceAsteroid = null,
  size = ASTEROID_SIZE_LARGE
) {
  for (let i = 0; i < count; i++) {
    let x, y, velX, velY;
    if (sourceAsteroid) {
      x = sourceAsteroid.x;
      y = sourceAsteroid.y;
      const angle = Math.random() * Math.PI * 2;
      velX = (Math.random() * ASTEROID_SPEED * 1.5 + 0.5) * Math.cos(angle);
      velY = (Math.random() * ASTEROID_SPEED * 1.5 + 0.5) * Math.sin(angle);
    } else {
      do {
        x = Math.random() * canvasWidth;
        y = Math.random() * canvasHeight;
      } while (
        ship &&
        distanceBetweenPoints(x, y, ship.x, ship.y) <
          ASTEROID_SIZE_LARGE * 3 + ship.radius
      );
      velX = (Math.random() - 0.5) * ASTEROID_SPEED * 2;
      velY = (Math.random() - 0.5) * ASTEROID_SPEED * 2;
    }
    asteroids.push(new Asteroid(x, y, size, velX, velY));
  }
}

function trySpawnPowerUp(x, y) {
  if (Math.random() < POWERUP_CHANCE) {
    const types = Object.values(PowerUpType);
    const randomType = types[Math.floor(Math.random() * types.length)];
    powerUps.push(new PowerUp(x, y, randomType));
  }
}

function distanceBetweenPoints(x1, y1, x2, y2) {
  return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}

function checkCollisions() {
  // Bullet vs Asteroid
  for (let i = bullets.length - 1; i >= 0; i--) {
    const bullet = bullets[i];
    for (let j = asteroids.length - 1; j >= 0; j--) {
      const asteroid = asteroids[j];
      if (
        distanceBetweenPoints(bullet.x, bullet.y, asteroid.x, asteroid.y) <
        asteroid.radius + bullet.radius
      ) {
        bullets.splice(i, 1);
        splitAsteroid(asteroid, j);
        break;
      }
    }
  }
  // Ship vs Asteroid
  if (shipInvincibleFrames <= 0) {
    for (let i = asteroids.length - 1; i >= 0; i--) {
      const asteroid = asteroids[i];
      if (
        distanceBetweenPoints(ship.x, ship.y, asteroid.x, asteroid.y) <
        asteroid.radius + ship.radius * 0.8
      ) {
        if (ship.shieldActive) {
          splitAsteroid(asteroid, i);
          ship.deactivatePowerUp();
        } else {
          loseLife();
        }
        break;
      }
    }
  }
  // Ship vs PowerUp
  for (let i = powerUps.length - 1; i >= 0; i--) {
    const powerUp = powerUps[i];
    if (
      distanceBetweenPoints(ship.x, ship.y, powerUp.x, powerUp.y) <
      ship.radius + powerUp.radius
    ) {
      ship.activatePowerUp(powerUp.type);
      powerUps.splice(i, 1);
      break;
    }
  }
}

function splitAsteroid(asteroid, index) {
  const asteroidX = asteroid.x;
  const asteroidY = asteroid.y;
  if (asteroid.size === ASTEROID_SIZE_LARGE) {
    score += ASTEROID_POINTS_LARGE;
    createAsteroids(2, asteroid, ASTEROID_SIZE_MEDIUM);
  } else if (asteroid.size === ASTEROID_SIZE_MEDIUM) {
    score += ASTEROID_POINTS_MEDIUM;
    createAsteroids(2, asteroid, ASTEROID_SIZE_SMALL);
  } else {
    score += ASTEROID_POINTS_SMALL;
  }
  asteroids.splice(index, 1);
  updateUI();
  trySpawnPowerUp(asteroidX, asteroidY);
  if (asteroids.length === 0 && !isGameOver) levelUp();
}

function loseLife() {
  lives--;
  updateUI();
  if (lives <= 0) gameOver();
  else ship.reset();
}

function gameOver() {
  isGameOver = true;
  ship.deactivatePowerUp();
  messageText.textContent = `GAME OVER! Score: ${score}`;
  messageArea.style.display = 'block';
  restartBtn.style.display = 'inline-block';
  clearTimeout(gameLoopTimeout);
}

function levelUp() {
  clearTimeout(gameLoopTimeout);
  ASTEROID_NUM_START++;
  messageText.textContent = `LEVEL CLEAR! Next wave...`;
  messageArea.style.display = 'block';
  ship.reset();
  setTimeout(() => {
    messageArea.style.display = 'none';
    createAsteroids(ASTEROID_NUM_START, null, ASTEROID_SIZE_LARGE);
    if (!isGameOver) gameLoop();
  }, 2000);
}

function adjustThrust(direction) {
  if (direction > 0)
    thrustMultiplier = Math.min(
      MAX_THRUST_MULTIPLIER,
      thrustMultiplier + THRUST_ADJUST_STEP
    );
  else
    thrustMultiplier = Math.max(
      MIN_THRUST_MULTIPLIER,
      thrustMultiplier - THRUST_ADJUST_STEP
    );
  updateUI();
}

function updateUI() {
  scoreEl.textContent = score;
  livesEl.textContent = lives;
  thrustLevelEl.textContent = thrustMultiplier.toFixed(1);
}

function gameLoop() {
  if (isGameOver) return;

  // --- Handle Input ---
  if (keys.left) ship.rotate(-1);
  if (keys.right) ship.rotate(1);
  if (keys.up) ship.thrust();
  if (keys.space) ship.tryShoot();
  if (keys.minus) {
    adjustThrust(-1);
    keys.minus = false;
  }
  if (keys.equal) {
    adjustThrust(1);
    keys.equal = false;
  }

  // --- Update ---
  ship.update();
  bullets.forEach((bullet, index) => {
    bullet.update();
    if (
      bullet.lifetime <= 0 ||
      bullet.x < 0 ||
      bullet.x > canvasWidth ||
      bullet.y < 0 ||
      bullet.y > canvasHeight
    )
      bullets.splice(index, 1);
  });
  asteroids.forEach((asteroid) => asteroid.update());
  powerUps.forEach((powerUp, index) => {
    powerUp.update();
    if (powerUp.lifetime <= 0) powerUps.splice(index, 1);
  });

  // --- Check Collisions ---
  checkCollisions();

  // --- Draw ---
  // Draw background first
  drawStars();

  // Draw game objects
  ship.draw();
  bullets.forEach((bullet) => bullet.draw());
  asteroids.forEach((asteroid) => asteroid.draw());
  powerUps.forEach((powerUp) => powerUp.draw());

  // --- Loop ---
  gameLoopTimeout = setTimeout(gameLoop, 1000 / FPS);
}

// --- Event Listeners ---
document.addEventListener('keydown', (e) => {
  if (isGameOver) return;
  let buttonToActivate = null;
  let handled = true;
  switch (e.code) {
    case 'ArrowLeft':
    case 'KeyA':
      keys.left = true;
      buttonToActivate = leftBtn;
      break;
    case 'ArrowRight':
    case 'KeyD':
      keys.right = true;
      buttonToActivate = rightBtn;
      break;
    case 'ArrowUp':
    case 'KeyW':
      keys.up = true;
      buttonToActivate = thrustBtn;
      break;
    case 'Space':
      keys.space = true;
      buttonToActivate = fireBtn;
      break;
    case 'Minus':
      keys.minus = true;
      break;
    case 'Equal':
      keys.equal = true;
      break;
    default:
      handled = false;
  }
  if (buttonToActivate) buttonToActivate.classList.add('active');
  if (handled) e.preventDefault();
});

document.addEventListener('keyup', (e) => {
  let buttonToDeactivate = null;
  switch (e.code) {
    case 'ArrowLeft':
    case 'KeyA':
      keys.left = false;
      buttonToDeactivate = leftBtn;
      break;
    case 'ArrowRight':
    case 'KeyD':
      keys.right = false;
      buttonToDeactivate = rightBtn;
      break;
    case 'ArrowUp':
    case 'KeyW':
      keys.up = false;
      buttonToDeactivate = thrustBtn;
      break;
    case 'Space':
      keys.space = false;
      buttonToDeactivate = fireBtn;
      break;
  }
  if (buttonToDeactivate) buttonToDeactivate.classList.remove('active');
});

// Button Input Listeners
function handleButtonDown(key, btn) {
  keys[key] = true;
  btn.classList.add('active');
  if (key === 'space') ship.tryShoot();
}
function handleButtonUp(key, btn) {
  keys[key] = false;
  btn.classList.remove('active');
}

leftBtn.addEventListener('mousedown', () => handleButtonDown('left', leftBtn));
leftBtn.addEventListener('mouseup', () => handleButtonUp('left', leftBtn));
leftBtn.addEventListener('mouseleave', () => handleButtonUp('left', leftBtn));
leftBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  handleButtonDown('left', leftBtn);
});
leftBtn.addEventListener('touchend', () => handleButtonUp('left', leftBtn));

rightBtn.addEventListener('mousedown', () =>
  handleButtonDown('right', rightBtn)
);
rightBtn.addEventListener('mouseup', () => handleButtonUp('right', rightBtn));
rightBtn.addEventListener('mouseleave', () =>
  handleButtonUp('right', rightBtn)
);
rightBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  handleButtonDown('right', rightBtn);
});
rightBtn.addEventListener('touchend', () => handleButtonUp('right', rightBtn));

thrustBtn.addEventListener('mousedown', () =>
  handleButtonDown('up', thrustBtn)
);
thrustBtn.addEventListener('mouseup', () => handleButtonUp('up', thrustBtn));
thrustBtn.addEventListener('mouseleave', () => handleButtonUp('up', thrustBtn));
thrustBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  handleButtonDown('up', thrustBtn);
});
thrustBtn.addEventListener('touchend', () => handleButtonUp('up', thrustBtn));

fireBtn.addEventListener('mousedown', () => handleButtonDown('space', fireBtn));
fireBtn.addEventListener('mouseup', () => handleButtonUp('space', fireBtn));
fireBtn.addEventListener('mouseleave', () => handleButtonUp('space', fireBtn));
fireBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  handleButtonDown('space', fireBtn);
});
fireBtn.addEventListener('touchend', () => handleButtonUp('space', fireBtn));

restartBtn.addEventListener('click', initGame);
window.addEventListener('resize', resizeCanvas);

// --- Initial Setup ---
resizeCanvas(); // Sets initial size and creates stars
initGame(); // Starts the game

Final Output:

create-asteroid-game-using-html-css-and-javascript.gif

Conclusion:

You have successfully built a simple Asteroid game using HTML, CSS, and JavaScript. You can enhance it by adding sounds, score tracking, and more effects. Keep experimenting and improving your game development skills!

That’s a wrap!

I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Post

Please allow ads on our site🥺