cazador de manzanas

This sketch is a fast-paced fruit-catching game where players move a basket with their mouse to catch falling fruits, special items, and avoid bombs. The game features a combo system, particle effects, scrolling clouds, and synthesized sound effects that reward quick reflexes and strategic catching.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change basket color
  2. Make games harder from the start — Increase initial fruit speed from 3 to 5 so the game feels intense immediately
  3. Spawn fruits more frequently — Lower the spawn frequency from 60 frames to 40, making fruits rain down harder
  4. Make the basket wider — Increase basket width from 100 to 200 pixels to make the game easier for beginners
  5. Boost gold fruit value — Change gold fruit from 10 points to 50 points—suddenly they're super rewarding
  6. Make combos worth more — The combo bonus starts at 1 point for 2 catches. Change it to 5 points for huge rewards
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a vibrant arcade-style game where fruits tumble from the sky and you slide a basket left and right to catch them before they hit the ground. It combines multiple p5.js techniques: the draw loop for animation, collision detection to catch items, mouse interaction for player control, the p5.sound library for synthesized audio feedback, and a Particle class that creates bursts of color when fruits are caught. The result is an engaging, juicy game experience that teaches you how professional game mechanics are built.

The code is organized into distinct systems: a game state machine (START, PLAYING, GAMEOVER) that controls what appears on screen, helper functions that draw each fruit type and the basket, an update loop that manages fruit spawning and movement, a combo system that rewards quick consecutive catches, and two custom classes (Particle and Cloud) that handle visual effects. By reading this sketch, you will learn how to build a complete interactive game from the ground up—including how to handle player input, manage multiple moving objects, detect collisions, create visual feedback, and structure game logic.

⚙️ How It Works

  1. When you first open the sketch, preload() initializes three p5.Oscillator objects for sound effects and one for background music. setup() creates the canvas, positions the basket at the bottom center, switches to HSB color mode for easier color adjustments, and spawns the initial clouds.
  2. The draw() loop runs 60 times per second and uses a switch statement to display the correct screen: a START screen with instructions, a PLAYING screen with the game in progress, or a GAMEOVER screen showing your final score.
  3. During gameplay, the basket follows your mouse horizontally and is constrained to stay within the canvas bounds. Every frame, frutas (fruits) fall down the screen—their y position increases by velocidadFrutas pixels.
  4. When a fruit reaches the basket's position, collision detection triggers: the game plays a sound, awards points or special effects based on fruit type (golden fruits give 10 points, hearts restore a life, bombs subtract 20 points and a life), and spawns colorful particles at that location.
  5. If a fruit falls off the bottom of the screen without being caught, you lose a life—and if a normal fruit is missed, a 'lose life' sound plays. The combo system tracks how many fruits you catch in quick succession (within 500 milliseconds); catching 2 in a row awards a bonus, 3 gives more, and 4+ gives maximum bonus points.
  6. When you run out of lives (vidas ≤ 0), the game state changes to GAMEOVER, the background music fades out, and a game-over sound plays. Clicking anywhere restarts the game and resets all variables.

🎓 Concepts You'll Learn

Game state machineCollision detectionMouse interaction and inputParticle systemsSynthesized sound with oscillatorsArray management (adding/removing items)Class-based designVector mathCombo mechanicsGame difficulty progression

📝 Code Breakdown

preload()

preload() runs once before setup() and is the perfect place to load sounds, images, or other assets. Notice how all oscillators start with amp(0) so they are silent until we explicitly make them audible in reproducirSonido(). This pattern prevents unexpected noise when the sketch loads.

function preload() {
  // Precargamos los sonidos
  // Usamos p5.Oscillator para sonidos simples, podrías cargar archivos .mp3 o .wav si los tuvieras.
  sonidoAtrapar = new p5.Oscillator();
  sonidoAtrapar.setType('triangle');
  sonidoAtrapar.freq(500);
  sonidoAtrapar.amp(0); // Empieza en silencio
  sonidoAtrapar.start();

  sonidoPerder = new p5.Oscillator();
  sonidoPerder.setType('sawtooth');
  sonidoPerder.freq(100);
  sonidoPerder.amp(0);
  sonidoPerder.start();

  sonidoGameOver = new p5.Oscillator();
  sonidoGameOver.setType('sine');
  sonidoGameOver.freq(50);
  sonidoGameOver.amp(0);
  sonidoGameOver.start();

  // Música de fondo simple
  musicaFondo = new p5.Oscillator();
  musicaFondo.setType('sine');
  musicaFondo.freq(220); // Nota La 2
  musicaFondo.amp(0.1); // Volumen bajo
  musicaFondo.start();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

object-creation Catch sound setup sonidoAtrapar = new p5.Oscillator();

Creates a triangle-wave oscillator that plays when you catch a fruit

object-creation Miss sound setup sonidoPerder = new p5.Oscillator();

Creates a sawtooth-wave oscillator that plays when a fruit falls unmissed

object-creation Game over sound setup sonidoGameOver = new p5.Oscillator();

Creates a sine-wave oscillator that plays when the game ends

object-creation Background music setup musicaFondo = new p5.Oscillator();

Creates a sine-wave oscillator that loops quietly during gameplay

sonidoAtrapar = new p5.Oscillator();
Creates a new oscillator object and assigns it to the global variable sonidoAtrapar
sonidoAtrapar.setType('triangle');
Sets the oscillator to produce a triangle wave, which sounds smooth and musical
sonidoAtrapar.freq(500);
Sets the frequency (pitch) to 500 Hz—a mid-range note that sounds satisfying when you catch a fruit
sonidoAtrapar.amp(0);
Sets the amplitude (volume) to 0, so the sound starts silent and only plays when triggered later
sonidoAtrapar.start();
Starts the oscillator running—it stays on the entire game but silent until we change its amplitude
musicaFondo.freq(220); // Nota La 2
Sets background music to 220 Hz (the note A3), a pleasant low frequency that loops continuously
musicaFondo.amp(0.1); // Volumen bajo
Sets background music to 10% volume so it does not drown out catch/miss sounds

setup()

setup() runs once when the sketch starts. It creates the canvas, initializes variables, and configures the visual style. Notice how we use width/2 and height to position objects—this makes the game responsive if the window is resized.

🔬 The basket starts 30 pixels from the bottom. What happens if you change height - 30 to height - 100 so it starts higher up? How does that change the game difficulty?

  canasta = createVector(width / 2, height - 30);
  colorMode(HSB, 360, 100, 100);
function setup() {
  createCanvas(windowWidth, windowHeight);
  // Inicializa la canasta en la parte inferior central
  canasta = createVector(width / 2, height - 30);
  colorMode(HSB, 360, 100, 100);
  noCursor();

  // Initialize clouds
  for (let i = 0; i < maxClouds; i++) {
    clouds.push(new Cloud(random(width), random(height / 2)));
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

canvas-setup Canvas creation createCanvas(windowWidth, windowHeight);

Makes the canvas fill the entire browser window

initialization Basket initialization canasta = createVector(width / 2, height - 30);

Positions the basket in the center-bottom of the screen

configuration HSB color mode colorMode(HSB, 360, 100, 100);

Switches to Hue-Saturation-Brightness color system for easier fruit coloring

for-loop Cloud spawning loop for (let i = 0; i < maxClouds; i++) {

Creates 5 cloud objects scattered across the top half of the screen

createCanvas(windowWidth, windowHeight);
Creates a canvas as wide and tall as your entire browser window—responsive to window size
canasta = createVector(width / 2, height - 30);
Creates a p5.Vector object at the horizontal center and 30 pixels from the bottom—this is where the basket starts
colorMode(HSB, 360, 100, 100);
Switches from RGB (Red-Green-Blue) to HSB (Hue-Saturation-Brightness)—makes it easier to create vibrant fruit colors
noCursor();
Hides the system mouse cursor so the game feels more immersive
for (let i = 0; i < maxClouds; i++) {
Loops 5 times (maxClouds = 5) to create an array of cloud objects
clouds.push(new Cloud(random(width), random(height / 2)));
Adds a new Cloud instance at a random x position and random y in the top half of the screen

draw()

The draw() function is the heart of every p5.js sketch—it runs 60 times per second. This sketch uses a switch statement as a 'state machine' to decide what to show on screen. This pattern makes it easy to add new screens or game modes without tangling all the logic together.

function draw() {
  switch (gameState) {
    case 'START':
      drawStartScreen();
      break;
    case 'PLAYING':
      drawPlayingScreen();
      break;
    case 'GAMEOVER':
      drawGameOverScreen();
      break;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Game state dispatcher switch (gameState) {

Routes to different screen functions based on whether the game is starting, running, or over

switch (gameState) {
Checks the current gameState variable (either 'START', 'PLAYING', or 'GAMEOVER')
case 'START':
If gameState is 'START', the next line runs
drawStartScreen();
Calls the function that displays the title, instructions, and 'Click to Start' message
case 'PLAYING':
If gameState is 'PLAYING', the game screen displays
drawPlayingScreen();
Calls the function that draws the basket, falling fruits, particles, and HUD
case 'GAMEOVER':
If gameState is 'GAMEOVER', this shows the end screen
drawGameOverScreen();
Calls the function that displays the final score and 'Click to Play Again' message

drawStartScreen()

This function demonstrates how to create a visually appealing menu screen using layered rectangles for a background gradient, animated clouds for life, and text at different sizes to create visual hierarchy. The sky background and drifting clouds set the scene before gameplay begins.

🔬 The title uses textSize(64) and the instructions use textSize(24). What happens if you swap these two numbers so instructions are huge and the title is tiny?

  fill(0, 0, 100); // Blanco
  textSize(64);
  textAlign(CENTER, CENTER);
  text("¡Atrapa las Frutas!", width / 2, height / 2 - 100);

  textSize(24);
function drawStartScreen() {
  background(210, 50, 95); // Azul claro
  fill(210, 50, 75);
  noStroke();
  rect(0, height / 2, width, height / 2); // Parte inferior más oscura

  // Draw clouds
  for (let cloud of clouds) {
    cloud.update();
    cloud.show();
  }

  fill(0, 0, 100); // Blanco
  textSize(64);
  textAlign(CENTER, CENTER);
  text("¡Atrapa las Frutas!", width / 2, height / 2 - 100);

  textSize(24);
  text("Instrucciones:", width / 2, height / 2 - 20);
  text("Usa el ratón para mover la canasta.", width / 2, height / 2 + 10);
  text("Atrapa frutas para ganar puntos.", width / 2, height / 2 + 40);
  text("Evita que caigan al suelo para no perder vidas.", width / 2, height / 2 + 70);
  text("Frutas especiales te darán bonus o retos.", width / 2, height / 2 + 100);

  textSize(32);
  text("Haz clic para Empezar", width / 2, height / 2 + 180);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

visual-effect Background gradient background(210, 50, 95); // Azul claro

Creates a light blue top half; the next rect makes the bottom half darker blue

for-loop Cloud animation loop for (let cloud of clouds) {

Updates and draws each cloud so they drift from right to left during the start screen

text-rendering Game title text("¡Atrapa las Frutas!", width / 2, height / 2 - 100);

Displays the game name in large white text centered on screen

text-rendering Instructions text text("Usa el ratón para mover la canasta.", width / 2, height / 2 + 10);

Shows game rules in smaller text below the title

background(210, 50, 95); // Azul claro
Fills the entire canvas with a light blue color (HSB: hue 210, saturation 50, brightness 95)
fill(210, 50, 75);
Sets the fill color to a darker blue (same hue, same saturation, but brightness 75)
rect(0, height / 2, width, height / 2);
Draws a rectangle starting at the vertical midpoint and extending to the bottom—creates a two-tone sky effect
for (let cloud of clouds) {
Loops through each Cloud object in the clouds array
cloud.update();
Calls the update method on the cloud, which moves it left and respawns it from the right if off-screen
cloud.show();
Calls the show method to draw the cloud at its current position
fill(0, 0, 100); // Blanco
Sets fill color to pure white (brightness 100, no saturation)
textSize(64);
Sets the size of subsequent text to 64 pixels—large and prominent for the title
text("¡Atrapa las Frutas!", width / 2, height / 2 - 100);
Draws the game title centered horizontally and 100 pixels above the vertical midpoint

drawPlayingScreen()

drawPlayingScreen() is where all the game action happens each frame. Notice the order: background clears the screen, clouds animate, game logic updates (fruit movement and collision), particles update and draw, HUD displays scores, combo messages show and fade, and finally the basket follows the mouse. This layering creates depth—sky, fruits, particles, UI, and basket appear in the correct visual order.

🔬 These lines control what draws in what order. What happens if you move dibujarCanasta() to AFTER the particle loop? Will the basket appear in front of the particles or behind them?

  dibujarCanasta();
  actualizarFrutas();
  dibujarFrutas();

  // Actualiza y dibuja partículas
  for (let i = particulas.length - 1; i >= 0; i--) {
function drawPlayingScreen() {
  background(210, 50, 95); // Azul claro
  fill(210, 50, 75);
  noStroke();
  rect(0, height / 2, width, height / 2); // Parte inferior más oscura

  // Draw clouds
  for (let cloud of clouds) {
    cloud.update();
    cloud.show();
  }

  dibujarCanasta();
  actualizarFrutas();
  dibujarFrutas();

  // Actualiza y dibuja partículas
  for (let i = particulas.length - 1; i >= 0; i--) {
    particulas[i].actualizar();
    particulas[i].dibujar();
    if (particulas[i].estaMuerta()) {
      particulas.splice(i, 1);
    }
  }

  dibujarHUD();

  // Display combo message
  if (comboMessage && millis() < comboMessageTimer + comboMessageDuration) {
    fill(0, 0, 0, 200); // Negro con transparencia
    textSize(36);
    textAlign(CENTER, CENTER);
    text(comboMessage, width / 2, height / 4);
  }

  canasta.x = mouseX;
  canasta.x = constrain(canasta.x, anchoCanasta / 2, width - anchoCanasta / 2);
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

visual-setup Background and clouds background(210, 50, 95);

Sets the sky color and draws drifting clouds every frame

game-update Game update sequence actualizarFrutas();

Handles fruit spawning, movement, collision detection, and state changes

for-loop Particle update and cleanup for (let i = particulas.length - 1; i >= 0; i--) {

Updates each particle and removes dead ones to prevent memory buildup

conditional Combo message timing if (comboMessage && millis() < comboMessageTimer + comboMessageDuration) {

Shows '+Combo x2!' messages that fade away after 1 second

input-handling Basket mouse control canasta.x = mouseX;

Makes the basket follow the mouse horizontally

background(210, 50, 95); // Azul claro
Clears the canvas with light blue every frame, erasing the previous frame's drawing
for (let cloud of clouds) {
Loops through all cloud objects
cloud.update();
Moves each cloud left; they respawn from the right if they drift off-screen
dibujarCanasta();
Draws the basket at canasta.x and canasta.y
actualizarFrutas();
The core game logic—spawns new fruits, moves falling ones, checks collisions with the basket, and ends the game if you run out of lives
dibujarFrutas();
Draws all fruits currently falling on screen using their type (apple, orange, banana, etc.)
for (let i = particulas.length - 1; i >= 0; i--) {
Loops backwards through the particles array (backwards is safer when removing items)
particulas[i].actualizar();
Updates the particle's position using its velocity and gravity
particulas[i].dibujar();
Draws the particle (a small colored circle) at its current position
if (particulas[i].estaMuerta()) {
Checks if the particle's lifespan has decreased to 0
particulas.splice(i, 1);
Removes the dead particle from the array, cleaning up memory
dibujarHUD();
Draws the score and lives display in the top-left and top-right corners
if (comboMessage && millis() < comboMessageTimer + comboMessageDuration) {
Checks if a combo message exists AND if the current time is still within 1 second of catching the fruit
text(comboMessage, width / 2, height / 4);
Displays the combo message (like '+Combo x2! +1') near the top-center of the screen
canasta.x = mouseX;
Moves the basket to follow the mouse's horizontal position
canasta.x = constrain(canasta.x, anchoCanasta / 2, width - anchoCanasta / 2);
Keeps the basket from moving off the left or right edges of the screen

drawGameOverScreen()

This screen demonstrates a common UI pattern: a semi-transparent overlay with centered text. The darkness draws the eye to the message while the translucency hints that the game world is still there underneath. The template literal syntax (backticks with ${}) is a clean way to insert variable values into strings.

function drawGameOverScreen() {
  fill(0, 0, 0, 150); // Fondo semitransparente
  rect(0, 0, width, height);

  fill(0, 0, 100); // Blanco
  textSize(64);
  textAlign(CENTER, CENTER);
  text("¡Juego Terminado!", width / 2, height / 2 - 50);

  textSize(32);
  text(`Tu Puntuación Final: ${puntuacion}`, width / 2, height / 2 + 20);

  textSize(24);
  text("Haz clic para Jugar de Nuevo", width / 2, height / 2 + 80);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

visual-effect Semi-transparent overlay fill(0, 0, 0, 150); // Fondo semitransparente

Darkens the screen with a translucent black rectangle so the game over message stands out

text-rendering Game over message text("¡Juego Terminado!", width / 2, height / 2 - 50);

Announces that the game is over in large white text

text-rendering Final score display text(`Tu Puntuación Final: ${puntuacion}`, width / 2, height / 2 + 20);

Shows the player their final score using template literal syntax

fill(0, 0, 0, 150); // Fondo semitransparente
Sets fill color to black with transparency (the fourth number 150 is alpha—lower = more transparent)
rect(0, 0, width, height);
Draws a semi-transparent rectangle covering the entire canvas, darkening everything underneath
fill(0, 0, 100); // Blanco
Sets fill color to pure white for text
textSize(64);
Makes the game over message very large
text("¡Juego Terminado!", width / 2, height / 2 - 50);
Draws the game over message centered and 50 pixels above the vertical midpoint
textSize(32);
Makes the score display medium-sized, smaller than the title but bigger than instructions
text(`Tu Puntuación Final: ${puntuacion}`, width / 2, height / 2 + 20);
Displays the score using template literal syntax—${puntuacion} inserts the current score value into the string

dibujarCanasta()

This function shows how to compose a complex shape from multiple simpler shapes—rectangles for the body and rim, arcs for the handles. Using variables like anchoCanasta and altoCanasta makes it easy to resize the entire basket by changing one number, which is much cleaner than hardcoding dimensions.

🔬 These two rectangles create the basket shape. What happens if you change altoCanasta from 20 to 50 to make the basket much taller? How does that affect gameplay and fruit catching?

  rect(canasta.x, canasta.y, anchoCanasta, altoCanasta, 5); // Canasta redondeada
  rect(canasta.x, canasta.y - 15, anchoCanasta - 20, 10, 3); // Borde superior
function dibujarCanasta() {
  fill(30, 80, 70); // Marrón oscuro
  noStroke();
  rectMode(CENTER);
  rect(canasta.x, canasta.y, anchoCanasta, altoCanasta, 5); // Canasta redondeada
  rect(canasta.x, canasta.y - 15, anchoCanasta - 20, 10, 3); // Borde superior

  // Dibuja un asa para la canasta
  stroke(30, 80, 70);
  strokeWeight(3);
  noFill();
  arc(canasta.x - anchoCanasta / 3, canasta.y - 30, anchoCanasta / 3, 20, PI, TWO_PI);
  arc(canasta.x + anchoCanasta / 3, canasta.y - 30, anchoCanasta / 3, 20, PI, TWO_PI);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

shape-drawing Main basket body rect(canasta.x, canasta.y, anchoCanasta, altoCanasta, 5);

Draws the main rectangular body of the basket with rounded corners

shape-drawing Basket rim rect(canasta.x, canasta.y - 15, anchoCanasta - 20, 10, 3);

Draws the top edge/rim of the basket slightly narrower than the body

shape-drawing Basket handles arc(canasta.x - anchoCanasta / 3, canasta.y - 30, anchoCanasta / 3, 20, PI, TWO_PI);

Draws two arc-shaped handles on the left and right sides of the basket

fill(30, 80, 70); // Marrón oscuro
Sets fill color to dark brown for the basket body
noStroke();
Removes outlines from shapes so only the filled areas show
rectMode(CENTER);
Changes rectangle behavior so the width and height are measured from the center point, not the top-left
rect(canasta.x, canasta.y, anchoCanasta, altoCanasta, 5);
Draws the main basket rectangle with rounded corners (the last 5 is the corner radius in pixels)
rect(canasta.x, canasta.y - 15, anchoCanasta - 20, 10, 3);
Draws a narrower rectangle above the main body as a rim, positioned 15 pixels higher
stroke(30, 80, 70);
Sets stroke color to dark brown so handles match the basket body
strokeWeight(3);
Makes the stroke 3 pixels thick for visible handles
noFill();
Turns off fill so only the arc outlines show—no solid shapes inside
arc(canasta.x - anchoCanasta / 3, canasta.y - 30, anchoCanasta / 3, 20, PI, TWO_PI);
Draws the left handle as an arc starting from PI radians (bottom) to TWO_PI (full circle), creating a semicircle above the basket
arc(canasta.x + anchoCanasta / 3, canasta.y - 30, anchoCanasta / 3, 20, PI, TWO_PI);
Draws the right handle at the same height as the left one, creating a symmetrical basket

actualizarFrutas()

This is the core of the game. It manages fruit spawning (using a counter to space them out), moves all fruits every frame, detects collisions with the basket using four simultaneous boundary checks, awards points and handles special items, plays feedback sounds, spawns particles, and detects when fruits are missed. The difficulty progression (increasing speed and frequency) keeps the game from getting stale. Understanding this function is key to grasping how real games work.

🔬 This block controls game difficulty—it increments a counter, spawns fruit, and speeds up the game. What happens if you change velocidadFrutas += 0.05 to velocidadFrutas += 0.5? How much faster and harder does the game become?

  contadorFrutas++;
  if (contadorFrutas >= frecuenciaFrutas) {
    crearFruta();
    contadorFrutas = 0;
    velocidadFrutas += 0.05;
    frecuenciaFrutas = constrain(frecuenciaFrutas - 1, 20, 60);

🔬 These four lines define the 'catch zone'—the box where fruits must be to be caught. What happens if you add 20 to the altoCanasta / 2 values? Does the catch zone get bigger or smaller? Try it and predict first!

    if (frutas[i].y > canasta.y - altoCanasta / 2 &&
        frutas[i].y < canasta.y + altoCanasta / 2 &&
        frutas[i].x > canasta.x - anchoCanasta / 2 &&
        frutas[i].x < canasta.x + anchoCanasta / 2) {
function actualizarFrutas() {
  contadorFrutas++;
  if (contadorFrutas >= frecuenciaFrutas) {
    crearFruta();
    contadorFrutas = 0;
    velocidadFrutas += 0.05;
    frecuenciaFrutas = constrain(frecuenciaFrutas - 1, 20, 60); // Más frecuente y rápido
  }

  for (let i = frutas.length - 1; i >= 0; i--) {
    frutas[i].y += velocidadFrutas;

    if (frutas[i].y > canasta.y - altoCanasta / 2 &&
        frutas[i].y < canasta.y + altoCanasta / 2 &&
        frutas[i].x > canasta.x - anchoCanasta / 2 &&
        frutas[i].x < canasta.x + anchoCanasta / 2) {
      
      // Atrapado: reproduce sonido y maneja el ítem
      reproducirSonido(sonidoAtrapar, 0.5, 0.1);
      
      // Combo Logic
      const currentTime = millis();
      if (currentTime - lastCatchTime < comboWindow) {
        comboCount++;
        let bonusPoints = 0;
        if (comboCount === 2) bonusPoints = 1;
        else if (comboCount === 3) bonusPoints = 2;
        else if (comboCount >= 4) bonusPoints = 3;
        
        if (bonusPoints > 0) {
          puntuacion += bonusPoints;
          comboMessage = `¡Combo x${comboCount}! +${bonusPoints}`;
          comboMessageTimer = currentTime;
          reproducirSonido(sonidoAtrapar, 0.7, 0.1, 900 + comboCount * 50); // Sonido más agudo para combos
        }
      } else {
        comboCount = 1;
        comboMessage = ''; // Reset message if combo broken
      }
      lastCatchTime = currentTime;

      manejarItem(frutas[i]);
      crearParticulas(frutas[i].x, frutas[i].y, 10, 0, 100, 80); // Partículas rojas por defecto
      frutas.splice(i, 1);
    }
    else if (frutas[i].y > height) {
      // Perdido: reproduce sonido y maneja el ítem (solo para frutas normales)
      if (frutas[i].tipo < ITEM_ORO) { // Solo frutas normales restan vida al caer
        vidas--;
        reproducirSonido(sonidoPerder, 0.5, 0.2);
        crearParticulas(frutas[i].x, frutas[i].y, 10, 0, 100, 80); // Partículas rojas
      } else if (frutas[i].tipo === ITEM_BOMBA) {
        // La bomba no hace daño si cae al suelo sin ser atrapada
      }
      frutas.splice(i, 1);
      // Reset combo if a fruit is lost
      comboCount = 0;
      comboMessage = '';
      if (vidas <= 0) {
        //juegoTerminado = true; // Redundant
        reproducirSonido(sonidoGameOver, 1.0, 1.0); // Sonido de Game Over
        musicaFondo.amp(0, 0.5); // Desvanece la música de fondo
        gameState = 'GAMEOVER';
      }
    }
  }
}
Line-by-line explanation (27 lines)

🔧 Subcomponents:

conditional Fruit spawn timer if (contadorFrutas >= frecuenciaFrutas) {

Tracks frames and spawns a new fruit when the counter reaches the frequency threshold

calculation Difficulty progression velocidadFrutas += 0.05;

Gradually increases fruit fall speed and spawn frequency each time a fruit is created

calculation Fruit fall update frutas[i].y += velocidadFrutas;

Moves each fruit down the screen by increasing its y position

conditional Catch detection if (frutas[i].y > canasta.y - altoCanasta / 2 &&

Checks if fruit overlaps with basket position using four boundary conditions

conditional Combo calculation if (currentTime - lastCatchTime < comboWindow) {

Checks if the new catch happened within 500ms of the last one to award bonus points

conditional Fruit missed logic else if (frutas[i].y > height) {

Handles fruit that falls off the bottom—loses a life unless it's a special item

contadorFrutas++;
Increments the counter by 1 each frame
if (contadorFrutas >= frecuenciaFrutas) {
Every 60 frames (initially), this condition becomes true and a new fruit spawns
crearFruta();
Calls the function that creates a new fruit object with random position and type
contadorFrutas = 0;
Resets the counter to 0 so the next fruit will spawn after another 60 frames (or fewer as the game speeds up)
velocidadFrutas += 0.05;
Increases fall speed by 0.05 pixels per frame each time a fruit spawns—the game gets harder over time
frecuenciaFrutas = constrain(frecuenciaFrutas - 1, 20, 60);
Decreases spawn frequency by 1 each time, making fruits appear more often, but constrain() keeps it between 20 and 60
for (let i = frutas.length - 1; i >= 0; i--) {
Loops backwards through all fruits (backwards is safer when we might remove items)
frutas[i].y += velocidadFrutas;
Moves the fruit down by adding its speed to its y position each frame
if (frutas[i].y > canasta.y - altoCanasta / 2 &&
First condition: checks if fruit's y is below the top edge of the basket
frutas[i].y < canasta.y + altoCanasta / 2 &&
Second condition: checks if fruit's y is above the bottom edge of the basket
frutas[i].x > canasta.x - anchoCanasta / 2 &&
Third condition: checks if fruit's x is to the right of the basket's left edge
frutas[i].x < canasta.x + anchoCanasta / 2) {
Fourth condition: checks if fruit's x is to the left of the basket's right edge—if all four are true, the fruit is caught
reproducirSonido(sonidoAtrapar, 0.5, 0.1);
Plays the catch sound at 50% amplitude, fading over 0.1 seconds
const currentTime = millis();
Stores the current time in milliseconds since the sketch started
if (currentTime - lastCatchTime < comboWindow) {
Checks if less than 500 milliseconds have passed since the last catch—if so, the combo continues
comboCount++;
Increments the combo counter by 1
if (comboCount === 2) bonusPoints = 1;
If this is the 2nd catch in a combo, award 1 bonus point
puntuacion += bonusPoints;
Adds the bonus points to the total score
comboMessage = `¡Combo x${comboCount}! +${bonusPoints}`;
Creates a message like '¡Combo x2! +1' using template literal syntax
manejarItem(frutas[i]);
Calls the function to handle the specific item—adds points or lives based on fruit type
crearParticulas(frutas[i].x, frutas[i].y, 10, 0, 100, 80);
Creates 10 red particles at the catch location to celebrate
frutas.splice(i, 1);
Removes the caught fruit from the frutas array
else if (frutas[i].y > height) {
Checks if the fruit has fallen below the bottom of the screen
if (frutas[i].tipo < ITEM_ORO) {
Checks if it's a normal fruit (types 0-3) that should cost a life when missed
vidas--;
Loses one life
if (vidas <= 0) {
If you run out of lives, the game ends
gameState = 'GAMEOVER';
Switches to the game over screen

crearFruta()

This function encodes the game's randomness strategy: most fruit is regular (70%), some are special (15% gold or heart), and a few are dangerous (15% bombs). The probability system is additive—each condition stacks on the previous ones (0-0.7, 0.7-0.85, 0.85-1.0). This is a clean way to set unequal probabilities without if-else chains. Tweaking these percentages is one of the easiest ways to rebalance your game.

🔬 This probability system uses three if-else conditions. What happens if you change ALL THREE thresholds to 0.33, 0.66, and beyond? You would have equal chances of normal, special, and bombs—try it and see how chaotic the game becomes!

  // Mayor probabilidad de frutas normales, menor de especiales
  let chance = random();
  if (chance < 0.7) { // 70% chance de fruta normal
    tipo = floor(random(4));
  } else if (chance < 0.85) { // 15% chance de fruta dorada o corazón
    tipo = random() < 0.5 ? ITEM_ORO : ITEM_CORAZON;
  } else { // 15% chance de bomba
function crearFruta() {
  let x = random(width);
  let y = -20;
  let tipo;

  // Mayor probabilidad de frutas normales, menor de especiales
  let chance = random();
  if (chance < 0.7) { // 70% chance de fruta normal
    tipo = floor(random(4));
  } else if (chance < 0.85) { // 15% chance de fruta dorada o corazón
    tipo = random() < 0.5 ? ITEM_ORO : ITEM_CORAZON;
  } else { // 15% chance de bomba
    tipo = ITEM_BOMBA;
  }
  frutas.push({x: x, y: y, tipo: tipo});
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Random spawn position let x = random(width);

Picks a random x position anywhere across the canvas width

conditional Fruit type probability let chance = random();

Generates a random number to determine which fruit type spawns

array-operation Add to array frutas.push({x: x, y: y, tipo: tipo});

Creates a new fruit object and adds it to the frutas array

let x = random(width);
Generates a random x position between 0 and the canvas width—fruit spawns at a random horizontal location
let y = -20;
Sets y to -20, which is above the visible canvas—fruits appear to fall in from the top
let chance = random();
Generates a random number between 0 and 1 (e.g., 0.35, 0.87, 0.12)
if (chance < 0.7) { // 70% chance de fruta normal
If the random number is less than 0.7, a normal fruit spawns 70% of the time
tipo = floor(random(4));
Picks a random integer from 0 to 3 (apple, orange, banana, or grapes)
} else if (chance < 0.85) { // 15% chance de fruta dorada o corazón
If the random number is between 0.7 and 0.85, a special fruit (gold or heart) spawns 15% of the time
tipo = random() < 0.5 ? ITEM_ORO : ITEM_CORAZON;
Uses a ternary operator (?:)—if another random number is less than 0.5, it's gold; otherwise it's a heart
} else { // 15% chance de bomba
If the random number is 0.85 or higher, a bomb spawns 15% of the time
tipo = ITEM_BOMBA;
Sets the fruit type to ITEM_BOMBA
frutas.push({x: x, y: y, tipo: tipo});
Creates a new object with x, y, and tipo properties and adds it to the frutas array using push()

dibujarManzana()

This is a simple fruit drawing function using only two shapes: an ellipse for the apple body and a rectangle for the stem. The stem is positioned y - 20 (20 pixels above the center) so it sits on top of the fruit. Each fruit type in this sketch follows a similar pattern—a main color for the body and a different color for details like stems or leaves.

function dibujarManzana(x, y) {
  fill(0, 100, 80); // Rojo brillante
  noStroke();
  ellipse(x, y, 30, 30);
  fill(60, 80, 50); // Verde oscuro para el tallo
  rect(x, y - 20, 2, 8);
}
Line-by-line explanation (5 lines)
fill(0, 100, 80); // Rojo brillante
Sets fill color to bright red using HSB (hue 0 is red, saturation 100, brightness 80)
noStroke();
Removes outline so only solid color shows
ellipse(x, y, 30, 30);
Draws a circle (ellipse with equal width and height) 30 pixels in diameter at position (x, y)
fill(60, 80, 50); // Verde oscuro para el tallo
Changes fill color to dark green for the stem
rect(x, y - 20, 2, 8);
Draws a small brown rectangle 2 pixels wide and 8 pixels tall, positioned above the apple circle as a stem

dibujarNaranja()

The orange is slightly larger than the apple (35 vs 30 pixels) and uses a small leaf instead of a stem. This visual variety helps players quickly distinguish fruits.

function dibujarNaranja(x, y) {
  fill(30, 100, 90); // Naranja brillante
  noStroke();
  ellipse(x, y, 35, 35);
  fill(60, 80, 50); // Verde oscuro para el tallo
  ellipse(x + 10, y - 15, 5, 5);
}
Line-by-line explanation (4 lines)
fill(30, 100, 90); // Naranja brillante
Sets fill color to bright orange (hue 30)
ellipse(x, y, 35, 35);
Draws a circle 35 pixels in diameter
fill(60, 80, 50); // Verde oscuro para el tallo
Changes fill to dark green
ellipse(x + 10, y - 15, 5, 5);
Draws a tiny circle (5x5 pixels) offset to the upper right as a leaf

dibujarPlatano()

The banana uses an arc plus rectangles to create an organic curved shape. This shows how combining multiple simple shapes creates visual interest.

function dibujarPlatano(x, y) {
  fill(60, 80, 90); // Amarillo
  noStroke();
  arc(x, y, 40, 30, PI, TWO_PI);
  rect(x - 20, y, 40, 10);
  fill(30, 80, 70); // Marrón oscuro para el extremo
  rect(x + 15, y - 5, 5, 10);
}
Line-by-line explanation (5 lines)
fill(60, 80, 90); // Amarillo
Sets fill to yellow
arc(x, y, 40, 30, PI, TWO_PI);
Draws the top curve of a banana as an arc from PI (bottom of circle) to TWO_PI (full rotation), creating an upside-down U
rect(x - 20, y, 40, 10);
Draws a rectangle below the arc to complete the banana body
fill(30, 80, 70); // Marrón oscuro para el extremo
Changes fill to dark brown
rect(x + 15, y - 5, 5, 10);
Draws a small brown rectangle at the end of the banana as detail

dibujarUvas()

Grapes are made from five overlapping circles arranged in a pyramid. This is a great example of creating complex shapes from repetition—you could easily add more grapes by adding more ellipse() lines.

function dibujarUvas(x, y) {
  fill(270, 70, 70); // Morado
  noStroke();
  ellipse(x, y, 10, 10);
  ellipse(x - 8, y + 5, 10, 10);
  ellipse(x + 8, y + 5, 10, 10);
  ellipse(x - 4, y + 15, 10, 10);
  ellipse(x + 4, y + 15, 10, 10);
  fill(60, 80, 50); // Verde oscuro para el tallo
  rect(x, y - 10, 2, 10);
}
Line-by-line explanation (7 lines)
fill(270, 70, 70); // Morado
Sets fill to purple (hue 270)
ellipse(x, y, 10, 10);
Draws the top grape in a bunch of five
ellipse(x - 8, y + 5, 10, 10);
Draws the left grape in the middle row
ellipse(x + 8, y + 5, 10, 10);
Draws the right grape in the middle row
ellipse(x - 4, y + 15, 10, 10);
Draws the left grape in the bottom row
ellipse(x + 4, y + 15, 10, 10);
Draws the right grape in the bottom row
rect(x, y - 10, 2, 10);
Draws a stem above the bunch

dibujarFrutaOro()

The golden fruit includes a semi-transparent highlight, which gives it a glossy, premium feeling. This special fruit is worth 10 points, so the shiny appearance visually signals its value.

function dibujarFrutaOro(x, y) {
  fill(50, 100, 90); // Dorado
  noStroke();
  ellipse(x, y, 35, 35);
  fill(60, 80, 50); // Verde oscuro para el tallo
  rect(x, y - 20, 2, 8);
  fill(50, 100, 100, 50); // Brillo
  ellipse(x + 5, y - 5, 15, 15);
}
Line-by-line explanation (4 lines)
fill(50, 100, 90); // Dorado
Sets fill to golden yellow (hue 50, full saturation, brightness 90)
ellipse(x, y, 35, 35);
Draws the main golden fruit body
fill(50, 100, 100, 50); // Brillo
Sets fill to a bright, semi-transparent golden color (the fourth value 50 is alpha transparency)
ellipse(x + 5, y - 5, 15, 15);
Draws a highlight on the upper right to make the fruit look shiny

dibujarCorazon()

The heart uses bezierVertex() instead of straight lines to create smooth, organic curves. Bezier curves are defined by control points—four curves stacked together create the heart shape. This is one of the most complex drawing functions in the sketch, but the pattern is: start with vertex(), add curves with bezierVertex(), and close with endShape().

function dibujarCorazon(x, y) {
  fill(0, 100, 100); // Rojo puro
  noStroke();
  beginShape();
  vertex(x, y + 10);
  bezierVertex(x + 5, y + 5, x + 15, y, x + 10, y - 10);
  bezierVertex(x + 5, y - 15, x, y - 10, x, y - 10);
  bezierVertex(x, y - 10, x - 5, y - 15, x - 10, y - 10);
  bezierVertex(x - 15, y, x - 5, y + 5, x, y + 10);
  endShape(CLOSE);
}
Line-by-line explanation (5 lines)
fill(0, 100, 100); // Rojo puro
Sets fill to pure bright red
beginShape();
Starts drawing a custom shape made of vertices and bezier curves
vertex(x, y + 10);
Anchors the first point at the bottom point of the heart
bezierVertex(...);
Each bezierVertex() draws a curved section connecting the current point to a new point using smooth cubic bezier curves (imagine dragging a curve with control handles)
endShape(CLOSE);
Closes the shape by connecting the last point back to the first, creating a complete heart outline

dibujarBomba()

The bomb is simple but distinctive—a dark circle with a brown fuse and spark. The dark color immediately signals danger, teaching players to avoid it.

function dibujarBomba(x, y) {
  fill(0, 0, 30); // Negro oscuro
  noStroke();
  ellipse(x, y, 40, 40);
  fill(30, 80, 70); // Marrón para la mecha
  rect(x, y - 25, 5, 10);
  ellipse(x, y - 30, 8, 8); // Punta de la mecha
}
Line-by-line explanation (5 lines)
fill(0, 0, 30); // Negro oscuro
Sets fill to very dark (nearly black)
ellipse(x, y, 40, 40);
Draws a dark circle for the bomb body
fill(30, 80, 70); // Marrón para la mecha
Changes fill to brown for the fuse
rect(x, y - 25, 5, 10);
Draws a thin brown rectangle as the fuse stem
ellipse(x, y - 30, 8, 8); // Punta de la mecha
Draws a small circle at the top of the fuse to represent the spark

manejarItem()

This function demonstrates how game rewards and punishments are handled. Each fruit type has different consequences: normal fruits +1 point, special fruits offer big rewards (gold +10, heart +1 life), and bombs are dangerous (-20 points, -1 life). The switch statement with case fallthrough is a clean way to handle multiple types with similar behavior. Notice how different sounds and particle colors provide immediate visual/audio feedback for each action.

🔬 The golden fruit gives 10 points. What happens if you change the 10 to 50? The golden fruit becomes much more valuable—and suddenly you might catch more of them to farm easy points. This is game balance—try different values and see what feels fair

    case ITEM_ORO:
      puntuacion += 10; // Puntos extra
      reproducirSonido(sonidoAtrapar, 0.7, 0.1, 800); // Sonido más agudo para oro
      crearParticulas(fruta.x, fruta.y, 20, 50, 100, 90); // Partículas doradas
      break;
function manejarItem(fruta) {
  switch (fruta.tipo) {
    case ITEM_MANZANA:
    case ITEM_NARANJA:
    case ITEM_PLATANO:
    case ITEM_UVAS:
      puntuacion++;
      break;
    case ITEM_ORO:
      puntuacion += 10; // Puntos extra
      reproducirSonido(sonidoAtrapar, 0.7, 0.1, 800); // Sonido más agudo para oro
      crearParticulas(fruta.x, fruta.y, 20, 50, 100, 90); // Partículas doradas
      break;
    case ITEM_CORAZON:
      vidas++; // Vida extra
      reproducirSonido(sonidoAtrapar, 0.7, 0.1, 700); // Sonido agudo para corazón
      crearParticulas(fruta.x, fruta.y, 20, 0, 100, 100); // Partículas rojas
      break;
    case ITEM_BOMBA:
      puntuacion = max(0, puntuacion - 20); // Resta puntos, no menos de 0
      vidas--; // Pierde una vida
      reproducirSonido(sonidoPerder, 0.8, 0.3); // Sonido de explosión (simulado)
      crearParticulas(fruta.x, fruta.y, 30, 30, 100, 100); // Partículas naranjas/rojas para explosión
      if (vidas <= 0) {
        //juegoTerminado = true; // Redundant
        reproducirSonido(sonidoGameOver, 1.0, 1.0);
        musicaFondo.amp(0, 0.5);
        gameState = 'GAMEOVER';
      }
      break;
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

switch-case Normal fruit handling case ITEM_MANZANA:

Cases 0-3 all grant 1 point using case fallthrough

switch-case Golden fruit handling case ITEM_ORO:

Awards 10 points, plays a higher pitch sound, and creates golden particles

switch-case Heart handling case ITEM_CORAZON:

Restores 1 life and creates red particles

switch-case Bomb handling case ITEM_BOMBA:

Subtracts 20 points and 1 life, plays an explosion sound, and potentially ends the game

switch (fruta.tipo) {
Checks the fruit's type constant and routes to the appropriate case
case ITEM_MANZANA:
If type is ITEM_MANZANA (0), this case runs
case ITEM_NARANJA:
If type is ITEM_NARANJA (1), this case also runs because there's no break—this is 'case fallthrough'
puntuacion++;
Adds 1 point to the score
break;
Exits the switch so the remaining cases don't run
case ITEM_ORO:
If type is ITEM_ORO (golden fruit), execute this case
puntuacion += 10;
Adds 10 points instead of 1
reproducirSonido(sonidoAtrapar, 0.7, 0.1, 800);
Plays the catch sound at a higher pitch (800 Hz vs 500 for normal fruits) to signal a special item
crearParticulas(fruta.x, fruta.y, 20, 50, 100, 90);
Creates 20 golden particles (hue 50 = yellow/gold)
case ITEM_CORAZON:
If type is ITEM_CORAZON, execute this case
vidas++;
Adds 1 life
case ITEM_BOMBA:
If type is ITEM_BOMBA, execute this case
puntuacion = max(0, puntuacion - 20);
Subtracts 20 points but uses max() to prevent the score from going below 0
vidas--;
Loses 1 life
reproducirSonido(sonidoPerder, 0.8, 0.3);
Plays the 'lose' sound (explosion-like) at 80% volume over 0.3 seconds
if (vidas <= 0) {
Checks if you've run out of lives after catching the bomb
gameState = 'GAMEOVER';
Switches to the game over screen

dibujarHUD()

HUD stands for 'Heads-Up Display'—the UI that shows score and lives while the game plays. This function is simple: it just draws two text strings, one left-aligned and one right-aligned, creating visual balance. Notice textAlign() controls where text anchors—LEFT, RIGHT, CENTER for horizontal, and TOP, BOTTOM, CENTER for vertical.

function dibujarHUD() {
  fill(0, 0, 0); // Negro
  noStroke();
  textSize(24);
  textAlign(LEFT, TOP);
  text(`Puntuación: ${puntuacion}`, 20, 20);

  textAlign(RIGHT, TOP);
  text(`Vidas: ${vidas}`, width - 20, 20);
}
Line-by-line explanation (6 lines)
fill(0, 0, 0); // Negro
Sets text color to black
textSize(24);
Sets text size to 24 pixels
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically for the score
text(`Puntuación: ${puntuacion}`, 20, 20);
Draws the score in the top-left corner, 20 pixels from the edges
textAlign(RIGHT, TOP);
Aligns text to the right side for the lives display
text(`Vidas: ${vidas}`, width - 20, 20);
Draws lives count in the top-right corner, 20 pixels from the right edge

mousePressed()

mousePressed() is a special p5.js function that runs whenever the mouse is clicked. This game uses it as the input for starting gameplay. Notice userStartAudio()—this is a p5.sound requirement that initializes audio on user interaction (browsers block autoplay sound).

function mousePressed() {
  // Asegúrate de que el audio se inicie solo con interacción del usuario
  userStartAudio();
  if (gameState === 'START' || gameState === 'GAMEOVER') {
    reiniciarJuego();
    gameState = 'PLAYING';
    musicaFondo.amp(0.1, 0.5); // Start/restart music
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

initialization Audio context start userStartAudio();

Initializes the Web Audio API context (required by browsers for sound to work)

conditional Game state transition if (gameState === 'START' || gameState === 'GAMEOVER') {

Detects when the player clicks on START or GAMEOVER screens to begin gameplay

userStartAudio();
Initializes the Web Audio API—modern browsers require user interaction before playing sound, so this must be called on a click
if (gameState === 'START' || gameState === 'GAMEOVER') {
Checks if the game is on the start screen OR the game over screen
reiniciarJuego();
Calls the function that resets all game variables to starting values
gameState = 'PLAYING';
Switches game state to PLAYING, which makes draw() show the gameplay screen next frame
musicaFondo.amp(0.1, 0.5);
Fades in the background music to 10% volume over 0.5 seconds

reiniciarJuego()

This function resets every game variable to its starting state. When a player clicks 'Play Again,' this function is called to clean the slate—clearing arrays, resetting counters, and returning difficulty to easy. It's essential for replayability.

function reiniciarJuego() {
  puntuacion = 0;
  vidas = 3;
  velocidadFrutas = 3;
  frecuenciaFrutas = 60;
  frutas = [];
  particulas = []; // Limpia las partículas
  comboCount = 0;
  lastCatchTime = 0;
  comboMessage = '';
  comboMessageTimer = 0;
  juegoTerminado = false; // Redundant but kept for safety
  contadorFrutas = 0;
  // Music handled in mousePressed
}
Line-by-line explanation (10 lines)
puntuacion = 0;
Resets score to 0
vidas = 3;
Restores lives to 3
velocidadFrutas = 3;
Resets fruit fall speed to the initial slow speed
frecuenciaFrutas = 60;
Resets spawn frequency so fruits appear slowly at first
frutas = [];
Clears the array of falling fruits—removes all fruits from the screen
particulas = [];
Clears the array of particles—removes all visual effects
comboCount = 0;
Resets the combo counter to 0
lastCatchTime = 0;
Resets the timestamp of the last catch
comboMessage = '';
Clears any displayed combo message
contadorFrutas = 0;
Resets the spawn counter so the first fruit appears after 60 frames

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized. This game uses it to update the canvas size, reposition the basket, and regenerate clouds to fit the new space. This makes the game responsive and playable on any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  canasta.x = constrain(canasta.x, anchoCanasta / 2, width - anchoCanasta / 2);
  canasta.y = height - 30;
  // Reset cloud positions to fit new window size
  clouds = [];
  for (let i = 0; i < maxClouds; i++) {
    clouds.push(new Cloud(random(width), random(height / 2)));
  }
}
Line-by-line explanation (6 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to fit the browser window if the user stretches it
canasta.x = constrain(canasta.x, anchoCanasta / 2, width - anchoCanasta / 2);
Ensures the basket stays within bounds after the window size changes
canasta.y = height - 30;
Repositions the basket vertically in case the window height changed
clouds = [];
Clears the clouds array
for (let i = 0; i < maxClouds; i++) {
Loops 5 times to create new clouds
clouds.push(new Cloud(random(width), random(height / 2)));
Creates new clouds at random positions fitting the new window size

reproducirSonido()

This helper function manages synthesized sound playback. It takes an oscillator and three parameters: amplitude (volume), fadeTime (how long to play), and optionally a new frequency. The function fades up then down, creating a short beep. Optional parameters (freqValue = null) make the function flexible—you can reuse the same oscillator at different pitches.

function reproducirSonido(oscillator, ampValue, fadeTime, freqValue = null) {
  if (freqValue !== null) {
    oscillator.freq(freqValue);
  }
  oscillator.amp(ampValue, fadeTime);
  oscillator.amp(0, fadeTime * 2); // Desvanece rápidamente después
}
Line-by-line explanation (4 lines)
if (freqValue !== null) {
Checks if a frequency was passed as an argument—if null, the oscillator keeps its current frequency
oscillator.freq(freqValue);
Sets the oscillator's pitch to the specified frequency
oscillator.amp(ampValue, fadeTime);
Fades the amplitude UP to the specified volume over fadeTime seconds
oscillator.amp(0, fadeTime * 2);
Immediately after, fades the amplitude DOWN to 0 over twice the fadeTime, so the sound is brief

Particle()

The Particle class demonstrates object-oriented p5.js code. Each particle is an independent object with its own position, velocity, and color. The physics are simple but effective: acceleration (gravity) affects velocity, velocity affects position, and lifespan decreases each frame until the particle dies and is removed. This pattern scales beautifully—you can create 1 particle or 10,000 and the logic stays the same.

🔬 This method applies gravity, moves the particle, and fades it. What happens if you change 'this.acc = createVector(0, 0.1)' in the constructor to createVector(0, 0.3)? Particles fall faster. Then what if you change it to createVector(0, -0.1) to make gravity point UP instead of DOWN?

  actualizar() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.lifespan -= 5;
  }
class Particle {
  constructor(x, y, hu, sat, bright) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D();
    this.vel.mult(random(1, 5));
    this.acc = createVector(0, 0.1); // Gravedad
    this.lifespan = 255;
    this.hu = hu;
    this.sat = sat;
    this.bright = bright;
  }

  actualizar() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.lifespan -= 5;
  }

  dibujar() {
    noStroke();
    fill(this.hu, this.sat, this.bright, this.lifespan);
    ellipse(this.pos.x, this.pos.y, 8, 8);
  }

  estaMuerta() {
    return this.lifespan < 0;
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

constructor Particle constructor constructor(x, y, hu, sat, bright) {

Creates a new particle at position (x, y) with a specific HSB color

method actualizar() physics this.vel.add(this.acc);

Applies acceleration (gravity) to velocity, causing particles to fall

method dibujar() rendering fill(this.hu, this.sat, this.bright, this.lifespan);

Draws the particle with decreasing opacity as it ages

class Particle {
Declares a new class named Particle—a blueprint for particle objects
constructor(x, y, hu, sat, bright) {
The constructor function runs when you create a new Particle—it sets initial values
this.pos = createVector(x, y);
Stores the particle's position as a p5.Vector object
this.vel = p5.Vector.random2D();
Creates a random 2D direction vector (a direction at a random angle)
this.vel.mult(random(1, 5));
Scales that direction by a random number between 1 and 5, giving each particle a different speed
this.acc = createVector(0, 0.1);
Creates an acceleration vector pointing downward (gravity)—0 horizontal, 0.1 vertical
this.lifespan = 255;
Starts with full alpha (255 = fully opaque)
this.hu = hu;
Stores the hue (color) passed in
actualizar() {
Method that updates the particle each frame
this.vel.add(this.acc);
Adds acceleration to velocity—over time, particles fall faster (gravity effect)
this.pos.add(this.vel);
Adds velocity to position—moves the particle
this.lifespan -= 5;
Decreases alpha by 5 each frame, making the particle fade out
dibujar() {
Method that renders the particle
fill(this.hu, this.sat, this.bright, this.lifespan);
Sets fill color using the stored HSB values, with alpha from lifespan
ellipse(this.pos.x, this.pos.y, 8, 8);
Draws an 8-pixel circle at the particle's current position
estaMuerta() {
Method that checks if the particle should be removed
return this.lifespan < 0;
Returns true if alpha has dropped below 0, signaling the particle is dead and can be deleted

crearParticulas()

This is a factory function that creates multiple Particle objects at once. It encapsulates the loop, making code that calls crearParticulas() much cleaner than writing the loop every time you want particles. It's called whenever a fruit is caught to create visual celebration bursts.

function crearParticulas(x, y, cantidad, hu, sat, bright) {
  for (let i = 0; i < cantidad; i++) {
    particulas.push(new Particle(x, y, hu, sat, bright));
  }
}
Line-by-line explanation (3 lines)
function crearParticulas(x, y, cantidad, hu, sat, bright) {
Defines a function that takes position (x, y), quantity, and HSB color values
for (let i = 0; i < cantidad; i++) {
Loops 'cantidad' times (e.g., 10, 20, 30)
particulas.push(new Particle(x, y, hu, sat, bright));
Creates a new Particle object with the specified parameters and adds it to the particulas array

Cloud()

The Cloud class creates scrolling background elements using simple overlapping circles. The update() method handles left-to-right looping (when a cloud exits the left side, it respawns on the right with new randomized properties). This is an efficient way to create continuous scrolling backgrounds without creating infinite clouds—just recycle them. The semi-transparent white color (alpha 70) ensures clouds don't block the game action.

class Cloud {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.size = random(30, 80);
    this.speed = random(0.5, 1.5);
  }

  update() {
    this.pos.x -= this.speed;
    if (this.pos.x < -this.size * 2) {
      this.pos.x = width + this.size * 2;
      this.pos.y = random(height / 2);
      this.size = random(30, 80);
      this.speed = random(0.5, 1.5);
    }
  }

  show() {
    noStroke();
    fill(0, 0, 100, 70); // White, semi-transparent
    ellipse(this.pos.x, this.pos.y, this.size, this.size * 0.7);
    ellipse(this.pos.x + this.size * 0.3, this.pos.y, this.size * 0.8, this.size * 0.6);
    ellipse(this.pos.x - this.size * 0.3, this.pos.y, this.size * 0.8, this.size * 0.6);
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

constructor Cloud constructor constructor(x, y) {

Creates a cloud at random size and horizontal speed

method update() scrolling this.pos.x -= this.speed;

Moves the cloud left each frame and respawns it from the right if it drifts off-screen

method show() drawing ellipse(this.pos.x, this.pos.y, this.size, this.size * 0.7);

Draws three overlapping circles to create a puffy cloud shape

class Cloud {
Declares the Cloud class
constructor(x, y) {
Initializes a cloud at position (x, y)
this.pos = createVector(x, y);
Stores the cloud's position as a vector
this.size = random(30, 80);
Gives each cloud a random size between 30 and 80 pixels—clouds look different
this.speed = random(0.5, 1.5);
Gives each cloud a random speed between 0.5 and 1.5 pixels per frame
update() {
Method that moves the cloud each frame
this.pos.x -= this.speed;
Moves the cloud left (subtracts speed from x)
if (this.pos.x < -this.size * 2) {
Checks if the cloud has moved so far left it's completely off-screen
this.pos.x = width + this.size * 2;
Teleports the cloud back to the right side of the screen
this.pos.y = random(height / 2);
Randomizes the vertical position as it respawns for variety
this.size = random(30, 80);
Randomizes size again
show() {
Method that draws the cloud
fill(0, 0, 100, 70);
Sets fill to semi-transparent white
ellipse(this.pos.x, this.pos.y, this.size, this.size * 0.7);
Draws the main (center) circle of the cloud
ellipse(this.pos.x + this.size * 0.3, this.pos.y, this.size * 0.8, this.size * 0.6);
Draws a slightly smaller circle to the right to create the puffy shape
ellipse(this.pos.x - this.size * 0.3, this.pos.y, this.size * 0.8, this.size * 0.6);
Draws another circle to the left, completing the cloud outline

📦 Key Variables

canasta p5.Vector

Stores the x and y position of the basket on the canvas

let canasta = createVector(200, 500);
anchoCanasta number

Width of the basket in pixels—controls how wide the catch zone is

let anchoCanasta = 100;
altoCanasta number

Height of the basket in pixels—controls how tall the catch zone is

let altoCanasta = 20;
frutas array of objects

Array of falling fruit objects, each with x, y, and tipo (type) properties

let frutas = [{x: 100, y: 50, tipo: 0}, ...];
velocidadFrutas number

How many pixels fruits fall down the screen each frame—increases as game progresses

let velocidadFrutas = 3;
frecuenciaFrutas number

How many frames between spawning new fruits—decreases as game progresses, making fruits more frequent

let frecuenciaFrutas = 60;
contadorFrutas number

Counter that increments each frame; when it reaches frecuenciaFrutas, a new fruit spawns

let contadorFrutas = 0;
puntuacion number

Current player score—increases when fruits are caught, decreases if bombs are caught

let puntuacion = 0;
vidas number

Number of lives remaining—starts at 3, decreases when fruits fall or bombs are caught, game ends at 0

let vidas = 3;
gameState string

Current game state—either 'START' (title screen), 'PLAYING' (active game), or 'GAMEOVER' (end screen)

let gameState = 'START';
sonidoAtrapar p5.Oscillator

Synthesized sound that plays when a fruit is caught

let sonidoAtrapar = new p5.Oscillator();
particulas array of Particle objects

Array of particle effects that burst out when fruits are caught—provides visual feedback

let particulas = [particle1, particle2, ...];
comboCount number

Number of fruits caught in succession within the combo window—resets to 0 if a fruit is missed

let comboCount = 0;
lastCatchTime number

Timestamp (in milliseconds) of the most recent fruit catch—used to calculate combo windows

let lastCatchTime = 0;
comboWindow number

Time window in milliseconds (500) during which you must catch the next fruit to continue a combo

const comboWindow = 500;
comboMessage string

Text displayed on screen like '+Combo x2! +1' when a combo is achieved—empty string when no message

let comboMessage = '¡Combo x2! +1';
clouds array of Cloud objects

Array of scrolling cloud objects that create a moving background—5 clouds recycled continuously

let clouds = [cloud1, cloud2, ...];
ITEM_MANZANA number constant

Type ID for apple fruit (value: 0)—grants 1 point when caught

const ITEM_MANZANA = 0;
ITEM_ORO number constant

Type ID for golden fruit (value: 4)—grants 10 points and particles when caught

const ITEM_ORO = 4;
ITEM_BOMBA number constant

Type ID for bomb (value: 6)—subtracts 20 points and 1 life when caught

const ITEM_BOMBA = 6;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG actualizarFrutas() collision detection

Collision detection uses exact boundary comparisons without accounting for fruit radius. Fruits (drawn as 30-35px circles) can be caught slightly off-screen or miss when they should hit

💡 Add a fruit radius property to each fruit object and adjust collision boundaries: frutas[i].y > canasta.y - altoCanasta / 2 - 15 (where 15 is the typical fruit radius)

PERFORMANCE dibujarFrutas() drawing

Every fruit is drawn with a separate function call each frame. For hundreds of fruits, this becomes slow. Also, drawing code is scattered across 7 different functions

💡 Create a draw() method inside the fruit object itself, or use a lookup table: const fruitDrawers = [dibujarManzana, dibujarNaranja, ...]; fruitDrawers[tipo](x, y);

STYLE Global variables at top of sketch

Mix of different naming conventions: 'canasta' and 'basket' concepts, Spanish and English mixing inconsistently throughout (dibujar vs draw, velocidad vs speed)

💡 Choose one language for the codebase (suggest English for international students). Use consistent naming: basketX, basketY instead of canasta.x, canasta.y; or fully commit to Spanish

FEATURE Game difficulty progression

Game difficulty increases forever (frequency and speed never cap), eventually becoming unplayable. Eventually frecuenciaFrutas will be 0 and fruits spawn every frame infinitely

💡 Add caps: velocidadFrutas = constrain(velocidadFrutas + 0.05, 0, 10); and frecuenciaFrutas = constrain(frecuenciaFrutas - 1, 15, 60);

FEATURE Combo system

Combo message displays only if bonusPoints > 0, so combos at x1 (starting) don't show any feedback. Players don't realize they've started a combo chain

💡 Show combo counter at all times or display 'Combo!' message even for first catch: move comboMessage assignment outside the bonusPoints check

BUG reiniciarJuego()

The juegoTerminado variable is set to false but is never used anywhere else in the code—it's a leftover from earlier versions and causes confusion

💡 Remove the line 'juegoTerminado = false;' entirely and rely solely on gameState for game state management

🔄 Code Flow

Code flow showing preload, setup, draw, drawstartscreen, drawplayingscreen, drawgameoverscreen, dibujarcanzasta, actualizarfrutas, criarfruta, dibujarmanzana, dibujarnaranja, dibujarpaltano, dibujaruvas, dibujafrutaoro, dibujarcorazon, dibujabomba, manejaritem, dibujarhud, mousepressed, reiniciarvjuego, windowresized, reproducirsonido, particle, criarparticulas, cloud

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[canvas-creation] canvas-creation --> basket-init[basket-init] basket-init --> color-mode[color-mode] color-mode --> cloud-loop[cloud-loop] cloud-loop --> state-switch[state-switch] draw --> state-switch state-switch --> drawstartscreen[drawstartscreen] state-switch --> drawplayingscreen[drawplayingscreen] state-switch --> drawgameoverscreen[drawgameoverscreen] click drawstartscreen href "#fn-drawstartscreen" click drawplayingscreen href "#fn-drawplayingscreen" click drawgameoverscreen href "#fn-drawgameoverscreen" drawstartscreen --> bg-gradient[bg-gradient] bg-gradient --> cloud-drawing[cloud-drawing] cloud-drawing --> title-text[title-text] title-text --> instructions-loop[instructions-loop] instructions-loop --> drawstartscreen drawplayingscreen --> gameplay-bg[gameplay-bg] gameplay-bg --> game-logic[game-logic] game-logic --> particle-loop[particle-loop] particle-loop --> combo-display[combo-display] combo-display --> basket-tracking[basket-tracking] basket-tracking --> drawplayingscreen game-logic --> spawn-control[spawn-control] spawn-control --> difficulty-scaling[difficulty-scaling] difficulty-scaling --> fruit-movement[fruit-movement] fruit-movement --> catch-collision[catch-collision] catch-collision --> combo-logic[combo-logic] combo-logic --> miss-detection[miss-detection] miss-detection --> position-init[position-init] position-init --> type-randomization[type-randomization] type-randomization --> fruit-push[fruit-push] fruit-push --> normal-fruit[normal-fruit] fruit-push --> golden-fruit[golden-fruit] fruit-push --> heart-item[heart-item] fruit-push --> bomb-item[bomb-item] drawgameoverscreen --> overlay[overlay] overlay --> gameover-text[gameover-text] gameover-text --> score-display[score-display] click canvas-creation href "#sub-canvas-creation" click basket-init href "#sub-basket-init" click color-mode href "#sub-color-mode" click cloud-loop href "#sub-cloud-loop" click state-switch href "#sub-state-switch" click bg-gradient href "#sub-bg-gradient" click cloud-drawing href "#sub-cloud-drawing" click title-text href "#sub-title-text" click instructions-loop href "#sub-instructions-loop" click gameplay-bg href "#sub-gameplay-bg" click game-logic href "#sub-game-logic" click particle-loop href "#sub-particle-loop" click combo-display href "#sub-combo-display" click basket-tracking href "#sub-basket-tracking" click spawn-control href "#sub-spawn-control" click difficulty-scaling href "#sub-difficulty-scaling" click fruit-movement href "#sub-fruit-movement" click catch-collision href "#sub-catch-collision" click combo-logic href "#sub-combo-logic" click miss-detection href "#sub-miss-detection" click position-init href "#sub-position-init" click type-randomization href "#sub-type-randomization" click fruit-push href "#sub-fruit-push" click normal-fruit href "#sub-normal-fruit" click golden-fruit href "#sub-golden-fruit" click heart-item href "#sub-heart-item" click bomb-item href "#sub-bomb-item" click overlay href "#sub-overlay" click gameover-text href "#sub-gameover-text" click score-display href "#sub-score-display"

❓ Frequently Asked Questions

What visual experience does the cazador de manzanas sketch provide?

The sketch creates a vibrant scene with colorful falling fruits against a bright sky filled with drifting clouds, enhanced by juicy visual effects.

How can players interact with the cazador de manzanas game?

Players can move a basket using their mouse to catch falling fruits, while dodging bombs and aiming for combos to boost their score.

What creative coding concepts are showcased in the cazador de manzanas sketch?

This sketch demonstrates concepts such as collision detection, particle systems, and interactive sound effects to enhance gameplay.

Preview

cazador de manzanas - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of cazador de manzanas - Code flow showing preload, setup, draw, drawstartscreen, drawplayingscreen, drawgameoverscreen, dibujarcanzasta, actualizarfrutas, criarfruta, dibujarmanzana, dibujarnaranja, dibujarpaltano, dibujaruvas, dibujafrutaoro, dibujarcorazon, dibujabomba, manejaritem, dibujarhud, mousepressed, reiniciarvjuego, windowresized, reproducirsonido, particle, criarparticulas, cloud
Code Flow Diagram