fruit ninja 🥷

This is a Fruit Ninja-style game where players slice colorful fruits that arc across a bright sky using mouse drags or touch swipes. The challenge is avoiding bombs while racking up points, managing three lives, and surviving increasingly difficult waves of flying fruit.

đź§Ş Try This!

Experiment with the code by making these changes:

  1. Make fruit bigger and easier to slice — Increasing itemSize makes all fruit larger, giving you a bigger target to swipe at—the game becomes noticeably easier
  2. Make bombs appear more often — Lowering the bomb probability increases the frequency of bombs, raising the game's difficulty and tension significantly
  3. Add more starting lives — More lives let you miss more fruit before game over, making the game more forgiving and longer-lasting
  4. Make fruit launch slower and hang in the air longer — Lowering initialLaunchSpeed reduces gravity effect, giving fruit more airtime and more opportunity to slice before it falls off screen
  5. Change slice color to neon green — The slice trail is drawn with white stroke; changing it to bright green gives the game a more stylized, futuristic look
  6. Skip the start screen — Change gameState to 'playing' at launch so the game begins immediately instead of waiting for a click—useful during testing
Prefer the full editor? Open it there →

đź“– About This Sketch

This sketch recreates the addictive Fruit Ninja game mechanic entirely in p5.js. Players swipe across the canvas to slice fruit and earn points, but touching a bomb ends the game instantly. The visual appeal comes from colorful fruits with stems launching from the bottom of the screen in realistic arcs, combined with instant visual feedback (slice trails) and retro synthesizer sound effects that trigger on every action.

The code teaches several essential game development concepts: game state management (start, playing, gameOver), physics simulation with gravity and velocity, line-circle collision detection for slice-fruit interactions, and responsive input handling for both mouse and touch. You will learn how to structure a complete playable game, spawn random enemies procedurally, increase difficulty over time, and create satisfying audio-visual feedback that makes a simple mechanic feel polished and fun.

⚙️ How It Works

  1. When the sketch loads, setup() initializes a full-screen canvas and creates three p5.Oscillator objects for sound effects (high-pitched sine wave for slicing, low square wave for bombs, triangle wave for misses). The sketch waits on the start screen until the player clicks or taps.
  2. Once playing, the draw loop continuously updates and displays all falling items. Every 60 frames (or fewer as difficulty increases), a new fruit or bomb spawns near the bottom with upward velocity, then gravity pulls it back down in a realistic parabolic arc.
  3. As the player drags their mouse or finger, the mouseDragged() and touchMoved() handlers record the cursor positions into a slicePoints array and draw a white trail line behind it.
  4. The slice collision detector uses line-circle math to test whether the dragged line segment intersects any fruit circle. When a hit is detected, fruits increase the score and play a high beep, while bombs trigger game over and a low buzz.
  5. Fruits that fall off the bottom without being sliced cost one life. When lives reach zero, the game over screen appears with the final score. Clicking or tapping anywhere resets the game and returns to playing state.
  6. Difficulty scales automatically: every 10 points, items launch slightly faster; every 15 points, they spawn slightly more frequently, creating mounting pressure as the score climbs.

🎓 Concepts You'll Learn

Game state machinePhysics simulation (gravity, velocity, parabolic motion)Line-circle collision detectionTouch and mouse input handlingProcedural difficulty scalingAudio synthesis and sound effectsArray management and object spawningResponsive canvas resizing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It configures the canvas size, text appearance, and initializes all three sound oscillators. The p5.sound library is powerful for creating both simple beeps and complex synthesized sounds—oscillators let you generate pure sine, square, triangle, or sawtooth waves and control their frequency and volume dynamically.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER, CENTER);
  textSize(32);

  // Initialize p5.sound oscillators for simple effects
  sliceSound = new p5.Oscillator('sine');
  sliceSound.amp(0);
  sliceSound.freq(880); // High pitch for slicing
  sliceSound.start();

  bombSound = new p5.Oscillator('square');
  bombSound.amp(0);
  bombSound.freq(100); // Low pitch for bomb
  bombSound.start();

  missedSound = new p5.Oscillator('triangle');
  missedSound.amp(0);
  missedSound.freq(220); // Low pitch for missed fruit
  missedSound.start();

  userStartAudio(); // Required to enable audio playback in browsers
}
Line-by-line explanation (8 lines)

đź”§ Subcomponents:

calculation Canvas initialization createCanvas(windowWidth, windowHeight);

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

calculation Text configuration textAlign(CENTER, CENTER); textSize(32);

Centers all text at the point given to text() and sets default font size for UI

calculation Sound effect initialization sliceSound = new p5.Oscillator('sine'); sliceSound.amp(0); sliceSound.freq(880); sliceSound.start();

Creates three synthesizer oscillators (sine, square, triangle) that will play sound effects when triggered

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window—windowWidth and windowHeight are p5.js variables that update if the window resizes
textAlign(CENTER, CENTER);
Centers all text both horizontally and vertically at the coordinates you provide to text()
textSize(32);
Sets the default font size to 32 pixels for all text drawn after this line
sliceSound = new p5.Oscillator('sine');
Creates a sine wave oscillator object that will generate the high-pitched slicing sound
sliceSound.amp(0);
Sets the volume to 0 (silent) initially—it will be ramped up when a fruit is sliced
sliceSound.freq(880);
Sets the frequency to 880 Hz, which is a high musical note (A5) that sounds like a sharp 'slice'
sliceSound.start();
Starts the oscillator running—it stays running in the background until the sketch closes, and we control its volume to trigger sounds
userStartAudio();
Required by modern browsers to allow audio playback—browsers block audio until the user interacts with the page

draw()

draw() is p5.js's main loop—it runs 60 times per second by default. This sketch uses draw() as a state dispatcher: instead of handling all logic inside draw(), it delegates to three different functions based on gameState, keeping the code organized and readable. This pattern scales well to games with many screens (menus, pauses, difficulty selection, etc.).

function draw() {
  background(200, 230, 255); // Light blue sky

  if (gameState === 'start') {
    drawStartScreen();
  } else if (gameState === 'playing') {
    playGame();
  } else if (gameState === 'gameOver') {
    drawGameOverScreen();
  }
}
Line-by-line explanation (7 lines)

đź”§ Subcomponents:

switch-case Game state dispatcher if (gameState === 'start') { drawStartScreen(); } else if (gameState === 'playing') { playGame(); } else if (gameState === 'gameOver') { drawGameOverScreen(); }

Directs execution to the correct function based on which screen should be shown

background(200, 230, 255); // Light blue sky
Fills the entire canvas with a light blue color (RGB: 200, 230, 255) every frame, erasing the previous frame and creating the 'sky' background
if (gameState === 'start') {
Checks if the game is in the 'start' state (player hasn't clicked yet)
drawStartScreen();
If in start state, call the function that displays the title and start instructions
} else if (gameState === 'playing') {
Otherwise, check if the game is in the 'playing' state (active gameplay)
playGame();
If playing, call the main game loop that spawns fruit, detects slices, and updates the score
} else if (gameState === 'gameOver') {
Otherwise, the game must be in the 'gameOver' state (player hit a bomb or ran out of lives)
drawGameOverScreen();
Display the game over screen with the final score and restart instruction

drawStartScreen()

drawStartScreen() displays a simple menu before the game begins. Notice how it uses height / 2 and width / 2 to position text at the center, then adds or subtracts pixel values to space lines vertically. This keeps text centered even if the window is resized.

function drawStartScreen() {
  fill(0);
  text("FRUIT NINJA!", width / 2, height / 2 - 80);
  textSize(20);
  text("Slice the fruits, but DON'T slice the bombs!", width / 2, height / 2 - 40);
  text("Missing a fruit costs a life. You have " + lives + " lives.", width / 2, height / 2);
  textSize(24);
  text("Tap or click to Start!", width / 2, height / 2 + 80);
}
Line-by-line explanation (7 lines)
fill(0);
Sets the text color to black (0, 0, 0 in RGB)
text("FRUIT NINJA!", width / 2, height / 2 - 80);
Draws the title centered horizontally (width / 2) and 80 pixels above the vertical center
textSize(20);
Changes the font size to 20 pixels for the next lines of text
text("Slice the fruits, but DON'T slice the bombs!", width / 2, height / 2 - 40);
Displays the game rule centered at 40 pixels above the vertical center
text("Missing a fruit costs a life. You have " + lives + " lives.", width / 2, height / 2);
Shows the starting lives, concatenating the lives variable into the string so the number updates if changed
textSize(24);
Increases the font size to 24 pixels for the final message
text("Tap or click to Start!", width / 2, height / 2 + 80);
Displays the start instruction centered 80 pixels below the vertical center

drawGameOverScreen()

drawGameOverScreen() mirrors drawStartScreen() but shows the final score. The score variable is concatenated into the text string, so the displayed number updates automatically. This is a simple pattern for displaying dynamic game data on screen.

function drawGameOverScreen() {
  fill(0);
  textSize(48);
  text("GAME OVER!", width / 2, height / 2 - 60);
  textSize(32);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(24);
  text("Tap or click to Play Again!", width / 2, height / 2 + 80);
}
Line-by-line explanation (6 lines)
fill(0);
Sets text color to black
textSize(48);
Sets font size to 48 pixels—large and prominent for the game over message
text("GAME OVER!", width / 2, height / 2 - 60);
Draws the game over message centered, 60 pixels above the vertical center
text("Final Score: " + score, width / 2, height / 2);
Displays the player's final score by concatenating the score variable into the string
textSize(24);
Reduces font size to 24 pixels for the restart instruction
text("Tap or click to Play Again!", width / 2, height / 2 + 80);
Shows the restart prompt centered 80 pixels below the vertical center

playGame()

playGame() is the heart of the game loop. It handles item spawning, physics updates, rendering, collision detection setup (the items array is what mouseDragged() checks against), and loss conditions. The backwards loop over items is a pattern used in many games—by iterating from the end of the array backwards, you can safely remove items without the indices shifting and causing you to skip elements. The slice trail is drawn here from the slicePoints array, which is populated by mouse/touch handlers.

🔬 These three lines make the parabolic arc. What happens if you remove the gravity line (item.vy -= gravity) entirely? Will the fruit still move, or will it fly off the top of the screen?

    item.y -= item.vy; // Move up, then down
    item.vy -= gravity; // Apply gravity
    item.x += item.vx; // Move horizontally

🔬 Bombs are drawn as dark circles with orange fuses, and fruits as colored circles. What happens if you swap the bomb and fruit drawing code—so bombs become colorful and fruits become dark gray?

    if (item.name === 'bomb') { // It's a bomb
      fill(50); // Dark gray bomb
      ellipse(item.x, item.y, item.size);
      fill(255, 165, 0); // Orange fuse
      rect(item.x, item.y - item.size / 2 - 10, 5, 20);
    } else { // It's a fruit
      fill(item.color[0], item.color[1], item.color[2]);
      ellipse(item.x, item.y, item.size);
function playGame() {
  // Spawn new items
  itemSpawnCounter++;
  if (itemSpawnCounter >= itemSpawnRate) {
    spawnItem();
    itemSpawnCounter = 0;
  }

  // Update and draw items
  for (let i = items.length - 1; i >= 0; i--) {
    let item = items[i];
    item.y -= item.vy; // Move up, then down
    item.vy -= gravity; // Apply gravity
    item.x += item.vx; // Move horizontally

    // Draw item based on its name and colors
    if (item.name === 'bomb') { // It's a bomb
      fill(50); // Dark gray bomb
      ellipse(item.x, item.y, item.size);
      fill(255, 165, 0); // Orange fuse
      rect(item.x, item.y - item.size / 2 - 10, 5, 20);
    } else { // It's a fruit
      fill(item.color[0], item.color[1], item.color[2]);
      ellipse(item.x, item.y, item.size);
      if (item.stemColor) { // Draw stem only for fruits
        fill(item.stemColor[0], item.stemColor[1], item.stemColor[2]);
        rect(item.x, item.y - item.size / 2 - 5, 5, 10);
      }
    }

    // Remove items that go off screen (top, sides, or bottom)
    if (item.y > height + item.size / 2) { // Went off bottom
      if (item.name !== 'bomb') { // Only lose life if it was a fruit
        lives--;
        playMissedSound();
      }
      items.splice(i, 1);
    } else if (item.y < -item.size / 2 || item.x < -item.size / 2 || item.x > width + item.size / 2) {
      items.splice(i, 1); // Remove if off top or sides
    }
  }

  // Draw slice line if slicing
  if (slicing && slicePoints.length > 1) {
    stroke(255, 255, 255, 180); // White, semi-transparent
    strokeWeight(5);
    noFill();
    beginShape();
    for (let p of slicePoints) {
      vertex(p.x, p.y);
    }
    endShape();
  }

  // Display score and lives
  noStroke();
  fill(0);
  textSize(24);
  text("Score: " + score, 80, 40);
  text("Lives: " + lives, width - 80, 40);

  // Check loss condition
  if (lives <= 0) {
    gameState = 'gameOver';
    items = []; // Clear remaining items
    slicePoints = []; // Clear slice
  }
}
Line-by-line explanation (39 lines)

đź”§ Subcomponents:

conditional Item spawn timing itemSpawnCounter++; if (itemSpawnCounter >= itemSpawnRate) { spawnItem(); itemSpawnCounter = 0; }

Counts frames and spawns a new fruit or bomb when the counter reaches the spawn rate threshold

for-loop Update and render items for (let i = items.length - 1; i >= 0; i--) {

Iterates through all active items in reverse order (so removing items doesn't skip any) to update physics and draw them

calculation Physics simulation item.y -= item.vy; item.vy -= gravity; item.x += item.vx;

Simulates parabolic motion: moves the item by its velocity, then reduces velocity to simulate falling under gravity

conditional Draw bomb or fruit if (item.name === 'bomb') {

Checks item type and draws either a dark bomb with orange fuse or a colored fruit with stem

conditional Remove offscreen items if (item.y > height + item.size / 2) {

Detects when an item falls off the bottom and deducts a life if it was a fruit, then removes the item

conditional Draw slice trail if (slicing && slicePoints.length > 1) {

Draws a white semi-transparent line connecting recent mouse/touch positions to show the slice stroke

calculation Score and lives display text("Score: " + score, 80, 40); text("Lives: " + lives, width - 80, 40);

Renders the current score and remaining lives in the top corners of the screen

conditional Game over trigger if (lives <= 0) {

Detects when the player has lost all lives and transitions to the game over screen

itemSpawnCounter++;
Increments a counter that tracks how many frames have passed since the last item spawn
if (itemSpawnCounter >= itemSpawnRate) {
Checks if enough frames have elapsed—when true, it's time to spawn a new item
spawnItem();
Calls the spawn function to create a new fruit or bomb and add it to the items array
itemSpawnCounter = 0;
Resets the counter to zero so we start counting toward the next spawn
for (let i = items.length - 1; i >= 0; i--) {
Loops through the items array backwards (from last to first) so we can safely remove items without skipping any
item.y -= item.vy; // Move up, then down
Subtracts the velocity from y position—if vy is positive, y decreases (moves up); if vy is negative, y increases (moves down). The minus sign is intentional because in p5.js, y=0 is at the top
item.vy -= gravity; // Apply gravity
Reduces the upward velocity by the gravity amount each frame. Eventually vy becomes negative, reversing the vertical movement to create falling motion
item.x += item.vx; // Move horizontally
Adds the horizontal velocity to x, making fruit drift left or right as it arcs through the air
if (item.name === 'bomb') { // It's a bomb
Checks if the item is a bomb (as opposed to a fruit) to decide how to draw it
fill(50); // Dark gray bomb
Sets the fill color to dark gray (50, 50, 50) for the bomb body
ellipse(item.x, item.y, item.size);
Draws a dark gray circle at the item's current position with diameter equal to item.size
fill(255, 165, 0); // Orange fuse
Changes fill color to orange (255, 165, 0) for the bomb's fuse
rect(item.x, item.y - item.size / 2 - 10, 5, 20);
Draws a small orange rectangle above the bomb to represent a lit fuse—positioned 10 pixels above the top of the bomb
fill(item.color[0], item.color[1], item.color[2]);
Sets the fill color using the RGB values stored in the fruit's color array
if (item.stemColor) { // Draw stem only for fruits
Checks if the item has a stemColor property (fruits do, bombs don't) before drawing a stem
fill(item.stemColor[0], item.stemColor[1], item.stemColor[2]);
Sets the fill color to the fruit's stem color (usually green)
rect(item.x, item.y - item.size / 2 - 5, 5, 10);
Draws a small green rectangle above the fruit to represent its stem
if (item.y > height + item.size / 2) { // Went off bottom
Checks if the item has fallen below the bottom of the screen
if (item.name !== 'bomb') { // Only lose life if it was a fruit
If it's a fruit (not a bomb), deduct one life—bombs don't cost lives if missed
lives--;
Subtracts one from the lives counter
playMissedSound();
Plays a low-pitched sound effect to signal that a fruit was missed
items.splice(i, 1);
Removes the item from the array—splice(i, 1) deletes one element at index i
} else if (item.y < -item.size / 2 || item.x < -item.size / 2 || item.x > width + item.size / 2) {
Checks if the item has left the screen on the top or sides (no life penalty, just clean up)
if (slicing && slicePoints.length > 1) {
Draws the slice trail only if the player is currently dragging and has recorded at least 2 points
stroke(255, 255, 255, 180); // White, semi-transparent
Sets the line color to white with transparency (alpha=180 out of 255)
strokeWeight(5);
Sets the line thickness to 5 pixels for a bold, visible slice trail
beginShape();
Starts defining a shape (in this case, a connected line through multiple points)
for (let p of slicePoints) {
Loops through each recorded point in the slice points array
vertex(p.x, p.y);
Adds a vertex (point) at each recorded mouse/touch position
endShape();
Completes the shape, drawing lines connecting all vertices in order
noStroke();
Disables stroke for the following text—text should not have an outline
fill(0);
Sets the fill color to black for the score and lives text
textSize(24);
Sets font size to 24 pixels for the UI text
text("Score: " + score, 80, 40);
Displays the current score 80 pixels from the left and 40 pixels from the top
text("Lives: " + lives, width - 80, 40);
Displays the remaining lives 80 pixels from the right edge and 40 pixels from the top
if (lives <= 0) {
Checks if the player has run out of lives
gameState = 'gameOver';
Changes the game state to 'gameOver', which will trigger the game over screen on the next draw() call
items = []; // Clear remaining items
Empties the items array so no fruit or bombs are drawn during the game over screen
slicePoints = []; // Clear slice
Clears the slice points array so the slice trail disappears

spawnItem()

spawnItem() is a factory function—it creates and configures new game objects. Notice how it uses random() twice: once to pick fruit vs. bomb, and again to pick a specific fruit type. The difficulty scaling here is elegant: every 10 points the speed increases, every 15 points spawn rate decreases, creating an exponential difficulty curve that keeps the game challenging without becoming impossible. The sizeMultiplier shows how to customize individual item properties (blueberries are 0.8x size, making them tiny and hard to slice).

function spawnItem() {
  let x = random(width * 0.2, width * 0.8); // Spawn in middle section
  let y = height + itemSize / 2; // Spawn from below the screen
  let vx = random(-2, 2); // Horizontal speed
  let vy = initialLaunchSpeed; // Initial upward speed
  let chosenItem;

  if (random() < 0.7) { // 70% chance for a fruit
    chosenItem = random(FRUIT_TYPES);
  } else { // 30% chance for a bomb
    chosenItem = { name: 'bomb', color: [50, 50, 50], stemColor: null, sizeMultiplier: 1 }; // Bomb details
  }

  // Apply size multiplier if defined (e.g., for blueberries)
  let actualItemSize = itemSize * (chosenItem.sizeMultiplier || 1);

  items.push({
    x, y,
    size: actualItemSize, // Store the actual calculated size here
    vx, vy,
    name: chosenItem.name, // Using name instead of type for more variety
    color: chosenItem.color,
    stemColor: chosenItem.stemColor,
    sliced: false
  });

  // Increase difficulty over time
  if (score % 10 === 0 && initialLaunchSpeed < 20) initialLaunchSpeed += 1;
  if (score % 15 === 0 && itemSpawnRate > 20) itemSpawnRate -= 5;
}
Line-by-line explanation (19 lines)

đź”§ Subcomponents:

calculation Starting position let x = random(width * 0.2, width * 0.8); let y = height + itemSize / 2;

Randomly places items in the middle 60% horizontally and below the screen vertically

conditional Item type selection if (random() < 0.7) { chosenItem = random(FRUIT_TYPES); } else { chosenItem = { name: 'bomb', color: [50, 50, 50], stemColor: null, sizeMultiplier: 1 }; }

Randomly chooses a fruit type (70% probability) or bomb (30% probability)

conditional Progressive difficulty if (score % 10 === 0 && initialLaunchSpeed < 20) initialLaunchSpeed += 1; if (score % 15 === 0 && itemSpawnRate > 20) itemSpawnRate -= 5;

Increases game difficulty over time by making items launch faster and spawn more frequently as the score increases

let x = random(width * 0.2, width * 0.8); // Spawn in middle section
Picks a random x position between 20% and 80% of the canvas width—keeps items centered rather than at the extreme edges
let y = height + itemSize / 2; // Spawn from below the screen
Places the item just below the bottom of the screen so it launches upward into view
let vx = random(-2, 2); // Horizontal speed
Gives each item a random horizontal velocity between -2 and 2 pixels per frame, so it drifts side to side as it arcs
let vy = initialLaunchSpeed; // Initial upward speed
Sets the initial upward velocity to initialLaunchSpeed, which increases as the game progresses
if (random() < 0.7) { // 70% chance for a fruit
Generates a random number 0–1; if it's less than 0.7, pick a fruit (70% probability)
chosenItem = random(FRUIT_TYPES);
Randomly selects one fruit type from the FRUIT_TYPES array (apple, orange, lemon, lime, or blueberry)
} else { // 30% chance for a bomb
Otherwise (30% of the time), create a bomb object
chosenItem = { name: 'bomb', color: [50, 50, 50], stemColor: null, sizeMultiplier: 1 };
Defines a bomb object with name 'bomb', dark gray color, no stem color, and a size multiplier of 1
let actualItemSize = itemSize * (chosenItem.sizeMultiplier || 1);
Calculates the actual size by multiplying the base itemSize by the chosen item's size multiplier (blueberries are 0.8x, all others are 1x)
items.push({
Adds a new item object to the items array with all properties needed for physics, rendering, and collision detection
x, y,
Shorthand for x: x, y: y—stores the starting position
size: actualItemSize,
Stores the calculated size for this particular item (so different fruits can have different sizes)
vx, vy,
Shorthand for vx: vx, vy: vy—stores the velocity for movement simulation
name: chosenItem.name,
Stores the item's name (e.g., 'apple', 'bomb') used to determine how to draw and handle it
color: chosenItem.color,
Stores the RGB color array for rendering
stemColor: chosenItem.stemColor,
Stores the stem color (green for fruits, null for bombs)
sliced: false
Initializes a flag set to false—this is set to true when the item is sliced to prevent double-counting
if (score % 10 === 0 && initialLaunchSpeed < 20) initialLaunchSpeed += 1;
Every time the score reaches a multiple of 10 (10, 20, 30...), increase launch speed by 1, capping it at 20
if (score % 15 === 0 && itemSpawnRate > 20) itemSpawnRate -= 5;
Every time the score reaches a multiple of 15 (15, 30, 45...), decrease the spawn rate by 5 frames (so items appear more frequently), capping at 20 frames minimum

checkSliceCollision()

checkSliceCollision() is the most mathematically sophisticated function in this game. It solves the line-segment-to-circle collision problem, which is essential for making the slice mechanic feel responsive. The function has two phases: first, a fast check of the endpoints (if you grabbed the fruit directly), then a precise vector projection to find the closest point on the slice line to the fruit's center. This algorithm ensures that fast, diagonal swipes still hit fruit even if they just barely graze it—which is what players expect from a 'slice' game.

🔬 This part only checks if either endpoint is inside the fruit. What happens if you remove these three lines? Will fast swipes still slice fruit, or will you miss fruits that don't have an endpoint directly inside them?

  let d1 = dist(x1, y1, itemX, itemY);
  let d2 = dist(x2, y2, itemX, itemY);
  if (d1 < r || d2 < r) return true;
function checkSliceCollision(x1, y1, x2, y2, itemX, itemY, itemSize) {
  let r = itemSize / 2;
  // Check if either endpoint of the slice is inside the item
  let d1 = dist(x1, y1, itemX, itemY);
  let d2 = dist(x2, y2, itemX, itemY);
  if (d1 < r || d2 < r) return true;

  // Check for intersection of the line segment with the circle
  let len = dist(x1, y1, x2, y2);
  // Avoid division by zero if len is 0 (mouse hasn't moved)
  if (len === 0) return false;

  let dot = ((itemX - x1) * (x2 - x1) + (itemY - y1) * (y2 - y1)) / (len * len);
  dot = constrain(dot, 0, 1); // Project point onto the line segment

  let closestX = x1 + dot * (x2 - x1);
  let closestY = y1 + dot * (y2 - y1);

  let distance = dist(closestX, closestY, itemX, itemY);
  return distance <= r;
}
Line-by-line explanation (12 lines)

đź”§ Subcomponents:

conditional Endpoint collision let d1 = dist(x1, y1, itemX, itemY); let d2 = dist(x2, y2, itemX, itemY); if (d1 < r || d2 < r) return true;

Quickly checks if either endpoint of the slice line is inside the fruit circle

calculation Line-circle intersection let dot = ((itemX - x1) * (x2 - x1) + (itemY - y1) * (y2 - y1)) / (len * len); dot = constrain(dot, 0, 1); let closestX = x1 + dot * (x2 - x1); let closestY = y1 + dot * (y2 - y1); let distance = dist(closestX, closestY, itemX, itemY); return distance <= r;

Uses vector projection to find the closest point on the line segment to the circle center, then checks if that point is inside the circle

let r = itemSize / 2;
Calculates the fruit's radius by dividing its diameter (itemSize) by 2
let d1 = dist(x1, y1, itemX, itemY);
Calculates the distance from the first point of the slice to the fruit's center
let d2 = dist(x2, y2, itemX, itemY);
Calculates the distance from the second point of the slice to the fruit's center
if (d1 < r || d2 < r) return true;
If either endpoint is inside the fruit (distance less than radius), immediately return true—collision detected
let len = dist(x1, y1, x2, y2);
Calculates the length of the line segment (distance between the two slice points)
if (len === 0) return false;
If the two points are the same (mouse hasn't moved), the line has no length—no collision possible
let dot = ((itemX - x1) * (x2 - x1) + (itemY - y1) * (y2 - y1)) / (len * len);
Calculates a dot product to find where on the line segment the fruit's center is closest. This is vector projection math—it finds the parameter (0 to 1) that represents the closest point on the line to the circle center
dot = constrain(dot, 0, 1);
Clamps the projection parameter to the range 0–1 so we only check the line segment, not the infinite line extending beyond it
let closestX = x1 + dot * (x2 - x1);
Calculates the x coordinate of the closest point on the line segment to the fruit's center
let closestY = y1 + dot * (y2 - y1);
Calculates the y coordinate of the closest point on the line segment to the fruit's center
let distance = dist(closestX, closestY, itemX, itemY);
Measures the distance from that closest point to the fruit's center
return distance <= r;
If the closest point is within the fruit's radius, the line intersects the circle—collision detected

playSliceSound()

This function demonstrates p5.sound's amplitude envelope API. Instead of a simple beep that's either on or off, the sound has an attack and decay—it ramps up quickly then fades out, creating a percussive 'pop' effect. The three parameters to amp() are (targetAmplitude, duration, startTime), allowing you to schedule volume changes. This is how you create dynamic, musical sound effects from simple oscillators.

function playSliceSound() {
  sliceSound.amp(0.5, 0.1);
  sliceSound.amp(0, 0.3, 0.2);
}
Line-by-line explanation (2 lines)
sliceSound.amp(0.5, 0.1);
Ramps the oscillator's amplitude (volume) from 0 to 0.5 over 0.1 seconds (100 milliseconds)—this creates a quick attack phase as the sound starts
sliceSound.amp(0, 0.3, 0.2);
Then ramps the amplitude back down from its current level to 0 over 0.3 seconds (300 milliseconds), with a delay of 0.2 seconds—this creates a fade-out after the attack

playBombSound()

The bomb sound uses a square wave (harsher than sine) and has a longer decay to sound more ominous and serious than the slice sound. The amplitude envelope is slightly longer, giving players an extra moment to register that they hit a bomb. Compare this to playSliceSound()—different audio characteristics can reinforce game feedback.

function playBombSound() {
  bombSound.amp(0.8, 0.1);
  bombSound.amp(0, 0.5, 0.3);
}
Line-by-line explanation (2 lines)
bombSound.amp(0.8, 0.1);
Ramps the bomb oscillator to 80% volume over 0.1 seconds—louder than the slice sound to emphasize danger
bombSound.amp(0, 0.5, 0.3);
Fades the sound to silence over 0.5 seconds (longer than the slice sound) after a 0.3-second delay, creating a deeper, longer 'rumble'

playMissedSound()

The missed sound is quiet and brief, signaling a mistake without harsh punishment. The triangle wave (chosen in setup()) has a tone between smooth sine and harsh square, making it sound neutral. Good games use audio feedback that matches game events—here, slice (positive) is highest pitch, miss (negative) is low, and bomb (catastrophic) is lowest.

function playMissedSound() {
  missedSound.amp(0.3, 0.1);
  missedSound.amp(0, 0.3, 0.2);
}
Line-by-line explanation (2 lines)
missedSound.amp(0.3, 0.1);
Ramps to 30% volume over 0.1 seconds—quieter than bomb or slice, indicating a negative outcome but less dramatic than hitting a bomb
missedSound.amp(0, 0.3, 0.2);
Fades out over 0.3 seconds after a 0.2-second delay, similar timing to the slice sound but with a lower overall volume

mousePressed()

mousePressed() is a p5.js event handler that fires once when the mouse button is pressed. This sketch uses it to distinguish between menu clicks (start/restart) and slice start (during gameplay). It's the beginning of a multi-frame gesture: mouse pressed → mouse dragged (repeatedly) → mouse released, tracked by the slicing flag and slicePoints array.

function mousePressed() {
  if (gameState === 'start' || gameState === 'gameOver') {
    resetGame();
  } else if (gameState === 'playing') {
    slicing = true;
    slicePoints = []; // Clear previous slice points
    slicePoints.push({ x: mouseX, y: mouseY });
  }
}
Line-by-line explanation (6 lines)

đź”§ Subcomponents:

conditional Menu interaction if (gameState === 'start' || gameState === 'gameOver') { resetGame(); }

When the player clicks on the start or game over screens, reset the game and begin playing

conditional Slice initialization } else if (gameState === 'playing') { slicing = true; slicePoints = []; slicePoints.push({ x: mouseX, y: mouseY }); }

When playing, begin tracking a slice by enabling the flag and recording the initial mouse position

if (gameState === 'start' || gameState === 'gameOver') {
Checks if the player is on the start or game over screen
resetGame();
Resets all game variables (score, lives, items) and changes gameState to 'playing'
} else if (gameState === 'playing') {
Otherwise, if the game is currently active, start a slice
slicing = true;
Sets the slicing flag to true, which allows mouseDragged() and playGame() to process the slice
slicePoints = []; // Clear previous slice points
Empties the slice points array so we start fresh with no old points from previous slices
slicePoints.push({ x: mouseX, y: mouseY });
Records the starting position of the slice as an object with x and y properties

mouseDragged()

mouseDragged() is the core of the slice mechanic. It fires continuously while the mouse button is held and the mouse moves. Each frame, it records the mouse position, then checks whether the line segment from the previous frame to the current frame intersects any unsliced items. This segment-by-segment approach (rather than testing a single point) ensures that fast swipes don't miss fruit—even if the fruit and cursor move so fast they 'skip over' each other between frames, the collision detector still catches them. The slicePoints array is maintained as a rolling buffer, keeping only the recent history for rendering the slice trail.

🔬 Fruits add 1 point, bombs end the game instantly. What if you change the fruit case to score += 5 and keep the bomb case the same? Does earning more points per fruit make the game feel more rewarding?

          if (item.name !== 'bomb') { // It's a fruit
            score++;
            playSliceSound();
          } else { // It's a bomb
            playBombSound();
            gameState = 'gameOver';
          }
function mouseDragged() {
  if (gameState === 'playing' && slicing) {
    // Add current mouse position to slice points
    slicePoints.push({ x: mouseX, y: mouseY });
    // Keep slice points limited for performance and visual effect
    if (slicePoints.length > 10) {
      slicePoints.shift(); // Remove the oldest point
    }

    // Check for slice collisions with all items
    for (let i = items.length - 1; i >= 0; i--) {
      let item = items[i];
      // Only slice if not already sliced in this drag, and if we have at least 2 points to form a line
      if (!item.sliced && slicePoints.length > 1) {
        let prevPoint = slicePoints[slicePoints.length - 2];
        let currentPoint = slicePoints[slicePoints.length - 1];

        // Perform line-circle collision detection
        if (checkSliceCollision(prevPoint.x, prevPoint.y, currentPoint.x, currentPoint.y, item.x, item.y, item.size)) {
          if (item.name !== 'bomb') { // It's a fruit
            score++;
            playSliceSound();
          } else { // It's a bomb
            playBombSound();
            gameState = 'gameOver'; // Instant game over for bombs
          }
          item.sliced = true; // Mark as sliced to prevent double-slicing
          items.splice(i, 1); // Remove sliced item
        }
      }
    }
  }
}
Line-by-line explanation (17 lines)

đź”§ Subcomponents:

calculation Slice point tracking slicePoints.push({ x: mouseX, y: mouseY }); if (slicePoints.length > 10) { slicePoints.shift(); }

Records the current mouse position and removes old points to keep the array small (performance and visual smoothness)

for-loop Collision detection loop for (let i = items.length - 1; i >= 0; i--) {

Iterates through all items backwards to detect whether the current slice line segment hits any fruit or bomb

conditional Line-circle collision test if (checkSliceCollision(prevPoint.x, prevPoint.y, currentPoint.x, currentPoint.y, item.x, item.y, item.size)) {

Tests whether the recent slice segment (from previous frame's last point to current frame's mouse position) intersects this item

conditional Fruit vs. bomb handling if (item.name !== 'bomb') { score++; playSliceSound(); } else { playBombSound(); gameState = 'gameOver'; }

Checks item type and either increments score + plays slice sound (fruit), or triggers game over + plays bomb sound (bomb)

if (gameState === 'playing' && slicing) {
Only process the drag if the game is active and a slice was started (mousePressed set slicing to true)
slicePoints.push({ x: mouseX, y: mouseY });
Adds the current mouse position to the slice points array, building a trail of positions
if (slicePoints.length > 10) {
Checks if the array has grown too large
slicePoints.shift(); // Remove the oldest point
Removes the first (oldest) point from the array, keeping only the most recent 10 positions for performance and to keep the visual trail short and responsive
for (let i = items.length - 1; i >= 0; i--) {
Loops through all items in reverse order to safely remove items when they're sliced
if (!item.sliced && slicePoints.length > 1) {
Only tests collision if the item hasn't been sliced yet in this drag gesture, and if we have at least 2 points (needed to form a line segment)
let prevPoint = slicePoints[slicePoints.length - 2];
Gets the second-to-last point in the array (the position from the previous frame)
let currentPoint = slicePoints[slicePoints.length - 1];
Gets the last point in the array (the current frame's position)
if (checkSliceCollision(prevPoint.x, prevPoint.y, currentPoint.x, currentPoint.y, item.x, item.y, item.size)) {
Calls checkSliceCollision() to test whether the line segment from the previous point to the current point intersects this item's circle
if (item.name !== 'bomb') { // It's a fruit
Checks if the item is a fruit (not a bomb)
score++;
Increments the score by one point
playSliceSound();
Plays the high-pitched slice sound effect
} else { // It's a bomb
If the item is a bomb, handle it differently
playBombSound();
Plays a low-pitched bomb sound effect
gameState = 'gameOver'; // Instant game over for bombs
Changes the game state to game over, ending the game immediately
item.sliced = true; // Mark as sliced to prevent double-slicing
Sets the item's sliced flag to true so it won't trigger collision again if the slice passes through it multiple times
items.splice(i, 1); // Remove sliced item
Removes the item from the items array so it disappears and won't be drawn again

mouseReleased()

mouseReleased() is a p5.js event handler that fires once when the mouse button is released. It completes the slice gesture by disabling the slicing flag and clearing the trail. Together, mousePressed() → mouseDragged() → mouseReleased() form a complete interaction pattern for touch-like input on desktop.

function mouseReleased() {
  slicing = false;
  slicePoints = []; // Clear slice points after release
}
Line-by-line explanation (2 lines)
slicing = false;
Sets the slicing flag to false, stopping mouseDragged() from processing further collision detection
slicePoints = []; // Clear slice points after release
Empties the slice points array, removing the visual slice trail from the screen

touchStarted()

touchStarted() is the mobile equivalent of mousePressed(). Instead of checking mouseX/mouseY, it accesses the touches[] array, which contains all active touch points on the screen. touches[0] is the first finger, touches[1] would be a second finger, etc. By checking touches.length > 0, we ensure at least one finger is present before starting a slice. Returning false is crucial—it prevents the browser from interpreting the touch as a scroll or pinch gesture.

function touchStarted() {
  if (gameState === 'start' || gameState === 'gameOver') {
    resetGame();
  } else if (gameState === 'playing' && touches.length > 0) {
    slicing = true;
    slicePoints = [];
    slicePoints.push({ x: touches[0].x, y: touches[0].y });
  }
  return false; // Prevent default browser touch actions (like scrolling/zooming)
}
Line-by-line explanation (7 lines)

đź”§ Subcomponents:

conditional Touch menu interaction if (gameState === 'start' || gameState === 'gameOver') { resetGame(); }

When the player taps the start or game over screens, restart the game

conditional Touch slice initialization } else if (gameState === 'playing' && touches.length > 0) { slicing = true; slicePoints = []; slicePoints.push({ x: touches[0].x, y: touches[0].y }); }

During gameplay, begin tracking a touch gesture by recording the initial touch position

if (gameState === 'start' || gameState === 'gameOver') {
Checks if the player is on the start or game over screen
resetGame();
Resets all game variables and begins playing
} else if (gameState === 'playing' && touches.length > 0) {
If the game is playing and at least one finger is touching the screen, start a slice
slicing = true;
Enables slice processing in touchMoved()
slicePoints = [];
Clears any previous slice points
slicePoints.push({ x: touches[0].x, y: touches[0].y });
Records the first (index 0) touch position from the p5.js touches array
return false; // Prevent default browser touch actions
Returning false tells the browser to NOT perform its default touch behavior (like scrolling or zooming), letting p5.js handle the touch exclusively

touchMoved()

touchMoved() is the mobile equivalent of mouseDragged(). It fires continuously while one or more fingers are on the screen. The logic is nearly identical to mouseDragged(), except it reads from the touches[] array instead of mouseX/mouseY. This sketch's touch implementation follows a best practice: check for at least one touch before processing, always return false to prevent default browser behavior, and handle both the start, move, and end events.

function touchMoved() {
  if (gameState === 'playing' && slicing && touches.length > 0) {
    slicePoints.push({ x: touches[0].x, y: touches[0].y });
    if (slicePoints.length > 10) {
      slicePoints.shift();
    }

    for (let i = items.length - 1; i >= 0; i--) {
      let item = items[i];
      if (!item.sliced && slicePoints.length > 1) {
        let prevPoint = slicePoints[slicePoints.length - 2];
        let currentPoint = slicePoints[slicePoints.length - 1];

        if (checkSliceCollision(prevPoint.x, prevPoint.y, currentPoint.x, currentPoint.y, item.x, item.y, item.size)) {
          if (item.name !== 'bomb') { // It's a fruit
            score++;
            playSliceSound();
          } else { // It's a bomb
            playBombSound();
            gameState = 'gameOver';
          }
          item.sliced = true;
          items.splice(i, 1);
        }
      }
    }
  }
  return false; // Prevent default browser touch actions
}
Line-by-line explanation (18 lines)

đź”§ Subcomponents:

calculation Touch point tracking slicePoints.push({ x: touches[0].x, y: touches[0].y }); if (slicePoints.length > 10) { slicePoints.shift(); }

Records the current touch position and trims the array to keep only the last 10 positions

for-loop Touch-based collision detection for (let i = items.length - 1; i >= 0; i--) {

Iterates through all items to test whether the current touch segment hits any fruit or bomb

if (gameState === 'playing' && slicing && touches.length > 0) {
Checks three conditions: game is active, slice was started, and at least one finger is on screen
slicePoints.push({ x: touches[0].x, y: touches[0].y });
Records the current position of the first touch point
if (slicePoints.length > 10) {
Checks if the array has grown beyond 10 points
slicePoints.shift();
Removes the oldest point to keep the array limited to 10 points
for (let i = items.length - 1; i >= 0; i--) {
Loops through all items in reverse order (safe for removal)
if (!item.sliced && slicePoints.length > 1) {
Only tests collision if the item hasn't been sliced and we have at least 2 points to form a line
let prevPoint = slicePoints[slicePoints.length - 2];
Gets the second-to-last touch point (from the previous frame)
let currentPoint = slicePoints[slicePoints.length - 1];
Gets the last touch point (current frame)
if (checkSliceCollision(prevPoint.x, prevPoint.y, currentPoint.x, currentPoint.y, item.x, item.y, item.size)) {
Tests whether the touch line segment intersects this item
if (item.name !== 'bomb') { // It's a fruit
If it's a fruit, increase score and play slice sound
score++;
Adds one point
playSliceSound();
Plays the slice sound effect
} else { // It's a bomb
If it's a bomb, play bomb sound and end the game
playBombSound();
Plays a low-pitched bomb sound
gameState = 'gameOver';
Ends the game immediately
item.sliced = true;
Marks the item as sliced to prevent double-counting
items.splice(i, 1);
Removes the item from the array
return false; // Prevent default browser touch actions
Tells the browser not to scroll or zoom—p5.js will handle this touch exclusively

touchEnded()

touchEnded() is the mobile equivalent of mouseReleased(). It fires once when all fingers leave the screen, completing the touch gesture. Like touchMoved(), it returns false to prevent the browser's default touch handling. Together, touchStarted() → touchMoved() → touchEnded() mirror the mouse event sequence, making the game playable on both desktop and mobile devices with identical code.

function touchEnded() {
  slicing = false;
  slicePoints = [];
  return false; // Prevent default browser touch actions
}
Line-by-line explanation (3 lines)
slicing = false;
Disables slice processing, stopping collision detection
slicePoints = [];
Clears the slice points array, removing the visual trail
return false; // Prevent default browser touch actions
Tells the browser not to process this touch event as a scroll, zoom, or other default action

resetGame()

resetGame() is a utility function that restores all game variables to their initial state. It's called when the player clicks/taps on the start or game over screens. Notice that it resets both gameplay variables (score, lives, items) and difficulty scaling variables (initialLaunchSpeed, itemSpawnRate), ensuring each new game starts fresh. This is a clean pattern—keep all initialization logic in one place so restarts are consistent.

function resetGame() {
  score = 0;
  lives = 3;
  items = [];
  initialLaunchSpeed = 10;
  itemSpawnRate = 60;
  itemSpawnCounter = 0;
  gameState = 'playing';
}
Line-by-line explanation (7 lines)
score = 0;
Resets the score to zero
lives = 3;
Resets the player to 3 lives
items = [];
Clears all flying fruit and bombs from the game
initialLaunchSpeed = 10;
Resets the launch speed to the starting value, removing difficulty scaling
itemSpawnRate = 60;
Resets the spawn rate to every 60 frames, removing the increased frequency from difficulty scaling
itemSpawnCounter = 0;
Resets the spawn counter to zero so the first item spawns after 60 frames
gameState = 'playing';
Changes the game state to 'playing', which triggers the main game loop

windowResized()

windowResized() is a p5.js event handler that fires whenever the browser window changes size (including orientation changes on mobile). Calling resizeCanvas(windowWidth, windowHeight) ensures the canvas always fills the screen. Without this function, the canvas would stay at its original size, leaving black bars when the window resizes. This is essential for mobile games that rotate between portrait and landscape.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
p5.js's resizeCanvas() updates the canvas size to match the new window dimensions whenever the browser window is resized

📦 Key Variables

gameState string

Tracks which screen or mode the game is in: 'start', 'playing', or 'gameOver'. Used by draw() to decide what to render.

let gameState = 'start';
items array

Stores all active fruit and bomb objects. Each item has properties like x, y, vx, vy, size, color, name, and sliced.

let items = [];
gravity number

Controls how quickly items fall—each frame, velocity is reduced by this amount, creating a downward acceleration.

let gravity = 0.2;
initialLaunchSpeed number

Sets how fast items launch upward from the bottom. Increases as the game progresses for difficulty scaling.

let initialLaunchSpeed = 10;
itemSize number

Base diameter in pixels for all items. Individual items may be scaled by their sizeMultiplier.

let itemSize = 40;
itemSpawnRate number

Number of frames between item spawns. Lower values spawn items faster. Decreases as the game progresses.

let itemSpawnRate = 60;
itemSpawnCounter number

Counts up to itemSpawnRate; when reached, a new item spawns and the counter resets.

let itemSpawnCounter = 0;
score number

Tracks the player's current score. Increases by 1 for each fruit sliced.

let score = 0;
lives number

Number of fruits the player can miss before game over. Each missed fruit decrements this.

let lives = 3;
slicing boolean

Flag that tracks whether the player is currently dragging/swiping. Set to true by mousePressed/touchStarted, false by mouseReleased/touchEnded.

let slicing = false;
slicePoints array

Stores recent mouse/touch positions as {x, y} objects, forming a trail. Limited to 10 points for performance.

let slicePoints = [];
sliceSound p5.Oscillator

A sine wave oscillator used to play a high-pitched sound effect when fruit is sliced.

let sliceSound;
bombSound p5.Oscillator

A square wave oscillator used to play a low-pitched sound effect when a bomb is hit.

let bombSound;
missedSound p5.Oscillator

A triangle wave oscillator used to play a sound effect when a fruit is missed and falls off screen.

let missedSound;
FRUIT_TYPES array

A constant array of fruit definitions, each with name, color (RGB), stemColor, and sizeMultiplier. Used by spawnItem() to randomly choose fruit types.

const FRUIT_TYPES = [ { name: 'apple', color: [255, 0, 0], stemColor: [0, 128, 0], sizeMultiplier: 1 }, ... ];

đź”§ Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG checkSliceCollision()

Fruit at the exact edge of the screen might register as 'sliced' even if the slice just barely touches the render circle. The collision uses the center + radius, not the actual pixel boundaries.

đź’ˇ The current approach is acceptable for game feel, but for pixel-perfect slicing, you could track the last rendered position of each fruit and test against that rather than recalculating from size.

PERFORMANCE mouseDragged() and touchMoved()

Both functions run nearly identical collision detection code. This duplication is inefficient and error-prone if you want to change the logic.

đź’ˇ Extract the collision checking into a helper function like checkCollisionsForSlice() that both mouseDragged() and touchMoved() call, eliminating code duplication.

FEATURE Game mechanics

Once an item is sliced, it's removed immediately. There's no visual feedback like a split animation or explosion.

đź’ˇ Add a 'splitItem' state or particle effect that shows the fruit splitting in two, lingering briefly before disappearing. This would make slicing feel more rewarding.

STYLE All event handlers

Several functions (mousePressed, mouseDragged, touchStarted, touchMoved) share very similar logic, but with slight differences (mouse vs. touch). This repetition makes the code harder to maintain.

đź’ˇ Consider creating a unified input handler function that normalizes mouse and touch input into a single event type, reducing duplication.

BUG spawnItem() difficulty scaling

The difficulty only increases when score % 10 === 0 and score % 15 === 0. If the player reaches score 100 but hasn't hit one of these milestones since startup, subsequent scores won't increase difficulty properly.

đź’ˇ Use a separate variable to track the 'last difficulty milestone' reached (e.g., lastDifficultyMilestone) and check if score has exceeded that, then update both the difficulty and the milestone. This ensures smooth scaling throughout the game.

FEATURE Game state

There's no pause feature. Once you start, you must play until game over.

đź’ˇ Add a 'paused' game state and let players press a key or button to pause/resume. This would improve accessibility and give players a break during intense moments.

🔄 Code Flow

Code flow showing setup, draw, drawstartscreen, drawgameoverscreen, playgame, spawnitem, checkslicecollision, playslicesound, playbombsound, playmissedsound, mousepressed, mousedragged, mousereleased, touchstarted, touchmoved, touchended, resetgame, windowresized

đź’ˇ Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> text-setup[Text Setup] setup --> sound-oscillators[Sound Oscillators] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click text-setup href "#sub-text-setup" click sound-oscillators href "#sub-sound-oscillators" draw --> state-routing[State Routing] state-routing --> drawstartscreen[drawStartScreen] state-routing --> playgame[playGame] state-routing --> drawgameoverscreen[drawGameOverScreen] click draw href "#fn-draw" click state-routing href "#sub-state-routing" click drawstartscreen href "#fn-drawstartscreen" click playgame href "#fn-playgame" click drawgameoverscreen href "#fn-drawgameoverscreen" drawstartscreen --> start-condition[Start Condition] start-condition --> resetgame[resetGame] click start-condition href "#sub-start-condition" click resetgame href "#fn-resetgame" playgame --> spawn-counter[Spawn Counter] spawn-counter --> spawnitem[spawnItem] spawnitem --> spawn-position[Spawn Position] spawnitem --> fruit-or-bomb[Fruit or Bomb] fruit-or-bomb --> difficulty-scaling[Difficulty Scaling] click spawn-counter href "#sub-spawn-counter" click spawnitem href "#fn-spawnitem" click spawn-position href "#sub-spawn-position" click fruit-or-bomb href "#sub-fruit-or-bomb" click difficulty-scaling href "#sub-difficulty-scaling" playgame --> item-update-loop[Item Update Loop] item-update-loop --> physics-update[Physics Update] physics-update --> bomb-drawing[Bomb Drawing] physics-update --> offscreen-check[Offscreen Check] click item-update-loop href "#sub-item-update-loop" click physics-update href "#sub-physics-update" click bomb-drawing href "#sub-bomb-drawing" click offscreen-check href "#sub-offscreen-check" playgame --> slice-rendering[Slice Rendering] slice-rendering --> point-recording[Point Recording] point-recording --> collision-loop[Collision Loop] collision-loop --> slice-test[Slice Test] slice-test --> fruit-or-bomb-response[Fruit vs. Bomb Response] click slice-rendering href "#sub-slice-rendering" click point-recording href "#sub-point-recording" click collision-loop href "#sub-collision-loop" click slice-test href "#sub-slice-test" click fruit-or-bomb-response href "#sub-fruit-or-bomb-response" drawgameoverscreen --> ui-display[UI Display] ui-display --> resetgame click ui-display href "#sub-ui-display" draw --> loss-check[Loss Check] loss-check --> drawgameoverscreen click loss-check href "#sub-loss-check" windowresized[windowResized] --> canvas-creation click windowresized href "#fn-windowresized"

âť“ Frequently Asked Questions

What visual experience does the fruit ninja sketch offer?

The sketch creates a vibrant scene where colorful fruits fly across a bright sky, providing a playful and dynamic backdrop for the gameplay.

How can players engage with the fruit ninja game?

Users can interact by tapping or clicking to start the game, and swiping to slice the fruits while avoiding bombs, with trails following their cursor or finger.

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

This sketch demonstrates techniques like object-oriented programming for managing game items, real-time user interaction, and sound synthesis for enhancing the gaming experience.

Preview

fruit ninja 🥷 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of fruit ninja 🥷 - Code flow showing setup, draw, drawstartscreen, drawgameoverscreen, playgame, spawnitem, checkslicecollision, playslicesound, playbombsound, playmissedsound, mousepressed, mousedragged, mousereleased, touchstarted, touchmoved, touchended, resetgame, windowresized
Code Flow Diagram