SpaceInvaders

This is a fully playable Space Invaders arcade game built with p5.js. Players control a spaceship at the bottom of the screen, dodge descending alien waves, and shoot them down while protective barriers crumble under enemy fire. The game features progressive difficulty, multiple lives, retro synthesizer sound effects, and responsive touch controls.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make aliens twice as fast — Changing the alienSpeed increment makes the game escalate difficulty faster, creating a more challenging gameplay arc.
  2. Aliens shoot constantly — Lowering the initial shootingRate floods the screen with alien fire, making survival much harder from the start.
  3. Give the player more lives — Starting with 5 lives instead of 3 makes the game much more forgiving and lets you experiment longer before failure.
  4. Change the barrier color to bright cyan — Barriers will now glow in cyan (bright blue-green) instead of grayscale, making them pop visually.
  5. Make player bullets faster — Player bullets will zip up the screen twice as fast, letting you take down aliens more quickly.
  6. Turn all aliens into shooters — The screen becomes a storm of incoming fire as every alien shoots with 60% frequency.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the iconic Space Invaders arcade game entirely in p5.js. Waves of aliens descend the screen in formation, shooting at your spaceship while you fire back and hide behind crumbling barriers. The game teaches essential game-dev patterns: state machines (start, playing, game over, win), object-oriented design with multiple classes, collision detection, progressive difficulty scaling, and real-time input handling for both keyboard and touch.

The code is organized into a game loop (setup and draw), six classes (Player, PlayerBullet, Alien, AlienBullet, Barrier), helper functions for spawning and collision, and event handlers for input. By studying it, you will master the game loop architecture that powers almost every interactive p5.js project: updating positions, checking collisions, and rendering every frame at 60 FPS. You'll also see how objects communicate through global arrays and how sound design adds feedback without external audio files.

⚙️ How It Works

  1. When the sketch loads, preload() creates six oscillator sound generators and loads a retro pixel font. setup() initializes the canvas and calls resetGame(), which spawns 55 aliens in a 5×11 grid formation and 4 protective barriers below the player.
  2. The game begins on the start screen. Pressing any key transitions to the playing state and begins the draw loop, which runs 60 times per second.
  3. Every frame, updateGame() moves the player (keyboard or touch input), updates all bullets and aliens, and handles the iconic typewriter movement: aliens march horizontally until they hit the screen edge, then drop down and reverse direction. After each drop, difficulty increases—alien speed, shooting rate, and drop speed all escalate.
  4. Aliens shoot randomly at a rate that increases as the game progresses. Only the lowest alien in each column can shoot, preventing excessive fire from above. Two alien types exist: basic (green, 20% fire chance) and shooter (red, 60% fire chance).
  5. checkCollisions() runs every frame, testing whether player bullets hit aliens, alien bullets hit the player or barriers, and removing destroyed objects. Hitting an alien adds 10 points; being hit by an alien costs a life.
  6. checkGameOver() ends the game if all aliens are destroyed (win state), if any alien reaches the player's area (lose state), or if the player runs out of lives. Each game state (start, playing, gameover, win) draws different text screens, and touch or keypress restarts the game.

🎓 Concepts You'll Learn

Game state machinesObject-oriented programming with classesCollision detection (AABB)Game loops and frame-based animationProgressive difficulty scalingInput handling (keyboard and touch)Procedural sound generationArray filtering and iterationCanvas boundaries and constraints

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs before setup() and draw(). It is the ideal place to load fonts, images, and initialize any objects (like p5.sound oscillators) that must exist before the game starts. All six sounds are initialized once here and reused throughout the game by changing their frequency and amplitude.

function preload() {
  gameFont = loadFont('https://unpkg.com/@fontsource/press-start-2p@5.0.0/files/press-start-2p-latin-400-normal.woff');
  playerShootSound = new p5.Oscillator('sine');
  playerShootSound.freq(600);
  playerShootSound.amp(0, 0.1);
  playerShootSound.start();
  alienShootSound = new p5.Oscillator('triangle');
  alienShootSound.freq(200);
  alienShootSound.amp(0, 0.1);
  alienShootSound.start();
  alienHitSound = new p5.Oscillator('square');
  alienHitSound.freq(800);
  alienHitSound.amp(0, 0.1);
  alienHitSound.start();
  playerHitSound = new p5.Oscillator('sawtooth');
  playerHitSound.freq(100);
  playerHitSound.amp(0, 0.1);
  playerHitSound.start();
  gameOverSound = new p5.Oscillator('sine');
  gameOverSound.freq(50);
  gameOverSound.amp(0, 0.1);
  gameOverSound.start();
  winSound = new p5.Oscillator('sine');
  winSound.freq(1000);
  winSound.amp(0, 0.1);
  winSound.start();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

function-call Load Retro Font gameFont = loadFont('https://unpkg.com/@fontsource/press-start-2p@5.0.0/files/press-start-2p-latin-400-normal.woff');

Loads a pixel-art font from CDN to give the game authentic retro arcade text

loop Initialize Six Oscillators playerShootSound = new p5.Oscillator('sine'); playerShootSound.freq(600); playerShootSound.amp(0, 0.1); playerShootSound.start();

Creates six procedural sound generators (sine, triangle, square, sawtooth waves) that play sound effects without loading audio files

gameFont = loadFont('https://unpkg.com/@fontsource/press-start-2p@5.0.0/files/press-start-2p-latin-400-normal.woff');
Fetches a retro pixel font from the @fontsource CDN and stores it in the global gameFont variable—this happens before setup() runs
playerShootSound = new p5.Oscillator('sine');
Creates a sine-wave oscillator object; this will generate the tone when the player shoots
playerShootSound.freq(600);
Sets the frequency to 600 Hz (hertz), which corresponds to a particular musical pitch—higher numbers sound higher
playerShootSound.amp(0, 0.1);
Sets the amplitude (volume) to 0 with a 0.1-second fade time, keeping the oscillator silent until it's time to play
playerShootSound.start();
Starts the oscillator running continuously in the background at zero volume; we will animate the volume up when a shot fires

setup()

setup() runs once when the sketch starts. It is the place to create your canvas, configure global drawing settings, and initialize all game objects. Notice that setup() does not draw anything itself—it only prepares the stage for draw() to animate.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont(gameFont);
  textAlign(CENTER, CENTER);
  textSize(24);
  noStroke();
  resetGame();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Create Responsive Canvas createCanvas(windowWidth, windowHeight);

Makes the canvas fill the entire browser window, allowing the game to be responsive on any device

sequence Configure Text Rendering textFont(gameFont); textAlign(CENTER, CENTER); textSize(24);

Sets all future text to use the retro pixel font, center-aligned, at size 24 for score and lives display

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches the browser window's width and height; this makes the game responsive
textFont(gameFont);
Tells p5.js to use the pixel font loaded in preload() for all text drawn after this point
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically around the x,y coordinates you provide to text()
textSize(24);
Sets the default font size to 24 pixels for score and lives text (will be overridden in draw functions for larger titles)
noStroke();
Disables outlines on all shapes (ellipse, rect, triangle) so aliens, bullets, and barriers render as solid filled shapes
resetGame();
Calls the resetGame() function to initialize all game variables, spawn aliens and barriers, and set gameState to 'start'

resetGame()

resetGame() is called once at startup and again every time the player restarts after game over or a win. It clears all arrays, reinitializes all variables, spawns the new alien and barrier formations, and sets the game state to 'start' (the title screen). This function is the single point of truth for 'resetting' the game to a known state.

🔬 These set the starting difficulty. What happens if you change alienSpeed to 3 and alienDropSpeed to 20 to start the game much harder?

  alienSpeed = 1;
  alienDropSpeed = 10;
function resetGame() {
  score = 0;
  lives = 3;
  playerBullets = [];
  aliens = [];
  alienBullets = [];
  barriers = [];
  player = new Player(width / 2, height - PLAYER_SIZE - PLAYER_START_Y_OFFSET, PLAYER_SIZE);
  alienDirection = 1;
  alienSpeed = 1;
  alienDropSpeed = 10;
  alienMoveCounter = 0;
  let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
  alienMoveLimit = floor((width - formationWidth) / 2 / alienSpeed);
  alienShootingRate = 120;
  alienShootingTimer = 0;
  spawnAliens();
  spawnBarriers();
  gameState = "start";
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

sequence Reset Score and Lives score = 0; lives = 3;

Clears the score and restores lives to 3, preparing a fresh game

sequence Clear All Arrays playerBullets = []; aliens = []; alienBullets = []; barriers = [];

Empties the arrays that hold game objects so no stale bullets or aliens carry over to the new game

calculation Calculate Alien Horizontal Movement Limit let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE; alienMoveLimit = floor((width - formationWidth) / 2 / alienSpeed);

Computes how many frames the aliens can move horizontally before hitting a screen edge, preventing them from escaping

score = 0;
Resets score to zero for a fresh game
lives = 3;
Restores the player to three lives at the start of each game
playerBullets = [];
Clears the array of player bullets—removes any bullets left on screen from a previous game
aliens = [];
Clears the aliens array so no old aliens remain from a restart
alienBullets = [];
Clears the aliens' bullets—empties any shots fired from the previous game
barriers = [];
Clears the barriers array; fresh barriers will be spawned next
player = new Player(width / 2, height - PLAYER_SIZE - PLAYER_START_Y_OFFSET, PLAYER_SIZE);
Creates a new Player object at the bottom-center of the canvas, positioned 20 pixels above the bottom edge
alienDirection = 1;
Sets aliens to move right (1) at the start; they will reverse to -1 when hitting the screen edge
alienSpeed = 1;
Resets alien horizontal speed to 1 pixel per frame (difficulty increases during gameplay)
alienDropSpeed = 10;
Resets alien vertical drop (when they hit an edge) to 10 pixels; this also increases as difficulty scales
alienMoveCounter = 0;
Resets the counter that tracks how many frames aliens have moved horizontally—used to trigger the next drop
let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
Calculates the total width of the 11-column alien formation: 10 spaces of 60 pixels plus one alien size (40 pixels)
alienMoveLimit = floor((width - formationWidth) / 2 / alienSpeed);
Calculates how many frames until the centered alien formation hits the screen edge; prevents aliens from escaping the sides
alienShootingRate = 120;
Resets the frame interval between alien shooting waves to 120 frames (approximately 2 seconds at 60 FPS)
spawnAliens();
Calls the helper function to create all 55 aliens in a 5×11 grid formation
spawnBarriers();
Calls the helper function to place 4 protective barriers in a line above the player
gameState = "start";
Sets the game state to 'start' so the next draw() will show the title screen instead of gameplay

draw()

draw() is called 60 times per second. It uses a state machine pattern to branch logic based on gameState. This pattern is fundamental in game development: instead of mixing all code together, you separate startup, gameplay, and end screens into distinct flows. Notice that updateGame(), checkCollisions(), and checkGameOver() are only called when gameState is 'playing'—they are skipped on the title screen.

🔬 The game state machine controls the entire game flow. What happens if you comment out the updateGame() and checkGameOver() lines so the game never progresses from the start screen?

  if (gameState === "start") {
    drawStartScreen();
  } else if (gameState === "playing") {
    updateGame();
    displayGame();
    checkCollisions();
    checkGameOver();
function draw() {
  background(0);
  if (gameState === "start") {
    drawStartScreen();
  } else if (gameState === "playing") {
    updateGame();
    displayGame();
    checkCollisions();
    checkGameOver();
  } else if (gameState === "gameover") {
    drawGameOverScreen();
  } else if (gameState === "win") {
    drawWinScreen();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

switch-case Game State Machine if (gameState === "start") { ... } else if (gameState === "playing") { ... }

Routes control flow based on the current game state—displays different screens and runs different logic for start, playing, game over, and win states

background(0);
Clears the canvas with black (RGB 0) every frame, erasing all previous drawings and preventing motion trails
if (gameState === "start") {
Checks if the game state is 'start'—the title screen where players see instructions and wait for input
drawStartScreen();
Calls drawStartScreen() to display the title, final score, and 'Press any key to start' text
} else if (gameState === "playing") {
If the game state is 'playing', the player is actively controlling the spaceship and aliens are descending
updateGame();
Updates the positions of the player, bullets, and aliens each frame
displayGame();
Renders all game objects (player, bullets, aliens, barriers) to the canvas
checkCollisions();
Tests whether bullets and aliens have hit each other and applies damage or removes objects
checkGameOver();
Checks win/loss conditions and transitions the game state if the game has ended
} else if (gameState === "gameover") {
If the player has run out of lives or aliens reached the bottom, show the game over screen
drawGameOverScreen();
Displays 'GAME OVER' text and the final score in red
} else if (gameState === "win") {
If all aliens have been destroyed, the player has won
drawWinScreen();
Displays 'YOU WIN!' text and the final score in green

updateGame()

updateGame() is the heart of the game loop. It moves every object, implements the alien typewriter movement (moving horizontally then dropping and reversing), scales difficulty after each drop, manages alien shooting logic using a column-based system, and cleans up off-screen bullets. Notice how only the lowest alien in each column can shoot—this prevents an overwhelming hail of bullets from above. The difficulty scaling (increasing alienSpeed, alienShootingRate, and alienDropSpeed) creates tension as the game progresses.

function updateGame() {
  player.update();
  playerBullets.forEach(bullet => bullet.update());
  aliens.forEach(alien => alien.update());
  alienBullets.forEach(bullet => bullet.update());
  aliens.forEach(alien => alien.x += alienSpeed * alienDirection);
  alienMoveCounter++;
  if (alienMoveCounter >= alienMoveLimit) {
    aliens.forEach(alien => alien.y += alienDropSpeed);
    alienDirection *= -1;
    alienMoveCounter = 0;
    alienSpeed += ALIEN_SPEED_INCREMENT;
    alienShootingRate = max(30, alienShootingRate - 10);
    alienDropSpeed += 2;
  }
  alienShootingTimer++;
  if (alienShootingTimer >= alienShootingRate) {
    let aliensByColumn = {};
    aliens.forEach(alien => {
      let col = floor((alien.x - ((width - ((ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE)) / 2)) / ALIEN_SPACING);
      if (!aliensByColumn[col]) {
        aliensByColumn[col] = [];
      }
      aliensByColumn[col].push(alien);
    });
    for (let col in aliensByColumn) {
      let lowestAlien = aliensByColumn[col].reduce((prev, curr) => (prev.y > curr.y ? prev : curr));
      if (lowestAlien.type === "shooter" && random() < 0.6) {
        lowestAlien.shoot();
        alienShootSound.amp(0.5, 0.1);
        alienShootSound.freq(random(150, 250));
        alienShootSound.amp(0, 0.5, 0.5);
      } else if (lowestAlien.type === "basic" && random() < 0.2) {
        lowestAlien.shoot();
        alienShootSound.amp(0.5, 0.1);
        alienShootSound.freq(random(150, 250));
        alienShootSound.amp(0, 0.5, 0.5);
      }
    }
    alienShootingTimer = 0;
  }
  playerBullets = playerBullets.filter(bullet => !bullet.isOffScreen());
  alienBullets = alienBullets.filter(bullet => !bullet.isOffScreen());
}
Line-by-line explanation (31 lines)

🔧 Subcomponents:

sequence Update All Game Objects player.update(); playerBullets.forEach(bullet => bullet.update()); aliens.forEach(alien => alien.update()); alienBullets.forEach(bullet => bullet.update());

Calls the update() method on every game object, moving them to new positions each frame

sequence Alien Typewriter Movement aliens.forEach(alien => alien.x += alienSpeed * alienDirection); alienMoveCounter++; if (alienMoveCounter >= alienMoveLimit) { ... }

Moves all aliens horizontally every frame, and when they reach the screen edge, drops them down and reverses direction

conditional Progressive Difficulty Scaling alienSpeed += ALIEN_SPEED_INCREMENT; alienShootingRate = max(30, alienShootingRate - 10); alienDropSpeed += 2;

After each drop, aliens speed up horizontally and vertically, and they shoot more frequently to increase challenge

conditional Intelligent Alien Shooting if (alienShootingTimer >= alienShootingRate) { ... }

Every alienShootingRate frames, finds the lowest alien in each column and fires from only that one to prevent excessive bullets

function-call Remove Off-Screen Bullets playerBullets = playerBullets.filter(bullet => !bullet.isOffScreen()); alienBullets = alienBullets.filter(bullet => !bullet.isOffScreen());

Removes bullets that have left the canvas to keep the arrays from growing infinitely

player.update();
Calls the player's update() method, which moves the spaceship left or right based on keyboard input
playerBullets.forEach(bullet => bullet.update());
Calls update() on every player bullet in the array, moving each one upward by its speed
aliens.forEach(alien => alien.update());
Calls update() on every alien (though alien.update() is currently empty since alien movement is handled centrally below)
alienBullets.forEach(bullet => bullet.update());
Calls update() on every alien bullet, moving each one downward toward the player
aliens.forEach(alien => alien.x += alienSpeed * alienDirection);
Moves all aliens horizontally: alienSpeed pixels per frame, in the direction of alienDirection (1 for right, -1 for left)
alienMoveCounter++;
Increments a counter each frame to track how many frames the aliens have been moving horizontally
if (alienMoveCounter >= alienMoveLimit) {
When the counter reaches alienMoveLimit, the alien formation has hit the screen edge and must drop and reverse
aliens.forEach(alien => alien.y += alienDropSpeed);
Moves all aliens down by alienDropSpeed pixels when reversing direction
alienDirection *= -1;
Multiplies alienDirection by -1: if it was 1 (moving right), it becomes -1 (moving left), and vice versa
alienMoveCounter = 0;
Resets the movement counter to zero so aliens can move horizontally again before the next drop
alienSpeed += ALIEN_SPEED_INCREMENT;
Increases alienSpeed by 0.2, making the formation sweep across the screen faster after each drop (difficulty scaling)
alienShootingRate = max(30, alienShootingRate - 10);
Decreases shootingRate by 10 (making aliens shoot more frequently), but caps it at a minimum of 30 frames to avoid chaos
alienDropSpeed += 2;
Increases the vertical drop speed by 2 pixels, making the aliens descend faster and creating urgency
alienShootingTimer++;
Increments a counter each frame to measure elapsed time since the last alien shooting wave
if (alienShootingTimer >= alienShootingRate) {
When alienShootingTimer reaches alienShootingRate, it is time for a new shooting wave
let aliensByColumn = {};
Creates an empty object (dictionary) that will group aliens by their column index on the screen
aliens.forEach(alien => {
Loops through every alien to assign it to a column group
let col = floor((alien.x - ((width - ((ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE)) / 2)) / ALIEN_SPACING);
Calculates which column the alien belongs to by subtracting the formation's left edge and dividing by the spacing between columns
if (!aliensByColumn[col]) { aliensByColumn[col] = []; }
If this column doesn't exist in the object yet, create an empty array for it
aliensByColumn[col].push(alien);
Adds this alien to the array for its column
for (let col in aliensByColumn) {
Loops through each column that has at least one alien
let lowestAlien = aliensByColumn[col].reduce((prev, curr) => (prev.y > curr.y ? prev : curr));
Finds the alien with the highest y-value (lowest on screen) in this column—only this alien will shoot
if (lowestAlien.type === "shooter" && random() < 0.6) {
If the lowest alien is a 'shooter' type, there is a 60% chance it will fire this wave
lowestAlien.shoot();
Calls the alien's shoot() method, which creates a new AlienBullet below the alien
alienShootSound.amp(0.5, 0.1);
Fades the sound's volume to 0.5 over 0.1 seconds, preparing to play a shooting sound
alienShootSound.freq(random(150, 250));
Sets the oscillator to a random frequency between 150 and 250 Hz for variety
alienShootSound.amp(0, 0.5, 0.5);
Fades the sound back to 0 over 0.5 seconds, creating a brief 'chirp' effect
} else if (lowestAlien.type === "basic" && random() < 0.2) {
If the alien is 'basic' type, there is only a 20% chance it will shoot (less aggressive)
alienShootingTimer = 0;
Resets the timer to zero so the next shooting wave will occur alienShootingRate frames from now
playerBullets = playerBullets.filter(bullet => !bullet.isOffScreen());
Creates a new array containing only player bullets that are still on screen, removing those that went off the top
alienBullets = alienBullets.filter(bullet => !bullet.isOffScreen());
Creates a new array containing only alien bullets that are still on screen, removing those that went off the bottom

displayGame()

displayGame() is called every frame from draw(). It doesn't update any positions—that happens in updateGame()—it only renders. By separating update and display, the code becomes clearer: one function changes state, the other visualizes it. Notice the use of forEach() loops: instead of writing five separate lines for player, bullets, aliens, etc., we use a loop. This is a pattern you will use everywhere in p5.js.

function displayGame() {
  player.display();
  playerBullets.forEach(bullet => bullet.display());
  aliens.forEach(alien => alien.display());
  alienBullets.forEach(bullet => bullet.display());
  barriers.forEach(barrier => barrier.display());
  fill(255);
  textSize(20);
  textAlign(LEFT, TOP);
  text(`Score: ${score}`, 20, 20);
  text(`Lives: ${lives}`, 20, 50);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

sequence Render All Game Objects player.display(); playerBullets.forEach(bullet => bullet.display()); aliens.forEach(alien => alien.display());

Calls the display() method on every game object to draw them to the canvas

sequence Render Score and Lives UI fill(255); textSize(20); textAlign(LEFT, TOP); text(`Score: ${score}`, 20, 20); text(`Lives: ${lives}`, 20, 50);

Displays the current score and remaining lives in white text at the top-left corner

player.display();
Calls the player's display() method to draw the blue triangle spaceship at the player's current position
playerBullets.forEach(bullet => bullet.display());
Loops through every player bullet and calls its display() method, drawing green rectangles on screen
aliens.forEach(alien => alien.display());
Loops through every alien and calls its display() method, drawing them as colored ellipses (green for basic, red for shooters)
alienBullets.forEach(bullet => bullet.display());
Loops through every alien bullet and calls its display() method, drawing yellow rectangles on screen
barriers.forEach(barrier => barrier.display());
Loops through every barrier and calls its display() method, drawing them as rectangles that darken as they take damage
fill(255);
Sets the fill color to white (RGB 255, 255, 255) for all subsequent drawing operations
textSize(20);
Sets the font size to 20 pixels for score and lives text (smaller than title text)
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically, so the position (20, 20) places text at the top-left corner
text(`Score: ${score}`, 20, 20);
Draws the score as a string at position (20, 20)—the template literal inserts the current score value
text(`Lives: ${lives}`, 20, 50);
Draws the remaining lives at position (20, 50), 30 pixels below the score

checkCollisions()

checkCollisions() is called every frame and performs three nested loops: player bullets vs. aliens, alien bullets vs. player, and alien bullets vs. barriers. Notice that all loops count backwards (from length-1 to 0)—this is because we use splice() to remove items from the array during iteration. Counting forward would skip elements. Each collision plays a different sound (high pitch for alien hit, low pitch for player hit) and updates the game state. This function is the core of game feedback: collisions are how the player and aliens interact.

function checkCollisions() {
  for (let i = playerBullets.length - 1; i >= 0; i--) {
    let bullet = playerBullets[i];
    for (let j = aliens.length - 1; j >= 0; j--) {
      let alien = aliens[j];
      if (bullet.hits(alien)) {
        score += 10;
        alienHitSound.amp(0.5, 0.1);
        alienHitSound.freq(random(700, 900));
        alienHitSound.amp(0, 0.5, 0.5);
        playerBullets.splice(i, 1);
        aliens.splice(j, 1);
        break;
      }
    }
  }
  for (let i = alienBullets.length - 1; i >= 0; i--) {
    let bullet = alienBullets[i];
    if (bullet.hits(player)) {
      lives--;
      playerHitSound.amp(0.5, 0.1);
      playerHitSound.freq(random(90, 110));
      playerHitSound.amp(0, 0.5, 0.5);
      alienBullets.splice(i, 1);
      if (lives <= 0) {
        gameState = "gameover";
        gameOverSound.amp(0.8, 0.5);
        gameOverSound.freq(random(40, 60));
        gameOverSound.amp(0, 2, 2);
      }
      break;
    }
  }
  for (let i = alienBullets.length - 1; i >= 0; i--) {
    let bullet = alienBullets[i];
    for (let j = barriers.length - 1; j >= 0; j--) {
      let barrier = barriers[j];
      if (bullet.hits(barrier)) {
        barrier.takeHit();
        alienBullets.splice(i, 1);
        if (barrier.health <= 0) {
          barriers.splice(j, 1);
        }
        break;
      }
    }
  }
}
Line-by-line explanation (32 lines)

🔧 Subcomponents:

nested-loop Player Bullets vs. Aliens for (let i = playerBullets.length - 1; i >= 0; i--) { for (let j = aliens.length - 1; j >= 0; j--) { if (bullet.hits(alien)) { ... } } }

Tests every player bullet against every alien; if a hit is found, adds score, plays a sound, and removes both objects

for-loop Alien Bullets vs. Player for (let i = alienBullets.length - 1; i >= 0; i--) { if (bullet.hits(player)) { ... } }

Tests every alien bullet against the player; if hit, decreases lives, plays a sound, and may end the game

nested-loop Alien Bullets vs. Barriers for (let i = alienBullets.length - 1; i >= 0; i--) { for (let j = barriers.length - 1; j >= 0; j--) { if (bullet.hits(barrier)) { ... } } }

Tests every alien bullet against every barrier; if hit, damages the barrier and removes the bullet

for (let i = playerBullets.length - 1; i >= 0; i--) {
Loops through the playerBullets array backwards (from last to first) so we can safely remove items during iteration
let bullet = playerBullets[i];
Gets the current bullet from the array
for (let j = aliens.length - 1; j >= 0; j--) {
For each bullet, loops through all aliens backwards
let alien = aliens[j];
Gets the current alien from the array
if (bullet.hits(alien)) {
Calls the bullet's hits() method, which uses AABB collision detection to test if the bullet overlaps the alien
score += 10;
If there is a hit, adds 10 points to the score
alienHitSound.amp(0.5, 0.1);
Fades the alien hit sound's volume to 0.5 over 0.1 seconds
alienHitSound.freq(random(700, 900));
Sets the sound to a random frequency between 700 and 900 Hz (a high-pitched beep)
alienHitSound.amp(0, 0.5, 0.5);
Fades the sound back to 0 over 0.5 seconds, creating a brief chirp
playerBullets.splice(i, 1);
Removes the bullet from the array (splice removes 1 element at index i)
aliens.splice(j, 1);
Removes the alien from the array
break;
Breaks out of the inner loop (aliens loop) because this bullet has been removed and no longer needs to check other aliens
for (let i = alienBullets.length - 1; i >= 0; i--) {
Loops through all alien bullets backwards
if (bullet.hits(player)) {
Tests if this alien bullet overlaps the player
lives--;
Decrements the lives counter by 1
playerHitSound.amp(0.5, 0.1);
Fades the player hit sound to audible volume
playerHitSound.freq(random(90, 110));
Sets the sound to a random frequency between 90 and 110 Hz (a low, ominous beep)
playerHitSound.amp(0, 0.5, 0.5);
Fades the sound out over 0.5 seconds
alienBullets.splice(i, 1);
Removes the alien bullet that hit the player
if (lives <= 0) {
Checks if the player has run out of lives
gameState = "gameover";
If so, transitions the game state to 'gameover' so the game over screen is displayed next frame
gameOverSound.amp(0.8, 0.5);
Plays a low, mournful game over sound by fading its volume to 0.8
gameOverSound.freq(random(40, 60));
Sets the frequency to a very low pitch (40–60 Hz) for dramatic effect
gameOverSound.amp(0, 2, 2);
Fades the sound out over 2 seconds, prolonging the game over theme
break;
Breaks out of the loop because the player can only be hit by one bullet at a time
for (let i = alienBullets.length - 1; i >= 0; i--) {
Loops through alien bullets to check collisions with barriers
for (let j = barriers.length - 1; j >= 0; j--) {
For each bullet, loops through all barriers
if (bullet.hits(barrier)) {
Tests if the bullet overlaps the barrier
barrier.takeHit();
Calls the barrier's takeHit() method, which decrements its health by 1
alienBullets.splice(i, 1);
Removes the bullet from the array
if (barrier.health <= 0) {
Checks if the barrier's health has reached zero or below
barriers.splice(j, 1);
If so, removes the barrier from the array (it is destroyed)

checkGameOver()

checkGameOver() is called every frame during gameplay and handles both win and loss conditions. The win condition is simple: if the aliens array is empty, the player has won. The loss condition is more subtle: aliens don't need to hit the player directly to end the game—if any alien descends past a calculated y-coordinate (the barrier line), the game is over. This recreates the original Space Invaders mechanic where the aliens reaching the bottom meant invasion. Both conditions play distinctive sounds (high pitch for victory, low pitch for defeat) and transition to their respective end screens.

🔬 This calculates the 'danger zone' below which aliens trigger a loss. What happens if you change the -60 offset to -120 so aliens get twice as close before the game ends? Does that feel more fair or less?

  let barrierY = height - PLAYER_SIZE - BARRIER_HEIGHT - 60;
  for (let alien of aliens) {
    if (alien.y + alien.h / 2 > barrierY + BARRIER_HEIGHT / 2) {
function checkGameOver() {
  if (aliens.length === 0) {
    gameState = "win";
    winSound.amp(0.8, 0.5);
    winSound.freq(random(900, 1100));
    winSound.amp(0, 2, 2);
  }
  let barrierY = height - PLAYER_SIZE - BARRIER_HEIGHT - 60;
  for (let alien of aliens) {
    if (alien.y + alien.h / 2 > barrierY + BARRIER_HEIGHT / 2) {
      gameState = "gameover";
      gameOverSound.amp(0.8, 0.5);
      gameOverSound.freq(random(40, 60));
      gameOverSound.amp(0, 2, 2);
      break;
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Win Condition if (aliens.length === 0) { ... }

Tests if all aliens have been destroyed; if so, transitions to the win state and plays a victory sound

for-loop Loss Condition (Aliens Reach Bottom) for (let alien of aliens) { if (alien.y + alien.h / 2 > barrierY + BARRIER_HEIGHT / 2) { ... } }

Checks if any alien has descended past the barrier line (into the player's territory), ending the game in a loss

if (aliens.length === 0) {
Checks if the aliens array is empty (all aliens have been killed)
gameState = "win";
If so, sets the game state to 'win', triggering the win screen next frame
winSound.amp(0.8, 0.5);
Fades the win sound to audible volume (0.8) over 0.5 seconds
winSound.freq(random(900, 1100));
Sets the win sound to a random high frequency (900–1100 Hz) for celebration
winSound.amp(0, 2, 2);
Fades the sound out over 2 seconds, prolonging the victory theme
let barrierY = height - PLAYER_SIZE - BARRIER_HEIGHT - 60;
Calculates the y-coordinate of the barrier line—aliens reaching beyond this line trigger a loss
for (let alien of aliens) {
Loops through all remaining aliens to check if any have descended too far
if (alien.y + alien.h / 2 > barrierY + BARRIER_HEIGHT / 2) {
Tests if the alien's center (alien.y + alien.h/2) has passed the barrier center—if so, the alien has invaded player territory
gameState = "gameover";
Sets the game state to 'gameover', triggering the game over screen next frame
gameOverSound.amp(0.8, 0.5);
Fades the game over sound to audible volume
gameOverSound.freq(random(40, 60));
Sets the frequency to a very low pitch (40–60 Hz) for a ominous feeling
gameOverSound.amp(0, 2, 2);
Fades the sound out over 2 seconds
break;
Breaks out of the loop since the game is already over—no need to check more aliens

drawStartScreen()

drawStartScreen() is called every frame while gameState is 'start'. It is a simple, static display of the title, previous score, and instructions. Notice that the score displayed is from the previous game (stored in the global score variable), not zero—this provides continuity and lets the player see their performance before restarting.

function drawStartScreen() {
  fill(255);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("SPACE INVADERS", width / 2, height / 3);
  textSize(24);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(20);
  text("Press any key or touch to start", width / 2, height * 2 / 3);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

sequence Draw Title Text fill(255); textSize(48); textAlign(CENTER, CENTER); text("SPACE INVADERS", width / 2, height / 3);

Displays the game title in large white text at the top third of the screen

sequence Draw Previous Score textSize(24); text("Final Score: " + score, width / 2, height / 2);

Shows the score from the previous game in the center of the screen

fill(255);
Sets the text color to white
textSize(48);
Sets the font size to 48 pixels for the title
textAlign(CENTER, CENTER);
Centers text around the x,y coordinate (useful for centering on screen)
text("SPACE INVADERS", width / 2, height / 3);
Draws the title at the horizontal center (width / 2) and one-third down the screen (height / 3)
textSize(24);
Reduces font size to 24 for the score display
text("Final Score: " + score, width / 2, height / 2);
Draws the previous game's score at the center of the screen (height / 2)
textSize(20);
Reduces font size further to 20 for instructions
text("Press any key or touch to start", width / 2, height * 2 / 3);
Displays instructions two-thirds down the screen, inviting the player to start

drawGameOverScreen()

drawGameOverScreen() is called every frame while gameState is 'gameover'. It is nearly identical to drawStartScreen() but uses red text to convey failure and says 'restart' instead of 'start'. The only logical difference is the color: red instead of white. This is a simple visual cue.

function drawGameOverScreen() {
  fill(255, 0, 0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("GAME OVER", width / 2, height / 3);
  textSize(24);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(20);
  text("Press any key or touch to restart", width / 2, height * 2 / 3);
}
Line-by-line explanation (6 lines)
fill(255, 0, 0);
Sets the text color to red (RGB 255, 0, 0) to signify defeat
textSize(48);
Sets the font size to 48 pixels for the 'GAME OVER' title
textAlign(CENTER, CENTER);
Centers text around its x,y coordinate
text("GAME OVER", width / 2, height / 3);
Displays 'GAME OVER' in red at the top third of the screen
text("Final Score: " + score, width / 2, height / 2);
Shows the final score at the center of the screen
text("Press any key or touch to restart", width / 2, height * 2 / 3);
Displays restart instructions two-thirds down

drawWinScreen()

drawWinScreen() mirrors drawGameOverScreen() but uses green text to celebrate victory. The structure is identical; only the text content and color differ. This demonstrates how p5.js's color and text functions allow you to create distinct visual states with minimal code changes.

function drawWinScreen() {
  fill(0, 255, 0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("YOU WIN!", width / 2, height / 3);
  textSize(24);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(20);
  text("Press any key or touch to restart", width / 2, height * 2 / 3);
}
Line-by-line explanation (6 lines)
fill(0, 255, 0);
Sets the text color to green (RGB 0, 255, 0) to signify victory
textSize(48);
Sets the font size to 48 pixels for the 'YOU WIN!' title
textAlign(CENTER, CENTER);
Centers text around its x,y coordinate
text("YOU WIN!", width / 2, height / 3);
Displays 'YOU WIN!' in green at the top third of the screen
text("Final Score: " + score, width / 2, height / 2);
Shows the final score at the center of the screen
text("Press any key or touch to restart", width / 2, height * 2 / 3);
Displays restart instructions two-thirds down

keyPressed()

keyPressed() is called once every time a key is held down. It handles all keyboard input: pressing any key on the title screen starts the game, arrow or WASD keys move the player, and the spacebar shoots. Notice the distinction between keyPressed() (called once per keydown) and keyReleased() (called once per keyup)—we use both to make smooth movement possible: holding a key starts movement, and releasing it stops it.

function keyPressed() {
  if (gameState === "start" || gameState === "gameover" || gameState === "win") {
    resetGame();
    gameState = "playing";
  } else if (gameState === "playing") {
    if (keyCode === LEFT_ARROW || key === 'a' || key === 'A') {
      player.startMove(-1);
    } else if (keyCode === RIGHT_ARROW || key === 'd' || key === 'D') {
      player.startMove(1);
    } else if (key === ' ' || key === ' ') {
      player.shoot();
      playerShootSound.amp(0.5, 0.1);
      playerShootSound.freq(random(550, 650));
      playerShootSound.amp(0, 0.5, 0.5);
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Start Game on Any Key if (gameState === "start" || gameState === "gameover" || gameState === "win") { resetGame(); gameState = "playing"; }

When any key is pressed on the title, game over, or win screen, reset the game and transition to playing

conditional Handle Movement Keys if (keyCode === LEFT_ARROW || key === 'a' || key === 'A') { player.startMove(-1); }

Detects left arrow or 'a' key and tells the player to start moving left

conditional Handle Shoot Key else if (key === ' ' || key === ' ') { player.shoot(); ... }

Detects spacebar and makes the player shoot, playing a sound effect

if (gameState === "start" || gameState === "gameover" || gameState === "win") {
Checks if the game is on any of the end screens (start, game over, or win)
resetGame();
Calls resetGame() to clear score, lives, and game objects
gameState = "playing";
Sets the game state to 'playing' so gameplay resumes next frame
} else if (gameState === "playing") {
If the game is already playing, handle input for movement and shooting
if (keyCode === LEFT_ARROW || key === 'a' || key === 'A') {
Checks if the left arrow or 'a' key was pressed (supports both arrow keys and WASD)
player.startMove(-1);
Calls the player's startMove() method with -1 to begin moving left
} else if (keyCode === RIGHT_ARROW || key === 'd' || key === 'D') {
Checks if the right arrow or 'd' key was pressed
player.startMove(1);
Calls startMove(1) to begin moving right
} else if (key === ' ' || key === ' ') {
Checks if the spacebar was pressed
player.shoot();
Calls the player's shoot() method to create a bullet
playerShootSound.amp(0.5, 0.1);
Fades the shoot sound's volume to 0.5 over 0.1 seconds to play it
playerShootSound.freq(random(550, 650));
Sets the sound frequency to a random pitch between 550 and 650 Hz
playerShootSound.amp(0, 0.5, 0.5);
Fades the sound back to 0 over 0.5 seconds, creating a brief beep

keyReleased()

keyReleased() is called once when a key is released. It stops the player's movement when any movement key is released. Combined with keyPressed(), this creates smooth continuous movement: press a key, movement starts and continues each frame until the key is released. Without keyReleased(), the player would only move on the frame the key was pressed, making control very difficult.

function keyReleased() {
  if (gameState === "playing") {
    if (key === 'a' || key === 'A' || keyCode === LEFT_ARROW ||
        key === 'd' || key === 'D' || keyCode === RIGHT_ARROW) {
      player.stopMove();
    }
  }
}
Line-by-line explanation (4 lines)
if (gameState === "playing") {
Only processes key releases during active gameplay
if (key === 'a' || key === 'A' || keyCode === LEFT_ARROW ||
Checks if the released key is a left movement key (left arrow or 'a')
key === 'd' || key === 'D' || keyCode === RIGHT_ARROW) {
Or a right movement key (right arrow or 'd')
player.stopMove();
Calls the player's stopMove() method to halt horizontal movement

touchStarted()

touchStarted() is called once when the user touches the screen. It implements mobile controls: touching the bottom-center zone shoots, and touching the left or right half moves the player. The shooting zone is defined as the bottom 150 pixels and the middle 50% of the screen width, giving players a large target. The function also defaults to mouseX/mouseY if touchX/touchY are unavailable, making the code testable on desktop with a mouse.

function touchStarted() {
  if (gameState === "start" || gameState === "gameover" || gameState === "win") {
    resetGame();
    gameState = "playing";
  } else if (gameState === "playing") {
    let touchX = touchX || mouseX;
    let touchY = touchY || mouseY;
    if (touchY > height - 150 && touchX > width / 4 && touchX < width * 3 / 4) {
      player.shoot();
      playerShootSound.amp(0.5, 0.1);
      playerShootSound.freq(random(550, 650));
      playerShootSound.amp(0, 0.5, 0.5);
    } else {
      if (touchX < width / 2) {
        player.startMove(-1);
      } else {
        player.startMove(1);
      }
    }
  }
  return false;
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Start Game on Touch if (gameState === "start" || gameState === "gameover" || gameState === "win") { resetGame(); gameState = "playing"; }

Touching the screen on any end screen restarts the game

conditional Touch Shoot Zone if (touchY > height - 150 && touchX > width / 4 && touchX < width * 3 / 4) { player.shoot(); ... }

If touch is in the bottom-center of the screen, shoot; otherwise, move left or right

if (gameState === "start" || gameState === "gameover" || gameState === "win") {
Checks if the game is on any of the end screens
resetGame();
Resets the game
gameState = "playing";
Transitions to playing state
} else if (gameState === "playing") {
If the game is already playing, handle the touch input for movement or shooting
let touchX = touchX || mouseX;
Gets the x-coordinate of the touch; falls back to mouseX if touchX is unavailable (for desktop testing)
let touchY = touchY || mouseY;
Gets the y-coordinate of the touch; falls back to mouseY
if (touchY > height - 150 && touchX > width / 4 && touchX < width * 3 / 4) {
Checks if the touch is in the bottom 150 pixels of the screen AND horizontally between 1/4 and 3/4 width—this defines the shooting zone
player.shoot();
If touch is in the shooting zone, fire a bullet
playerShootSound.amp(0.5, 0.1);
Play the shoot sound by fading its volume up
playerShootSound.freq(random(550, 650));
Set sound pitch to a random frequency
playerShootSound.amp(0, 0.5, 0.5);
Fade sound back to 0 to complete the chirp
} else {
If touch is NOT in the shooting zone, interpret it as a move command
if (touchX < width / 2) {
If touch is on the left half of the screen, move left
player.startMove(-1);
Start moving left
} else {
Otherwise (right half of screen)
player.startMove(1);
Start moving right
return false;
Returns false to prevent the browser from processing default touch behaviors like scrolling

touchEnded()

touchEnded() is called once when the user lifts their finger from the screen. It stops the player's movement, mirroring the behavior of keyReleased(). Together, touchStarted() and touchEnded() create a complete mobile input system.

function touchEnded() {
  if (gameState === "playing") {
    player.stopMove();
  }
  return false;
}
Line-by-line explanation (3 lines)
if (gameState === "playing") {
Only stops movement during active gameplay
player.stopMove();
Calls the player's stopMove() method to halt movement when touch ends
return false;
Prevents the browser from processing default touch behaviors

windowResized()

windowResized() is a special p5.js function that is called automatically whenever the browser window is resized. It resizes the canvas and adjusts game object positions and calculations to maintain proper gameplay on any screen size. This makes the game responsive and playable on phones, tablets, and desktops.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  player.x = width / 2;
  player.y = height - PLAYER_SIZE - PLAYER_START_Y_OFFSET;
  spawnBarriers();
  let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
  alienMoveLimit = floor((width - formationWidth) / 2 / alienSpeed);
}
Line-by-line explanation (6 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions
player.x = width / 2;
Re-centers the player horizontally when the window width changes
player.y = height - PLAYER_SIZE - PLAYER_START_Y_OFFSET;
Adjusts the player's y-position for the new window height
spawnBarriers();
Respawns the barriers to position them correctly in the new canvas size
let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
Recalculates the formation width in case the window size affected spacing calculations
alienMoveLimit = floor((width - formationWidth) / 2 / alienSpeed);
Recalculates how many frames aliens can move before hitting the new screen edges

Player class

The Player class encapsulates all player-related logic and data: position, speed, movement flags, and methods to update, display, and shoot. Notice the two-flag system (isMovingLeft and isMovingRight) instead of a single direction variable—this allows the game to know if a movement key is currently held down, enabling smooth continuous movement. The startMove() and stopMove() methods are called by keyPressed() and keyReleased(), respectively.

class Player {
  constructor(x, y, size) {
    this.x = x;
    this.y = y;
    this.w = size;
    this.h = size;
    this.speed = PLAYER_BASE_SPEED;
    this.lives = lives;
    this.isMovingLeft = false;
    this.isMovingRight = false;
  }
  update() {
    if (this.isMovingLeft) {
      this.x -= this.speed;
    }
    if (this.isMovingRight) {
      this.x += this.speed;
    }
    this.x = constrain(this.x, this.w / 2, width - this.w / 2);
  }
  display() {
    fill(0, 0, 255);
    triangle(
      this.x, this.y - this.h / 2,
      this.x - this.w / 2, this.y + this.h / 2,
      this.x + this.w / 2, this.y + this.h / 2
    );
  }
  startMove(direction) {
    if (direction === -1) {
      this.isMovingLeft = true;
      this.isMovingRight = false;
    } else if (direction === 1) {
      this.isMovingRight = true;
      this.isMovingLeft = false;
    }
  }
  stopMove() {
    this.isMovingLeft = false;
    this.isMovingRight = false;
  }
  shoot() {
    let bullet = new PlayerBullet(this.x, this.y - this.h / 2);
    playerBullets.push(bullet);
  }
}
Line-by-line explanation (34 lines)

🔧 Subcomponents:

function-call Constructor (Initialize) constructor(x, y, size) { ... }

Initializes a new player object with position, size, speed, and movement flags

sequence Update Position update() { if (this.isMovingLeft) { this.x -= this.speed; } ... }

Called every frame to move the player left or right based on input flags, with boundary checking

function-call Draw Player display() { fill(0, 0, 255); triangle(...); }

Draws a blue triangle representing the player's spaceship

constructor(x, y, size) {
The constructor method is called when you create a new Player object—it sets up initial values
this.x = x;
Stores the player's x-position passed in from the constructor call
this.y = y;
Stores the player's y-position
this.w = size;
Stores the player's width (size)
this.h = size;
Stores the player's height (size)—a square object
this.speed = PLAYER_BASE_SPEED;
Sets the player's movement speed to the constant PLAYER_BASE_SPEED (5 pixels per frame)
this.lives = lives;
Stores the current lives count (though this is also tracked globally)
this.isMovingLeft = false;
Initializes the left movement flag to false—the player starts stationary
this.isMovingRight = false;
Initializes the right movement flag to false
update() {
The update() method is called every frame to move the player
if (this.isMovingLeft) {
Checks if the left movement flag is true
this.x -= this.speed;
If so, decreases x by the speed amount, moving the player left
if (this.isMovingRight) {
Checks if the right movement flag is true
this.x += this.speed;
If so, increases x by the speed amount, moving the player right
this.x = constrain(this.x, this.w / 2, width - this.w / 2);
Constrains the player's x position to stay within canvas bounds: at least w/2 from the left edge, and at most width - w/2 from the right
display() {
The display() method is called every frame to draw the player on screen
fill(0, 0, 255);
Sets the fill color to blue (RGB 0, 0, 255)
triangle(
Draws a triangle to represent the spaceship
this.x, this.y - this.h / 2,
First vertex: top point of the triangle at (x, y - h/2)
this.x - this.w / 2, this.y + this.h / 2,
Second vertex: bottom-left corner at (x - w/2, y + h/2)
this.x + this.w / 2, this.y + this.h / 2
Third vertex: bottom-right corner at (x + w/2, y + h/2)
startMove(direction) {
The startMove() method is called when the player presses a movement key
if (direction === -1) {
If direction is -1 (left)
this.isMovingLeft = true;
Sets the left movement flag to true
this.isMovingRight = false;
Ensures the right movement flag is false so only one direction is active
} else if (direction === 1) {
If direction is 1 (right)
this.isMovingRight = true;
Sets the right movement flag to true
this.isMovingLeft = false;
Ensures the left movement flag is false
stopMove() {
The stopMove() method is called when the player releases a movement key
this.isMovingLeft = false;
Sets the left movement flag to false
this.isMovingRight = false;
Sets the right movement flag to false, stopping all movement
shoot() {
The shoot() method is called when the player presses the spacebar or touches the shoot zone
let bullet = new PlayerBullet(this.x, this.y - this.h / 2);
Creates a new PlayerBullet object at the player's position, starting above the ship (y - h/2)
playerBullets.push(bullet);
Adds the bullet to the global playerBullets array so it will be updated and drawn each frame

PlayerBullet class

PlayerBullet is a simple class that manages a single bullet fired by the player. Each bullet has a position (x, y), a size (w, h), and a speed. The update() method moves it upward every frame, and the hits() method tests collision with targets. The isOffScreen() method helps the main game loop remove bullets that have escaped the top of the canvas, preventing infinite growth of the bullets array.

class PlayerBullet {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.w = 5;
    this.h = 15;
    this.speed = PLAYER_BULLET_SPEED;
  }
  update() {
    this.y -= this.speed;
  }
  display() {
    fill(0, 255, 0);
    rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
  }
  isOffScreen() {
    return this.y < -this.h;
  }
  hits(target) {
    return collideRectRect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h, target.x - target.w / 2, target.y - target.h / 2, target.w, target.h);
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

sequence Move Bullet Upward update() { this.y -= this.speed; }

Moves the bullet up the screen each frame

function-call Collision Detection hits(target) { return collideRectRect(...); }

Tests if this bullet overlaps a target object using axis-aligned bounding box collision

constructor(x, y) {
The constructor creates a bullet at the given x,y position
this.x = x;
Stores the bullet's x position (inherited from the player)
this.y = y;
Stores the bullet's y position (just above the player)
this.w = 5;
Sets the bullet width to 5 pixels—a thin vertical rectangle
this.h = 15;
Sets the bullet height to 15 pixels—taller than wide
this.speed = PLAYER_BULLET_SPEED;
Sets the bullet speed to PLAYER_BULLET_SPEED (7 pixels per frame)
update() {
Called every frame to move the bullet
this.y -= this.speed;
Decreases y by the speed amount, moving the bullet upward toward the aliens
display() {
Called every frame to draw the bullet
fill(0, 255, 0);
Sets the fill color to green (RGB 0, 255, 0)
rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
Draws a green rectangle centered at (x, y) with width w and height h
isOffScreen() {
Called to check if the bullet has left the canvas
return this.y < -this.h;
Returns true if the bullet's y position is above the top of the canvas (negative with some margin)
hits(target) {
Called to test collision with a target object (alien, barrier, etc.)
return collideRectRect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h, target.x - target.w / 2, target.y - target.h / 2, target.w, target.h);
Uses the helper function collideRectRect() to test if this bullet's rectangle overlaps the target's rectangle

Alien class

The Alien class represents a single enemy. Two types exist: "basic" aliens (green) that shoot infrequently, and "shooter" aliens (red) that shoot more often. The update() method is currently empty because alien movement is handled globally in updateGame() to create the typewriter formation effect. The display() method differentiates color by type, and the shoot() method creates bullets below the alien.

class Alien {
  constructor(x, y, size, type) {
    this.x = x;
    this.y = y;
    this.w = size;
    this.h = size;
    this.type = type;
    this.image = alienImages[type === "shooter" ? 1 : 0];
  }
  update() {
  }
  display() {
    if (this.type === "shooter") {
      fill(255, 0, 0);
    } else {
      fill(0, 255, 0);
    }
    ellipse(this.x, this.y, this.w, this.h);
  }
  shoot() {
    let bullet = new AlienBullet(this.x, this.y + this.h / 2);
    alienBullets.push(bullet);
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Alien Type Differentiation if (this.type === "shooter") { fill(255, 0, 0); } else { fill(0, 255, 0); }

Draws different colors for different alien types: red for shooters, green for basic

constructor(x, y, size, type) {
The constructor creates an alien at the given position with a type ("basic" or "shooter")
this.x = x;
Stores the alien's x position
this.y = y;
Stores the alien's y position
this.w = size;
Stores the alien's width
this.h = size;
Stores the alien's height (aliens are square)
this.type = type;
Stores the alien's type: "basic" or "shooter"
this.image = alienImages[type === "shooter" ? 1 : 0];
Attempts to load an image for the alien based on type (shooter aliens use index 1, basic use index 0)—currently unused since images aren't loaded
update() {
The update() method is called every frame—currently empty because alien movement is handled centrally in updateGame()
display() {
Called every frame to draw the alien
if (this.type === "shooter") {
Checks if the alien is a shooter type
fill(255, 0, 0);
If so, sets the fill color to red
} else {
If the alien is basic type
fill(0, 255, 0);
Sets the fill color to green
ellipse(this.x, this.y, this.w, this.h);
Draws a circle (ellipse with equal width and height) at the alien's position
shoot() {
Called when the alien fires
let bullet = new AlienBullet(this.x, this.y + this.h / 2);
Creates a new AlienBullet below the alien (y + h/2)
alienBullets.push(bullet);
Adds the bullet to the global alienBullets array

AlienBullet class

AlienBullet mirrors PlayerBullet but moves downward instead of upward. It is identical in structure but with opposite direction (+= instead of -=) and yellow color instead of green. This demonstrates how two similar objects can be specialized by small changes in a single parameter or property.

class AlienBullet {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.w = 5;
    this.h = 15;
    this.speed = ALIEN_BULLET_SPEED;
  }
  update() {
    this.y += this.speed;
  }
  display() {
    fill(255, 255, 0);
    rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
  }
  isOffScreen() {
    return this.y > height + this.h;
  }
  hits(target) {
    return collideRectRect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h, target.x - target.w / 2, target.y - target.h / 2, target.w, target.h);
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

sequence Move Bullet Downward update() { this.y += this.speed; }

Moves the bullet down the screen each frame toward the player

constructor(x, y) {
Creates an alien bullet at the given position
this.x = x;
Stores the bullet's x position (inherited from the alien)
this.y = y;
Stores the bullet's y position (just below the alien)
this.w = 5;
Sets bullet width to 5 pixels
this.h = 15;
Sets bullet height to 15 pixels
this.speed = ALIEN_BULLET_SPEED;
Sets the bullet speed to ALIEN_BULLET_SPEED (3 pixels per frame, slower than player bullets)
update() {
Called every frame to move the bullet
this.y += this.speed;
Increases y by the speed amount, moving the bullet downward toward the player
display() {
Called every frame to draw the bullet
fill(255, 255, 0);
Sets the fill color to yellow (RGB 255, 255, 0) to distinguish alien bullets from player bullets
rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
Draws a yellow rectangle at the bullet's position
isOffScreen() {
Checks if the bullet has left the canvas
return this.y > height + this.h;
Returns true if the bullet's y position is below the bottom of the canvas
hits(target) {
Tests collision with a target object
return collideRectRect(...);
Uses AABB collision detection to test overlap with the target

Barrier class

The Barrier class represents a protective shield above the player. It has health (0–3) and darkens as it takes damage—this visual feedback is created by the map() function, which linearly interpolates the health value to a grayscale color. When health reaches 0, the barrier is removed from the game. Barriers provide strategic cover but are destructible, creating a dynamic defensive landscape.

class Barrier {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.health = BARRIER_HEALTH;
  }
  display() {
    let colorValue = map(this.health, 0, BARRIER_HEALTH, 50, 200);
    fill(colorValue);
    rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
  }
  takeHit() {
    this.health--;
  }
  hits(target) {
    return collideRectRect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h, target.x - target.w / 2, target.y - target.h / 2, target.w, target.h);
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Dynamic Health-Based Color let colorValue = map(this.health, 0, BARRIER_HEALTH, 50, 200);

Maps the barrier's health to a grayscale value so it darkens as it takes damage

constructor(x, y, w, h) {
Creates a barrier at the given position with the given width and height
this.x = x;
Stores the barrier's center x position
this.y = y;
Stores the barrier's center y position
this.w = w;
Stores the barrier's width
this.h = h;
Stores the barrier's height
this.health = BARRIER_HEALTH;
Initializes the barrier's health to BARRIER_HEALTH (3), meaning it can take 3 hits before being destroyed
display() {
Called every frame to draw the barrier
let colorValue = map(this.health, 0, BARRIER_HEALTH, 50, 200);
Uses the map() function to convert health (0 to 3) to a grayscale color (50 to 200). Full health = 200 (bright), no health = 50 (dark)
fill(colorValue);
Sets the fill color to the calculated grayscale value
rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
Draws a rectangle at the barrier's center position
takeHit() {
Called when an alien bullet hits the barrier
this.health--;
Decrements health by 1
hits(target) {
Tests collision with a target
return collideRectRect(...);
Uses AABB collision detection

spawnAliens()

spawnAliens() creates the 55 alien formation seen at the start of each game. It uses nested loops to place aliens in a grid, calculates the starting x position to center the formation on the screen, and randomly assigns 40% of aliens in the bottom two rows as "shooter" types. This setup creates the iconic Space Invaders 'wall' of enemies that descends the screen.

function spawnAliens() {
  aliens = [];
  let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
  let startX = (width - formationWidth) / 2 + ALIEN_SIZE / 2;
  let startY = 100;
  for (let r = 0; r < ALIEN_ROWS; r++) {
    for (let c = 0; c < ALIEN_COLS; c++) {
      let x = startX + c * ALIEN_SPACING;
      let y = startY + r * ALIEN_SPACING;
      let type = "basic";
      if (r >= ALIEN_ROWS - 2 && random() < 0.4) {
        type = "shooter";
      }
      aliens.push(new Alien(x, y, ALIEN_SIZE, type));
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Calculate Centered Formation let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE; let startX = (width - formationWidth) / 2 + ALIEN_SIZE / 2;

Calculates the horizontal position of the first alien so the formation is centered on screen

nested-loop Nested Loop to Create 5×11 Grid for (let r = 0; r < ALIEN_ROWS; r++) { for (let c = 0; c < ALIEN_COLS; c++) { ... } }

Loops through 5 rows and 11 columns to create 55 alien objects in a grid formation

conditional Random Shooter Assignment if (r >= ALIEN_ROWS - 2 && random() < 0.4) { type = "shooter"; }

Assigns a 40% chance for aliens in the bottom two rows to be shooters, making them more dangerous

aliens = [];
Clears the aliens array to prepare for spawning a new formation
let formationWidth = (ALIEN_COLS - 1) * ALIEN_SPACING + ALIEN_SIZE;
Calculates the total width of the 11-column formation: 10 gaps of 60 pixels each (ALIEN_COLS - 1 = 10) plus one alien size (40 pixels) = 640 pixels
let startX = (width - formationWidth) / 2 + ALIEN_SIZE / 2;
Calculates the x position of the first alien: center the formation on the screen, then add half the alien size to get the center of the first alien
let startY = 100;
Sets the top row of aliens to y = 100 pixels below the top of the canvas
for (let r = 0; r < ALIEN_ROWS; r++) {
Loops through 5 rows (0, 1, 2, 3, 4)
for (let c = 0; c < ALIEN_COLS; c++) {
For each row, loops through 11 columns (0 to 10)
let x = startX + c * ALIEN_SPACING;
Calculates the x position of this alien: start position plus column index times spacing (60 pixels)
let y = startY + r * ALIEN_SPACING;
Calculates the y position of this alien: start position plus row index times spacing
let type = "basic";
Sets the default type to "basic"
if (r >= ALIEN_ROWS - 2 && random() < 0.4) {
If this alien is in one of the bottom two rows (r >= 3) AND a random number is less than 0.4 (40% chance)
type = "shooter";
Change the type to "shooter", making it fire more often
aliens.push(new Alien(x, y, ALIEN_SIZE, type));
Creates a new Alien object and adds it to the aliens array

spawnBarriers()

spawnBarriers() creates 4 evenly-spaced protective barriers above the player. By dividing the screen width by (BARRIER_COUNT + 1), the function ensures barriers are spread across the screen with equal margins on both sides. The barriers provide strategic cover from alien fire but can be destroyed when hit.

function spawnBarriers() {
  barriers = [];
  let barrierSpacing = width / (BARRIER_COUNT + 1);
  let barrierY = height - PLAYER_SIZE - BARRIER_HEIGHT - 60;
  for (let i = 0; i < BARRIER_COUNT; i++) {
    let x = (i + 1) * barrierSpacing;
    let y = barrierY;
    barriers.push(new Barrier(x, y, BARRIER_WIDTH, BARRIER_HEIGHT));
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Calculate Barrier Positions let barrierSpacing = width / (BARRIER_COUNT + 1);

Divides the screen width evenly among 4 barriers plus edges, spacing them out

barriers = [];
Clears the barriers array to prepare for spawning new barriers
let barrierSpacing = width / (BARRIER_COUNT + 1);
Calculates the horizontal spacing: divide the canvas width by 5 (BARRIER_COUNT + 1) to distribute 4 barriers evenly with margins
let barrierY = height - PLAYER_SIZE - BARRIER_HEIGHT - 60;
Calculates the vertical position: places barriers above the player with 60 pixels of gap
for (let i = 0; i < BARRIER_COUNT; i++) {
Loops 4 times to create 4 barriers
let x = (i + 1) * barrierSpacing;
Calculates the x position: (i + 1) ensures barriers start at position 1, not 0, so they are not at the screen edges
let y = barrierY;
Uses the pre-calculated y position for all barriers
barriers.push(new Barrier(x, y, BARRIER_WIDTH, BARRIER_HEIGHT));
Creates a new Barrier object and adds it to the barriers array

collideRectRect()

collideRectRect() is a utility function that implements axis-aligned bounding box (AABB) collision detection. It tests whether two rectangles overlap by checking if they overlap on both the X and Y axes. This simple algorithm is fast and works well for games with rectangular objects (bullets, aliens, barriers, player). It is used by the hits() methods in PlayerBullet, AlienBullet, and Barrier classes.

function collideRectRect(x1, y1, w1, h1, x2, y2, w2, h2) {
  return (x1 < x2 + w2 &&
          x1 + w1 > x2 &&
          y1 < y2 + h2 &&
          y1 + h1 > y2);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Check X-Axis Overlap x1 < x2 + w2 && x1 + w1 > x2

Tests whether the rectangles overlap horizontally

conditional Check Y-Axis Overlap y1 < y2 + h2 && y1 + h1 > y2

Tests whether the rectangles overlap vertically

function collideRectRect(x1, y1, w1, h1, x2, y2, w2, h2) {
Takes 8 parameters: the x, y, width, and height of two rectangles
return (x1 < x2 + w2 &&
Checks if the left edge of rectangle 1 (x1) is to the left of the right edge of rectangle 2 (x2 + w2)
x1 + w1 > x2 &&
Checks if the right edge of rectangle 1 (x1 + w1) is to the right of the left edge of rectangle 2 (x2)
y1 < y2 + h2 &&
Checks if the top edge of rectangle 1 (y1) is above the bottom edge of rectangle 2 (y2 + h2)
y1 + h1 > y2);
Checks if the bottom edge of rectangle 1 (y1 + h1) is below the top edge of rectangle 2 (y2)

📦 Key Variables

gameState string

Tracks the current game state: 'start' (title screen), 'playing' (active game), 'gameover' (loss screen), or 'win' (victory screen)

let gameState = "start";
score number

Tracks the player's current score (incremented by 10 for each alien destroyed)

let score = 0;
lives number

Tracks the number of remaining lives (starts at 3, decrements when hit by alien bullets)

let lives = 3;
player object

Reference to the Player object (the spaceship the player controls)

let player;
playerBullets array

Array of PlayerBullet objects currently on screen (bullets fired by the player)

let playerBullets = [];
aliens array

Array of Alien objects in the current formation (destroyed aliens are removed)

let aliens = [];
alienBullets array

Array of AlienBullet objects currently on screen (bullets fired by aliens)

let alienBullets = [];
alienDirection number

Direction aliens move horizontally: 1 for right, -1 for left (reverses when hitting screen edge)

let alienDirection = 1;
alienSpeed number

Speed at which aliens move horizontally (increases as game difficulty scales)

let alienSpeed = 1;
alienDropSpeed number

Number of pixels aliens drop vertically when reversing direction (increases over time)

let alienDropSpeed = 10;
alienMoveCounter number

Counter tracking frames since last direction reversal (triggers drop when reaching alienMoveLimit)

let alienMoveCounter = 0;
alienMoveLimit number

Maximum frames aliens can move horizontally before hitting screen edge and dropping (calculated dynamically)

let alienMoveLimit;
alienShootingRate number

Frame interval between alien shooting waves (decreases as game progresses, minimum 30 frames)

let alienShootingRate = 120;
alienShootingTimer number

Counter tracking frames elapsed since last alien shooting event

let alienShootingTimer = 0;
barriers array

Array of Barrier objects providing protection above the player

let barriers = [];
gameFont object

p5.Font object for the retro pixel font (loaded from @fontsource CDN)

let gameFont;
playerShootSound object

p5.Oscillator for the player shooting sound effect

let playerShootSound;
alienHitSound object

p5.Oscillator for the sound effect when an alien is destroyed

let alienHitSound;
playerHitSound object

p5.Oscillator for the sound effect when the player is hit

let playerHitSound;
alienShootSound object

p5.Oscillator for the sound effect when aliens shoot

let alienShootSound;
gameOverSound object

p5.Oscillator for the game over sound effect

let gameOverSound;
winSound object

p5.Oscillator for the victory sound effect

let winSound;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE updateGame() alien shooting logic

The aliensByColumn object is recalculated every frame, grouping aliens by column and sorting for the lowest in each. This is an O(n) operation repeated 60 times per second (or more frequently as shootingRate decreases).

💡 Cache the aliensByColumn grouping and update it only when aliens are destroyed or moved significantly (e.g., after a drop). Alternatively, track which aliens can shoot via a property on the Alien class instead of recalculating every frame.

BUG touchStarted() input handling

The line 'let touchX = touchX || mouseX;' uses touchX as both the variable name and the p5.js property, creating shadowing. If touchX is undefined, the fallback to mouseX works, but the code is confusing.

💡 Rename the local variables: 'let tx = touches[0].x || mouseX;' to clearly distinguish between the local variable and the p5.js API. Or check touches array length to differentiate touch from mouse input.

FEATURE Alien and AlienBullet classes

Aliens always spawn in the same formation and pattern. Once the player learns the rhythm, the game becomes predictable.

💡 Add randomization: vary the initial number of aliens, their spacing, or spawn alien groups dynamically when the current wave is mostly destroyed. This would extend gameplay and create replayability.

STYLE Collision detection

The checkCollisions() function contains three nearly-identical nested loops (player bullets vs. aliens, alien bullets vs. player, alien bullets vs. barriers). Code repetition makes maintenance harder.

💡 Extract a helper function like testCollisionPair(bulletArray, targetArray) that handles the loop structure and collision logic generically, reducing duplication.

BUG windowResized() player repositioning

When the window is resized during gameplay, the player is repositioned to the bottom center, but alien positions are not recalculated. If aliens were near the screen edge before resize, they might suddenly appear off-screen or in unexpected positions.

💡 Recalculate the starting x position for all aliens when the window resizes, or apply a scaling factor to their positions proportional to the canvas width change.

FEATURE Game progression

The game has no escalation beyond the initial difficulty scaling. Once aliens become very fast, the challenge plateaus.

💡 Introduce waves: after defeating all aliens, spawn a new, harder formation. Add boss aliens, moving barriers, or time limits to create varied and memorable moments.

🔄 Code Flow

Code flow showing preload, setup, resetgame, draw, updategame, displaygame, checkcollisions, checkgameover, drawstartscreen, drawgameoverscreen, drawwinscreen, keypressed, keyreleased, touchstarted, touchended, windowresized, player, playerbullet, alien, alienbullet, barrier, spawnaliens, spawnbarriers, colliderect

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

graph TD start[Start] --> setup[setup] setup --> preload[preload] preload --> fontload[font-load] preload --> oscillatorinit[oscillator-init] setup --> canvascreation[canvas-creation] setup --> textconfig[text-config] setup --> resetgame[resetgame] resetgame --> resetstate[reset-state] resetgame --> cleararrays[clear-arrays] resetgame --> spawnaliens[spawnaliens] resetgame --> spawnbarriers[spawnbarriers] setup --> draw[draw loop] draw --> stateMachine[state-machine] stateMachine --> drawstartscreen[drawStartScreen] drawstartscreen --> startkeypress[start-keypress] stateMachine --> updategame[updateGame] updategame --> objectupdates[object-updates] objectupdates --> alienmovement[alien-movement] objectupdates --> difficultyscale[difficulty-scale] objectupdates --> alienshooting[alien-shooting] updategame --> offscreencleanup[offscreen-cleanup] draw --> displaygame[displayGame] displaygame --> renderobjects[render-objects] displaygame --> renderui[render-ui] draw --> checkcollisions[checkCollisions] checkcollisions --> playerbulletalien[player-bullet-alien] checkcollisions --> alienbulletplayer[alien-bullet-player] checkcollisions --> alienbulletbarrier[alien-bullet-barrier] draw --> checkgameover[checkGameOver] checkgameover --> wincheck[win-check] checkgameover --> losecheck[lose-check] click preload href "#fn-preload" click setup href "#fn-setup" click resetgame href "#fn-resetgame" click draw href "#fn-draw" click updategame href "#fn-updategame" click displaygame href "#fn-displaygame" click checkcollisions href "#fn-checkcollisions" click checkgameover href "#fn-checkgameover" click drawstartscreen href "#fn-drawstartscreen" click startkeypress href "#sub-start-keypress" click objectupdates href "#sub-object-updates" click alienmovement href "#sub-alien-movement" click difficultyscale href "#sub-difficulty-scale" click alienshooting href "#sub-alien-shooting" click offscreencleanup href "#sub-offscreen-cleanup" click renderobjects href "#sub-render-objects" click renderui href "#sub-render-ui" click playerbulletalien href "#sub-player-bullets-vs-aliens" click alienbulletplayer href "#sub-alien-bullets-vs-player" click alienbulletbarrier href "#sub-alien-bullets-vs-barriers" click wincheck href "#sub-win-check" click losecheck href "#sub-lose-check" click fontload href "#sub-font-load" click oscillatorinit href "#sub-oscillator-init" click canvascreation href "#sub-canvas-creation" click textconfig href "#sub-text-config" click resetstate href "#sub-reset-state" click cleararrays href "#sub-clear-arrays" click spawnaliens href "#fn-spawnaliens" click spawnbarriers href "#fn-spawnbarriers"

Preview

SpaceInvaders - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of SpaceInvaders - Code flow showing preload, setup, resetgame, draw, updategame, displaygame, checkcollisions, checkgameover, drawstartscreen, drawgameoverscreen, drawwinscreen, keypressed, keyreleased, touchstarted, touchended, windowresized, player, playerbullet, alien, alienbullet, barrier, spawnaliens, spawnbarriers, colliderect
Code Flow Diagram