smilefacecatch

This is a fun arcade-style game where players move a paddle left and right to catch falling smiley faces. Each catch increases the score, but missing too many faces triggers a game over screen. The sketch combines object-oriented programming, collision detection, and p5.sound to create an interactive game experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the game difficulty — Lower the spawnInterval to make faces appear more frequently and test your reflexes harder
  2. Make the paddle wider and easier to use — Increase the catcher's width so you have a bigger target area for catching faces
  3. Change the miss sound to a higher pitch — Replace the low-pitched miss sound with a higher frequency to make it less somber
  4. Allow more misses before game over — Increase the tolerance so you can miss more faces and stay in the game longer
  5. Paint the caught faces purple instead of yellow — Before removing a caught face, change its color to provide extra visual feedback
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable arcade game where smiley faces fall from the top of the screen and you catch them with a blue paddle using arrow keys. Visually, it's engaging and colorful, with yellow falling faces, a responsive blue catcher paddle, and a score counter. The sketch teaches fundamental game development ideas: object-oriented design using ES6 classes for both the SmileyFace and Catcher entities, circle-to-rectangle collision detection with sophisticated distance calculations, game state management with constants, and audio feedback using p5.sound oscillators and envelopes.

The code is organized into two custom classes (SmileyFace and Catcher), several p5.js core functions (preload, setup, draw, keyPressed, windowResized), and global variables for game state. By reading it, you will learn how to structure a complete game with spawning objects, collision checks, score tracking, and state transitions—skills that apply to almost any interactive p5.js project.

⚙️ How It Works

  1. When the sketch loads, preload() initializes two p5.sound oscillators (one high-pitched sine wave for catches, one low-pitched triangle for misses) and their ADSR envelopes that shape how the sound volume changes over time. setup() then creates the canvas, initializes the catcher paddle in the center-bottom, and resets all game variables.
  2. Every frame, draw() checks if the game is in PLAY state. If so, it spawns a new SmileyFace every 1000 milliseconds by pushing a fresh object into the smileyFaces array.
  3. The catcher updates its position by reading LEFT_ARROW and RIGHT_ARROW key presses and constrain() keeps it on-screen. Each smiley face updates by falling down the screen (incrementing y by its speed).
  4. A loop iterates through every face backward (from last to first, so array removal doesn't skip items) and checks two things: does it collide with the catcher (using a circle-to-rectangle hits() function), or has it fallen off-screen? Collisions trigger the catch sound envelope, increment score, and remove the face. Off-screen faces trigger the miss sound, increment the missed counter, and are removed.
  5. The score and missed count are drawn in the top-left. If missed reaches maxMissed (5), gameState flips to GAMEOVER, which displays a red 'GAME OVER!' message and instructions to press Space.
  6. When the player presses Space during game over, keyPressed() calls setup() again to reset everything and return to play.

🎓 Concepts You'll Learn

ES6 ClassesObject-oriented programmingCollision detection (circle-to-rectangle)Game state managementArray manipulationp5.sound oscillators and envelopesKeyboard input handlingCanvas resizing

📝 Code Breakdown

SmileyFace constructor()

The constructor runs once for each new SmileyFace object. By using random() here, we ensure that every spawned face is unique in position, speed, and size—key to making the game feel dynamic and unpredictable.

constructor() {
    this.x = random(width);      // Random starting X position
    this.y = -50;                // Start above the canvas
    this.speed = random(2, 5);   // Random falling speed
    this.size = random(30, 60);  // Random size for the smiley face
    this.caught = false;         // Flag to check if the face has been caught
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Random X spawn position this.x = random(width);

Ensures each smiley face appears at a different horizontal position across the canvas

calculation Random falling speed this.speed = random(2, 5);

Varies how fast each face falls, making the game less predictable

calculation Random face size this.size = random(30, 60);

Makes some faces bigger targets than others, adding visual variety

this.x = random(width);
Generates a random number between 0 and the canvas width, placing this smiley face somewhere across the top
this.y = -50;
Sets the face to spawn 50 pixels ABOVE the top of the canvas so it falls into view
this.speed = random(2, 5);
Each face falls at a different speed (2-5 pixels per frame), so some are faster than others
this.size = random(30, 60);
Smiley faces have different diameters (30-60 pixels), making some easier and some harder to catch
this.caught = false;
Initializes a flag that tracks whether this face has been caught; used to prevent double-collisions

SmileyFace.update()

update() is called every frame for every smiley face. This tiny function is responsible for the animation—incrementing y creates the illusion of continuous motion.

🔬 Right now faces fall straight down. What if you added this.x += random(-1, 1) before this.y += this.speed to make them wobble left and right?

  update() {
    if (!this.caught) {
      this.y += this.speed;
    }
  }
update() {
    if (!this.caught) {
      this.y += this.speed;
    }
  }
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Only update if not caught if (!this.caught) {

Prevents the face from continuing to fall after it's been caught and marked for removal

if (!this.caught) {
Checks if this face hasn't been caught yet (the ! means 'not')
this.y += this.speed;
If not caught, adds the speed value to the y position, making the face fall down the screen each frame

SmileyFace.display()

display() is called every frame and draws the smiley face at its current position. Notice how all measurements (eye position, eye size, mouth dimensions) use this.size * multipliers—this ensures the face looks proportional no matter how large or small it is.

🔬 These three lines draw two black circles for eyes. What happens if you change fill(0) to fill(255, 0, 0) to make the eyes red?

      // Eyes
      fill(0);
      circle(this.x - this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);
      circle(this.x + this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);
display() {
    if (!this.caught) {
      fill(255, 255, 0); // Yellow face color
      stroke(0);         // Black outline
      strokeWeight(2);
      circle(this.x, this.y, this.size); // Draw the main circle for the face

      // Eyes
      fill(0);
      circle(this.x - this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);
      circle(this.x + this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);

      // Mouth (a simple arc)
      noFill();
      arc(this.x, this.y + this.size * 0.1, this.size * 0.4, this.size * 0.2, 0, PI);
    }
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Main face circle circle(this.x, this.y, this.size);

Draws the yellow head—sized proportionally via this.size

calculation Eye positioning using size scaling circle(this.x - this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);

Scales eye position and size relative to the face so big faces have proportionally big eyes

calculation Smile mouth arc arc(this.x, this.y + this.size * 0.1, this.size * 0.4, this.size * 0.2, 0, PI);

Draws the curved smile that scales with the face size

if (!this.caught) {
Only draw the face if it hasn't been caught—caught faces vanish instantly
fill(255, 255, 0); // Yellow face color
Sets all subsequent shapes to be filled with yellow (RGB: max red, max green, no blue)
stroke(0); // Black outline
Sets the outline color to black for visual definition
circle(this.x, this.y, this.size);
Draws the main yellow circle at this face's current position, using its random size
fill(0);
Changes the fill color to black for the eyes
circle(this.x - this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);
Left eye: positioned 20% of the size to the left, 10% up, with diameter 10% of the face size
circle(this.x + this.size * 0.2, this.y - this.size * 0.1, this.size * 0.1);
Right eye: positioned 20% of the size to the right, same vertical offset and eye size
noFill();
Switches to drawing outlines only (no interior fill) for the smile
arc(this.x, this.y + this.size * 0.1, this.size * 0.4, this.size * 0.2, 0, PI);
Draws a curved arc (half-circle from 0 to PI radians) for the smile, scaled to 40% width and 20% height of the face

SmileyFace.isOffscreen()

This simple boolean function is called in the main draw loop to detect missed faces. Adding this.size / 2 as a buffer means even the very bottom edge of a large face won't trigger 'missed' until it's truly gone.

isOffscreen() {
    return this.y > height + this.size / 2;
  }
Line-by-line explanation (1 lines)
return this.y > height + this.size / 2;
Checks if the face's center has fallen past the bottom of the canvas (with a small buffer of half the face size). Returns true if off-screen, false if still visible

SmileyFace.hits(catcher)

This is the most sophisticated collision detection in the sketch. It implements circle-to-rectangle collision by finding the closest point on the catcher rectangle to the face's center, then checking if that distance is within the face's radius. This is a classic technique used in professional games.

🔬 This code checks if distance is within the face's radius. What if you changed the last line to return distance <= this.size to make the collision zone twice as big?

    // Calculate the distance between the closest point and the circle's center
    let distX = this.x - testX;
    let distY = this.y - testY;
    let distance = sqrt((distX * distX) + (distY * distY));

    // If the distance is less than or equal to the smiley face's radius, they are colliding
    return distance <= this.size / 2;
hits(catcher) {
    // Simple square collision detection for the catcher, circle for smiley face
    let catcherLeft = catcher.x - catcher.w / 2;
    let catcherRight = catcher.x + catcher.w / 2;
    let catcherTop = catcher.y - catcher.h / 2;
    let catcherBottom = catcher.y + catcher.h / 2;

    // Find the closest point on the catcher rectangle to the center of the smiley face circle
    let testX = this.x;
    let testY = this.y;

    if (this.x < catcherLeft) testX = catcherLeft;
    else if (this.x > catcherRight) testX = catcherRight;

    if (this.y < catcherTop) testY = catcherTop;
    else if (this.y > catcherBottom) testY = catcherBottom;

    // Calculate the distance between the closest point and the circle's center
    let distX = this.x - testX;
    let distY = this.y - testY;
    let distance = sqrt((distX * distX) + (distY * distY));

    // If the distance is less than or equal to the smiley face's radius, they are colliding
    return distance <= this.size / 2;
  }
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Calculate catcher rectangle edges let catcherLeft = catcher.x - catcher.w / 2;

Defines the four corners of the catcher's rectangular bounding box for collision math

conditional Find closest point on catcher to face center if (this.x < catcherLeft) testX = catcherLeft;

Clamps the face's center to the nearest point on the catcher rectangle (circle-to-rectangle collision)

calculation Pythagorean distance calculation let distance = sqrt((distX * distX) + (distY * distY));

Computes the distance between the face center and the closest point on the catcher

conditional Collision comparison return distance <= this.size / 2;

Returns true if the distance is within the face's radius, indicating a hit

let catcherLeft = catcher.x - catcher.w / 2;
Calculates the left edge of the catcher paddle by subtracting half its width from its center x
let catcherRight = catcher.x + catcher.w / 2;
Calculates the right edge by adding half its width to the center x
let catcherTop = catcher.y - catcher.h / 2;
Calculates the top edge by subtracting half its height from its center y
let catcherBottom = catcher.y + catcher.h / 2;
Calculates the bottom edge by adding half its height to the center y
let testX = this.x;
Starts with the face's x position as the test point
if (this.x < catcherLeft) testX = catcherLeft;
If the face is to the left of the catcher, move testX to the catcher's left edge—this finds the closest point on the rectangle
else if (this.x > catcherRight) testX = catcherRight;
If the face is to the right, snap testX to the right edge
if (this.y < catcherTop) testY = catcherTop;
If the face is above the catcher, snap testY to the top edge
else if (this.y > catcherBottom) testY = catcherBottom;
If the face is below, snap testY to the bottom edge
let distX = this.x - testX;
Calculates the horizontal distance between the face center and the closest point on the catcher
let distY = this.y - testY;
Calculates the vertical distance
let distance = sqrt((distX * distX) + (distY * distY));
Uses the Pythagorean theorem to compute the actual distance: √(dx² + dy²)
return distance <= this.size / 2;
Returns true if the distance is less than or equal to the face's radius, meaning they are touching

Catcher constructor()

The constructor sets up all the catcher's properties. Notice the y-position math: height - this.h / 2 - 10 ensures the paddle's center is positioned 10 pixels from the very bottom, leaving a small margin.

constructor() {
    this.w = 120; // Catcher width
    this.h = 20;  // Catcher height
    this.x = width / 2; // Start in the middle
    this.y = height - this.h / 2 - 10; // Position slightly above the bottom edge
    this.speed = 10; // Movement speed
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Catcher size initialization this.w = 120; // Catcher width

Sets the paddle's width to 120 pixels—a comfortable size to catch faces

calculation Center-bottom positioning this.x = width / 2;

Places the catcher horizontally in the center of the canvas

this.w = 120;
Sets the catcher's width to 120 pixels
this.h = 20;
Sets the catcher's height to 20 pixels—narrow to make catching challenging
this.x = width / 2;
Centers the catcher horizontally by placing it at the midpoint of the canvas width
this.y = height - this.h / 2 - 10;
Positions the catcher near the bottom: canvas height minus half its own height minus 10 pixels of padding
this.speed = 10;
Sets how many pixels the catcher moves per frame when the arrow keys are held

Catcher.update()

update() is called every frame and reads continuous keyboard input. keyIsDown() is the right choice here because it allows smooth movement—holding the key continuously moves the paddle rather than moving once per key press.

🔬 These two conditions control left/right movement. What if you added a third condition for UP_ARROW that changes this.y? Could you move the paddle up and down too?

    if (keyIsDown(LEFT_ARROW)) {
      this.x -= this.speed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += this.speed;
    }
update() {
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= this.speed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += this.speed;
    }

    // Constrain the catcher to stay within the canvas boundaries
    this.x = constrain(this.x, this.w / 2, width - this.w / 2);
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Left arrow key handler if (keyIsDown(LEFT_ARROW)) {

Checks if the left arrow is currently pressed (keyIsDown checks every frame, not just once)

conditional Right arrow key handler if (keyIsDown(RIGHT_ARROW)) {

Checks if the right arrow is currently pressed

calculation Prevent paddle from leaving screen this.x = constrain(this.x, this.w / 2, width - this.w / 2);

Keeps the paddle's center within safe bounds so it never goes off-screen

if (keyIsDown(LEFT_ARROW)) {
keyIsDown() returns true if a key is currently being held down (different from keyPressed which fires once per key press)
this.x -= this.speed;
Subtracts the speed from x, moving the paddle left by 10 pixels per frame
if (keyIsDown(RIGHT_ARROW)) {
Checks if the right arrow is being held
this.x += this.speed;
Adds the speed to x, moving the paddle right by 10 pixels per frame
this.x = constrain(this.x, this.w / 2, width - this.w / 2);
constrain() clamps the x position between two bounds: the paddle's left edge (this.w / 2) and right edge (width - this.w / 2), preventing it from exiting the canvas

Catcher.display()

display() is simple but important—it's called every frame to redraw the catcher at its current position. The rectMode(CENTER) call is key because it makes this.x and this.y refer to the paddle's center, which aligns with how movement and collision are calculated.

display() {
    fill(0, 150, 255); // Blue catcher color
    stroke(0);
    strokeWeight(2);
    rectMode(CENTER);
    rect(this.x, this.y, this.w, this.h, 5); // Draw a rounded rectangle for the catcher
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Blue fill color fill(0, 150, 255);

Sets the catcher's color to a bright blue (0 red, 150 green, 255 blue)

calculation Rounded rectangle with corner radius rect(this.x, this.y, this.w, this.h, 5);

Draws the paddle with slightly rounded corners (5-pixel radius) for a polished look

fill(0, 150, 255);
Sets the fill color to bright blue using RGB: 0 red, 150 green, 255 blue (creates cyan-blue)
stroke(0);
Sets the outline color to black for contrast
strokeWeight(2);
Makes the outline 2 pixels thick
rectMode(CENTER);
Changes how the rect() function interprets its x, y coordinates—now they refer to the center rather than the top-left
rect(this.x, this.y, this.w, this.h, 5);
Draws a rectangle centered at (this.x, this.y) with width this.w, height this.h, and 5-pixel rounded corners

preload()

preload() runs before setup() and is the perfect place to initialize audio resources. Using p5.sound oscillators and envelopes, you generate sound effects programmatically—no audio files needed! The ADSR envelope mimics real instrument behavior: a quick attack makes the note feel immediate, and a release creates a natural fade.

function preload() {
  // Setup generated sound for catching a smiley face
  catchSound = new p5.Oscillator('sine'); // Sine wave oscillator
  catchSound.amp(0);                      // Start with 0 volume
  catchSound.freq(880);                   // Frequency: A5 (high pitch)
  catchEnv = new p5.Envelope();           // Envelope to control volume over time
  catchEnv.setADSR(0.01, 0.1, 0.5, 0.2);  // Attack, Decay, Sustain, Release times

  // Setup generated sound for missing a smiley face
  missSound = new p5.Oscillator('triangle'); // Triangle wave oscillator
  missSound.amp(0);
  missSound.freq(220);                    // Frequency: A3 (low pitch)
  missEnv = new p5.Envelope();
  missEnv.setADSR(0.01, 0.2, 0.1, 0.3);   // Attack, Decay, Sustain, Release times
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Catch sound oscillator setup catchSound = new p5.Oscillator('sine');

Creates a sine wave sound generator for the 'catch' audio feedback

calculation Catch sound envelope ADSR catchEnv.setADSR(0.01, 0.1, 0.5, 0.2);

Shapes the catch sound: quick attack (0.01s), quick decay (0.1s), medium sustain (0.5), medium release (0.2s)

calculation Miss sound oscillator setup missSound = new p5.Oscillator('triangle');

Creates a triangle wave sound generator for the 'miss' audio feedback

calculation Miss sound envelope ADSR missEnv.setADSR(0.01, 0.2, 0.1, 0.3);

Shapes the miss sound: quick attack, medium decay, low sustain, medium release

catchSound = new p5.Oscillator('sine');
Creates a new oscillator object using a sine wave—the smoothest sounding waveform
catchSound.amp(0);
Sets the initial amplitude (volume) to 0 so the sound is silent until triggered
catchSound.freq(880);
Sets the frequency to 880 Hz, which is the musical note A5 (a high, pleasant pitch)
catchEnv = new p5.Envelope();
Creates an envelope object that will control how the volume changes over time when the sound plays
catchEnv.setADSR(0.01, 0.1, 0.5, 0.2);
Sets the ADSR times in seconds: Attack (0.01s = instant), Decay (0.1s), Sustain level (0.5 = half volume), Release (0.2s = fade out)
missSound = new p5.Oscillator('triangle');
Creates an oscillator using a triangle wave, which sounds slightly harsher—appropriate for a 'miss' sound
missSound.amp(0);
Starts the miss sound silent too
missSound.freq(220);
Sets the frequency to 220 Hz, the musical note A3 (one octave below the catch sound, much lower pitch)
missEnv = new p5.Envelope();
Creates an envelope for the miss sound
missEnv.setADSR(0.01, 0.2, 0.1, 0.3);
Similar ADSR shape but with slightly longer decay and release, making the 'miss' sound linger a bit

setup()

setup() runs once when the sketch launches. It initializes the canvas, creates game objects, and prepares audio resources. Using windowWidth and windowHeight makes the game adapt to any screen size. Calling catchSound.start() and missSound.start() is essential—without these lines, the oscillators won't be able to produce sound later.

function setup() {
  createCanvas(windowWidth, windowHeight); // Create a canvas that fills the window
  catcher = new Catcher();                 // Initialize the catcher
  gameState = PLAY;                        // Set initial game state
  score = 0;
  missed = 0;
  smileyFaces = [];
  lastSpawnTime = millis();                // Record the current time for spawning

  // Start the sound oscillators (they will only make sound when their amplitude is increased by the envelope)
  catchSound.start();
  missSound.start();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Responsive canvas sizing createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window for a full-screen game experience

calculation Initialize the catcher paddle catcher = new Catcher();

Creates a new Catcher object with default position and properties

calculation Reset all game variables gameState = PLAY;

Resets the game state and score counters for a fresh game

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches the full browser window size, making the game responsive
catcher = new Catcher();
Instantiates a new Catcher object, which runs the Catcher constructor and initializes all its properties
gameState = PLAY;
Sets the game state to PLAY (which equals 0), ensuring the game starts in play mode
score = 0;
Resets the player's score to zero
missed = 0;
Resets the missed count to zero
smileyFaces = [];
Clears the array by assigning an empty array, removing any existing smiley faces
lastSpawnTime = millis();
Records the current time in milliseconds, which is used to spawn new faces at regular intervals
catchSound.start();
Starts the catch sound oscillator (p5.sound oscillators must be started before they can make noise)
missSound.start();
Starts the miss sound oscillator

draw()

draw() is the heart of the sketch—it runs 60 times per second and contains the entire game loop. Notice how it checks gameState to determine what to display: either the live game or the game over screen. The backward for-loop (i >= 0) is critical because splice() removes elements, and a forward loop would skip items. The spawnInterval logic uses millis() to spawn faces at precise time intervals rather than counting frames, making timing frame-rate independent.

🔬 This loop updates and displays each falling face. What happens if you add face.speed *= 1.01 inside the loop to make faces gradually accelerate as they fall?

    // Iterate through all smiley faces
    for (let i = smileyFaces.length - 1; i >= 0; i--) {
      let face = smileyFaces[i];
      face.update();
      face.display();

🔬 When you catch a face, it triggers a sound and increments score. What if you multiplied the score increase by the face size to reward catching bigger faces more? Change score++ to score += ceil(face.size / 10)?

      // Check for collision with the catcher
      if (face.hits(catcher) && !face.caught) {
        face.caught = true; // Mark as caught to prevent multiple collisions
        score++;
        catchSound.amp(catchEnv); // Trigger the catch sound envelope
        catchEnv.play(catchSound);
        smileyFaces.splice(i, 1); // Remove the caught face from the array
      }
function draw() {
  background(220); // Light gray background

  if (gameState === PLAY) {
    // --- Game Logic when playing ---

    // Spawn new smiley faces at regular intervals
    if (millis() - lastSpawnTime > spawnInterval) {
      smileyFaces.push(new SmileyFace()); // Add a new smiley face to the array
      lastSpawnTime = millis();
    }

    // Update and display the catcher
    catcher.update();
    catcher.display();

    // Iterate through all smiley faces
    for (let i = smileyFaces.length - 1; i >= 0; i--) {
      let face = smileyFaces[i];
      face.update();
      face.display();

      // Check for collision with the catcher
      if (face.hits(catcher) && !face.caught) {
        face.caught = true; // Mark as caught to prevent multiple collisions
        score++;
        catchSound.amp(catchEnv); // Trigger the catch sound envelope
        catchEnv.play(catchSound);
        smileyFaces.splice(i, 1); // Remove the caught face from the array
      }

      // Check if the smiley face has gone off-screen (missed)
      if (face.isOffscreen() && !face.caught) {
        missed++;
        missSound.amp(missEnv); // Trigger the miss sound envelope
        missEnv.play(missSound);
        smileyFaces.splice(i, 1); // Remove the missed face from the array
      }
    }

    // Display the current score and missed count
    fill(0);
    textSize(24);
    textAlign(LEFT, TOP);
    text(`Score: ${score}`, 10, 10);
    text(`Missed: ${missed}/${maxMissed}`, 10, 40);

    // Check for game over condition
    if (missed >= maxMissed) {
      gameState = GAMEOVER;
    }
  } else if (gameState === GAMEOVER) {
    // --- Game Over Screen ---
    fill(255, 0, 0); // Red text for "GAME OVER!"
    textSize(64);
    textAlign(CENTER, CENTER);
    text('GAME OVER!', width / 2, height / 2 - 50);

    fill(0); // Black text for score and restart message
    textSize(32);
    text(`Final Score: ${score}`, width / 2, height / 2);
    text('Press Space to Restart', width / 2, height / 2 + 50);
  }
}
Line-by-line explanation (38 lines)

🔧 Subcomponents:

calculation Clear canvas each frame background(220);

Clears the entire canvas with light gray, preventing animation trails and preparing for the next frame's drawing

conditional Time-based spawning logic if (millis() - lastSpawnTime > spawnInterval) {

Checks if enough milliseconds have passed since the last spawn to create a new smiley face

for-loop Backward iteration through faces for (let i = smileyFaces.length - 1; i >= 0; i--) {

Loops backward through the array so removing items (splice) doesn't skip any items

conditional Catch collision detection if (face.hits(catcher) && !face.caught) {

Checks if a face collides with the catcher and hasn't already been caught

conditional Miss detection if (face.isOffscreen() && !face.caught) {

Checks if a face has fallen off-screen without being caught, incrementing the miss counter

conditional Game over condition if (missed >= maxMissed) {

Checks if the player has missed too many faces, triggering the game over state

background(220);
Fills the canvas with light gray (220 on a 0-255 scale), erasing everything from the previous frame
if (gameState === PLAY) {
Checks if the game is in play mode (not game over); only run game logic if true
if (millis() - lastSpawnTime > spawnInterval) {
Checks if the time since the last spawn exceeds the spawn interval—millis() returns total milliseconds since the sketch started
smileyFaces.push(new SmileyFace());
Creates a new SmileyFace object and adds it to the end of the smileyFaces array
lastSpawnTime = millis();
Updates the spawn timer to the current time, resetting the interval countdown
catcher.update();
Calls the catcher's update method, which reads keyboard input and updates its position
catcher.display();
Calls the catcher's display method to draw it at its current position
for (let i = smileyFaces.length - 1; i >= 0; i--) {
Loops backward through the array (from the last element to the first); this is essential when using splice() inside the loop
let face = smileyFaces[i];
Stores a reference to the current smiley face object for easier reading
face.update();
Updates the face's position by calling its update method, which increments y by its speed
face.display();
Draws the smiley face at its current position
if (face.hits(catcher) && !face.caught) {
Checks two things: does the face collide with the catcher (face.hits returns true), AND has it not already been caught (!face.caught)
face.caught = true;
Sets the caught flag to true, preventing this face from being caught multiple times or counted as missed
score++;
Increments the score by 1
catchSound.amp(catchEnv);
Connects the envelope to the oscillator's amplitude, preparing it to shape the sound volume
catchEnv.play(catchSound);
Triggers the envelope, which plays the ADSR shape and causes the catch sound to be heard
smileyFaces.splice(i, 1);
Removes the caught face from the array at index i (splice removes 1 element at that position)
if (face.isOffscreen() && !face.caught) {
Checks if the face has fallen off-screen AND hasn't been caught
missed++;
Increments the missed counter by 1
missSound.amp(missEnv);
Connects the miss envelope to the miss oscillator
missEnv.play(missSound);
Triggers the miss sound envelope to play the miss audio
smileyFaces.splice(i, 1);
Removes the missed face from the array
fill(0);
Sets the fill color to black for the score text
textSize(24);
Sets the font size to 24 pixels
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically, so (10, 10) places the text in the top-left
text(`Score: ${score}`, 10, 10);
Draws the score text at position (10, 10) using template literal syntax to embed the score variable
text(`Missed: ${missed}/${maxMissed}`, 10, 40);
Draws the missed count as a fraction (e.g., '2/5') at position (10, 40)
if (missed >= maxMissed) {
Checks if the missed count has reached or exceeded the maximum allowed misses
gameState = GAMEOVER;
Switches the game state to GAMEOVER, which causes draw() to show the game over screen next frame
} else if (gameState === GAMEOVER) {
If the game is over, display the game over screen instead of the game
fill(255, 0, 0);
Sets the fill color to red for dramatic 'GAME OVER!' text
textSize(64);
Sets the font size to 64 pixels for large, prominent text
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically, so the next text() calls will be centered on their coordinates
text('GAME OVER!', width / 2, height / 2 - 50);
Draws 'GAME OVER!' in red, centered horizontally and 50 pixels above the canvas center
textSize(32);
Reduces font size to 32 pixels for the smaller score and restart messages
fill(0);
Changes fill color back to black
text(`Final Score: ${score}`, width / 2, height / 2);
Displays the final score centered on the canvas
text('Press Space to Restart', width / 2, height / 2 + 50);
Displays restart instructions 50 pixels below the canvas center

keyPressed()

keyPressed() is called once per key press (unlike keyIsDown, which is checked every frame). By calling setup() inside it, we elegantly reset the entire game state with one function call—a common pattern in game development.

function keyPressed() {
  // If the game is over and the Space key is pressed, restart the game
  if (gameState === GAMEOVER && key === ' ') {
    setup(); // Call setup() again to reset the game
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Restart game on Space if (gameState === GAMEOVER && key === ' ') {

Checks if the game is over AND the Space key was just pressed, enabling game restart

if (gameState === GAMEOVER && key === ' ') {
Checks two conditions using &&: (1) is gameState equal to GAMEOVER, and (2) is the key variable equal to a space character. Both must be true.
setup();
Calls setup() again, which resets all game variables, clears the smiley faces array, and returns gameState to PLAY

windowResized()

windowResized() is a p5.js callback that runs whenever the browser window is resized. It's essential for maintaining responsive, full-screen games. Notice how we update catcher.y but not catcher.x—the x position doesn't need adjustment since the catcher's update() method constrains it to the current canvas width automatically.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize the canvas to fill the new window size
  catcher.y = height - catcher.h / 2 - 10; // Adjust the catcher's Y position to stay at the bottom
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Responsive canvas resize resizeCanvas(windowWidth, windowHeight);

Automatically resizes the p5.js canvas whenever the browser window is resized

calculation Reposition catcher after resize catcher.y = height - catcher.h / 2 - 10;

Recalculates the catcher's Y position so it stays properly positioned at the bottom after a window resize

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions; called automatically by p5.js whenever the window size changes
catcher.y = height - catcher.h / 2 - 10;
Recalculates the catcher's Y position using the new canvas height, ensuring it stays 10 pixels from the bottom

📦 Key Variables

smileyFaces array

Stores all currently active SmileyFace objects falling on the screen

let smileyFaces = [];
catcher object

References the player-controlled Catcher paddle object

let catcher;
score number

Tracks how many smiley faces the player has successfully caught

let score = 0;
missed number

Counts how many smiley faces the player has failed to catch

let missed = 0;
gameState number

Stores the current game state: PLAY (0) or GAMEOVER (1), controlling what draw() displays

let gameState = PLAY;
catchSound p5.Oscillator

A sine wave oscillator that generates the audio feedback when a face is caught

let catchSound;
missSound p5.Oscillator

A triangle wave oscillator that generates the audio feedback when a face is missed

let missSound;
catchEnv p5.Envelope

Controls the volume envelope of the catch sound—shapes how loudness changes over time (ADSR)

let catchEnv;
missEnv p5.Envelope

Controls the volume envelope of the miss sound—shapes how loudness changes over time (ADSR)

let missEnv;
spawnInterval number

The time in milliseconds between spawning new smiley faces (how often new faces appear)

let spawnInterval = 1000;
lastSpawnTime number

Stores the timestamp (in milliseconds) of the last time a smiley face was spawned, used to track spawn intervals

let lastSpawnTime = 0;
maxMissed number

The maximum number of smiley faces that can be missed before the game ends

let maxMissed = 5;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

FEATURE SmileyFace class

Caught faces disappear instantly with no visual feedback

💡 Add a 'scale' property that shrinks the face for a few frames after being caught before removal, creating a satisfying 'pop' effect. Or change the face color briefly to purple to show it was caught.

PERFORMANCE draw() game over screen

The game over screen is redrawn 60 times per second even though it's static

💡 Use a flag or counter to draw the game over screen only once, then skip redrawing it. This is a micro-optimization but good practice for larger sketches.

STYLE draw() spawn logic

The spawn interval logic is somewhat repetitive

💡 Create a helper function like function trySpawnFace() to encapsulate the spawn check logic and keep draw() cleaner and more readable.

FEATURE Catcher class

The catcher can move past the edges if moving fast enough before constrain() clamps it

💡 Clamp the position BEFORE calculating movement, or use a smaller speed multiplier near edges: if (this.x > width - 50) speed *= 0.5

BUG SmileyFace.hits() collision detection

At very high speeds, smiley faces can pass through the catcher without triggering collision

💡 Implement continuous collision detection or use a velocity-based prediction to check for collision along the path the face travels, not just at its current position.

FEATURE Game state management

No pause feature—the game only pauses if you stop input

💡 Add a third game state (PAUSED) that can be toggled with the 'P' key. This gives players control over game flow.

🔄 Code Flow

Code flow showing smileyface_constructor, smileyface_update, smileyface_display, smileyface_isoffscreen, smileyface_hits, catcher_constructor, catcher_update, catcher_display, preload, setup, draw, keypressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> spawn-check[spawn-check] draw --> background-clear[background-clear] draw --> face-loop[face-loop] face-loop --> smileyface_isoffscreen[smileyface_isoffscreen] face-loop --> smileyface_hits[smileyface_hits] face-loop --> uncaught-check[uncaught-check] face-loop --> smileyface_update[smileyface_update] face-loop --> smileyface_display[smileyface_display] smileyface_display --> face-circle[face-circle] smileyface_display --> eye-positioning[eye-positioning] smileyface_display --> mouth-arc[mouth-arc] smileyface_hits --> catcher-bounds[catcher-bounds] catcher-bounds --> closest-point[closest-point] closest-point --> distance-calc[distance-calc] distance-calc --> collision-check[collision-check] collision-check --> offscreen-check[offscreen-check] offscreen-check --> gameover-check[gameover-check] gameover-check --> restart-condition[restart-condition] restart-condition --> keypressed[keypressed] click setup href "#fn-setup" click draw href "#fn-draw" click spawn-check href "#sub-spawn-check" click background-clear href "#sub-background-clear" click face-loop href "#sub-face-loop" click smileyface_isoffscreen href "#fn-smileyface_isoffscreen" click smileyface_hits href "#fn-smileyface_hits" click uncaught-check href "#sub-uncaught-check" click smileyface_update href "#fn-smileyface_update" click smileyface_display href "#fn-smileyface_display" click face-circle href "#sub-face-circle" click eye-positioning href "#sub-eye-positioning" click mouth-arc href "#sub-mouth-arc" click catcher-bounds href "#sub-catcher-bounds" click closest-point href "#sub-closest-point" click distance-calc href "#sub-distance-calc" click collision-check href "#sub-collision-check" click offscreen-check href "#sub-offscreen-check" click gameover-check href "#sub-gameover-check" click restart-condition href "#sub-restart-condition" click keypressed href "#fn-keypressed"

Preview

smilefacecatch - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of smilefacecatch - Code flow showing smileyface_constructor, smileyface_update, smileyface_display, smileyface_isoffscreen, smileyface_hits, catcher_constructor, catcher_update, catcher_display, preload, setup, draw, keypressed, windowresized
Code Flow Diagram