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.

Table of Contents
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 viasetTimeout
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:

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 😊