AI Bubble Pop - Satisfying Click Game Pop colorful bubbles as they float up! Click bubbles to burst

This sketch creates an interactive bubble pop game where colorful bubbles float upward from the bottom of the screen. Players click bubbles to burst them, triggering particle effects and satisfying pop sounds that vary in pitch based on bubble size. The game tracks score and continuously spawns new bubbles.

๐ŸŽ“ Concepts You'll Learn

Animation loopObject-oriented programming with classesCollision detectionParticle systemsSound synthesis with p5.soundVector mathematicsColor modes (HSB)User input handlingCanvas resizingPhysics simulation

๐Ÿ”„ Code Flow

Code flow showing setup, draw, windowresized, bubble, particle, ensureaudiostarted, playpopsound, createparticleburst, mousepressed, touchstarted, handlepop, drawscore

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Creation] setup --> color-mode-setup[Color Mode Configuration] setup --> sound-setup[Audio Synthesis Setup] setup --> bubble-initialization[Initial Bubble Creation] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click color-mode-setup href "#sub-color-mode-setup" click sound-setup href "#sub-sound-setup" click bubble-initialization href "#sub-bubble-initialization" draw --> background-draw[Background Rendering] draw --> bubble-loop[Bubble Update and Display] draw --> particle-loop[Particle Update and Cleanup] draw --> score-display[Score Rendering] click draw href "#fn-draw" click background-draw href "#sub-background-draw" click bubble-loop href "#sub-bubble-loop" click particle-loop href "#sub-particle-loop" click score-display href "#sub-score-display" bubble-loop --> bubble-constructor[Constructor] bubble-constructor --> bubble-reset[Reset Method] bubble-reset --> bubble-update[Update Method] bubble-update --> bubble-show[Show Method] bubble-loop --> bubble-contains[Contains Method] bubble-loop --> bubble-pop[Pop Method] click bubble-constructor href "#sub-bubble-constructor" click bubble-reset href "#sub-bubble-reset" click bubble-update href "#sub-bubble-update" click bubble-show href "#sub-bubble-show" click bubble-contains href "#sub-bubble-contains" click bubble-pop href "#sub-bubble-pop" particle-loop --> particle-constructor[Constructor] particle-constructor --> particle-update[Update Method] particle-update --> particle-isdead[IsDead Method] particle-update --> particle-show[Show Method] particle-loop --> particle-creation-loop[Particle Creation Loop] particle-creation-loop --> particle-count[Particle Count] click particle-constructor href "#sub-particle-constructor" click particle-update href "#sub-particle-update" click particle-isdead href "#sub-particle-isdead" click particle-show href "#sub-particle-show" click particle-creation-loop href "#sub-particle-creation-loop" click particle-count href "#sub-particle-count" windowresized[windowResized] --> draw click windowresized href "#fn-windowresized" mousepressed[mousePressed] --> handlepop[handlePop] click mousepressed href "#fn-mousepressed" click handlepop href "#fn-handlepop" touchstarted[touchStarted] --> handlepop click touchstarted href "#fn-touchstarted" ensureaudiostarted[ensureAudioStarted] --> audio-check[Audio Context Check] audio-check --> audio-init[Audio Initialization] audio-init --> draw click ensureaudiostarted href "#fn-ensureaudiostarted" click audio-check href "#sub-audio-check" click audio-init href "#sub-audio-init" playpopsound[playPopSound] --> frequency-mapping[Frequency Calculation] frequency-mapping --> sound-trigger[Sound Playback] click playpopsound href "#fn-playpopsound" click frequency-mapping href "#sub-frequency-mapping" click sound-trigger href "#sub-sound-trigger" createparticleburst[createParticleBurst] --> particle-count createparticleburst --> particle-creation-loop click createparticleburst href "#fn-createparticleburst" drawscore[drawScore] --> text-styling[Text Styling] click drawscore href "#fn-drawscore" click text-styling href "#sub-text-styling"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, sets up the color mode, configures the sound system, and creates the initial bubbles. The oscillator is started but silent until the envelope is triggered by a bubble pop.

function setup() {
  createCanvas(windowWidth, windowHeight); // 2D canvas

  // Use HSB for nice rainbow colors
  colorMode(HSB, 360, 100, 100, 255);
  noStroke();
  textFont('system-ui');
  textSize(24);

  // Setup pop sound: one oscillator + envelope reused for all pops
  popOsc = new p5.Oscillator('sine');
  popEnv = new p5.Envelope();
  // Fast, percussive pop: short attack/decay
  popEnv.setADSR(0.001, 0.1, 0.0, 0.1);
  popEnv.setRange(0.5, 0.0); // max amp, sustain amp
  popOsc.amp(popEnv);
  popOsc.start();
  popOsc.freq(400); // base; will be changed per bubble

  // Create initial bubbles
  for (let i = 0; i < NUM_BUBBLES; i++) {
    bubbles.push(new Bubble());
  }
}

๐Ÿ”ง Subcomponents:

calculation Canvas Creation createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window

calculation Color Mode Configuration colorMode(HSB, 360, 100, 100, 255);

Switches to HSB color mode for easier rainbow color generation

calculation Audio Synthesis Setup popOsc = new p5.Oscillator('sine'); popEnv = new p5.Envelope(); popEnv.setADSR(0.001, 0.1, 0.0, 0.1); popEnv.setRange(0.5, 0.0); popOsc.amp(popEnv); popOsc.start(); popOsc.freq(400);

Creates a reusable sine wave oscillator with an envelope for percussive pop sounds

for-loop Initial Bubble Creation for (let i = 0; i < NUM_BUBBLES; i++) { bubbles.push(new Bubble()); }

Creates 18 bubbles at the start of the game

Line by Line:

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches the full browser window size, making the game responsive
colorMode(HSB, 360, 100, 100, 255);
Switches from RGB to HSB (Hue, Saturation, Brightness) color mode, which makes it easier to generate rainbow colors by varying hue
noStroke();
Removes outlines from all shapes, so bubbles and particles appear as solid filled circles
textFont('system-ui');
Sets the font for text display to system-ui, a clean default font
textSize(24);
Sets the text size to 24 pixels for the score display
popOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator that will generate the pop sound
popEnv = new p5.Envelope();
Creates an envelope object that controls how the sound starts, sustains, and ends
popEnv.setADSR(0.001, 0.1, 0.0, 0.1);
Sets Attack (0.001s), Decay (0.1s), Sustain (0.0), Release (0.1s) for a quick percussive pop sound
popEnv.setRange(0.5, 0.0);
Sets the envelope's amplitude range from 0.5 (peak) to 0.0 (silent), controlling volume
popOsc.amp(popEnv);
Connects the envelope to the oscillator so the envelope controls the sound's volume
popOsc.start();
Starts the oscillator running continuously (it will be silent until the envelope is triggered)
popOsc.freq(400);
Sets the initial frequency to 400 Hz, which will be changed for each bubble based on its size
for (let i = 0; i < NUM_BUBBLES; i++) { bubbles.push(new Bubble()); }
Creates 18 new Bubble objects and adds them to the bubbles array

draw()

draw() runs 60 times per second, creating the animation. Each frame, it clears the background, updates and displays all bubbles, updates and displays particles (removing dead ones), and shows the score. The backward loop for particles is important because removing items from an array while looping forward would skip elements.

function draw() {
  // Slightly animated background
  background(230, 50, 10); // HSB: deep blue-ish background

  // Draw and update bubbles
  for (let b of bubbles) {
    b.update();
    b.show();
  }

  // Draw and update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.update();
    p.show();
    if (p.isDead()) {
      particles.splice(i, 1);
    }
  }

  // Draw score
  drawScore();
}

๐Ÿ”ง Subcomponents:

calculation Background Rendering background(230, 50, 10);

Clears the canvas each frame with a deep blue color

for-loop Bubble Update and Display for (let b of bubbles) { b.update(); b.show(); }

Updates each bubble's position and redraws it

for-loop Particle Update and Cleanup for (let i = particles.length - 1; i >= 0; i--) { let p = particles[i]; p.update(); p.show(); if (p.isDead()) { particles.splice(i, 1); } }

Updates particles, displays them, and removes dead particles from the array

calculation Score Rendering drawScore();

Displays the current score on screen

Line by Line:

background(230, 50, 10);
Clears the canvas with a deep blue color (HSB: hue 230, saturation 50, brightness 10) each frame, creating a fresh slate for animation
for (let b of bubbles) { b.update(); b.show(); }
Loops through each bubble, updates its position (floating up with drift), and draws it on screen
for (let i = particles.length - 1; i >= 0; i--) {
Loops backward through the particles array (from end to start) so we can safely remove items during iteration
p.update(); p.show();
Updates the particle's position based on velocity and gravity, then draws it
if (p.isDead()) { particles.splice(i, 1); }
Checks if the particle's life has reached zero, and if so, removes it from the array to save memory
drawScore();
Calls the drawScore function to render the current score in the top-left corner

windowResized()

windowResized() is a p5.js built-in function that automatically triggers whenever the browser window is resized. This ensures the game canvas always fills the entire screen, making it responsive to different device sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

Line by Line:

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions when the browser is resized

class Bubble

The Bubble class represents each floating bubble in the game. It handles its own movement (floating up with drifting), rendering (with a three-layer visual effect), collision detection (contains method), and popping behavior. Each bubble is independent and manages its own lifecycle.

class Bubble {
  constructor() {
    this.reset(true);
  }

  reset(initial = false) {
    this.r = random(BUBBLE_MIN_R, BUBBLE_MAX_R);
    this.x = random(this.r, width - this.r);

    // Start from bottom; if initial, randomize a bit up the screen
    if (initial) {
      this.y = random(height + this.r * 0.5, height + this.r * 4);
    } else {
      this.y = height + this.r * 2;
    }

    this.speed = random(0.7, 1.8);
    this.driftAmount = random(0.2, 0.8);
    this.driftOffset = random(TWO_PI);

    const hue = random(180, 300); // cool colors
    const sat = random(60, 100);
    const bri = random(70, 100);
    this.col = color(hue, sat, bri, 180);
  }

  update() {
    // Float up
    this.y -= this.speed;

    // Gentle horizontal drift using a sine wave
    this.x += sin(frameCount * 0.01 + this.driftOffset) * this.driftAmount;

    // Keep within horizontal bounds
    this.x = constrain(this.x, this.r, width - this.r);

    // If bubble went off the top, respawn at bottom
    if (this.y + this.r < 0) {
      this.reset(false);
    }
  }

  show() {
    // Outer soft bubble
    fill(hue(this.col), saturation(this.col), brightness(this.col), 80);
    circle(this.x, this.y, this.r * 2.3);

    // Main bubble
    fill(this.col);
    circle(this.x, this.y, this.r * 2);

    // Highlight
    fill(0, 0, 100, 180);
    circle(this.x - this.r * 0.4, this.y - this.r * 0.4, this.r * 0.6);
  }

  contains(px, py) {
    return dist(px, py, this.x, this.y) < this.r;
  }

  pop() {
    // Play sound and create particle burst
    playPopSound(this);
    createParticleBurst(this.x, this.y, this.col);

    // Respawn bubble at bottom
    this.reset(false);
  }
}

๐Ÿ”ง Subcomponents:

calculation Constructor constructor() { this.reset(true); }

Initializes a new bubble by calling reset with initial=true

calculation Reset Method reset(initial = false) { ... }

Initializes or resets all bubble properties including size, position, speed, drift, and color

calculation Update Method update() { ... }

Updates bubble position each frame (floating up with horizontal drift)

calculation Show Method show() { ... }

Draws the bubble with three layers: outer soft glow, main bubble, and highlight

calculation Contains Method contains(px, py) { return dist(px, py, this.x, this.y) < this.r; }

Checks if a point (click) is inside the bubble using distance calculation

calculation Pop Method pop() { ... }

Handles bubble pop event: plays sound, creates particles, and respawns bubble

Line by Line:

this.r = random(BUBBLE_MIN_R, BUBBLE_MAX_R);
Sets the bubble's radius to a random value between 20 and 60 pixels
this.x = random(this.r, width - this.r);
Sets the bubble's x position randomly across the screen width, keeping it fully visible (accounting for radius)
if (initial) { this.y = random(height + this.r * 0.5, height + this.r * 4); }
For initial bubbles, starts them below the screen at varying depths so they don't all appear at once
this.speed = random(0.7, 1.8);
Sets how fast the bubble floats upward each frame (varies per bubble for visual interest)
this.driftAmount = random(0.2, 0.8);
Sets the amplitude of horizontal drift (how far left/right the bubble sways)
this.driftOffset = random(TWO_PI);
Sets a random starting phase for the sine wave drift so bubbles don't all sway in sync
const hue = random(180, 300);
Picks a hue between 180-300 degrees (cyan to purple range) for cool colors
this.col = color(hue, sat, bri, 180);
Creates the bubble's color with HSB values and alpha transparency of 180
this.y -= this.speed;
Moves the bubble upward each frame by subtracting its speed from y position
this.x += sin(frameCount * 0.01 + this.driftOffset) * this.driftAmount;
Adds gentle horizontal drift using a sine wave that varies with frameCount, creating a swaying motion
this.x = constrain(this.x, this.r, width - this.r);
Keeps the bubble within screen bounds by limiting x position to valid range
if (this.y + this.r < 0) { this.reset(false); }
When the bubble floats completely off the top of the screen, respawn it at the bottom
fill(hue(this.col), saturation(this.col), brightness(this.col), 80); circle(this.x, this.y, this.r * 2.3);
Draws a larger, semi-transparent outer circle for a soft glow effect around the bubble
fill(this.col); circle(this.x, this.y, this.r * 2);
Draws the main bubble with full opacity using the bubble's assigned color
fill(0, 0, 100, 180); circle(this.x - this.r * 0.4, this.y - this.r * 0.4, this.r * 0.6);
Draws a small white highlight circle offset to the upper-left to create a shiny 3D appearance
return dist(px, py, this.x, this.y) < this.r;
Returns true if the clicked point is within the bubble's radius (collision detection)
playPopSound(this); createParticleBurst(this.x, this.y, this.col); this.reset(false);
Plays a pop sound, creates particle effects at the bubble's location, and respawns the bubble

class Particle

The Particle class represents individual particles in the burst effect when a bubble pops. Each particle has its own position, velocity, and lifespan. Particles are affected by gravity and friction, creating a realistic burst that spreads outward and falls downward while fading away. This creates the satisfying pop effect.

class Particle {
  constructor(x, y, c) {
    this.pos = createVector(x, y);

    const angle = random(TWO_PI);
    const speed = random(1.5, 4);
    this.vel = p5.Vector.fromAngle(angle).mult(speed);

    this.gravity = createVector(0, 0.04);
    this.friction = 0.96;

    this.life = random(25, 45);
    this.totalLife = this.life;

    const baseHue = hue(c);
    const sat = saturation(c);
    const bri = brightness(c);
    // Slight color variation
    this.col = color(
      baseHue + random(-10, 10),
      sat,
      bri,
      255
    );
    this.size = random(3, 7);
  }

  update() {
    this.vel.mult(this.friction);
    this.vel.add(this.gravity);
    this.pos.add(this.vel);
    this.life--;
  }

  isDead() {
    return this.life <= 0;
  }

  show() {
    const alpha = map(this.life, 0, this.totalLife, 0, 255);
    fill(hue(this.col), saturation(this.col), brightness(this.col), alpha);
    circle(this.pos.x, this.pos.y, this.size);
  }
}

๐Ÿ”ง Subcomponents:

calculation Constructor constructor(x, y, c) { ... }

Initializes a particle with position, velocity in a random direction, physics properties, and color

calculation Update Method update() { ... }

Updates particle physics each frame: applies friction, gravity, and decreases life

calculation IsDead Method isDead() { return this.life <= 0; }

Returns true when the particle's life has expired

calculation Show Method show() { ... }

Draws the particle with alpha that fades as life decreases

Line by Line:

this.pos = createVector(x, y);
Creates a vector to store the particle's position, starting at the burst location
const angle = random(TWO_PI); const speed = random(1.5, 4); this.vel = p5.Vector.fromAngle(angle).mult(speed);
Creates a velocity vector in a random direction with random speed, making particles burst outward in all directions
this.gravity = createVector(0, 0.04);
Creates a gravity vector that pulls particles downward (0.04 pixels per frame acceleration)
this.friction = 0.96;
Sets friction to 0.96, meaning velocity is multiplied by 0.96 each frame (slowing particles down)
this.life = random(25, 45); this.totalLife = this.life;
Sets the particle's lifespan to 25-45 frames and stores the original value for fade calculations
this.col = color( baseHue + random(-10, 10), sat, bri, 255 );
Creates a color similar to the parent bubble but with ยฑ10 hue variation for visual interest
this.size = random(3, 7);
Sets the particle's diameter to a random size between 3 and 7 pixels
this.vel.mult(this.friction);
Applies friction by multiplying velocity by 0.96, slowing the particle each frame
this.vel.add(this.gravity);
Adds gravity to the velocity, making particles accelerate downward over time
this.pos.add(this.vel);
Updates the particle's position by adding its current velocity
this.life--;
Decreases the particle's life by 1 each frame, counting down to death
const alpha = map(this.life, 0, this.totalLife, 0, 255);
Maps the remaining life to an alpha value: when life is 0, alpha is 0 (invisible); when life equals totalLife, alpha is 255 (opaque)
fill(hue(this.col), saturation(this.col), brightness(this.col), alpha); circle(this.pos.x, this.pos.y, this.size);
Draws the particle as a circle with the calculated alpha, creating a fade-out effect as the particle dies

ensureAudioStarted()

Modern browsers require user interaction before playing audio. This function ensures the audio context is initialized on the first click or touch, allowing sound to play for subsequent bubble pops. The userStartAudio() function is provided by p5.sound.

function ensureAudioStarted() {
  if (!audioStarted) {
    userStartAudio(); // resumes AudioContext on first user gesture
    audioStarted = true;
  }
}

๐Ÿ”ง Subcomponents:

conditional Audio Context Check if (!audioStarted) { userStartAudio(); audioStarted = true; }

Initializes the audio context on first user interaction

Line by Line:

if (!audioStarted) {
Checks if audio has not been started yet (audioStarted is false)
userStartAudio();
Calls p5.sound's built-in function to resume the AudioContext, required by browsers for sound to work
audioStarted = true;
Sets the flag to true so this initialization only happens once

playPopSound(bubble)

This function creates the satisfying pop sound effect by mapping bubble size to pitch. The reusable oscillator and envelope are triggered each time a bubble pops, creating a different sound for each bubble based on its size. This is more efficient than creating new sound objects for each pop.

function playPopSound(bubble) {
  // Map bubble size to pitch: small bubbles -> higher pitch
  const freq = map(
    bubble.r,
    BUBBLE_MIN_R,
    BUBBLE_MAX_R,
    800, // small radius
    200, // large radius
    true
  );
  popOsc.freq(freq);
  popEnv.play(); // triggers envelope on the oscillator
}

๐Ÿ”ง Subcomponents:

calculation Frequency Calculation const freq = map(bubble.r, BUBBLE_MIN_R, BUBBLE_MAX_R, 800, 200, true);

Maps bubble radius to frequency, making small bubbles sound higher-pitched

calculation Sound Playback popOsc.freq(freq); popEnv.play();

Sets the oscillator frequency and triggers the envelope to play the sound

Line by Line:

const freq = map(bubble.r, BUBBLE_MIN_R, BUBBLE_MAX_MAX_R, 800, 200, true);
Maps the bubble's radius (20-60) to a frequency range (800-200 Hz). Small bubbles (r=20) get 800 Hz (high pitch), large bubbles (r=60) get 200 Hz (low pitch). The 'true' parameter constrains values to the range.
popOsc.freq(freq);
Sets the oscillator's frequency to the calculated value before playing
popEnv.play();
Triggers the envelope, which plays the sound with the fast attack/decay defined in setup()

createParticleBurst(x, y, col)

This function creates a burst of particles when a bubble pops. Each burst creates 12-21 particles, all starting at the bubble's location with the bubble's color. The variation in count makes each pop feel slightly different and more organic.

function createParticleBurst(x, y, col) {
  const count = floor(random(12, 22));
  for (let i = 0; i < count; i++) {
    particles.push(new Particle(x, y, col));
  }
}

๐Ÿ”ง Subcomponents:

calculation Particle Count const count = floor(random(12, 22));

Determines how many particles to create (12-21 particles per burst)

for-loop Particle Creation Loop for (let i = 0; i < count; i++) { particles.push(new Particle(x, y, col)); }

Creates and adds the specified number of particles to the particles array

Line by Line:

const count = floor(random(12, 22));
Generates a random number between 12 and 22, then floors it to get a whole number of particles
for (let i = 0; i < count; i++) {
Loops the specified number of times to create particles
particles.push(new Particle(x, y, col));
Creates a new Particle at the burst location with the bubble's color and adds it to the particles array

mousePressed()

mousePressed() is a p5.js built-in function that triggers whenever the mouse button is pressed. It delegates to handlePop() to check if any bubbles were clicked.

function mousePressed() {
  handlePop(mouseX, mouseY);
}

Line by Line:

handlePop(mouseX, mouseY);
Calls the handlePop function with the current mouse coordinates when the mouse is clicked

touchStarted()

touchStarted() is a p5.js function that triggers on touch events. By returning false, we prevent the browser from scrolling or zooming, keeping the game responsive on mobile devices. This ensures the game works on both desktop (mouse) and mobile (touch) platforms.

function touchStarted() {
  // On many platforms, touch also triggers mousePressed, but this ensures
  // touch-only environments still work nicely.
  handlePop(mouseX, mouseY);
  return false; // prevent default scrolling on touch
}

Line by Line:

handlePop(mouseX, mouseY);
Calls handlePop with the touch coordinates (p5.js converts touch to mouseX/mouseY)
return false;
Returns false to prevent the browser's default touch behavior (like scrolling or zooming)

handlePop(px, py)

handlePop() is called when the player clicks or touches the screen. It loops backward through bubbles to check the topmost one first (since later items in the array are drawn on top). This ensures that overlapping bubbles behave intuitively - you pop the one you see on top. The break statement ensures only one bubble pops per click.

function handlePop(px, py) {
  ensureAudioStarted();

  // Pop only one bubble per click, preferring the one drawn last (topmost)
  for (let i = bubbles.length - 1; i >= 0; i--) {
    if (bubbles[i].contains(px, py)) {
      bubbles[i].pop();
      score++;
      break;
    }
  }
}

๐Ÿ”ง Subcomponents:

calculation Audio Initialization ensureAudioStarted();

Ensures audio context is ready before playing sound

for-loop Bubble Collision Detection for (let i = bubbles.length - 1; i >= 0; i--) { if (bubbles[i].contains(px, py)) { bubbles[i].pop(); score++; break; } }

Checks bubbles from last to first (topmost first) and pops the first one clicked

Line by Line:

ensureAudioStarted();
Initializes the audio context on first click, allowing sounds to play
for (let i = bubbles.length - 1; i >= 0; i--) {
Loops backward through the bubbles array, starting from the last bubble (which is drawn on top)
if (bubbles[i].contains(px, py)) {
Checks if the clicked point is inside the current bubble
bubbles[i].pop();
Calls the bubble's pop method to trigger sound, particles, and respawn
score++;
Increments the score by 1 for popping a bubble
break;
Exits the loop immediately so only one bubble pops per click (the topmost one)

drawScore()

drawScore() displays the player's current score in the top-left corner. Using push() and pop() ensures that the text styling (color, alignment) doesn't affect other drawings in the sketch. This is a best practice for keeping drawing code organized and preventing unintended side effects.

function drawScore() {
  push();
  textAlign(LEFT, TOP);
  fill(0, 0, 100, 220); // bright text
  text("Score: " + score, 16, 16);
  pop();
}

๐Ÿ”ง Subcomponents:

calculation Text Styling push(); textAlign(LEFT, TOP); fill(0, 0, 100, 220); text("Score: " + score, 16, 16); pop();

Applies styling to the score text and displays it in the top-left corner

Line by Line:

push();
Saves the current drawing state (color, alignment, etc.) so changes don't affect other drawings
textAlign(LEFT, TOP);
Sets text alignment to left-aligned and top-aligned, so the text starts at the specified coordinates
fill(0, 0, 100, 220);
Sets the text color to bright white (HSB: hue 0, saturation 0, brightness 100) with slight transparency
text("Score: " + score, 16, 16);
Displays the text 'Score: ' followed by the current score value at position (16, 16) - the top-left corner with 16-pixel padding
pop();
Restores the previous drawing state so the text styling doesn't affect other drawings

๐Ÿ“ฆ Key Variables

NUM_BUBBLES number

Configuration constant that sets how many bubbles exist in the game at any time

const NUM_BUBBLES = 18;
BUBBLE_MIN_R number

Configuration constant for the minimum bubble radius in pixels

const BUBBLE_MIN_R = 20;
BUBBLE_MAX_R number

Configuration constant for the maximum bubble radius in pixels

const BUBBLE_MAX_R = 60;
bubbles array

Array that stores all active Bubble objects in the game

let bubbles = [];
particles array

Array that stores all active Particle objects from bubble bursts

let particles = [];
score number

Tracks the player's current score (incremented each time a bubble is popped)

let score = 0;
popOsc p5.Oscillator

A reusable sine wave oscillator that generates the pop sound for all bubbles

let popOsc = new p5.Oscillator('sine');
popEnv p5.Envelope

An envelope that controls the pop sound's attack, decay, sustain, and release for a percussive effect

let popEnv = new p5.Envelope();
audioStarted boolean

Flag that tracks whether the audio context has been initialized (required for sound on web)

let audioStarted = false;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change NUM_BUBBLES from 18 to 30 to make the game more challenging with more bubbles on screen at once
  2. Modify BUBBLE_MAX_R from 60 to 80 to create larger bubbles, then notice how the pop sounds become lower-pitched
  3. In the Bubble class show() method, change the highlight circle color from white (0, 0, 100, 180) to a bright color like (60, 100, 100, 180) to create colored highlights
  4. In the Particle class constructor, change the gravity value from 0.04 to 0.1 to make particles fall faster after bursting
  5. In the playPopSound() function, swap the frequency mapping so small bubbles play low sounds (200 Hz) and large bubbles play high sounds (800 Hz) - reverse the physics!
  6. Add a line in the handlePop() function to log the score to the console each time a bubble is popped: console.log('Score: ' + score);
  7. In the Bubble class reset() method, change the hue range from (180, 300) to (0, 360) to allow all rainbow colors instead of just cool colors
  8. Modify the particle friction from 0.96 to 0.98 to make particles slow down more gradually, creating longer-lasting bursts
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

BUG handlePop() function

If multiple bubbles overlap, only the topmost one can be popped, making lower bubbles unreachable

๐Ÿ’ก Consider allowing multiple bubbles to pop if they all contain the click point, or add visual feedback showing which bubble would be popped

PERFORMANCE draw() function - particle loop

Looping backward through particles array is correct for removal, but checking isDead() every frame for every particle is redundant

๐Ÿ’ก Move the isDead() check into the update() method or combine it with the update call to reduce function calls

STYLE Bubble class - show() method

The show() method extracts hue, saturation, and brightness separately three times, creating repetitive code

๐Ÿ’ก Store these values as properties in reset() so they can be reused: this.hue = hue(this.col); this.sat = saturation(this.col); this.bri = brightness(this.col);

FEATURE Game mechanics

There is no difficulty progression - the game stays the same throughout

๐Ÿ’ก Add difficulty scaling: increase bubble speed or spawn rate based on score, or decrease bubble size as score increases

FEATURE User interface

No visual feedback when a bubble is clicked (other than the pop sound and particles)

๐Ÿ’ก Add a brief screen flash, score popup at click location, or combo counter to enhance feedback

BUG Particle class - show() method

The alpha fade calculation uses map() which could produce values outside 0-255 if life exceeds totalLife

๐Ÿ’ก Use constrain() to ensure alpha stays within valid range: const alpha = constrain(map(this.life, 0, this.totalLife, 0, 255), 0, 255);

Preview

AI Bubble Pop - Satisfying Click Game Pop colorful bubbles as they float up! Click bubbles to burst - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Bubble Pop - Satisfying Click Game Pop colorful bubbles as they float up! Click bubbles to burst - Code flow showing setup, draw, windowresized, bubble, particle, ensureaudiostarted, playpopsound, createparticleburst, mousepressed, touchstarted, handlepop, drawscore
Code Flow Diagram