DIE

This is a Flappy Bird-style game where a fish navigates through moving seaweed obstacles while jumping and shooting missiles. The player controls the fish with the spacebar or mouse clicks to jump, presses 'x' to shoot missiles at obstacles, and tries to pass through as many obstacles as possible without hitting them or the screen edges. The game tracks score and ends on collision.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the game — Obstacles move faster across the screen, making the game harder within seconds—visible difficulty change immediately.
  2. Make jumping more powerful — Negative JUMP_FORCE increases upward velocity, so the fish reaches higher and has more air time—test your reaction skills.
  3. Tighten the gaps — Reduce the gap between top and bottom seaweed obstacles, making it harder to safely pass through—instantly raises difficulty.
  4. Spawn obstacles faster — New obstacles appear more frequently because OBSTACLE_SPAWN_INTERVAL is shorter—the density of obstacles increases noticeably.
  5. Invincible mode
  6. Change the fish color — The orange fish becomes a different color instantly—RGB values control the hue (try 0, 200, 200 for cyan or 200, 100, 0 for brown).
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable arcade-style game featuring a fish that must navigate through moving seaweed obstacles. The player jumps using the spacebar or mouse clicks and shoots missiles by pressing 'x' to destroy obstacles. The game combines physics (gravity and jumping), collision detection, dynamic obstacle spawning, multiple game states (start, playing, game over), and synthesized sound effects to create an engaging interactive experience.

The code is organized around three core systems: a fish object with movement and shooting methods, an obstacles array that spawns and moves continuously, and a missiles array that handles projectile collision detection. You will learn how to structure a game with state machines, how to manage multiple objects and arrays, how bounding-box collision detection works in practice, and how to use p5.sound's oscillators to create simple audio feedback for player actions.

⚙️ How It Works

  1. When the sketch loads, setup() creates the game canvas, initializes the fish object with its position and methods, and creates five p5.Oscillator sound objects (jump, hit, score, shoot, explode) that stay silent until triggered.
  2. The draw loop continuously renders based on gameState: START shows instructions, PLAYING runs the active game logic, and GAME_OVER shows the final score.
  3. During gameplay, the fish is updated with gravity pulling it downward each frame; the player makes it jump upward or shoot missiles forward via input handlers.
  4. Obstacles spawn at regular intervals on the right side of the canvas and move leftward. The game checks if the fish collides with any obstacle using bounding-box rectangle collision detection; if so, the game ends.
  5. When the fish passes an obstacle without collision (the obstacle moves past the fish's x-position), the score increments and a sound plays.
  6. Missiles fired by the fish move rightward and are checked for collision with obstacles. When a missile hits an obstacle, both are removed and an explosion sound plays.
  7. Off-screen obstacles and missiles are removed from their arrays to conserve memory, and the current score is drawn in the top-left corner every frame.

🎓 Concepts You'll Learn

Game state machinesObject-oriented designCollision detection (bounding boxes)Physics simulation (gravity and velocity)Dynamic object spawningArray managementInput handling (keyboard and mouse)Synthesized sound effects

📝 Code Breakdown

setup()

setup() runs once when the program starts. It initializes the canvas size, creates the fish object with all its methods (show, update, jump, shoot, hits), and prepares the five sound oscillators that the game will use. This is the perfect place to set up game entities and resources that don't change every frame.

function setup() {
  createCanvas(1366, 768);
  background(0, 191, 255);

  fish = {
    x: width / 4,
    y: height / 2,
    vy: 0,

    show: function() {
      fill(255, 165, 0);
      noStroke();
      ellipse(this.x, this.y, FISH_SIZE * 1.5, FISH_SIZE);
      triangle(this.x - FISH_SIZE * 0.75, this.y,
               this.x - FISH_SIZE * 1.5, this.y - FISH_SIZE / 2,
               this.x - FISH_SIZE * 1.5, this.y + FISH_SIZE / 2);
      fill(100);
      rect(this.x + FISH_SIZE * 0.5, this.y - FISH_SIZE * 0.3, FISH_SIZE * 0.7, FISH_SIZE * 0.2);
    },

    update: function() {
      this.vy += GRAVITY;
      this.y += this.vy;
      this.y = constrain(this.y, FISH_SIZE / 2, height - FISH_SIZE / 2);
      if (this.y === FISH_SIZE / 2 || this.y === height - FISH_SIZE / 2) {
        hitSound.amp(0.5, 0.1);
        setTimeout(() => hitSound.amp(0, 0.1), 100);
        gameState = GAME_OVER;
      }
    },

    jump: function() {
      this.vy = JUMP_FORCE;
      jumpSound.amp(0.5, 0.1);
      setTimeout(() => jumpSound.amp(0, 0.1), 100);
    },

    shoot: function() {
      if (millis() - lastShotTime > SHOOT_COOLDOWN) {
        let missile = {
          x: this.x + FISH_SIZE * 1.2,
          y: this.y - FISH_SIZE * 0.2,
          speed: MISSILE_SPEED,

          show: function() {
            fill(255, 0, 0);
            noStroke();
            ellipse(this.x, this.y, MISSILE_SIZE);
          },

          update: function() {
            this.x += this.speed;
          },

          offscreen: function() {
            return this.x > width + MISSILE_SIZE;
          }
        };
        missiles.push(missile);
        shootSound.amp(0.5, 0.1);
        setTimeout(() => shootSound.amp(0, 0.1), 100);
        lastShotTime = millis();
      }
    },

    hits: function(obstacle) {
      let fishRect = {
        x: this.x - FISH_SIZE * 0.75,
        y: this.y - FISH_SIZE / 2,
        width: FISH_SIZE * 1.5,
        height: FISH_SIZE
      };

      let obstacleTopRect = {
        x: obstacle.x,
        y: 0,
        width: obstacle.width,
        height: obstacle.y1
      };
      if (checkRectCollision(fishRect, obstacleTopRect)) {
        return true;
      }

      let obstacleBottomRect = {
        x: obstacle.x,
        y: obstacle.y2,
        width: obstacle.width,
        height: height - obstacle.y2
      };
      if (checkRectCollision(fishRect, obstacleBottomRect)) {
        return true;
      }

      return false;
    }
  };

  jumpSound = new p5.Oscillator();
  jumpSound.setType('sine');
  jumpSound.freq(660);
  jumpSound.amp(0);
  jumpSound.start();

  hitSound = new p5.Oscillator();
  hitSound.setType('sawtooth');
  hitSound.freq(220);
  hitSound.amp(0);
  hitSound.start();

  scoreSound = new p5.Oscillator();
  scoreSound.setType('triangle');
  scoreSound.freq(880);
  scoreSound.amp(0);
  scoreSound.start();

  shootSound = new p5.Oscillator();
  shootSound.setType('triangle');
  shootSound.freq(1000);
  shootSound.amp(0);
  shootSound.start();

  explodeSound = new p5.Oscillator();
  explodeSound.setType('noise');
  explodeSound.amp(0);
  explodeSound.start();

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

🔧 Subcomponents:

object-creation Fish object with methods fish = { x: width / 4, y: height / 2, ... }

Creates a single fish object that holds position, velocity, and methods for drawing, updating, jumping, shooting, and collision detection

method Fish show() method show: function() { ellipse(...); triangle(...); rect(...); }

Draws the fish as an orange ellipse body, a triangle tail, and a grey missile launcher rectangle

method Fish update() method update: function() { this.vy += GRAVITY; this.y += this.vy; ... }

Applies gravity to the fish's vertical velocity, moves the fish downward, constrains it to the canvas, and ends the game if it hits the top or bottom edge

initialization Sound oscillator setup jumpSound = new p5.Oscillator(); jumpSound.setType('sine'); ...

Creates five oscillator objects (jump, hit, score, shoot, explode) that generate synthesized sound effects on demand

createCanvas(1366, 768);
Creates a 1366x768 pixel canvas, the main drawing area for the game
background(0, 191, 255);
Fills the initial canvas with a cyan/sky blue color (RGB: 0, 191, 255) to simulate an underwater or sky background
fish = {
Begins the creation of a fish object that will hold all the fish's properties and methods
x: width / 4,
Sets the fish's starting x-position to one-quarter of the canvas width from the left edge
y: height / 2,
Sets the fish's starting y-position to the vertical center of the canvas
vy: 0,
Initializes the fish's vertical velocity to 0 (not moving up or down at the start)
ellipse(this.x, this.y, FISH_SIZE * 1.5, FISH_SIZE);
Draws the fish's main body as an ellipse 1.5 times the fish size horizontally and the full fish size vertically
triangle(this.x - FISH_SIZE * 0.75, this.y, this.x - FISH_SIZE * 1.5, this.y - FISH_SIZE / 2, this.x - FISH_SIZE * 1.5, this.y + FISH_SIZE / 2);
Draws the fish's tail as a triangle pointing to the left, making it look like a swimming fish
rect(this.x + FISH_SIZE * 0.5, this.y - FISH_SIZE * 0.3, FISH_SIZE * 0.7, FISH_SIZE * 0.2);
Draws a grey rectangle on the fish's right side to represent the missile launcher
this.vy += GRAVITY;
Each frame, adds the gravity constant to the vertical velocity, accelerating the fish downward
this.y += this.vy;
Updates the fish's y-position by adding its vertical velocity, moving it down when gravity is positive
this.y = constrain(this.y, FISH_SIZE / 2, height - FISH_SIZE / 2);
Forces the fish's y-position to stay within the canvas bounds, preventing it from moving beyond the top or bottom
if (this.y === FISH_SIZE / 2 || this.y === height - FISH_SIZE / 2) {
Checks if the fish has hit the top or bottom edge of the canvas—if so, the game ends
this.vy = JUMP_FORCE;
Sets the fish's vertical velocity to a negative value (JUMP_FORCE = -10) to make it move upward
jumpSound.amp(0.5, 0.1); setTimeout(() => jumpSound.amp(0, 0.1), 100);
Plays the jump sound by setting its amplitude to 0.5 over 0.1 seconds, then fades it out after 100 milliseconds
let missile = { x: this.x + FISH_SIZE * 1.2, y: this.y - FISH_SIZE * 0.2, ... };
Creates a new missile object at a position slightly ahead of and above the fish's launcher
missiles.push(missile);
Adds the newly created missile to the missiles array, making it active in the game
lastShotTime = millis();
Records the current time in milliseconds, used to enforce the SHOOT_COOLDOWN between shots
let fishRect = { x: this.x - FISH_SIZE * 0.75, y: this.y - FISH_SIZE / 2, width: FISH_SIZE * 1.5, height: FISH_SIZE };
Defines a bounding rectangle around the fish's body for accurate collision detection
if (checkRectCollision(fishRect, obstacleTopRect)) { return true; }
Tests whether the fish's bounding box overlaps with the top seaweed obstacle using rectangle collision detection
userStartAudio();
Initializes the web audio context, required by browsers before any sound can play

draw()

draw() runs 60 times per second and is the heartbeat of the sketch. It clears the background each frame and uses a state machine (the if-else chain) to show different content depending on gameState. This pattern—checking a state variable and calling different functions accordingly—is a fundamental technique for organizing game code.

function draw() {
  background(0, 191, 255);

  if (gameState === START) {
    drawStartScreen();
  } else if (gameState === PLAYING) {
    gameLoop();
  } else if (gameState === GAME_OVER) {
    drawGameOverScreen();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Game state routing if (gameState === START) { ... } else if (gameState === PLAYING) { ... } else if (gameState === GAME_OVER) { ... }

Routes execution to different functions based on the current game state, creating the three distinct screens (start, playing, game over)

background(0, 191, 255);
Clears the canvas with a cyan background every frame, erasing the previous frame's content and creating a fresh slate for new drawing
if (gameState === START) {
Checks if the game is in the START state, meaning the player is viewing the instructions before beginning
drawStartScreen();
Calls the function that displays the start screen with the game title and instructions
} else if (gameState === PLAYING) {
Checks if the game is actively being played (the main gameplay is happening)
gameLoop();
Calls the main game loop function, which updates the fish, obstacles, missiles, and handles all game logic
} else if (gameState === GAME_OVER) {
Checks if the game has ended and the player is viewing the game over screen
drawGameOverScreen();
Calls the function that displays the game over message and the final score

gameLoop()

gameLoop() contains all the active gameplay logic that runs every frame when gameState === PLAYING. It updates and draws the fish, spawns obstacles on a timer, loops through obstacles and missiles checking collisions, awards score, and cleans up off-screen objects. This is where the game truly comes alive—understanding this function is key to mastering game loop architecture in p5.js.

function gameLoop() {
  fish.update();
  fish.show();

  if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) {
    spawnObstacle();
    lastObstacleSpawnTime = millis();
  }

  for (let i = obstacles.length - 1; i >= 0; i--) {
    let obstacle = obstacles[i];
    obstacle.update();
    obstacle.show();

    if (fish.hits(obstacle)) {
      hitSound.amp(0.5, 0.1);
      setTimeout(() => hitSound.amp(0, 0.1), 100);
      gameState = GAME_OVER;
    }

    if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {
      score++;
      obstacle.passed = true;
      scoreSound.amp(0.5, 0.1);
      setTimeout(() => scoreSound.amp(0, 0.1), 100);
    }

    if (obstacle.offscreen()) {
      obstacles.splice(i, 1);
    }
  }

  for (let i = missiles.length - 1; i >= 0; i--) {
    let missile = missiles[i];
    missile.update();
    missile.show();

    let missileDestroyed = false;

    for (let j = obstacles.length - 1; j >= 0; j--) {
      let obstacle = obstacles[j];

      let missileRect = {
        x: missile.x - MISSILE_SIZE / 2,
        y: missile.y - MISSILE_SIZE / 2,
        width: MISSILE_SIZE,
        height: MISSILE_SIZE
      };

      let obstacleTopRect = {
        x: obstacle.x,
        y: 0,
        width: obstacle.width,
        height: obstacle.y1
      };

      let obstacleBottomRect = {
        x: obstacle.x,
        y: obstacle.y2,
        width: obstacle.width,
        height: height - obstacle.y2
      };

      if (checkRectCollision(missileRect, obstacleTopRect) || checkRectCollision(missileRect, obstacleBottomRect)) {
        obstacles.splice(j, 1);
        missiles.splice(i, 1);
        explodeSound.amp(0.5, 0.1);
        setTimeout(() => explodeSound.amp(0, 0.1), 100);
        missileDestroyed = true;
        break;
      }
    }

    if (missileDestroyed) {
      continue;
    }

    if (missile.offscreen()) {
      missiles.splice(i, 1);
    }
  }

  drawScore();
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

function-call Fish update and draw fish.update(); fish.show();

Applies gravity to the fish each frame and draws it on the canvas

conditional Obstacle spawn timer if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) { spawnObstacle(); lastObstacleSpawnTime = millis(); }

Checks whether enough time has passed to spawn a new obstacle and creates one if the interval has elapsed

for-loop Obstacle update and collision loop for (let i = obstacles.length - 1; i >= 0; i--) { ... }

Updates and draws every active obstacle, checks for collisions with the fish, awards score when obstacles are passed, and removes off-screen obstacles

for-loop Missile update and collision loop for (let i = missiles.length - 1; i >= 0; i--) { ... }

Updates and draws every active missile, checks for collisions with obstacles (destroying both on hit), and removes off-screen missiles

nested-for-loop Missile-obstacle collision check for (let j = obstacles.length - 1; j >= 0; j--) { ... if (checkRectCollision(...)) { ... } }

Tests each missile against every obstacle and removes both if they collide

fish.update();
Calls the fish's update method, which applies gravity and checks for boundary collisions
fish.show();
Calls the fish's show method, drawing the orange fish body, tail, and launcher on the canvas at its current position
if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) {
Checks if enough milliseconds have passed since the last obstacle spawn—millis() returns the total milliseconds since the program started
spawnObstacle();
Creates a new obstacle with random top height and adds it to the obstacles array
lastObstacleSpawnTime = millis();
Records the current time, resetting the spawn interval timer
for (let i = obstacles.length - 1; i >= 0; i--) {
Loops through the obstacles array backwards (from end to start), allowing safe removal with splice() during iteration
obstacle.update();
Calls the obstacle's update method, which moves it leftward by OBSTACLE_SPEED pixels
obstacle.show();
Calls the obstacle's show method, drawing the green top and bottom seaweed rectangles
if (fish.hits(obstacle)) {
Tests whether the fish's bounding box overlaps with this obstacle's bounding boxes
gameState = GAME_OVER;
Ends the game by changing the game state, which will trigger the game over screen on the next draw
if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {
Checks if this obstacle has not been scored yet AND the obstacle's right edge has passed the fish's x-position (meaning the fish safely passed through)
score++;
Increments the player's score by 1 point
obstacle.passed = true;
Marks this obstacle as already scored, preventing multiple points from the same obstacle
if (obstacle.offscreen()) { obstacles.splice(i, 1); }
Removes the obstacle from the array if it has moved completely off the left side of the canvas
for (let i = missiles.length - 1; i >= 0; i--) {
Loops through the missiles array backwards, updating and checking collisions for each
missile.update();
Calls the missile's update method, which moves it rightward by MISSILE_SPEED pixels
missile.show();
Calls the missile's show method, drawing a red circle at the missile's current position
let missileDestroyed = false;
Flag to track whether this missile has already been destroyed by collision in the inner obstacle loop
for (let j = obstacles.length - 1; j >= 0; j--) {
Inner loop checking this missile against every obstacle
if (checkRectCollision(missileRect, obstacleTopRect) || checkRectCollision(missileRect, obstacleBottomRect)) {
Tests if the missile collides with either the top or bottom part of the obstacle
obstacles.splice(j, 1); missiles.splice(i, 1);
Removes both the obstacle and the missile from their respective arrays
missileDestroyed = true; break;
Sets the flag to true and breaks from the inner obstacle loop, since this missile is now gone
if (missileDestroyed) { continue; }
Skips the rest of the missile loop iteration if the missile was destroyed, avoiding null reference errors
if (missile.offscreen()) { missiles.splice(i, 1); }
Removes the missile from the array if it has traveled off the right side of the canvas
drawScore();
Draws the current score on the screen in the top-left corner

spawnObstacle()

spawnObstacle() is called from gameLoop() at a regular interval to create new moving obstacles. It generates a random height for the top seaweed that guarantees the gap and bottom seaweed fit on screen, then creates an obstacle object with update(), show(), and offscreen() methods. This pattern of object creation with built-in methods is a core technique in p5.js game development.

function spawnObstacle() {
  let minHeight = 100;
  let maxHeight = height - minHeight - OBSTACLE_GAP;
  let topHeight = random(minHeight, maxHeight);
  let bottomY = topHeight + OBSTACLE_GAP;

  let obstacle = {
    x: width,
    y1: topHeight,
    y2: bottomY,
    width: OBSTACLE_WIDTH,
    speed: OBSTACLE_SPEED,
    passed: false,

    show: function() {
      fill(0, 128, 0);
      noStroke();
      rect(this.x, 0, this.width, this.y1);
      rect(this.x, this.y2, this.width, height - this.y2);
    },

    update: function() {
      this.x -= this.speed;
    },

    offscreen: function() {
      return this.x + this.width < 0;
    }
  };
  obstacles.push(obstacle);
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Gap height constraints let minHeight = 100; let maxHeight = height - minHeight - OBSTACLE_GAP;

Calculates the valid range for the top seaweed height so the gap between top and bottom always fits on screen

calculation Random obstacle position let topHeight = random(minHeight, maxHeight); let bottomY = topHeight + OBSTACLE_GAP;

Picks a random top height and calculates the bottom position based on the fixed gap size

object-creation Obstacle object creation let obstacle = { x: width, y1: topHeight, y2: bottomY, width: OBSTACLE_WIDTH, speed: OBSTACLE_SPEED, passed: false, ... }

Creates an obstacle object with position, dimensions, speed, and methods to update and draw itself

let minHeight = 100;
Minimum height in pixels for the top seaweed—prevents the gap from being placed too high on the screen
let maxHeight = height - minHeight - OBSTACLE_GAP;
Maximum height for the top seaweed, calculated to ensure the bottom seaweed fits on screen with at least 100 pixels below it
let topHeight = random(minHeight, maxHeight);
Picks a random height between minHeight and maxHeight for the top seaweed, creating variety in obstacle placement
let bottomY = topHeight + OBSTACLE_GAP;
Calculates where the bottom seaweed starts by adding the fixed gap size to the top height
x: width,
Places the new obstacle at the right edge of the canvas, where it will begin moving leftward
y1: topHeight,
Sets the height of the top seaweed rectangle to the randomly selected topHeight
y2: bottomY,
Sets the y-position where the bottom seaweed begins
passed: false,
Flag to track whether the fish has passed this obstacle for scoring—initially false because the fish hasn't passed it yet
rect(this.x, 0, this.width, this.y1);
Draws the top seaweed as a green rectangle from the top of the canvas down to y1
rect(this.x, this.y2, this.width, height - this.y2);
Draws the bottom seaweed as a green rectangle from y2 down to the bottom of the canvas
this.x -= this.speed;
Moves the obstacle leftward each frame by subtracting OBSTACLE_SPEED (4 pixels by default)
return this.x + this.width < 0;
Returns true if the obstacle has completely moved off the left edge and is no longer visible
obstacles.push(obstacle);
Adds the newly created obstacle to the obstacles array so it will be updated and drawn each frame

checkRectCollision()

checkRectCollision() is a utility function implementing axis-aligned bounding box (AABB) collision detection. It tests whether two rectangles overlap by checking if they don't miss each other on any of the four sides. This is the fastest and most common collision detection technique in game development and powers all the collision checks in this game.

🔬 This function tests if two rectangles touch. What happens if you change < to <= or > to >=? Strictly speaking, these create slightly different behavior at the pixel boundaries—try it to see if collision detection feels tighter or looser.

function checkRectCollision(rect1, rect2) {
  return rect1.x < rect2.x + rect2.width &&
         rect1.x + rect1.width > rect2.x &&
         rect1.y < rect2.y + rect2.height &&
         rect1.y + rect1.height > rect2.y;
}
function checkRectCollision(rect1, rect2) {
  return rect1.x < rect2.x + rect2.width &&
         rect1.x + rect1.width > rect2.x &&
         rect1.y < rect2.y + rect2.height &&
         rect1.y + rect1.height > rect2.y;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Axis-aligned bounding box collision return rect1.x < rect2.x + rect2.width && ...

Tests whether two axis-aligned rectangles overlap using four boundary comparisons

return rect1.x < rect2.x + rect2.width &&
Returns true only if the left edge of rect1 is to the left of the right edge of rect2 (rect1 hasn't passed completely past rect2 to the right)
rect1.x + rect1.width > rect2.x &&
Returns true only if the right edge of rect1 is to the right of the left edge of rect2 (rect1 hasn't passed completely past rect2 to the left)
rect1.y < rect2.y + rect2.height &&
Returns true only if the top edge of rect1 is above the bottom edge of rect2 (rect1 hasn't passed completely below rect2)
rect1.y + rect1.height > rect2.y;
Returns true only if the bottom edge of rect1 is below the top edge of rect2 (rect1 hasn't passed completely above rect2)

drawScore()

drawScore() is a simple UI function called at the end of gameLoop() to display the player's score in the top-left corner. It uses p5.js text functions (fill, textSize, textAlign, text) to render text on the canvas. This is a good example of a utility function that handles one specific piece of the user interface.

function drawScore() {
  fill(0);
  textSize(32);
  textAlign(LEFT, TOP);
  text(`Score: ${score}`, 10, 10);
}
Line-by-line explanation (4 lines)
fill(0);
Sets the fill color to black (RGB: 0, 0, 0) for the text
textSize(32);
Sets the font size to 32 pixels for the score display
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically, so the text starts at the specified x, y coordinates
text(`Score: ${score}`, 10, 10);
Draws the score text using a template literal to insert the current score value, positioned 10 pixels from the left and 10 pixels from the top

drawStartScreen()

drawStartScreen() is called from draw() when gameState === START. It displays the game title and instructions centered on the canvas. This function creates the first impression of the game and teaches the player the controls before they start playing.

function drawStartScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Fish Fly Game", width / 2, height / 2 - 80);
  textSize(24);
  text("Click or press SPACE to jump", width / 2, height / 2 - 20);
  text("Press 'x' to shoot missiles", width / 2, height / 2 + 20);
  text("Click or press SPACE to start", width / 2, height / 2 + 80);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

text-drawing Game title text("Fish Fly Game", width / 2, height / 2 - 80);

Displays the game title in large text at the top-center of the start screen

text-drawing Control instructions text("Click or press SPACE to jump", ...); text("Press 'x' to shoot missiles", ...); text("Click or press SPACE to start", ...);

Displays three lines of instructions telling the player how to control the game and how to start

fill(0);
Sets text color to black
textSize(48);
Sets font size to 48 pixels for the title
textAlign(CENTER, CENTER);
Aligns text to center both horizontally and vertically so coordinates mark the text's center
text("Fish Fly Game", width / 2, height / 2 - 80);
Draws the title 'Fish Fly Game' at the horizontal center and 80 pixels above the vertical center
textSize(24);
Reduces font size to 24 pixels for the instruction text
text("Click or press SPACE to jump", width / 2, height / 2 - 20);
Displays the jump instruction 20 pixels above the vertical center
text("Press 'x' to shoot missiles", width / 2, height / 2 + 20);
Displays the shoot instruction 20 pixels below the vertical center
text("Click or press SPACE to start", width / 2, height / 2 + 80);
Displays the start instruction 80 pixels below the vertical center

drawGameOverScreen()

drawGameOverScreen() is called from draw() when gameState === GAME_OVER. It displays the 'GAME OVER' message, the player's final score, and instructions to restart. This provides clear feedback that the game has ended and gives the player a path forward.

function drawGameOverScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("GAME OVER", width / 2, height / 2 - 50);
  textSize(32);
  text(`Final Score: ${score}`, width / 2, height / 2 + 10);
  textSize(24);
  text("Click or press SPACE to restart", width / 2, height / 2 + 50);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

text-drawing Game over message text("GAME OVER", width / 2, height / 2 - 50);

Displays the 'GAME OVER' message in large text

text-drawing Final score display text(`Final Score: ${score}`, width / 2, height / 2 + 10);

Shows the player's final score using a template literal to insert the score value

text-drawing Restart instruction text("Click or press SPACE to restart", width / 2, height / 2 + 50);

Tells the player how to restart the game

fill(0);
Sets text color to black
textSize(48);
Sets font size to 48 pixels for the 'GAME OVER' text
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically
text("GAME OVER", width / 2, height / 2 - 50);
Displays 'GAME OVER' at the center of the canvas, offset 50 pixels upward
textSize(32);
Reduces font size to 32 pixels for the score
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Displays the final score 10 pixels below the vertical center, inserting the score value into the string
textSize(24);
Reduces font size to 24 pixels for the restart instruction
text("Click or press SPACE to restart", width / 2, height / 2 + 50);
Displays the restart instruction 50 pixels below the vertical center

mousePressed()

mousePressed() is a built-in p5.js function that runs automatically whenever the user clicks the mouse on the canvas. Rather than handling input directly, it delegates to handleInput() to keep game state logic in one place. This is a clean pattern that separates input detection from input response.

function mousePressed() {
  handleInput();
}
Line-by-line explanation (2 lines)
function mousePressed() {
This is a p5.js special function that automatically runs whenever the mouse is clicked anywhere on the canvas
handleInput();
Calls the handleInput() function, which responds to the mouse click based on the current game state

keyPressed()

keyPressed() is a built-in p5.js function that runs whenever any key is pressed. This sketch uses it to detect the spacebar (which calls handleInput() for game state logic) and the 'x' key (which shoots when playing). Returning false prevents the browser's default key behavior, important for spacebar to not scroll the page.

function keyPressed() {
  if (key === ' ' || keyCode === 32) {
    handleInput();
  } else if (key === 'x' || key === 'X') {
    if (gameState === PLAYING) {
      fish.shoot();
    }
  }
  return false;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Spacebar input if (key === ' ' || keyCode === 32) { handleInput(); }

Detects spacebar press and calls handleInput() to process it based on game state

conditional Shoot key input else if (key === 'x' || key === 'X') { if (gameState === PLAYING) { fish.shoot(); } }

Detects 'x' or 'X' key press and makes the fish shoot only when actively playing

function keyPressed() {
This is a built-in p5.js function that automatically runs whenever any key is pressed on the keyboard
if (key === ' ' || keyCode === 32) {
Checks if the pressed key is the spacebar (either checking the key character ' ' or the keyCode 32)
handleInput();
Calls handleInput() to process the spacebar, which will jump if playing, start if on start screen, or restart if game over
} else if (key === 'x' || key === 'X') {
Checks if the pressed key is 'x' or 'X' (lowercase or uppercase)
if (gameState === PLAYING) {
Only allows shooting when the game is actively being played, not on start or game over screens
fish.shoot();
Calls the fish's shoot method to create a new missile
return false;
Returns false to prevent the browser from executing default actions for these keys (e.g., scrolling on spacebar)

handleInput()

handleInput() is the central dispatcher for all player input, routing spacebar and mouse clicks to appropriate actions based on gameState. When on the start screen, it initializes a new game by resetting all variables and transitioning to PLAYING state. During gameplay, it makes the fish jump. On game over, it returns to the start screen. This is a clean pattern that keeps all input response logic in one function.

function handleInput() {
  if (gameState === START) {
    gameState = PLAYING;
    score = 0;
    obstacles = [];
    missiles = [];
    fish.y = height / 2;
    fish.vy = 0;
    lastObstacleSpawnTime = millis();
    lastShotTime = 0;
  } else if (gameState === PLAYING) {
    fish.jump();
  } else if (gameState === GAME_OVER) {
    gameState = START;
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Start state input if (gameState === START) { gameState = PLAYING; score = 0; ... }

Transitions from start screen to playing state and resets all game variables

conditional Playing state input else if (gameState === PLAYING) { fish.jump(); }

Makes the fish jump when spacebar or mouse is pressed during gameplay

conditional Game over state input else if (gameState === GAME_OVER) { gameState = START; }

Transitions from game over screen back to start screen to allow restart

if (gameState === START) {
Checks if the game is on the start screen (showing title and instructions)
gameState = PLAYING;
Changes the game state to PLAYING, which causes draw() to run gameLoop() and start the actual game
score = 0;
Resets the score to 0 for a new game
obstacles = [];
Clears the obstacles array, removing any obstacles from a previous game
missiles = [];
Clears the missiles array, removing any missiles from a previous game
fish.y = height / 2;
Resets the fish's y-position to the center of the canvas
fish.vy = 0;
Resets the fish's vertical velocity to 0, removing any lingering motion from the previous game
lastObstacleSpawnTime = millis();
Records the current time as the start of the obstacle spawn timer, so the first obstacle spawns after OBSTACLE_SPAWN_INTERVAL milliseconds
lastShotTime = 0;
Resets the missile cooldown timer to 0, allowing immediate shooting at game start
} else if (gameState === PLAYING) {
Checks if the game is currently being played
fish.jump();
Calls the fish's jump method, setting its velocity upward
} else if (gameState === GAME_OVER) {
Checks if the game is on the game over screen
gameState = START;
Transitions back to the start screen, allowing the player to read the final score and choose to restart

📦 Key Variables

gameState number

Tracks which screen the game is showing: START (0) for title/instructions, PLAYING (1) for active gameplay, or GAME_OVER (2) for the end screen. Controls which code runs in draw().

let gameState = START;
fish object

The main player character with properties (x, y, vy) and methods (show, update, jump, shoot, hits). Represents the fish's position, velocity, and behavior.

let fish; // Initialized as an object in setup()
obstacles array

Stores all active obstacle objects currently on screen. Each obstacle has position (x, y1, y2), dimensions, and methods to update and draw itself.

let obstacles = [];
missiles array

Stores all active missile objects fired by the player. Each missile has position (x, y), speed, and methods to update and draw itself.

let missiles = [];
score number

Tracks the player's current score, incremented by 1 each time the fish successfully passes an obstacle. Displayed on screen and shown on game over.

let score = 0;
lastObstacleSpawnTime number

Stores the millisecond timestamp of the last obstacle spawned, used to control the spawn rate. Checked against millis() to know when to spawn the next obstacle.

let lastObstacleSpawnTime = 0;
lastShotTime number

Stores the millisecond timestamp of the last missile fired, used to enforce the SHOOT_COOLDOWN between shots. Prevents firing too many missiles per second.

let lastShotTime = 0;
jumpSound p5.Oscillator

A synthesized sound effect played when the fish jumps. Uses a sine wave at 660 Hz frequency.

let jumpSound; // Initialized in setup()
hitSound p5.Oscillator

A synthesized sound effect played when the fish collides with an obstacle or screen edge. Uses a sawtooth wave at 220 Hz frequency for a 'thud'.

let hitSound; // Initialized in setup()
scoreSound p5.Oscillator

A synthesized sound effect played when the player scores by passing an obstacle. Uses a triangle wave at 880 Hz frequency for a clear chime.

let scoreSound; // Initialized in setup()
shootSound p5.Oscillator

A synthesized sound effect played when the player fires a missile. Uses a triangle wave at 1000 Hz frequency for a sharp 'pew' sound.

let shootSound; // Initialized in setup()
explodeSound p5.Oscillator

A synthesized sound effect played when a missile hits and destroys an obstacle. Uses noise for an explosion sound.

let explodeSound; // Initialized in setup()

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG fish.update()

The boundary collision check uses === (exact equality), which might fail at high frame rates or floating-point precision issues. The fish might slip past boundaries.

💡 Use >= and <= instead of === for boundary checks: if (this.y >= height - FISH_SIZE / 2 || this.y <= FISH_SIZE / 2)

PERFORMANCE gameLoop() - missile collision detection

Nested for-loops checking every missile against every obstacle create O(n*m) complexity. With many missiles and obstacles, this becomes slow.

💡 Consider spatial partitioning or quadtrees for large numbers of objects, or limit maximum missiles/obstacles

STYLE Sound initialization in setup()

Five separate oscillator declarations are verbose and repetitive. Creating identical sound objects with different frequencies could be refactored.

💡 Create a helper function createOscillator(frequency, type) that returns a configured oscillator to reduce code duplication

FEATURE handleInput()

No high score tracking across multiple games—the score resets to 0 every time.

💡 Add a highScore variable and compare final score to it on game over, displaying 'NEW HIGH SCORE!' if beaten

BUG fish.shoot()

The cooldown check millis() - lastShotTime might fail if millis() overflows (after ~24 days of continuous runtime), though this is rare in practice.

💡 For robustness, use a frame counter instead: let framesSinceShot = 0; increment it in update(); reset on shoot; check if framesSinceShot > SHOOT_COOLDOWN_FRAMES

STYLE Obstacle and missile objects

Methods like show(), update(), and offscreen() are defined inline for every obstacle/missile created, wasting memory and CPU.

💡 Move these methods to a shared prototype or use ES6 classes: class Obstacle { ... } to reduce memory footprint with many objects

🔄 Code Flow

Code flow showing setup, draw, gameloop, spawnobstacle, checkrectcollision, drawscore, drawstartscreen, drawgameoverscreen, mousepressed, keypressed, handleinput

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] setup --> oscillator-init[oscillator-init] setup --> fish-object-creation[fish-object-creation] click setup href "#fn-setup" click oscillator-init href "#sub-oscillator-init" click fish-object-creation href "#sub-fish-object-creation" draw --> state-dispatch[state-dispatch] state-dispatch -->|gameState === START| drawstartscreen[drawStartScreen] state-dispatch -->|gameState === PLAYING| gameloop[gameLoop] state-dispatch -->|gameState === GAME_OVER| drawgameoverscreen[drawGameOverScreen] click state-dispatch href "#sub-state-dispatch" click drawstartscreen href "#fn-drawstartscreen" click gameloop href "#fn-gameloop" click drawgameoverscreen href "#fn-drawgameoverscreen" gameloop --> fish-update-draw[fish-update-draw] fish-update-draw --> fish-update-method[fish-update-method] fish-update-draw --> fish-show-method[fish-show-method] click fish-update-draw href "#sub-fish-update-draw" click fish-update-method href "#sub-fish-update-method" click fish-show-method href "#sub-fish-show-method" gameloop --> obstacle-spawn-check[obstacle-spawn-check] obstacle-spawn-check -->|if enough time| spawnobstacle[spawnObstacle] click obstacle-spawn-check href "#sub-obstacle-spawn-check" click spawnobstacle href "#fn-spawnobstacle" gameloop --> obstacle-loop[obstacle-loop] obstacle-loop -->|for each obstacle| obstacle-object[obstacle-object] obstacle-loop -->|check collisions| checkrectcollision[checkRectCollision] obstacle-loop -->|award score| drawscore[drawScore] obstacle-loop -->|remove off-screen| drawscore click obstacle-loop href "#sub-obstacle-loop" click obstacle-object href "#sub-obstacle-object" click checkrectcollision href "#fn-checkrectcollision" click drawscore href "#fn-drawscore" gameloop --> missile-loop[missile-loop] missile-loop -->|for each missile| missile-object[missile-object] missile-loop -->|check collisions| nested-obstacle-collision[nested-obstacle-collision] missile-loop -->|remove off-screen| missile-object click missile-loop href "#sub-missile-loop" click missile-object href "#sub-missile-object" click nested-obstacle-collision href "#sub-nested-obstacle-collision" mousepressed[mousePressed] --> handleinput[handleInput] click mousepressed href "#fn-mousepressed" click handleinput href "#fn-handleinput" keypressed[keyPressed] --> spacebar-check[spacebar-check] keypressed --> shoot-key-check[shoot-key-check] click keypressed href "#fn-keypressed" click spacebar-check href "#sub-spacebar-check" click shoot-key-check href "#sub-shoot-key-check" handleinput -->|if gameState === START| start-state-handler[start-state-handler] handleinput -->|if gameState === PLAYING| playing-state-handler[playing-state-handler] handleinput -->|if gameState === GAME_OVER| game-over-state-handler[game-over-state-handler] click start-state-handler href "#sub-start-state-handler" click playing-state-handler href "#sub-playing-state-handler" click game-over-state-handler href "#sub-game-over-state-handler" drawstartscreen --> title-text[title-text] drawstartscreen --> instruction-text[instruction-text] click title-text href "#sub-title-text" click instruction-text href "#sub-instruction-text" drawgameoverscreen --> game-over-text[game-over-text] drawgameoverscreen --> final-score-text[final-score-text] drawgameoverscreen --> restart-instruction[restart-instruction] click game-over-text href "#sub-game-over-text" click final-score-text href "#sub-final-score-text" click restart-instruction href "#sub-restart-instruction"

❓ Frequently Asked Questions

What visual elements can users expect to see in the DIE sketch?

The sketch features an underwater scene with a main fish character navigating through seaweed obstacles, creating a vibrant aquatic environment.

How can users interact with the DIE creative coding sketch?

Users can control the fish by making it jump to avoid obstacles and shoot missiles to clear the path.

What creative coding concepts are demonstrated in the DIE sketch?

The sketch showcases game state management, object-oriented design for fish and obstacles, and sound effects integration using p5.js.

Preview

DIE - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of DIE - Code flow showing setup, draw, gameloop, spawnobstacle, checkrectcollision, drawscore, drawstartscreen, drawgameoverscreen, mousepressed, keypressed, handleinput
Code Flow Diagram