FLYING FISH

Flying Fish is a Flappy Bird-style game where the player controls an orange fish navigating through vertical gaps between green seaweed obstacles. The fish falls due to gravity and jumps when clicked or pressing spacebar, with the goal of passing through as many obstacles as possible without collision.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the obstacles — Obstacles move faster left, making the game harder and more intense—test your reflexes with higher difficulty
  2. Make the gaps tighter — Narrower gaps between top and bottom seaweed require more precise timing and jumps—increases challenge dramatically
  3. Increase gravity to fall faster — Stronger gravity pulls the fish down more aggressively each frame, forcing more frequent jumps and faster gameplay
  4. Change the fish to a red square — Replaces the orange ellipse-based fish drawing with a red rectangle for a completely different visual style
  5. Turn obstacles blue — Changes seaweed color from green to blue for a visual variation—great for testing color schemes
  6. Spawn obstacles faster — Obstacles appear more frequently on screen, creating denser, more challenging gameplay with less breathing room
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a complete, playable Flappy Bird-style game where you control an orange fish swimming through gaps in seaweed obstacles. What makes it visually engaging is the smooth animation of falling fish, scrolling obstacles, and state-driven screens. The code demonstrates three essential p5.js techniques: the draw loop for continuous animation, collision detection using axis-aligned bounding boxes, and oscillator-based sound effects from the p5.sound library.

The sketch is organized around three game states (START, PLAYING, GAME_OVER) that determine what happens each frame. You'll learn how to structure a complete game with player input handling, obstacle generation, score tracking, and audio feedback. By studying this code, you'll understand how professional game logic separates concerns into a fish object, obstacle objects, and state management functions.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 1366×768 canvas and initializes a fish object at the center of the screen with three methods: show() to draw it, update() to apply gravity, and hits() to detect collisions. Three oscillator sounds are created for jump, hit, and score events. The game starts in the START state.
  2. The draw() function runs 60 times per second and calls one of three screen functions based on gameState: drawStartScreen() prompts the player, gameLoop() runs active gameplay, or drawGameOverScreen() shows the final score.
  3. When the player clicks or presses spacebar during START, handleInput() moves to PLAYING state and resets the fish position and obstacle list.
  4. During gameplay, gameLoop() updates the fish's position (applying gravity), spawns new obstacles at regular intervals using timestamps, moves each obstacle left, and checks collisions between the fish and every obstacle.
  5. Collision detection uses checkRectCollision() which compares the fish's bounding box against both the top and bottom seaweed rectangles of each obstacle.
  6. When the fish passes an obstacle without hitting it, the score increments and a chime sound plays. If the fish hits anything or reaches the canvas edge, the game ends and returns to the START state on any input.

🎓 Concepts You'll Learn

Game state managementGravity and physicsCollision detection (AABB)Object-oriented designEvent handling (mouse and keyboard)Audio synthesis with oscillatorsAnimation loopDynamic obstacle generation

📝 Code Breakdown

setup()

setup() runs once at sketch startup. Here it initializes the canvas, creates the fish object with all its methods (which p5.js calls when fish.show() and fish.update() are invoked), and sets up the sound oscillators. Notice the fish object uses 'this' to reference its own properties—a key pattern in object-oriented game programming.

🔬 The fish is drawn with an ellipse body and triangle tail. What happens if you change FISH_SIZE * 1.5 to FISH_SIZE * 3 in the ellipse? What about the tail positions?

    // Main body of the fish (ellipse)
      ellipse(this.x, this.y, FISH_SIZE * 1.5, FISH_SIZE);
      // Tail of the fish (triangle)
      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);
function setup() {
  createCanvas(1366, 768); // Match user's screen size
  background(0, 191, 255); // Initial underwater blue background

  // Initialize fish object
  fish = {
    x: width / 4, // X-position of the fish (quarter way across the screen)
    y: height / 2, // Initial Y-position (center of the screen)
    vy: 0,        // Vertical velocity of the fish

    // Method to draw the fish
    show: function() {
      fill(255, 165, 0); // Orange color for the fish
      noStroke();
      // Main body of the fish (ellipse)
      ellipse(this.x, this.y, FISH_SIZE * 1.5, FISH_SIZE);
      // Tail of the fish (triangle)
      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);
    },

    // Method to update the fish's position and velocity
    update: function() {
      this.vy += GRAVITY; // Apply gravity to vertical velocity
      this.y += this.vy;  // Update y-position based on velocity

      // Constrain fish to stay within the canvas height
      this.y = constrain(this.y, FISH_SIZE / 2, height - FISH_SIZE / 2);

      // Check if fish hits the top or bottom of the screen
      if (this.y === FISH_SIZE / 2 || this.y === height - FISH_SIZE / 2) {
        hitSound.amp(0.5, 0.1); // Play hit sound
        setTimeout(() => hitSound.amp(0, 0.1), 100); // Fade out sound
        gameState = GAME_OVER; // End the game
      }
    },

    // Method to make the fish jump
    jump: function() {
      this.vy = JUMP_FORCE; // Set negative velocity to move upwards
      jumpSound.amp(0.5, 0.1); // Play jump sound
      setTimeout(() => jumpSound.amp(0, 0.1), 100); // Fade out sound
    },

    // Method to check for collision with an obstacle
    hits: function(obstacle) {
      // Define the fish's bounding box for collision detection
      let fishRect = {
        x: this.x - FISH_SIZE * 0.75, // Left edge of the fish's body
        y: this.y - FISH_SIZE / 2,   // Top edge of the fish's body
        width: FISH_SIZE * 1.5,
        height: FISH_SIZE
      };

      // Define the top obstacle's bounding box
      let obstacleTopRect = {
        x: obstacle.x,
        y: 0,
        width: obstacle.width,
        height: obstacle.y1 // y1 is the height of the top obstacle
      };
      if (checkRectCollision(fishRect, obstacleTopRect)) {
        return true; // Collision with top obstacle
      }

      // Define the bottom obstacle's bounding box
      let obstacleBottomRect = {
        x: obstacle.x,
        y: obstacle.y2, // y2 is the y-position of the bottom obstacle
        width: obstacle.width,
        height: height - obstacle.y2 // Height from y2 to the bottom of the canvas
      };
      if (checkRectCollision(fishRect, obstacleBottomRect)) {
        return true; // Collision with bottom obstacle
      }

      return false; // No collision
    }
  };

  // Create simple oscillator sound effects using p5.sound
  // These will be used for jump, hit, and score events.
  jumpSound = new p5.Oscillator();
  jumpSound.setType('sine');  // Sine wave for a smooth jump sound
  jumpSound.freq(660);        // Higher frequency for a distinct jump
  jumpSound.amp(0);           // Start silent
  jumpSound.start();          // Start the oscillator

  hitSound = new p5.Oscillator();
  hitSound.setType('sawtooth'); // Sawtooth wave for a harsher hit sound
  hitSound.freq(220);           // Lower frequency for a 'thud'
  hitSound.amp(0);
  hitSound.start();

  scoreSound = new p5.Oscillator();
  scoreSound.setType('triangle'); // Triangle wave for a pleasant score chime
  scoreSound.freq(880);           // High frequency for a clear chime
  scoreSound.amp(0);
  scoreSound.start();

  userStartAudio(); // Enable audio, required for web audio to work in browsers
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

object-creation Fish Object Initialization fish = { x: width / 4, y: height / 2, vy: 0, ... }

Creates a fish object with position, velocity, and methods for drawing, updating, jumping, and collision detection

loop Sound Oscillator Setup jumpSound = new p5.Oscillator(); jumpSound.setType('sine'); jumpSound.freq(660);

Initializes three p5.Oscillator objects with different wave types and frequencies for distinct audio feedback

createCanvas(1366, 768);
Creates a 1366×768 pixel canvas—a widescreen aspect ratio that gives plenty of space for obstacles and scrolling motion
background(0, 191, 255);
Sets the initial background to a sky-blue color (RGB: 0, 191, 255), creating an underwater or sky theme
fish = {
Begins creation of the fish object that will hold the fish's position, velocity, and all its behavior methods
x: width / 4,
Places the fish one-quarter of the way across the screen horizontally, leaving room for obstacles to approach from the right
y: height / 2,
Starts the fish at the vertical center of the canvas—a neutral starting position before gravity pulls it down
vy: 0,
Initializes vertical velocity to 0—the fish is not moving up or down when the game starts
ellipse(this.x, this.y, FISH_SIZE * 1.5, FISH_SIZE);
Draws the fish's main body as an orange ellipse, 1.5 times wider than it is tall to create a fish-like shape
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 a triangular tail on the left side of the fish, pointing backward—the three points create a classic fish silhouette
this.vy += GRAVITY;
Adds the GRAVITY constant to the fish's vertical velocity each frame, making it accelerate downward realistically
this.y += this.vy;
Updates the fish's y position by adding its velocity—higher velocity means bigger downward movement each frame
this.y = constrain(this.y, FISH_SIZE / 2, height - FISH_SIZE / 2);
Clamps the fish's y position between upper and lower bounds so it cannot move outside the canvas vertically
if (this.y === FISH_SIZE / 2 || this.y === height - FISH_SIZE / 2) {
Checks if the fish has reached either the top or bottom edge—if true, the fish has hit a boundary and the game ends
jumpSound = new p5.Oscillator();
Creates a new oscillator object that will be connected to the speaker to produce sounds when the fish jumps
jumpSound.setType('sine');
Sets the oscillator to use a sine wave, which produces a smooth, pure tone ideal for a jump sound effect
jumpSound.freq(660);
Sets the oscillator frequency to 660 Hz, a high-pitched tone that signals the jump action to the player's ear
userStartAudio();
Enables web audio in the browser—required because modern browsers require user interaction before audio can play

draw()

draw() is p5.js's main loop function, called 60 times per second. This sketch's entire flow—screens, gameplay, collisions, scoring—all revolves around which gameState branch executes. This state-machine pattern is fundamental to game architecture.

function draw() {
  background(0, 191, 255); // Clear background each frame to create animation

  // Game state management
  if (gameState === START) {
    drawStartScreen();
  } else if (gameState === PLAYING) {
    gameLoop();
  } else if (gameState === GAME_OVER) {
    drawGameOverScreen();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Game State Router if (gameState === START) { ... } else if (gameState === PLAYING) { ... } else if (gameState === GAME_OVER) { ... }

Routes execution to different functions based on the current game state, directing the entire flow of the game

background(0, 191, 255);
Clears the entire canvas with the sky-blue color every frame—this erases the previous frame so motion appears smooth instead of leaving trails
if (gameState === START) {
Checks if the game is in the START state—if true, shows the title and instructions instead of running gameplay
gameLoop();
When gameState is PLAYING, calls the gameLoop() function which updates fish position, spawns obstacles, checks collisions, and increments score
drawGameOverScreen();
When gameState is GAME_OVER, displays the game over message and final score, waiting for the player to restart

gameLoop()

gameLoop() is the core gameplay function called every frame when gameState is PLAYING. It orchestrates the entire game turn: updating the fish, spawning obstacles on a timer, updating and drawing each obstacle, checking collisions, awarding points, and cleaning up old obstacles. The backward loop is a classic pattern when removing items from an array during iteration.

🔬 This collision check ends the game immediately when fish.hits() returns true. What happens if you remove the gameState = GAME_OVER line? Could the fish pass through obstacles?

    // Check for collision with the fish
    if (fish.hits(obstacle)) {
      hitSound.amp(0.5, 0.1); // Play hit sound
      setTimeout(() => hitSound.amp(0, 0.1), 100); // Fade out sound
      gameState = GAME_OVER; // End the game
    }

🔬 The score only increments when obstacle.x + obstacle.width < fish.x. What happens if you change < to > so the fish scores BEFORE passing the obstacle?

    // Score if fish passes the obstacle and hasn't been scored yet
    if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {
      score++;
      obstacle.passed = true; // Mark obstacle as passed to avoid re-scoring
function gameLoop() {
  fish.update(); // Update fish's position and velocity
  fish.show();   // Draw the fish

  // Generate new obstacles at regular intervals
  if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) {
    spawnObstacle();
    lastObstacleSpawnTime = millis();
  }

  // Update, show, and check collision for each obstacle
  for (let i = obstacles.length - 1; i >= 0; i--) {
    let obstacle = obstacles[i];
    obstacle.update(); // Move obstacle to the left
    obstacle.show();   // Draw the obstacle

    // Check for collision with the fish
    if (fish.hits(obstacle)) {
      hitSound.amp(0.5, 0.1); // Play hit sound
      setTimeout(() => hitSound.amp(0, 0.1), 100); // Fade out sound
      gameState = GAME_OVER; // End the game
    }

    // Score if fish passes the obstacle and hasn't been scored yet
    if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {
      score++;
      obstacle.passed = true; // Mark obstacle as passed to avoid re-scoring
      scoreSound.amp(0.5, 0.1); // Play score sound
      setTimeout(() => scoreSound.amp(0, 0.1), 100); // Fade out sound
    }

    // Remove off-screen obstacles to keep the array clean and save memory
    if (obstacle.offscreen()) {
      obstacles.splice(i, 1);
    }
  }

  drawScore(); // Display the current score
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Obstacle Spawn Timer if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) {

Checks elapsed time since last obstacle and spawns a new one at regular intervals using millis() timestamps

for-loop Obstacle Update and Collision Loop for (let i = obstacles.length - 1; i >= 0; i--) {

Iterates through all obstacles backward (to safely remove items) to update positions, draw them, check collisions, and clean up off-screen obstacles

conditional Collision Detection if (fish.hits(obstacle)) {

Calls the fish's collision detection method to test if it has hit this obstacle, ending the game if true

conditional Score Increment Logic if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {

Awards one point when the fish's x position passes the right edge of an obstacle, using a flag to prevent duplicate scoring

fish.update();
Calls the fish object's update method, which applies gravity, updates position, checks boundaries, and potentially ends the game
fish.show();
Calls the fish object's show method, which draws the orange ellipse body and triangle tail at the current position
if (millis() - lastObstacleSpawnTime > OBSTACLE_SPAWN_INTERVAL) {
Compares the elapsed time in milliseconds since the last obstacle was spawned against the interval—spawns a new obstacle only when enough time has passed
lastObstacleSpawnTime = millis();
Records the current time in milliseconds, resetting the timer so the next obstacle won't spawn until OBSTACLE_SPAWN_INTERVAL milliseconds later
for (let i = obstacles.length - 1; i >= 0; i--) {
Loops backward through the obstacles array (from last to first)—this lets us safely remove items with splice() without skipping any
obstacle.update();
Moves this obstacle left by subtracting OBSTACLE_SPEED from its x position each frame
obstacle.show();
Draws the top and bottom green rectangles of the obstacle at its current position
if (fish.hits(obstacle)) {
Calls the fish's hits() method to check if this obstacle's rectangles overlap with the fish's bounding box
gameState = GAME_OVER;
When a collision is detected, immediately changes gameState to GAME_OVER, which stops gameLoop() and shows the game-over screen next frame
if (!obstacle.passed && obstacle.x + obstacle.width < fish.x) {
Checks two conditions: the obstacle hasn't been scored yet (!obstacle.passed) AND the obstacle's right edge is to the left of the fish's x position, meaning the fish has passed it
score++;
Increments the score by 1 point when the fish successfully passes an obstacle
obstacle.passed = true;
Sets the passed flag to true so this obstacle will never award points again, even if the loop checks it multiple frames
if (obstacle.offscreen()) {
Checks if the entire obstacle has left the left side of the canvas using the obstacle's offscreen() method
obstacles.splice(i, 1);
Removes this obstacle from the array to prevent checking it in future frames and to save memory

spawnObstacle()

spawnObstacle() creates a new obstacle object and adds it to the obstacles array. Each obstacle is a mini-object with its own show(), update(), and offscreen() methods. The height calculation uses random() to vary gap positions, creating challenge and replayability. Notice how minHeight and maxHeight constrain the randomness to keep the game playable.

🔬 These four lines calculate obstacle gap positions using random(). What happens if you change topHeight = random(minHeight, maxHeight) to topHeight = height / 2 - OBSTACLE_GAP / 2? Where would every gap appear?

  let minHeight = 100; // Minimum height for top or bottom seaweed
  let maxHeight = height - minHeight - OBSTACLE_GAP; // Max height for top seaweed to leave room for gap
  let topHeight = random(minHeight, maxHeight); // Random height for the top seaweed
  let bottomY = topHeight + OBSTACLE_GAP;
function spawnObstacle() {
  let minHeight = 100; // Minimum height for top or bottom seaweed
  let maxHeight = height - minHeight - OBSTACLE_GAP; // Max height for top seaweed to leave room for gap
  let topHeight = random(minHeight, maxHeight); // Random height for the top seaweed
  let bottomY = topHeight + OBSTACLE_GAP;       // Y-position where the bottom seaweed starts

  let obstacle = {
    x: width,           // Start obstacle at the right edge of the canvas
    y1: topHeight,      // Height of the top seaweed
    y2: bottomY,        // Y-position of the bottom seaweed
    width: OBSTACLE_WIDTH,
    speed: OBSTACLE_SPEED,
    passed: false,      // Flag to track if the fish has passed this obstacle for scoring

    // Method to draw the obstacle
    show: function() {
      fill(0, 128, 0); // Green color for seaweed
      noStroke();
      // Top seaweed segment
      rect(this.x, 0, this.width, this.y1);
      // Bottom seaweed segment
      rect(this.x, this.y2, this.width, height - this.y2);
    },

    // Method to update the obstacle's position
    update: function() {
      this.x -= this.speed; // Move obstacle to the left
    },

    // Method to check if the obstacle is off-screen
    offscreen: function() {
      return this.x + this.width < 0; // True if the entire obstacle is off the left side
    }
  };
  obstacles.push(obstacle); // Add the new obstacle to the array
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Random Gap Positioning let topHeight = random(minHeight, maxHeight);

Generates a random height for each obstacle's top seaweed while ensuring room for the gap and safe play space

object-creation Obstacle Object let obstacle = { x: width, y1: topHeight, ... show, update, offscreen }

Creates an obstacle object with position, size, and three methods for drawing, movement, and screen boundary detection

let minHeight = 100;
Sets the minimum height for top seaweed, ensuring there's always visible seaweed and safe space at the top of the canvas
let maxHeight = height - minHeight - OBSTACLE_GAP;
Calculates the maximum possible top-seaweed height—this leaves room for both the gap and minimum bottom-seaweed space
let topHeight = random(minHeight, maxHeight);
Generates a random value between minHeight and maxHeight for this obstacle's top seaweed, making each gap a different height
let bottomY = topHeight + OBSTACLE_GAP;
Calculates where the bottom seaweed starts by adding the gap size to the top seaweed height
x: width,
Positions the new obstacle at the right edge of the canvas so it scrolls into view from the right
rect(this.x, 0, this.width, this.y1);
Draws a green rectangle from the top of the canvas down to y1, representing the top seaweed segment
rect(this.x, this.y2, this.width, height - this.y2);
Draws a green rectangle from y2 down to the bottom of the canvas, representing the bottom seaweed segment
this.x -= this.speed;
Subtracts the obstacle's speed from its x position each frame, moving it leftward across the canvas
return this.x + this.width < 0;
Returns true if the obstacle's right edge has passed the left side of the canvas (x position + width is less than 0)
obstacles.push(obstacle);
Adds the newly created obstacle object to the obstacles array so gameLoop() will update and draw it next frame

checkRectCollision(rect1, rect2)

checkRectCollision() implements axis-aligned bounding box (AABB) collision detection, the gold standard for simple 2D games. It tests if two axis-aligned rectangles overlap by checking all four sides—if any side doesn't overlap, the rectangles don't collide. This function is called by the fish's hits() method to check both top and bottom seaweed.

🔬 This function uses four && conditions to check overlap in both dimensions. What if you change one of the && to || (OR)? Would the collision detection become stricter 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 (5 lines)

🔧 Subcomponents:

conditional AABB Collision Test 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;

Axis-aligned bounding box collision: returns true if rectangles overlap in both x and y dimensions

rect1.x < rect2.x + rect2.width
Checks if rect1's left edge is to the left of rect2's right edge—if false, rect1 is completely to the right
rect1.x + rect1.width > rect2.x
Checks if rect1's right edge is to the right of rect2's left edge—if false, rect1 is completely to the left
rect1.y < rect2.y + rect2.height
Checks if rect1's top edge is above rect2's bottom edge—if false, rect1 is completely below
rect1.y + rect1.height > rect2.y
Checks if rect1's bottom edge is below rect2's top edge—if false, rect1 is completely above
return ... && ... && ... && ...
Returns true only if ALL four conditions are true—meaning the rectangles must overlap horizontally AND vertically to collide

handleInput()

handleInput() is the central state-transition controller. It responds to player input by changing gameState, which determines what happens in draw() next frame. Notice how it resets all the game state variables (score, obstacles, fish properties, timer) when starting a new game—this is critical to prevent bugs from leftover state.

🔬 This block resets everything when the game starts. What happens if you remove the obstacles = [] line? Would old obstacles from the previous game still be visible?

    // If on start screen, start the game
    gameState = PLAYING;
    score = 0;
    obstacles = []; // Clear any previous obstacles
    fish.y = height / 2; // Reset fish position
    fish.vy = 0;        // Reset fish velocity
    lastObstacleSpawnTime = millis(); // Reset obstacle spawn timer
function handleInput() {
  if (gameState === START) {
    // If on start screen, start the game
    gameState = PLAYING;
    score = 0;
    obstacles = []; // Clear any previous obstacles
    fish.y = height / 2; // Reset fish position
    fish.vy = 0;        // Reset fish velocity
    lastObstacleSpawnTime = millis(); // Reset obstacle spawn timer
  } else if (gameState === PLAYING) {
    // If playing, make the fish jump
    fish.jump();
  } else if (gameState === GAME_OVER) {
    // If game over, go back to the start screen to allow restarting
    gameState = START;
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Game Start and Reset if (gameState === START) { gameState = PLAYING; score = 0; obstacles = []; ... }

Transitions from START to PLAYING, resetting all game state (score, obstacles, fish position, timers)

conditional Jump During Play } else if (gameState === PLAYING) { fish.jump(); }

Makes the fish jump when the player provides input during active gameplay

conditional Restart Transition } else if (gameState === GAME_OVER) { gameState = START; }

Returns to the START state from GAME_OVER, allowing the player to restart

if (gameState === START) {
Checks if the game is currently on the start screen waiting for player input
gameState = PLAYING;
Changes the game state to PLAYING so the next frame, draw() will call gameLoop() instead of drawStartScreen()
score = 0;
Resets the score to 0 for a fresh game
obstacles = [];
Clears the obstacles array, removing all obstacles from the previous game so they don't appear in the new game
fish.y = height / 2;
Resets the fish's y position to the center of the screen so it starts from a neutral position
fish.vy = 0;
Resets the fish's velocity to 0 so it starts with no momentum before gravity begins pulling it down
lastObstacleSpawnTime = millis();
Records the current time as the starting point for the obstacle spawn timer so the first obstacle spawns at the correct interval
fish.jump();
Calls the fish's jump() method, which sets its velocity to JUMP_FORCE and plays the jump sound
gameState = START;
Returns to the START state, which will show the start screen again for the player to begin another round

mousePressed()

mousePressed() is a p5.js event handler automatically called whenever the mouse is clicked. This sketch delegates to handleInput() to keep the input logic centralized—the same function handles both mouse and keyboard input.

function mousePressed() {
  handleInput();
}
Line-by-line explanation (1 lines)
handleInput();
Calls the handleInput() function, which responds to the mouse click by starting the game, making the fish jump, or restarting after game over

keyPressed()

keyPressed() is p5.js's keyboard event handler, called whenever any key is pressed. This sketch listens specifically for the spacebar to match the mouse-click input. The return false statement is crucial—it prevents the browser's default scrolling behavior when spacebar is pressed.

function keyPressed() {
  if (key === ' ' || keyCode === 32) { // Check for spacebar
    handleInput();
  }
  return false; // Prevent default browser behavior (e.g., scrolling with spacebar)
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Spacebar Detection if (key === ' ' || keyCode === 32) {

Checks if the pressed key is spacebar using two methods (the 'key' variable and keyCode constant) for maximum compatibility

if (key === ' ' || keyCode === 32) {
Checks if the pressed key is a spacebar—the condition uses two checks because different browsers may expose it differently
handleInput();
Calls handleInput() to respond to the spacebar press with the same logic as a mouse click
return false;
Prevents p5.js from passing the spacebar event to the browser, which would otherwise scroll the page

drawStartScreen()

drawStartScreen() displays the title and instructions when gameState === START. It uses textAlign(CENTER, CENTER) to position text from its center point rather than its top-left, making layout calculations simpler. The two different textSize() calls create visual hierarchy.

function drawStartScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Fish Fly Game", width / 2, height / 2 - 50);
  textSize(24);
  text("Click or press SPACE to jump", width / 2, height / 2 + 10);
  text("Click or press SPACE to start", width / 2, height / 2 + 50);
}
Line-by-line explanation (7 lines)
fill(0);
Sets the text color to black (RGB: 0, 0, 0) for all following text
textSize(48);
Sets the font size to 48 pixels for the title text
textAlign(CENTER, CENTER);
Centers all text both horizontally and vertically around the coordinates used in text() calls
text("Fish Fly Game", width / 2, height / 2 - 50);
Draws the game title at the center-top of the screen (x = center, y = center minus 50 pixels)
textSize(24);
Reduces the font size to 24 pixels for the instruction text that follows
text("Click or press SPACE to jump", width / 2, height / 2 + 10);
Displays instructions slightly below center about how to make the fish jump
text("Click or press SPACE to start", width / 2, height / 2 + 50);
Displays instructions on how to start the game below the previous instruction text

drawGameOverScreen()

drawGameOverScreen() displays the game-over state with the final score. Notice the template literal syntax ${score}—this is a clean way to embed variables into strings. The three different textSize() calls create visual hierarchy: heading (48), score (32), instructions (24).

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 (6 lines)
textSize(48);
Sets font size to 48 pixels for the large GAME OVER heading
text("GAME OVER", width / 2, height / 2 - 50);
Displays the game-over message at the upper-center of the screen
textSize(32);
Changes font size to 32 pixels for the score display
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Uses a template literal with ${score} to dynamically insert the player's final score into the text
textSize(24);
Reduces font size to 24 pixels for the restart instructions
text("Click or press SPACE to restart", width / 2, height / 2 + 50);
Displays instructions for restarting the game at the lower-center of the screen

drawScore()

drawScore() displays the running score in the top-left corner during gameplay. It's called once per frame from gameLoop(), always keeping the current score visible. The textAlign(LEFT, TOP) choice makes positioning at (10, 10) intuitive.

function drawScore() {
  fill(0); // Black color for text
  textSize(32);
  textAlign(LEFT, TOP);
  text(`Score: ${score}`, 10, 10);
}
Line-by-line explanation (4 lines)
fill(0);
Sets the text color to black so the score is visible against the light blue background
textSize(32);
Sets a readable 32-pixel font size for the in-game score display
textAlign(LEFT, TOP);
Aligns text from its top-left corner, making it easy to position at the screen's top-left corner without calculation
text(`Score: ${score}`, 10, 10);
Displays 'Score: [current score]' at pixel position (10, 10) near the top-left corner of the screen

📦 Key Variables

gameState number

Tracks the current game state (START, PLAYING, or GAME_OVER) to determine which screen and logic runs each frame

let gameState = START;
fish object

Stores the fish's position (x, y), velocity (vy), and methods for drawing (show), updating (update), jumping (jump), and collision detection (hits)

let fish = { x: 0, y: 0, vy: 0, show() {...}, update() {...}, jump() {...}, hits() {...} }
obstacles array

Holds all active obstacle objects currently on screen; each obstacle has position, dimensions, methods to draw and update, and a passed flag for scoring

let obstacles = [];
score number

Tracks the number of obstacles the player has successfully passed, incremented each time the fish clears a gap

let score = 0;
lastObstacleSpawnTime number

Records the timestamp (in milliseconds) of the last obstacle spawn, used to control spawn frequency via OBSTACLE_SPAWN_INTERVAL

let lastObstacleSpawnTime = 0;
jumpSound object

A p5.Oscillator object that produces a high-frequency sine wave (660 Hz) when the fish jumps

let jumpSound = new p5.Oscillator();
hitSound object

A p5.Oscillator object that produces a low-frequency sawtooth wave (220 Hz) when the fish collides with an obstacle or boundary

let hitSound = new p5.Oscillator();
scoreSound object

A p5.Oscillator object that produces a high-frequency triangle wave (880 Hz) when the fish successfully passes an obstacle

let scoreSound = new p5.Oscillator();

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG fish.update() - boundary collision detection

Using === to compare a floating-point y position with exact boundary values is unreliable due to floating-point precision errors. The fish may pass the boundary before the collision is detected.

💡 Replace the exact equality check with a range check: if (this.y <= FISH_SIZE / 2 || this.y >= height - FISH_SIZE / 2) to catch collisions reliably.

PERFORMANCE gameLoop() - sound effects

Each collision or event creates a new setTimeout() callback. If the player dies repeatedly, these callbacks accumulate in the event queue, potentially causing memory leaks and delayed audio.

💡 Use a debounce mechanism or store the timestamp of the last sound so sounds only play if enough time has elapsed since the previous one, preventing rapid event spam.

STYLE spawnObstacle()

The obstacle object is created inline with all methods defined inside it. This is readable but creates a new object definition every spawn, which is inefficient for a repeatedly-called function.

💡 Define the obstacle structure as a class (e.g., class Obstacle { ... }) and instantiate it in spawnObstacle(), making the code more maintainable and slightly more efficient.

FEATURE handleInput() and draw()

The game has no difficulty progression—the game speed and gap size remain constant no matter how high the score. Players hit a difficulty ceiling quickly.

💡 Increase OBSTACLE_SPEED or decrease OBSTACLE_GAP based on score: e.g., if (score > 0 && score % 5 === 0) { OBSTACLE_SPEED += 0.2; } to create escalating difficulty as the player improves.

BUG gameLoop() - obstacle collision loop

The loop iterates backward (i--) to safely remove obstacles, which is correct, but the comment suggests removing off-screen obstacles is for memory cleanup. In a real long-play session with hundreds of obstacles, the array could still grow large if new obstacles are spawned faster than old ones are cleaned up.

💡 Monitor the obstacles array length during development; if it grows unbounded, double-check that offscreen() is correctly detecting all obstacles past the left edge, or add a hard limit on array size as a safety valve.

🔄 Code Flow

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

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

graph TD start[Start] --> setup[setup] setup --> fishobjectinit[Fish Object Initialization] setup --> oscillatorcreation[Sound Oscillator Setup] setup --> draw[draw loop] click setup href "#fn-setup" click fishobjectinit href "#sub-fish-object-init" click oscillatorcreation href "#sub-oscillator-creation" draw --> statebranch[Game State Router] statebranch -->|START| drawstartscreen[drawStartScreen] statebranch -->|PLAYING| gameloop[gameLoop] statebranch -->|GAME_OVER| drawgameoverscreen[drawGameOverScreen] click draw href "#fn-draw" click statebranch href "#sub-state-branch" click drawstartscreen href "#fn-drawstartscreen" click drawgameoverscreen href "#fn-drawgameoverscreen" gameloop --> spawncheck[Obstacle Spawn Timer] spawncheck -->|spawn| spawobstacle[spawnObstacle] spawobstacle --> heightcalc[Random Gap Positioning] heightcalc --> obstacleobj[Obstacle Object] obstacleobj -->|add to array| gameloop gameloop --> obstacleloop[Obstacle Update and Collision Loop] obstacleloop -->|for each obstacle| collisioncheck[Collision Detection] collisioncheck -->|if hit| drawgameoverscreen collisioncheck --> scorecheck[Score Increment Logic] scorecheck -->|if scored| gameloop click spawobstacle href "#fn-spawobstacle" click heightcalc href "#sub-height-calc" click obstacleobj href "#sub-obstacle-obj" click obstacleloop href "#sub-obstacle-loop" click collisioncheck href "#sub-collision-check" click scorecheck href "#sub-score-check" draw --> drawscore[drawScore] drawscore --> draw draw --> handleinput[handleInput] handleinput --> startreset[Game Start and Reset] startreset -->|reset| gameloop click handleinput href "#fn-handleinput" click startreset href "#sub-start-reset" mousepressed[mousePressed] --> handleinput click mousepressed href "#fn-mousepressed" keypressed[keyPressed] --> spacebarcheck[Spacebar Detection] spacebarcheck -->|if space| jumpaction[Jump During Play] click keypressed href "#fn-keypressed" click spacebarcheck href "#sub-spacebar-check" click jumpaction href "#sub-jump-action" drawgameoverscreen --> restarttransition[Restart Transition] restarttransition --> startreset click restarttransition href "#sub-restart-transition"

❓ Frequently Asked Questions

What visual elements are present in the FLYING FISH sketch?

The sketch features a vibrant underwater scene with a colorful fish represented by an orange ellipse and a triangle tail, along with dynamic seaweed obstacles.

How can users engage with the FLYING FISH sketch while playing?

Users can interact by pressing a key to make the fish jump, navigating through seaweed obstacles to score points and avoid game over.

What creative coding concepts are showcased in the FLYING FISH sketch?

The sketch demonstrates game state management, object-oriented programming for the fish, and the use of sound effects with p5.js to enhance interactivity.

Preview

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