AI Bubble Pop Game - Click to Pop

This sketch creates a playful bubble-popping game where colorful bubbles rise from the bottom of the screen against a soft sky gradient. Clicking a bubble pops it, increases your score, and triggers a quick red flash effect.

🧪 Try This!

Experiment with the code by making these changes:

  1. Spawn bubbles faster — Lowering the frame divisor makes bubbles appear much more frequently, ramping up the difficulty and chaos.
  2. Make bubbles bigger — Increasing the random radius range makes every bubble larger and easier (or harder, depending on density) to click.
  3. Rainbow pop flash — Instead of always flashing red when you pop a bubble, this makes the flash color random each time.
  4. Sunset sky instead of daytime sky — Swapping the two gradient colors changes the whole mood of the background from a clear blue sky to a warm sunset.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns the canvas into a casual arcade game: soft-colored bubbles drift upward from the bottom of the screen, complete with a subtle 3D highlight, while a smooth blue-to-white gradient fills the sky behind them. Clicking a bubble makes it pop, bumps up your score, and flashes the screen red for a split second. Under the hood it relies on p5.js fundamentals like the draw loop, mouse interaction with mousePressed(), color blending with lerpColor(), and object-oriented programming through a custom Bubble class.

The code is organized around a single Bubble class that stores each bubble's position, size, color, and speed, plus methods to move it, draw it, and check if it has been clicked or has floated off-screen. The global draw() loop repaints the gradient sky every frame, updates and removes bubbles from an array, and spawns new ones on a timer. Studying this sketch teaches you how to manage a growing and shrinking collection of objects, how to detect clicks on circular shapes using distance math, and how per-pixel gradients are built with lerpColor().

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, turns off shape outlines, and configures the score text's alignment and size.
  2. Every frame, draw() first repaints the entire background by drawing one horizontal line per pixel row, blending from sky blue at the top to white lower down using lerpColor().
  3. draw() then loops backward through the bubbles array, moving each bubble upward and drawing it; any bubble that has floated above the top of the screen is removed from the array with splice().
  4. Every 30 frames, a fresh bubble is created at a random x position along the bottom edge with a random size, color, and rising speed, and pushed into the bubbles array.
  5. When the mouse is pressed, mousePressed() checks each bubble (from newest to oldest) to see if the click landed inside its radius using dist(); the first match is popped, the score increases, a brief red flash covers the screen, and the loop stops so only one bubble pops per click.
  6. If the browser window is resized, windowResized() resizes the canvas to match so the game always fills the screen.

🎓 Concepts You'll Learn

ES6 classes and object-oriented programmingArrays of objects and splice() for removalMouse interaction with mousePressed()Color blending with lerpColor()Distance-based collision detection with dist()Frame-based timing with frameCount

📝 Code Breakdown

Bubble (class)

This class packages all the data and behavior for a single bubble into one reusable blueprint. Every bubble on screen is a separate instance, letting you manage dozens of independent objects with the same code.

🔬 This uses the bubble's radius as the click tolerance. What happens if you multiply this.r by 1.5 to make bubbles easier to pop, or by 0.5 to make popping require pixel-perfect accuracy?

  isClicked(mx, my) { return dist(mx, my, this.x, this.y) < this.r; }
class Bubble {
  constructor(x, y, r, c, s) {
    this.x = x; this.y = y; this.r = r; this.color = c; this.speed = s;
  }
  move() { this.y -= this.speed; }
  display() {
    fill(this.color); circle(this.x, this.y, this.r * 2);
    fill(255, 100); circle(this.x - this.r / 3, this.y - this.r / 3, this.r * 0.7);
  }
  isOffScreen() { return this.y < -this.r; }
  isClicked(mx, my) { return dist(mx, my, this.x, this.y) < this.r; }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Constructor constructor(x, y, r, c, s) { this.x = x; this.y = y; this.r = r; this.color = c; this.speed = s; }

Stores the bubble's starting position, radius, color, and speed as properties on the object

calculation Draw Bubble With Highlight fill(255, 100); circle(this.x - this.r / 3, this.y - this.r / 3, this.r * 0.7);

Draws a smaller semi-transparent white circle offset toward the top-left to fake a glossy 3D highlight

calculation Click Detection return dist(mx, my, this.x, this.y) < this.r;

Uses distance math to check if the mouse click point falls within the bubble's circular area

constructor(x, y, r, c, s) {
Runs once when a new Bubble is created, receiving its starting x/y position, radius, color, and speed
this.x = x; this.y = y; this.r = r; this.color = c; this.speed = s;
Saves each parameter onto the object so it can be reused later in move() and display()
move() { this.y -= this.speed; }
Decreases the y position each frame - since y grows downward in p5.js, subtracting makes the bubble rise
fill(this.color); circle(this.x, this.y, this.r * 2);
Sets the fill color to the bubble's assigned color and draws the main circle body (diameter is twice the radius)
fill(255, 100); circle(this.x - this.r / 3, this.y - this.r / 3, this.r * 0.7);
Draws a small semi-transparent white circle shifted up and left to simulate a glossy light reflection
isOffScreen() { return this.y < -this.r; }
Returns true once the bubble has fully floated above the top edge of the canvas, so it can be removed
isClicked(mx, my) { return dist(mx, my, this.x, this.y) < this.r; }
Calculates the straight-line distance between the mouse click and the bubble's center; if it's less than the radius, the click is inside the circle

setup()

setup() runs once when the sketch starts, ideal for creating the canvas and setting drawing defaults that apply for the whole program.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke(); textAlign(RIGHT, TOP); textSize(24);
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, using p5's built-in windowWidth/windowHeight variables
noStroke();
Turns off outlines for all shapes drawn afterward, so bubbles and gradient lines look smooth without borders
textAlign(RIGHT, TOP);
Sets text alignment so any text drawn is anchored from its right edge and top, useful for positioning the score in a corner
textSize(24);
Sets the font size used whenever text() is called later, like the score display

draw()

draw() runs continuously at roughly 60 frames per second. This function shows three common patterns together: pixel-by-pixel gradients, iterating an array backward to safely remove items, and timed events using frameCount.

🔬 This block controls how often and how bubbles are born. What happens if you change 30 to 5? What if you push two bubbles instead of one each time?

  if (frameCount % 30 == 0) { // Spawn new bubble every 30 frames
    let r = random(20, 50), x = random(r, width - r), y = height + r;
    let c = bubbleColors[floor(random(bubbleColors.length))];
    let s = random(1, 3);
    bubbles.push(new Bubble(x, y, r, c, s));
  }
function draw() {
  for (let i = 0; i < height; i++) { // Gradient background (sky)
    let inter = map(i, 0, height, 0, 1);
    let c = lerpColor(color(135, 206, 235), color(255, 255, 255), inter);
    stroke(c); line(0, i, width, i);
  }

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

  if (frameCount % 30 == 0) { // Spawn new bubble every 30 frames
    let r = random(20, 50), x = random(r, width - r), y = height + r;
    let c = bubbleColors[floor(random(bubbleColors.length))];
    let s = random(1, 3);
    bubbles.push(new Bubble(x, y, r, c, s));
  }
  fill(0); text(`Score: ${score}`, width - 10, 10);
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

for-loop Sky Gradient for (let i = 0; i < height; i++) { ... stroke(c); line(0, i, width, i); }

Draws one horizontal line per pixel row, blending color from sky blue at the top to white at the bottom

for-loop Update and Cull Bubbles for (let i = bubbles.length - 1; i >= 0; i--) { bubbles[i].move(); bubbles[i].display(); if (bubbles[i].isOffScreen()) bubbles.splice(i, 1); }

Moves and draws every bubble, removing any that have floated off the top of the screen

conditional Spawn New Bubble if (frameCount % 30 == 0) { ... bubbles.push(new Bubble(x, y, r, c, s)); }

Every 30 frames, creates a brand new bubble with random size, position, color, and speed

for (let i = 0; i < height; i++) { // Gradient background (sky)
Loops once for every vertical pixel row on the canvas to build a smooth gradient
let inter = map(i, 0, height, 0, 1);
Converts the current row number into a 0-to-1 fraction representing how far down the canvas we are
let c = lerpColor(color(135, 206, 235), color(255, 255, 255), inter);
Blends between sky blue and white based on that fraction, so the color gradually shifts down the screen
stroke(c); line(0, i, width, i);
Draws a full-width horizontal line in the blended color at the current row, building the gradient one line at a time
for (let i = bubbles.length - 1; i >= 0; i--) {
Loops backward through the bubbles array - counting down is important because splice() removes items and shifts indices
bubbles[i].move(); bubbles[i].display();
Calls each bubble's own move() and display() methods to update its position and draw it
if (bubbles[i].isOffScreen()) bubbles.splice(i, 1);
Removes the bubble from the array once it has floated above the visible canvas, keeping the array from growing forever
if (frameCount % 30 == 0) { // Spawn new bubble every 30 frames
frameCount increases by 1 every frame; this checks if it's evenly divisible by 30, which happens once every 30 frames
let r = random(20, 50), x = random(r, width - r), y = height + r;
Picks a random radius, then a random x position that keeps the whole bubble within the canvas width, and starts it just below the bottom edge
let c = bubbleColors[floor(random(bubbleColors.length))];
Picks a random color name from the bubbleColors array by generating a random index and rounding it down to a whole number
let s = random(1, 3);
Chooses a random rising speed for this bubble between 1 and 3 pixels per frame
bubbles.push(new Bubble(x, y, r, c, s));
Creates a new Bubble object with the chosen values and adds it to the end of the bubbles array
fill(0); text(`Score: ${score}`, width - 10, 10);
Sets the text color to black and draws the current score in the top-right corner using a template string

mousePressed()

mousePressed() is a p5.js event function that fires once per click, separate from the continuous draw() loop. It's the standard way to handle discrete user interactions like clicking or tapping.

🔬 What happens if you remove the break statement? Since multiple bubbles could overlap near your click, would more than one ever pop at once, and how would the score change?

    if (bubbles[i].isClicked(mouseX, mouseY)) {
      score++;
      background(255, 0, 0, 50); // Simple pop effect: brief red flash
      bubbles.splice(i, 1);
      break; // Only pop one bubble per click
    }
function mousePressed() {
  for (let i = bubbles.length - 1; i >= 0; i--) {
    if (bubbles[i].isClicked(mouseX, mouseY)) {
      score++;
      background(255, 0, 0, 50); // Simple pop effect: brief red flash
      bubbles.splice(i, 1);
      break; // Only pop one bubble per click
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Check Every Bubble for (let i = bubbles.length - 1; i >= 0; i--) {

Checks bubbles from newest to oldest to find one under the mouse click

conditional Pop On Match if (bubbles[i].isClicked(mouseX, mouseY)) { ... }

If a bubble contains the click point, increases the score, flashes red, removes the bubble, and stops checking

function mousePressed() {
p5.js automatically calls this function whenever the mouse button is clicked anywhere on the canvas
for (let i = bubbles.length - 1; i >= 0; i--) {
Loops through the bubbles array backward, checking the most recently spawned bubbles first
if (bubbles[i].isClicked(mouseX, mouseY)) {
Uses the bubble's own isClicked() method with the current mouse coordinates to see if this bubble was hit
score++;
Increases the score variable by one point for a successful pop
background(255, 0, 0, 50); // Simple pop effect: brief red flash
Immediately paints a translucent red rectangle over the whole canvas as a quick visual pop effect
bubbles.splice(i, 1);
Removes the popped bubble from the array so it disappears and stops being drawn
break; // Only pop one bubble per click
Exits the loop immediately so a single click can only pop one bubble even if several overlap

windowResized()

windowResized() is a built-in p5.js event function, useful for keeping full-window sketches responsive to browser resizing.

function windowResized() { resizeCanvas(windowWidth, windowHeight); }
Line-by-line explanation (1 lines)
function windowResized() { resizeCanvas(windowWidth, windowHeight); }
p5.js automatically calls this whenever the browser window changes size; resizeCanvas() updates the canvas to match the new window dimensions so the game always fills the screen

📦 Key Variables

bubbles array

Holds every active Bubble object currently on screen; grows when new bubbles spawn and shrinks when bubbles are popped or float off-screen

let bubbles = [];
score number

Tracks how many bubbles the player has successfully popped, displayed in the top-right corner

let score = 0;
bubbleColors array

A fixed list of CSS color names randomly assigned to new bubbles when they spawn

const bubbleColors = ['red', 'yellow', 'green', 'blue', 'pink'];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw() gradient loop

The sky gradient is redrawn pixel-row by pixel-row (using line()) every single frame, which means a full-HD window draws over 1000 lines 60 times per second - this is expensive and can slow down the sketch on large screens.

💡 Render the gradient once into an offscreen graphics buffer with createGraphics(), or draw it once to a p5.Image, and simply display that cached image each frame instead of recalculating it every time.

BUG mousePressed() pop flash

background(255, 0, 0, 50) is called inside mousePressed(), but draw() runs again almost immediately afterward and completely repaints the canvas with the gradient loop, so the red flash is likely never visible to the player.

💡 Introduce a global flash timer variable (e.g. let flashAlpha = 0) that gets set on click and is drawn as an overlay rectangle inside draw() itself, fading out over a few frames, so it actually appears on screen.

STYLE Bubble class / bubbleColors array

Bubble colors are stored as plain CSS color name strings rather than p5.Color objects, which limits variety and makes it harder to do things like adjust transparency or brightness per bubble.

💡 Store bubbleColors as an array of color() objects (e.g. color(255,0,0)) so you can easily add alpha, blend colors, or randomize hue with colorMode(HSB).

FEATURE overall game design

The game currently has no way to lose, no difficulty progression, and no sound feedback, which limits replay value.

💡 Add a timer or life system, gradually increase spawn rate or bubble speed over time using frameCount, and trigger a short pop sound with the p5.sound library when a bubble is clicked.

🔄 Code Flow

Code flow showing bubble, setup, draw, mousepressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gradientloop[gradient-loop] gradientloop --> bubbleupdateloop[bubble-update-loop] bubbleupdateloop --> spawnconditional[spawn-conditional] spawnconditional --> draw draw --> mousepressed[mousepressed] mousepressed --> clickloop[click-loop] clickloop --> clickconditional[click-conditional] clickconditional --> draw clickconditional --> bubbleclicked[bubble-clicked] bubbleclicked --> draw draw --> bubbleconstructor[bubble-constructor] bubbleconstructor --> bubbledisplay[bubble-display] bubbledisplay --> draw windowresized[windowresized] --> draw click setup href "#fn-setup" click draw href "#fn-draw" click mousepressed href "#fn-mousepressed" click windowresized href "#fn-windowresized" click gradientloop href "#sub-gradient-loop" click bubbleupdateloop href "#sub-bubble-update-loop" click spawnconditional href "#sub-spawn-conditional" click clickloop href "#sub-click-loop" click clickconditional href "#sub-click-conditional" click bubbleclicked href "#sub-bubble-clicked" click bubbleconstructor href "#sub-bubble-constructor" click bubbledisplay href "#sub-bubble-display"

❓ Frequently Asked Questions

What visual elements are featured in the AI Bubble Pop Game sketch?

The sketch showcases colorful bubbles floating upwards against a gradient sky background, with smooth animations and 3D highlights for each bubble.

How can players interact with the AI Bubble Pop Game?

Users can click on the bubbles to pop them, scoring points with each successful click, while experiencing a brief visual pop effect.

What creative coding concepts are demonstrated in this p5.js sketch?

The sketch illustrates concepts like object-oriented programming through the Bubble class, animation techniques, and dynamic background rendering using color interpolation.

Preview

AI Bubble Pop Game - Click to Pop - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Bubble Pop Game - Click to Pop - Code flow showing bubble, setup, draw, mousepressed, windowresized
Code Flow Diagram