bullet heaven

Bullet Heaven is an action-packed arcade game where you pilot a glowing mech through a star-filled space, automatically targeting and firing at swarming enemies while dodging with keyboard controls. Collect rotating power-ups to boost your health, damage, fire rate, and score multiplier as waves of enemies attack from all sides.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the player — Higher PLAYER_SPEED makes the mech zip around faster, making dodging easier but more twitchy.
  2. Make enemies spawn faster — Lower ENEMY_SPAWN_INTERVAL fills the screen with enemies quicker, ramping up the difficulty instantly.
  3. Boost projectile damage — Higher PROJECTILE_BASE_DAMAGE kills enemies in fewer shots, making you feel more powerful.
  4. Recolor your mech — Change the mechSkin colors to transform your mech from cyan to any color you want.
  5. Change the background color — The dark blue space background becomes a different hue, completely changing the aesthetic.
  6. Slow down the projectiles — Lower PROJECTILE_SPEED makes bullets move slower, making it harder to hit distant enemies.
Prefer the full editor? Open it there →

📖 About This Sketch

Bullet Heaven is a complete arcade shooter built entirely in p5.js. You pilot a cyan-glowing mech through an animated starfield, automatically firing projectiles at the nearest enemy while dodging incoming threats and collecting power-ups. The visual style combines a grid-based background, hand-drawn enemy sprites, procedurally-colored projectiles, and pulsing power-up icons to create an action game that feels polished and alive. Under the hood, it demonstrates object-oriented design with four main classes (Player, Enemy, Projectile, PowerUp), collision detection, array management, vector math for aiming, and a game state machine that handles menus and game-over screens.

The code is organized into class definitions, drawing and update functions, and utility helpers. By reading it, you will learn how to structure a full game in p5.js—how to spawn and remove objects efficiently, how to detect collisions between multiple object types, how to use frameCount for timing, and how to manage game states (menu → playing → game over). This sketch is a masterclass in combining p5.js fundamentals into a playable, engaging interactive experience.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas, initializes the starfield background, loads sound effects, and displays a START MISSION button. The player waits on the menu screen until they click the button or press any key.
  2. Once the game starts, the draw loop calls gameUpdate() and gameDraw() every frame. gameUpdate() moves the player based on keyboard input, automatically fires the closest enemy, spawns new enemies every 60 frames from screen edges, and checks all collision pairs.
  3. Collision detection happens in three layers: projectiles hitting enemies (damage and removal), enemies hitting the player (health loss), and the player touching power-ups (stat boosts). Projectiles auto-target by finding the closest enemy using distance-squared math to avoid expensive square root calls.
  4. Enemies spawn on random edges and move toward the player while wiggling slightly for unpredictability. Each enemy has randomized size, speed, and health to create variety. Projectiles are fired in the direction of the target and removed if they leave the screen.
  5. Power-ups spawn in the safe center area every 300 frames in four types: health (restores), damage (boost), cooldown (faster firing), and score multiplier (payout bonus). Each pulses with a sine wave glow and plays an upgrade sound when collected.
  6. When the player's health reaches 0, gameOver becomes true and a game-over screen appears with the final score. Clicking RELAUNCH resets all arrays and variables, then restarts the game loop.

🎓 Concepts You'll Learn

Game state machineObject-oriented design with classesCollision detection (distance-based)Automatic targeting and vector mathArray management and iterationKeyboard input handlingSound playback with p5.soundScore tracking and game balance

📝 Code Breakdown

setup()

setup() runs once when the sketch first loads. It's where you create your canvas, initialize variables, load external assets (like sounds), and set up the UI. This is the perfect place to establish colors, sizes, and game constants that stay the same for the entire sketch.

function setup() {
  createCanvas(windowWidth, windowHeight);

  mechSkin = {
    body: color(70, 180, 255),
    trim: color(255, 255, 255, 120),
    core: color(255, 150, 0),
  };
  thrusterColor = color(0, 255, 255, 180);

  initStars();
  resetGame();

  if (shootSound) shootSound.setVolume(0.2);
  if (hitSound) hitSound.setVolume(0.3);
  if (upgradeSound) upgradeSound.setVolume(0.4);

  startButton = createButton('START MISSION');
  startButton.addClass('start-btn');
  startButton.mousePressed(() => {
    if (!gameStarted || gameOver) {
      resetGame();
      gameStarted = true;
    }
  });

  userStartAudio();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas that covers the entire browser window

calculation Mech Color Scheme mechSkin = { body: color(70, 180, 255), trim: color(255, 255, 255, 120), core: color(255, 150, 0), };

Defines the RGB colors for the player mech's body, trim, and core so it looks cohesive and glowing

calculation Start Button Setup startButton = createButton('START MISSION'); startButton.addClass('start-btn'); startButton.mousePressed(() => { if (!gameStarted || gameOver) { resetGame(); gameStarted = true; } });

Creates a clickable START MISSION button with CSS styling that resets and starts the game

createCanvas(windowWidth, windowHeight);
Makes a canvas as large as the browser window so the game fills the entire screen
mechSkin = { body: color(70, 180, 255), trim: color(255, 255, 255, 120), core: color(255, 150, 0), };
Stores three RGB colors in an object so you can reuse them when drawing the player mech without repeating color() calls
thrusterColor = color(0, 255, 255, 180);
Pre-defines the cyan thruster color so it matches everywhere and can be changed in one place
initStars();
Calls the function that populates the stars array with 200 random starfield points for the background animation
resetGame();
Initializes the player and empties all the enemy, projectile, and power-up arrays to prepare a fresh game state
if (shootSound) shootSound.setVolume(0.2);
Sets the volume of the shoot sound to 20% only if it successfully loaded from the web URL
startButton = createButton('START MISSION');
Creates a DOM button element with the text START MISSION that will appear on screen
startButton.addClass('start-btn');
Applies the CSS class 'start-btn' from style.css to style the button with cyan border and glow effects
startButton.mousePressed(() => { ... });
Defines a callback function that fires when the button is clicked, resetting and starting the game
userStartAudio();
p5.sound helper that enables audio playback by asking the browser for permission on first user interaction

draw()

draw() runs 60 times per second and is the heartbeat of your sketch. This function uses a state machine pattern—it checks gameStarted and gameOver booleans to decide which screen and logic to run. This is the classic way to build games with multiple scenes (menu, play, game-over). Notice how each state returns early to prevent mixing game states.

🔬 This is the game state machine in action. What happens if you remove the 'return;' so that gameUpdate() and gameDraw() still run while viewing the menu? Can you predict the chaos?

  if (!gameStarted) {
    drawMenuScreen();
    updateStartButton('START MISSION', height / 2 + 40);
    return;
  }
function draw() {
  drawBackground();

  if (!gameStarted) {
    drawMenuScreen();
    updateStartButton('START MISSION', height / 2 + 40);
    return;
  }

  if (gameOver) {
    drawGameOverScreen();
    updateStartButton('RELAUNCH', height / 2 + 60);
    return;
  }

  startButton.hide();
  gameUpdate();
  gameDraw();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Menu State Check if (!gameStarted) { drawMenuScreen(); updateStartButton('START MISSION', height / 2 + 40); return; }

If the game hasn't started, draw the title screen and reposition the button, then skip the rest of draw()

conditional Game Over State Check if (gameOver) { drawGameOverScreen(); updateStartButton('RELAUNCH', height / 2 + 60); return; }

If the player died, draw the game-over screen and button, then skip the update and draw calls

calculation Active Gameplay startButton.hide(); gameUpdate(); gameDraw();

Hide the button and run the main game logic (update positions, check collisions) then draw all game objects

drawBackground();
Clears the canvas and redraws the animated starfield and grid every frame, creating the moving space background
if (!gameStarted) {
Checks if gameStarted is false—meaning the game hasn't begun yet, so show the menu
drawMenuScreen();
Displays the title 'BULLET HEAVEN' and the instruction text on the menu screen
updateStartButton('START MISSION', height / 2 + 40);
Positions the START MISSION button below the menu text and makes it visible
return;
Stops draw() here so the rest of the function doesn't run—menu state is complete
if (gameOver) {
Checks if gameOver is true—the player's health reached 0 and they need to restart
drawGameOverScreen();
Displays 'SYSTEM FAILURE', the final score, and restart instructions
updateStartButton('RELAUNCH', height / 2 + 60);
Repositions and relabels the button to say RELAUNCH, inviting the player to try again
startButton.hide();
Hides the button during active gameplay so it doesn't obscure the action
gameUpdate();
Calls the function that moves enemies, checks collisions, spawns power-ups, and updates game state
gameDraw();
Calls the function that draws the player, all enemies, projectiles, power-ups, and HUD text on the screen

gameUpdate()

gameUpdate() is where all the logic happens—it's the 'brain' of your game. It's called once per frame (60 times per second) and handles movement, spawning, collision detection, and state changes. Notice the backward loops when removing items: this is essential because removing an item shifts all the indices, so going backwards prevents skipping items. Also notice the nested loops checking projectiles against enemies—this is O(n²) but fine for small counts. The `continue` statement skips projectile collision checks for enemies that already collided with the player.

🔬 This inner loop checks every projectile against the current enemy. What happens if you change the collision distance threshold from 'enemy.size * 0.5 + proj.size' to just '15'? Will hits feel harder or easier to land?

    for (let j = projectiles.length - 1; j >= 0; j--) {
      const proj = projectiles[j];
      if (dist(enemy.x, enemy.y, proj.x, proj.y) < enemy.size * 0.5 + proj.size) {
        if (enemy.takeDamage(proj.damage)) {
          score += 12 * player.scoreMultiplier;
          enemies.splice(i, 1);
        }
        projectiles.splice(j, 1);
        break;
      }
    }
function gameUpdate() {
  player.move();
  player.attack();

  if (frameCount % ENEMY_SPAWN_INTERVAL === 0) {
    const side = floor(random(4));
    const padding = 40;
    let x, y;
    if (side === 0) { x = random(width); y = -padding; }
    if (side === 1) { x = width + padding; y = random(height); }
    if (side === 2) { x = random(width); y = height + padding; }
    if (side === 3) { x = -padding; y = random(height); }
    enemies.push(new Enemy(x, y));
  }

  for (let i = enemies.length - 1; i >= 0; i--) {
    const enemy = enemies[i];
    enemy.move(player);

    if (dist(player.x, player.y, enemy.x, enemy.y) < player.size * 0.6 + enemy.size * 0.4) {
      player.takeDamage(enemy.damage);
      enemies.splice(i, 1);
      continue;
    }

    for (let j = projectiles.length - 1; j >= 0; j--) {
      const proj = projectiles[j];
      if (dist(enemy.x, enemy.y, proj.x, proj.y) < enemy.size * 0.5 + proj.size) {
        if (enemy.takeDamage(proj.damage)) {
          score += 12 * player.scoreMultiplier;
          enemies.splice(i, 1);
        }
        projectiles.splice(j, 1);
        break;
      }
    }
  }

  for (let i = projectiles.length - 1; i >= 0; i--) {
    projectiles[i].move();
    if (projectiles[i].isOffscreen()) projectiles.splice(i, 1);
  }

  if (frameCount % POWER_UP_SPAWN_INTERVAL === 0) {
    const type = random(['health', 'damage', 'cooldown', 'score']);
    powerUps.push(new PowerUp(
      random(width * 0.15, width * 0.85),
      random(height * 0.15, height * 0.85),
      type
    ));
  }

  for (let i = powerUps.length - 1; i >= 0; i--) {
    if (dist(player.x, player.y, powerUps[i].x, powerUps[i].y) < player.size) {
      powerUps[i].applyEffect(player);
      powerUps.splice(i, 1);
    }
  }
}
Line-by-line explanation (23 lines)

🔧 Subcomponents:

calculation Player Input player.move(); player.attack();

Reads keyboard input to move the mech and automatically fires at the nearest enemy

conditional Enemy Spawn Logic if (frameCount % ENEMY_SPAWN_INTERVAL === 0) { const side = floor(random(4)); const padding = 40; let x, y; if (side === 0) { x = random(width); y = -padding; } if (side === 1) { x = width + padding; y = random(height); } if (side === 2) { x = random(width); y = height + padding; } if (side === 3) { x = -padding; y = random(height); } enemies.push(new Enemy(x, y)); }

Every 60 frames, spawns a new enemy on a random side of the screen

for-loop Enemy Movement and Collisions for (let i = enemies.length - 1; i >= 0; i--) { ... }

Loops through all enemies backwards, moving each one and checking collisions with player and projectiles

for-loop Projectile Movement and Removal for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].move(); if (projectiles[i].isOffscreen()) projectiles.splice(i, 1); }

Updates each projectile's position and removes it if it leaves the canvas to free memory

conditional Power-Up Spawn if (frameCount % POWER_UP_SPAWN_INTERVAL === 0) { const type = random(['health', 'damage', 'cooldown', 'score']); powerUps.push(new PowerUp(...)); }

Every 300 frames, spawns a random power-up in the safe center area of the canvas

for-loop Power-Up Collection for (let i = powerUps.length - 1; i >= 0; i--) { if (dist(player.x, player.y, powerUps[i].x, powerUps[i].y) < player.size) { powerUps[i].applyEffect(player); powerUps.splice(i, 1); } }

Checks if the player is close enough to collect each power-up and applies its effect

player.move();
Calls the player's move method, which reads keyboard input and updates player.x and player.y
player.attack();
Calls the player's attack method, which finds the closest enemy, calculates the firing angle, and creates a new Projectile
if (frameCount % ENEMY_SPAWN_INTERVAL === 0) {
Checks if the current frame number is divisible by 60—this happens every 60 frames (about 1 second), timing enemy spawns
const side = floor(random(4));
Picks a random integer 0, 1, 2, or 3 representing which edge of the screen the enemy spawns from
if (side === 0) { x = random(width); y = -padding; }
If side is 0 (top), pick a random x position across the width and place y above the canvas at -40
if (side === 1) { x = width + padding; y = random(height); }
If side is 1 (right), place x off-screen to the right and pick a random y position
enemies.push(new Enemy(x, y));
Creates a new Enemy at the calculated (x, y) position and adds it to the enemies array
for (let i = enemies.length - 1; i >= 0; i--) {
Loops through enemies backwards (from last to first) so you can safely remove items without skipping any
enemy.move(player);
Updates the enemy's position by making it move toward the player with some wiggle for unpredictability
if (dist(player.x, player.y, enemy.x, enemy.y) < player.size * 0.6 + enemy.size * 0.4) {
Calculates distance from player to enemy and checks if they're overlapping—a circle collision test
player.takeDamage(enemy.damage);
The enemy touched the player, so the player loses health and may die if health reaches 0
enemies.splice(i, 1);
Removes the colliding enemy from the array so it disappears and won't be updated again
continue;
Skips the rest of this loop iteration so you don't check projectile collisions for an already-removed enemy
for (let j = projectiles.length - 1; j >= 0; j--) {
For each enemy, loops through all projectiles backwards to check if any hit this enemy
if (dist(enemy.x, enemy.y, proj.x, proj.y) < enemy.size * 0.5 + proj.size) {
Checks if projectile and enemy circles overlap using distance—another collision test
if (enemy.takeDamage(proj.damage)) {
Damages the enemy; takeDamage() returns true if the enemy died (health <= 0)
score += 12 * player.scoreMultiplier;
If the enemy died, add 12 points times the current score multiplier to the player's total score
projectiles[i].move();
Updates each projectile's position by adding its velocity (vx, vy) each frame
if (projectiles[i].isOffscreen()) projectiles.splice(i, 1);
If the projectile left the canvas boundaries, remove it to save memory
const type = random(['health', 'damage', 'cooldown', 'score']);
Randomly picks one of four power-up types as a string
powerUps.push(new PowerUp( random(width * 0.15, width * 0.85), random(height * 0.15, height * 0.85), type ));
Creates a new power-up at a random position in the safe center area (not near edges) with the chosen type
if (dist(player.x, player.y, powerUps[i].x, powerUps[i].y) < player.size) {
Checks if the player is close enough to the power-up (within player.size pixels)
powerUps[i].applyEffect(player);
Calls the power-up's applyEffect method, which boosts one of the player's stats based on power-up type

gameDraw()

gameDraw() is the 'renderer'—it draws everything that should appear on screen this frame. It's called once per frame after gameUpdate(), so the positions are current. Notice it uses forEach loops instead of for loops because it's only reading (drawing), not modifying the arrays. The HUD text uses template literals with Math.floor() and .toFixed() to format numbers nicely for the player to read.

function gameDraw() {
  player.draw();
  enemies.forEach(e => e.draw());
  projectiles.forEach(p => p.draw());
  powerUps.forEach(p => p.draw());

  fill(255);
  textAlign(LEFT, TOP);
  textSize(18);
  text(`Score: ${Math.floor(score)}`, 20, 20);
  text(`Hull: ${player.health}/${player.maxHealth}`, 20, 45);
  text(`x${player.scoreMultiplier.toFixed(2)} payout`, 20, 70);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Game Object Rendering player.draw(); enemies.forEach(e => e.draw()); projectiles.forEach(p => p.draw()); powerUps.forEach(p => p.draw());

Draws the player mech, all enemies, all projectiles, and all power-ups by calling their draw() methods

calculation Heads-Up Display fill(255); textAlign(LEFT, TOP); textSize(18); text(`Score: ${Math.floor(score)}`, 20, 20); text(`Hull: ${player.health}/${player.maxHealth}`, 20, 45); text(`x${player.scoreMultiplier.toFixed(2)} payout`, 20, 70);

Draws white text in the top-left corner showing score, health, and score multiplier

player.draw();
Calls the player's draw method, which renders the cyan mech, shield, thrusters, and health bar
enemies.forEach(e => e.draw());
Loops through every enemy in the array and calls their draw method using the compact forEach syntax
projectiles.forEach(p => p.draw());
Loops through every projectile and draws it as an orange dot with a cyan trail line
powerUps.forEach(p => p.draw());
Loops through every power-up and draws its unique icon (cross, triangle, circle, or star) with pulsing glow
fill(255);
Sets the text color to white so the HUD is readable against the dark background
textAlign(LEFT, TOP);
Aligns text to the left and top edges so it starts at position (20, 20) in the top-left corner
textSize(18);
Sets the font size to 18 pixels so the HUD text is readable but not huge
text(`Score: ${Math.floor(score)}`, 20, 20);
Displays the current score rounded down to a whole number at position (20, 20)
text(`Hull: ${player.health}/${player.maxHealth}`, 20, 45);
Shows current health and max health (e.g., 'Hull: 85/100') at position (20, 45)
text(`x${player.scoreMultiplier.toFixed(2)} payout`, 20, 70);
Displays the score multiplier rounded to 2 decimal places (e.g., 'x1.50 payout') at position (20, 70)

Player.move()

move() reads keyboard state every frame and updates the player's position. Using keyIsDown() instead of keyPressed() allows smooth continuous movement when a key is held. The constrain() function is a safety net that keeps the mech from escaping the canvas boundaries. This is a simple but essential part of game input handling.

🔬 These lines keep the player inside the canvas. What happens if you remove them? Can the mech escape off-screen and get stuck?

    this.x = constrain(this.x, this.size, width - this.size);
    this.y = constrain(this.y, this.size, height - this.size);
  move() {
    if (keyIsDown(87) || keyIsDown(UP_ARROW)) this.y -= PLAYER_SPEED;
    if (keyIsDown(83) || keyIsDown(DOWN_ARROW)) this.y += PLAYER_SPEED;
    if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) this.x -= PLAYER_SPEED;
    if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) this.x += PLAYER_SPEED;

    this.x = constrain(this.x, this.size, width - this.size);
    this.y = constrain(this.y, this.size, height - this.size);
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Vertical Movement if (keyIsDown(87) || keyIsDown(UP_ARROW)) this.y -= PLAYER_SPEED; if (keyIsDown(83) || keyIsDown(DOWN_ARROW)) this.y += PLAYER_SPEED;

Decreases y to move up or increases y to move down based on W, S, or arrow keys being held

conditional Horizontal Movement if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) this.x -= PLAYER_SPEED; if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) this.x += PLAYER_SPEED;

Decreases x to move left or increases x to move right based on A, D, or arrow keys being held

calculation Screen Boundary Clamping this.x = constrain(this.x, this.size, width - this.size); this.y = constrain(this.y, this.size, height - this.size);

Prevents the player from moving off-screen by clamping x and y to stay inside the canvas

if (keyIsDown(87) || keyIsDown(UP_ARROW)) this.y -= PLAYER_SPEED;
Checks if W (key code 87) or the up arrow is being held; if so, subtract PLAYER_SPEED from y to move the mech up
if (keyIsDown(83) || keyIsDown(DOWN_ARROW)) this.y += PLAYER_SPEED;
Checks if S (key code 83) or the down arrow is held; if so, add PLAYER_SPEED to y to move down
if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) this.x -= PLAYER_SPEED;
Checks if A (key code 65) or the left arrow is held; if so, subtract PLAYER_SPEED from x to move left
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) this.x += PLAYER_SPEED;
Checks if D (key code 68) or the right arrow is held; if so, add PLAYER_SPEED to x to move right
this.x = constrain(this.x, this.size, width - this.size);
Clamps x so it's never less than this.size (13 pixels from left) or greater than width - this.size (13 from right)
this.y = constrain(this.y, this.size, height - this.size);
Clamps y so it's never less than this.size (13 pixels from top) or greater than height - this.size (13 from bottom)

Player.attack()

attack() is the automatic aiming logic. Every frame, it finds the closest enemy (without expensive sqrt() by comparing squared distances), calculates the firing angle using atan2() and trigonometry, and creates a projectile with velocity components pointing at the target. The cooldown mechanism (checking frameCount - lastAttack) prevents spam-firing, making the game balanced. This is how countless video games implement auto-aim.

🔬 This loop finds the closest enemy by comparing squared distances. What happens if you change 'if (d2 < distSq)' to 'if (d2 > distSq)' to target the farthest enemy instead? Does dodging feel harder?

    let closest = null;
    let distSq = Infinity;
    for (const enemy of enemies) {
      const dx = enemy.x - this.x;
      const dy = enemy.y - this.y;
      const d2 = dx * dx + dy * dy;
      if (d2 < distSq) {
        distSq = d2;
        closest = enemy;
      }
    }
  attack() {
    if (!enemies.length) return;
    if (frameCount - this.lastAttack <= this.attackCooldown) return;

    let closest = null;
    let distSq = Infinity;
    for (const enemy of enemies) {
      const dx = enemy.x - this.x;
      const dy = enemy.y - this.y;
      const d2 = dx * dx + dy * dy;
      if (d2 < distSq) {
        distSq = d2;
        closest = enemy;
      }
    }
    if (!closest) return;

    const angle = atan2(closest.y - this.y, closest.x - this.x);
    const vx = cos(angle) * PROJECTILE_SPEED;
    const vy = sin(angle) * PROJECTILE_SPEED;
    projectiles.push(new Projectile(this.x, this.y, vx, vy, this.projectileDamage));
    if (shootSound) shootSound.play();
    this.lastAttack = frameCount;
  }
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Fire Guard if (!enemies.length) return; if (frameCount - this.lastAttack <= this.attackCooldown) return;

Prevents firing if there are no enemies or if the attack cooldown hasn't elapsed yet

for-loop Closest Enemy Targeting let closest = null; let distSq = Infinity; for (const enemy of enemies) { const dx = enemy.x - this.x; const dy = enemy.y - this.y; const d2 = dx * dx + dy * dy; if (d2 < distSq) { distSq = d2; closest = enemy; } }

Loops through all enemies, calculating squared distance to each, and keeps track of the closest one

calculation Projectile Creation const angle = atan2(closest.y - this.y, closest.x - this.x); const vx = cos(angle) * PROJECTILE_SPEED; const vy = sin(angle) * PROJECTILE_SPEED; projectiles.push(new Projectile(this.x, this.y, vx, vy, this.projectileDamage));

Calculates the angle to the target, converts it to velocity components, and creates a new projectile fired in that direction

if (!enemies.length) return;
If the enemies array is empty (no targets), exit early so the player doesn't fire into nothing
if (frameCount - this.lastAttack <= this.attackCooldown) return;
If the frames since the last attack are less than attackCooldown (30 frames), exit early—can't fire yet
let closest = null;
Initializes a variable to store a reference to the closest enemy, starting as null
let distSq = Infinity;
Initializes a variable to track the smallest distance found; starts as Infinity so any real distance is smaller
for (const enemy of enemies) {
Loops through every enemy in the enemies array
const dx = enemy.x - this.x; const dy = enemy.y - this.y;
Calculates the horizontal and vertical distance components from the player to this enemy
const d2 = dx * dx + dy * dy;
Calculates squared distance (dx² + dy²) without using expensive sqrt(), which is fine for comparisons
if (d2 < distSq) { distSq = d2; closest = enemy; }
If this enemy is closer than any previous one, update the closest reference and distSq record
const angle = atan2(closest.y - this.y, closest.x - this.x);
Calculates the angle from player to target using atan2, which handles all four quadrants correctly
const vx = cos(angle) * PROJECTILE_SPEED;
Converts the angle to a horizontal velocity component using cosine and scales by projectile speed
const vy = sin(angle) * PROJECTILE_SPEED;
Converts the angle to a vertical velocity component using sine and scales by projectile speed
projectiles.push(new Projectile(this.x, this.y, vx, vy, this.projectileDamage));
Creates a new Projectile starting at the player's position with calculated velocity and damage, and adds it to the projectiles array
if (shootSound) shootSound.play();
If the shoot sound loaded successfully, play it to give audio feedback of the shot
this.lastAttack = frameCount;
Records the current frame number so the cooldown timer can be calculated next time

Enemy.move()

move() implements two behaviors: chase and wiggle. The chase logic uses atan2() and trig to move toward the player like a homing missile. The wiggle adds procedural wobble using sine/cosine waves keyed to frameCount, making the motion feel alive and slightly unpredictable without requiring random() each frame. This combination creates enemies that are threatening but not mechanical.

  move(player) {
    const angle = atan2(player.y - this.y, player.x - this.x);
    this.x += cos(angle) * this.speed;
    this.y += sin(angle) * this.speed;

    this.x += cos(frameCount * 0.15 + this.wiggleOffset) * 0.6;
    this.y += sin(frameCount * 0.1 + this.wiggleOffset * 1.5) * 0.4;
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Chase Player const angle = atan2(player.y - this.y, player.x - this.x); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;

Calculates the angle to the player and moves toward them using velocity components

calculation Wiggle Motion this.x += cos(frameCount * 0.15 + this.wiggleOffset) * 0.6; this.y += sin(frameCount * 0.1 + this.wiggleOffset * 1.5) * 0.4;

Adds a sine/cosine-based wobbling motion to make enemy movement less predictable and more organic

const angle = atan2(player.y - this.y, player.x - this.x);
Calculates the angle from this enemy to the player using atan2
this.x += cos(angle) * this.speed;
Moves the enemy horizontally toward the player by a distance determined by speed and cosine of angle
this.y += sin(angle) * this.speed;
Moves the enemy vertically toward the player by a distance determined by speed and sine of angle
this.x += cos(frameCount * 0.15 + this.wiggleOffset) * 0.6;
Adds a small oscillating horizontal offset using cosine of frame count, creating a wiggle effect
this.y += sin(frameCount * 0.1 + this.wiggleOffset * 1.5) * 0.4;
Adds a small oscillating vertical offset using sine of frame count, making the enemy's path wavy and unpredictable

Projectile.draw()

draw() uses push/pop to isolate transforms, keeping this projectile's drawing code local and clean. The cyan line is drawn in the direction of velocity (vx, vy), creating a motion blur effect. The orange dot is the actual projectile. This two-part design makes bullets feel fast and energetic.

  draw() {
    push();
    translate(this.x, this.y);
    stroke(0, 255, 255);
    strokeWeight(3);
    line(0, 0, this.vx * 1.2, this.vy * 1.2);
    noStroke();
    fill(255, 180, 80);
    ellipse(0, 0, this.size);
    pop();
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Projectile Trail stroke(0, 255, 255); strokeWeight(3); line(0, 0, this.vx * 1.2, this.vy * 1.2);

Draws a cyan line behind the projectile pointing in the direction of travel, creating a glowing trail effect

calculation Projectile Core noStroke(); fill(255, 180, 80); ellipse(0, 0, this.size);

Draws an orange dot at the projectile's center to represent the actual projectile

push();
Saves the current graphics state so transformations don't affect other objects
translate(this.x, this.y);
Moves the origin to the projectile's position so drawing is local (simpler to think about)
stroke(0, 255, 255);
Sets the line color to cyan (R=0, G=255, B=255)
strokeWeight(3);
Sets the line thickness to 3 pixels so the trail is visible and glowing
line(0, 0, this.vx * 1.2, this.vy * 1.2);
Draws a line from the projectile center (0,0) in the direction of velocity, scaled by 1.2 to make it visible
noStroke();
Disables stroke drawing so the ellipse is filled only
fill(255, 180, 80);
Sets the fill color to orange (R=255, G=180, B=80)
ellipse(0, 0, this.size);
Draws a circle at the projectile's position (0,0 in local coords) with diameter equal to this.size (6 pixels)
pop();
Restores the graphics state so subsequent objects are unaffected by the translate

PowerUp.draw()

draw() uses a switch statement to render different shapes for each power-up type. The 'glow' variable creates smooth pulsing using sine waves—frameCount * 0.1 makes it animate every frame, and this.pulse randomizes the phase so power-ups pulse out of sync. The visual distinction (cross for health, triangle for damage, circle for cooldown, star for score) helps players instantly recognize what they're collecting.

🔬 This creates the pulsing effect. What happens if you change 0.2 to 0.5? Will the power-ups pulse more dramatically? What if you change 0.1 to 0.05 to slow down the pulse?

    const glow = 1 + 0.2 * sin(frameCount * 0.1 + this.pulse);
  draw() {
    push();
    translate(this.x, this.y);
    const glow = 1 + 0.2 * sin(frameCount * 0.1 + this.pulse);
    noStroke();
    switch (this.type) {
      case 'health':
        fill(0, 255, 180, 180);
        rectMode(CENTER);
        rect(0, 0, this.size * glow, this.size * 0.6 * glow, 4);
        rect(0, 0, this.size * 0.6 * glow, this.size * glow, 4);
        break;
      case 'damage':
        fill(255, 120, 0, 200);
        triangle(-this.size * 0.5, this.size * 0.4,
                 this.size * 0.5, this.size * 0.4,
                 0, -this.size * 0.6 * glow);
        break;
      case 'cooldown':
        fill(0, 160, 255, 200);
        ellipse(0, 0, this.size * glow);
        stroke(255);
        strokeWeight(3);
        line(0, 0, 0, -this.size * 0.3);
        line(0, 0, this.size * 0.25, 0);
        break;
      case 'score':
        fill(255, 230, 80, 200);
        star(0, 0, this.size * 0.3 * glow, this.size * 0.6 * glow, 5);
        break;
    }
    pop();
  }
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Glow Pulse const glow = 1 + 0.2 * sin(frameCount * 0.1 + this.pulse);

Calculates a pulsing scale factor between 0.8 and 1.2 that makes the power-up shrink and grow smoothly

switch-case Type-Based Rendering switch (this.type) { ... }

Draws different shapes based on power-up type: health (cross), damage (triangle), cooldown (circle), score (star)

push();
Saves graphics state before transformations
translate(this.x, this.y);
Moves origin to power-up position for cleaner local drawing
const glow = 1 + 0.2 * sin(frameCount * 0.1 + this.pulse);
Calculates a glow factor: sine oscillates between -1 and 1, so 0.2 * sine ranges -0.2 to 0.2, plus 1 gives 0.8 to 1.2 for smooth pulsing
case 'health':
If power-up type is 'health', draw a glowing cyan cross (medical plus sign)
rect(0, 0, this.size * glow, this.size * 0.6 * glow, 4);
Draws a horizontal rectangle scaled by glow factor with 4-pixel corner radius
rect(0, 0, this.size * 0.6 * glow, this.size * glow, 4);
Draws a vertical rectangle scaled by glow factor, overlapping the first to form a plus sign
case 'damage':
If type is 'damage', draw an orange triangle pointing upward (arrow/spike shape)
triangle(-this.size * 0.5, this.size * 0.4, this.size * 0.5, this.size * 0.4, 0, -this.size * 0.6 * glow);
Defines three vertices: left and right bottom corners, and a pulsing top point
case 'cooldown':
If type is 'cooldown', draw a blue circle with white clock hands
line(0, 0, 0, -this.size * 0.3); line(0, 0, this.size * 0.25, 0);
Draws two white lines from center at 12 and 3 o'clock positions, like a clock face
case 'score':
If type is 'score', draw a yellow star using the star() helper function
star(0, 0, this.size * 0.3 * glow, this.size * 0.6 * glow, 5);
Calls star() with pulsing inner and outer radii to draw a 5-pointed star that glows
pop();
Restores graphics state so other objects aren't affected

drawHealthBar()

drawHealthBar() demonstrates the map() function, which rescales values from one range to another. Here it converts health (0–100) to bar width (0–60 pixels). The two-rectangle approach (dark background + colored fill) is a classic UI pattern. It's called once per frame during player.draw(), always appearing above the mech.

function drawHealthBar(x, y, value, maxValue) {
  push();
  rectMode(CORNER);
  noStroke();
  fill(80, 0, 0);
  rect(x - 30, y - 10, 60, 6);
  fill(0, 255, 140);
  const w = map(value, 0, maxValue, 0, 60);
  rect(x - 30, y - 10, w, 6);
  pop();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Health Bar Background fill(80, 0, 0); rect(x - 30, y - 10, 60, 6);

Draws a dark red background bar (empty state) 60 pixels wide above the player

calculation Health Bar Fill const w = map(value, 0, maxValue, 0, 60); rect(x - 30, y - 10, w, 6);

Maps health value to bar width and draws bright green fill that shrinks as health decreases

push();
Saves graphics state before mode changes
rectMode(CORNER);
Sets rect drawing mode to CORNER so the first two arguments are the top-left corner, not the center
noStroke();
Disables outline drawing so only filled shapes appear
fill(80, 0, 0);
Sets fill color to dark red to represent empty health
rect(x - 30, y - 10, 60, 6);
Draws the dark red background bar: 60 pixels wide, 6 pixels tall, centered at x, above y by 10 pixels
fill(0, 255, 140);
Changes fill color to bright cyan-green (the health indicator color)
const w = map(value, 0, maxValue, 0, 60);
Converts health value (e.g., 75/100) to a bar width (e.g., 45 pixels) using map() to scale from health range to pixel range
rect(x - 30, y - 10, w, 6);
Draws the bright green fill bar starting at the same position, but with width w that shrinks as health decreases
pop();
Restores graphics state so other drawing isn't affected

initStars()

initStars() generates a starfield by creating 200 small objects with random properties. Each star has a layer (0=far, 1=middle, 2=near) that affects its speed in drawBackground() via parallax scrolling. This is called in setup() and again in windowResized() to regenerate stars when the browser window is resized. Using objects instead of separate arrays keeps related data grouped.

function initStars() {
  stars = [];
  for (let i = 0; i < STAR_COUNT; i++) {
    stars.push({
      x: random(width),
      y: random(height),
      size: random(1, 3),
      speed: random(0.1, 0.6),
      layer: random([0, 1, 2]),
    });
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Array Initialization stars = [];

Clears the stars array so a fresh starfield is created

for-loop Star Creation Loop for (let i = 0; i < STAR_COUNT; i++) { stars.push({...}); }

Loops 200 times, creating an object for each star with random position, size, speed, and depth layer

stars = [];
Empties the stars array so previous stars don't accumulate (useful when called from windowResized)
for (let i = 0; i < STAR_COUNT; i++) {
Loops exactly STAR_COUNT times (200), creating one star per iteration
stars.push({
Creates a new object and adds it to the stars array
x: random(width),
Assigns a random x position between 0 and canvas width
y: random(height),
Assigns a random y position between 0 and canvas height
size: random(1, 3),
Gives the star a random size between 1 and 3 pixels for visual variation
speed: random(0.1, 0.6),
Assigns a random horizontal scroll speed to make the parallax starfield feel dynamic
layer: random([0, 1, 2]),
Randomly picks a depth layer (0, 1, or 2) so stars have depth and some are far, some near

drawBackground()

drawBackground() creates a dynamic space environment using two techniques: a scrolling grid for sci-fi vibes and parallax stars for depth. The grid wraps seamlessly using modulo (%), and stars wrap by teleporting. Parallax is the key: near stars move faster than far stars, creating a sense of depth and speed. This is called every frame before anything else, so it's the base layer.

🔬 This makes the grid scroll. What happens if you change 0.5 to 2.0? Does the grid move faster or slower? What if you change it to -1.0 to scroll the opposite direction?

  const offset = (frameCount * 0.5) % spacing;
function drawBackground() {
  background(10, 10, 30);

  stroke(20, 90, 120, 150);
  strokeWeight(1);
  const spacing = 60;
  const offset = (frameCount * 0.5) % spacing;
  for (let x = -spacing; x <= width + spacing; x += spacing) {
    line(x + offset, 0, x + offset, height);
  }
  for (let y = -spacing; y <= height + spacing; y += spacing) {
    line(0, y + offset, width, y + offset);
  }

  noStroke();
  for (const star of stars) {
    const speedFactor = (star.layer + 1) * 0.3;
    star.x -= star.speed * speedFactor;
    if (star.x < 0) star.x = width;
    fill(200 + star.layer * 20, 200, 255, 120);
    ellipse(star.x, star.y, star.size);
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Background Color background(10, 10, 30);

Clears the canvas with a dark navy blue, establishing the space theme

for-loop Animated Grid stroke(20, 90, 120, 150); strokeWeight(1); const spacing = 60; const offset = (frameCount * 0.5) % spacing; for (let x = -spacing; x <= width + spacing; x += spacing) { line(x + offset, 0, x + offset, height); } for (let y = -spacing; y <= height + spacing; y += spacing) { line(0, y + offset, width, y + offset); }

Draws vertical and horizontal lines that scroll and wrap, creating an illusion of forward movement

for-loop Star Parallax Scrolling for (const star of stars) { const speedFactor = (star.layer + 1) * 0.3; star.x -= star.speed * speedFactor; if (star.x < 0) star.x = width; fill(200 + star.layer * 20, 200, 255, 120); ellipse(star.x, star.y, star.size); }

Scrolls stars horizontally at different speeds based on depth layer, creating a parallax depth effect

background(10, 10, 30);
Fills the entire canvas with a dark blue (RGB 10,10,30) to clear previous frame and set the space color
stroke(20, 90, 120, 150);
Sets grid line color to a dark cyan-blue with 150 alpha (semi-transparent)
strokeWeight(1);
Makes grid lines 1 pixel thick so they're subtle and don't overpower the gameplay
const spacing = 60;
Sets the distance between grid lines to 60 pixels
const offset = (frameCount * 0.5) % spacing;
Calculates a scrolling offset: frameCount * 0.5 moves by 0.5 pixels per frame, and % spacing wraps it back to 0 after 60 pixels so it loops infinitely
for (let x = -spacing; x <= width + spacing; x += spacing) {
Loops horizontally, creating vertical lines. Starts before the canvas and extends past it so lines wrap seamlessly
line(x + offset, 0, x + offset, height);
Draws a vertical line at x+offset, making it scroll left as offset increases
for (let y = -spacing; y <= height + spacing; y += spacing) {
Loops vertically, creating horizontal lines with the same offset for consistent grid motion
for (const star of stars) {
Loops through every star object in the stars array
const speedFactor = (star.layer + 1) * 0.3;
Calculates speed multiplier: layer 0 (far) gets 0.3x, layer 1 gets 0.6x, layer 2 (near) gets 0.9x, creating depth
star.x -= star.speed * speedFactor;
Moves star left by subtracting its speed times depth factor, so near stars move faster (parallax effect)
if (star.x < 0) star.x = width;
When a star exits the left edge, teleport it to the right edge so the scrolling loops infinitely
fill(200 + star.layer * 20, 200, 255, 120);
Colors star based on layer: farther stars (layer 0) are dimmer (200 red), nearer stars (layer 2) are brighter (240 red)
ellipse(star.x, star.y, star.size);
Draws the star as a small circle at its current position

star()

star() is a utility function that generates any n-pointed star using trigonometry and beginShape/vertex. It alternates between outer (radius2) and inner (radius1) points around a circle, creating the classic star shape. The math uses cos/sin to place points on a circle. This is called by PowerUp.draw() to render the score multiplier icon. Understanding this function teaches you how to generate complex shapes procedurally.

🔬 This function creates an n-pointed star by alternating between outer and inner radii. What happens if you call it with npoints = 7? Or npoints = 3? Can you visualize a 3-pointed star?

function star(x, y, radius1, radius2, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}
function star(x, y, radius1, radius2, npoints) {
  let angle = TWO_PI / npoints;
  let halfAngle = angle / 2.0;
  beginShape();
  for (let a = 0; a < TWO_PI; a += angle) {
    let sx = x + cos(a) * radius2;
    let sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a + halfAngle) * radius1;
    sy = y + sin(a + halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Angle Calculation let angle = TWO_PI / npoints; let halfAngle = angle / 2.0;

Divides the circle into npoints equal angles, and calculates the half-angle between points

for-loop Vertex Generation for (let a = 0; a < TWO_PI; a += angle) { let sx = x + cos(a) * radius2; let sy = y + sin(a) * radius2; vertex(sx, sy); sx = x + cos(a + halfAngle) * radius1; sy = y + sin(a + halfAngle) * radius1; vertex(sx, sy); }

Loops around the circle, alternating between outer points (radius2) and inner points (radius1) to form star spikes

let angle = TWO_PI / npoints;
Calculates the angle between each star point: TWO_PI (360°) divided by number of points (e.g., 5 points = 72° apart)
let halfAngle = angle / 2.0;
Calculates half the angle so inner points are offset halfway between outer points
beginShape();
Starts recording vertices for a polygon shape
for (let a = 0; a < TWO_PI; a += angle) {
Loops around the circle, incrementing by angle each time until a full rotation is complete
let sx = x + cos(a) * radius2;
Calculates the x-coordinate of an outer point using cosine scaled by radius2
let sy = y + sin(a) * radius2;
Calculates the y-coordinate of an outer point using sine scaled by radius2
vertex(sx, sy);
Records this outer point as a vertex of the star shape
sx = x + cos(a + halfAngle) * radius1;
Calculates the x-coordinate of an inner point at halfway between the outer points, using radius1
sy = y + sin(a + halfAngle) * radius1;
Calculates the y-coordinate of the inner point
vertex(sx, sy);
Records the inner point as a vertex, creating the indentation between spikes
endShape(CLOSE);
Finishes the shape and closes it by drawing a line back to the first vertex

resetGame()

resetGame() is called when the player starts a new game (from menu or after death). It clears all arrays and creates a fresh player, essentially returning the game to its initial state. This is a simple but essential function for any game with a start/restart mechanic.

function resetGame() {
  player = new Player();
  enemies = [];
  projectiles = [];
  powerUps = [];
  score = 0;
  gameOver = false;
}
Line-by-line explanation (6 lines)
player = new Player();
Creates a new Player object, resetting health, position, and all stats to starting values
enemies = [];
Empties the enemies array so all enemies disappear
projectiles = [];
Empties the projectiles array so all active projectiles are removed
powerUps = [];
Empties the powerUps array so all power-ups on screen vanish
score = 0;
Resets the score to 0 for a fresh start
gameOver = false;
Clears the gameOver flag so draw() stops showing the game-over screen

📦 Key Variables

player object

Stores the player mech object with position, health, damage, cooldown, and all control methods

let player;
enemies array

Holds all active Enemy objects currently spawned and pursuing the player

let enemies = [];
projectiles array

Holds all active Projectile objects currently flying toward enemies

let projectiles = [];
powerUps array

Holds all active PowerUp objects available for the player to collect

let powerUps = [];
score number

Tracks the player's current score, incremented when enemies are defeated

let score = 0;
gameOver boolean

Flag that is true when the player's health reaches 0, triggering the game-over screen

let gameOver = false;
gameStarted boolean

Flag that is true once the player clicks START, false on the menu screen

let gameStarted = false;
shootSound object

p5.sound SoundFile object loaded from the web, plays when projectiles are fired

let shootSound;
hitSound object

p5.sound SoundFile object that plays when enemies are defeated

let hitSound;
upgradeSound object

p5.sound SoundFile object that plays when power-ups are collected

let upgradeSound;
startButton object

p5.js DOM button object created in setup() that starts or restarts the game

let startButton;
stars array

Array of star objects for the parallax background, each with x, y, size, speed, and layer properties

let stars = [];
mechSkin object

Object storing three colors (body, trim, core) used to paint the player mech

let mechSkin;
thrusterColor object

p5.color object for the cyan glowing thruster elements on the player mech

let thrusterColor;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Enemy collision detection in gameUpdate()

When the player collides with an enemy, the enemy is removed but if multiple enemies overlap the player in the same frame, only one collision is detected because the loop continues after splice.

💡 After enemy.splice(i, 1), consider breaking out of the projectile loop or using a damage queue to handle multiple simultaneous collisions more fairly.

PERFORMANCE gameUpdate() projectile-enemy collision detection

The nested loops checking every projectile against every enemy are O(n²) in complexity. With hundreds of projectiles and dozens of enemies, this becomes expensive.

💡 Implement spatial partitioning (grid-based or quadtree) to reduce collision checks, or pre-calculate the closest enemies per projectile.

STYLE Player class and Enemy class

The mech and enemy visual code is complex and hard to read, with many magic numbers (0.4, 1.2, 0.6, etc.) for sizing body parts.

💡 Extract proportions into named constants at the top of each class (e.g., HEAD_SCALE = 0.6, THRUSTER_WIDTH = 10) to make the drawing code more self-documenting.

FEATURE gameUpdate()

Enemies spawn at the exact edge of the canvas and may clip through walls before the player notices them.

💡 Add a small grace period or spawn shield that prevents collisions for the first 30 frames, or spawn enemies further off-screen (padding = 100 instead of 40).

BUG drawBackground() star wrapping

Stars wrap horizontally but the starfield doesn't account for window resizing during gameplay—stars may look uneven if the window is resized after game start.

💡 Either prevent resizing during gameplay or call initStars() more frequently to refresh the layout on every resize, not just in windowResized().

🔄 Code Flow

Code flow showing setup, draw, gameupdate, gamedraw, playermove, playerattack, enemymove, projectiledraw, powerupdraw, drawhealth, initstars, drawbackground, star, resetgame

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Setup] setup --> color-definition[Mech Color Scheme] setup --> button-creation[Start Button Setup] setup --> initstars[initStars] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click color-definition href "#sub-color-definition" click button-creation href "#sub-button-creation" click initstars href "#fn-initstars" draw --> state-menu[Menu State Check] draw --> state-gameover[Game Over State Check] draw --> state-gameplay[Active Gameplay] click draw href "#fn-draw" click state-menu href "#sub-state-menu" click state-gameover href "#sub-state-gameover" click state-gameplay href "#sub-state-gameplay" state-menu --> drawbackground[drawBackground] state-gameover --> drawbackground state-gameplay --> drawbackground state-gameplay --> player-input[Player Input] state-gameplay --> enemy-spawn[Enemy Spawn Logic] state-gameplay --> powerup-spawn[Power-Up Spawn] state-gameplay --> object-drawing[Game Object Rendering] state-gameplay --> hud-text[Heads-Up Display] click drawbackground href "#fn-drawbackground" click player-input href "#sub-player-input" click enemy-spawn href "#sub-enemy-spawn" click powerup-spawn href "#sub-powerup-spawn" click object-drawing href "#sub-object-drawing" click hud-text href "#sub-hud-text" player-input --> vertical-movement[Vertical Movement] player-input --> horizontal-movement[Horizontal Movement] player-input --> boundary-constraint[Screen Boundary Clamping] player-input --> fire-guard[Fire Guard] player-input --> playerattack[playerattack] click vertical-movement href "#sub-vertical-movement" click horizontal-movement href "#sub-horizontal-movement" click boundary-constraint href "#sub-boundary-constraint" click fire-guard href "#sub-fire-guard" click playerattack href "#fn-playerattack" enemy-spawn --> enemy-update[Enemy Movement and Collisions] click enemy-update href "#sub-enemy-update" enemy-update --> player-chase[Chase Player] enemy-update --> wiggle-motion[Wiggle Motion] enemy-update --> projectile-cleanup[Projectile Movement and Removal] click player-chase href "#sub-player-chase" click wiggle-motion href "#sub-wiggle-motion" click projectile-cleanup href "#sub-projectile-cleanup" powerup-spawn --> powerup-collection[Power-Up Collection] click powerup-collection href "#sub-powerup-collection" object-drawing --> gamedraw[gamedraw] click gamedraw href "#fn-gamedraw" gamedraw --> projectiledraw[projectiledraw] gamedraw --> powerupdraw[powerupdraw] gamedraw --> drawhealth[drawHealthBar] click projectiledraw href "#fn-projectiledraw" click powerupdraw href "#fn-powerupdraw" click drawhealth href "#fn-drawhealth" projectiledraw --> trail-line[Projectile Trail] projectiledraw --> projectile-core[Projectile Core] click trail-line href "#sub-trail-line" click projectile-core href "#sub-projectile-core" powerupdraw --> glow-pulse[Glow Pulse] powerupdraw --> type-switch[Type-Based Rendering] click glow-pulse href "#sub-glow-pulse" click type-switch href "#sub-type-switch" drawhealth --> background-bar[Health Bar Background] drawhealth --> fill-bar[Health Bar Fill] click background-bar href "#sub-background-bar" click fill-bar href "#sub-fill-bar" initstars --> array-init[Array Initialization] array-init --> star-creation[Star Creation Loop] click array-init href "#sub-array-init" click star-creation href "#sub-star-creation" star-creation --> star-vertex-loop[Vertex Generation] star-creation --> angle-calculation[Angle Calculation] click star-vertex-loop href "#sub-star-vertex-loop" click angle-calculation href "#sub-angle-calculation" drawbackground --> background-clear[Background Color] drawbackground --> grid-animation[Animated Grid] drawbackground --> star-parallax[Star Parallax Scrolling] click background-clear href "#sub-background-clear" click grid-animation href "#sub-grid-animation" click star-parallax href "#sub-star-parallax"

❓ Frequently Asked Questions

What visual experience does the Bullet Heaven sketch offer?

The Bullet Heaven sketch creates a vibrant space environment where players pilot a glowing mech amidst a star-filled backdrop, showcasing colorful projectiles and animated enemies.

How can players interact with the Bullet Heaven game?

Players can control the mech using keyboard inputs to move and automatically fire at enemies while dodging incoming attacks and collecting power-ups.

What creative coding concepts are demonstrated in the Bullet Heaven sketch?

The sketch illustrates concepts such as object-oriented programming for game entities, real-time collision detection, and dynamic audio integration to enhance the gaming experience.

Preview

bullet heaven - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of bullet heaven - Code flow showing setup, draw, gameupdate, gamedraw, playermove, playerattack, enemymove, projectiledraw, powerupdraw, drawhealth, initstars, drawbackground, star, resetgame
Code Flow Diagram