Airplane

This sketch creates an interactive airplane game where a plane continuously flies across the screen dropping supply crates with parachutes. Players click or tap the falling crates to collect them and increase their food counter, while particle effects trail behind the airplane.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the airplane — The plane will fly across the screen faster, forcing you to react quicker to collect crates
  2. Make crates fall slower — Crates will drift down gently, giving you more time to collect them before they fall off-screen
  3. Drop supplies more frequently — The airplane will drop crates more often, making the game busier and challenging you to collect more
  4. Make particles last longer — The airplane's smoke trail will linger on-screen for a longer time, creating a more dramatic effect
  5. Earn more points per crate — Each collected crate increases your food count by 5 instead of 1, rewarding you more generously
  6. Change the plane color to blue — The airplane shifts from gray to blue, giving it a different visual identity
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a charming airplane supply-drop game where a gray airplane flies across the screen at regular intervals dropping parachuted food crates. Players collect crates by clicking or tapping them before they fall off-screen, earning points. The sketch showcases three key p5.js techniques: custom classes that organize game objects (the Airplane, SupplyDrop, and Particle classes), a particle system that creates a smoke trail behind the plane, and collision detection that registers when the mouse/touch overlaps a crate.

The code is structured around three interactive objects—the airplane, falling supply drops, and particle effects—each with its own class that handles position updates and rendering. By reading this sketch, you will learn how to organize complex games using ES6 classes, manage arrays of dynamic objects that are added and removed each frame, and combine sound effects with visual feedback to create a satisfying player experience.

⚙️ How It Works

  1. When the sketch loads, setup() initializes three p5.Oscillators for sound effects, creates a full-window canvas, and instantiates the Airplane object at the left edge of the screen
  2. Every frame, draw() updates and displays the airplane, which moves steadily to the right across the screen and wraps back to the left when it exits
  3. As the airplane flies, emit particles every 5 frames from its rear position to create a smoke trail effect; each particle fades out over time and is removed when its lifespan reaches zero
  4. Every 4 seconds, drop a new SupplyDrop object at the airplane's current position; the crate accelerates downward due to simulated gravity but is capped at a terminal velocity
  5. When the player clicks or touches the canvas, check all falling crates to see if the click position overlaps any box; if so, mark it as collected, increment the food counter, and play a sound effect
  6. Collected crates or crates that fall below the screen are removed from the array; the food count is displayed in the top-left corner continuously

🎓 Concepts You'll Learn

Object-oriented programming with classesParticle systems and trailsCollision detectionGame loops and state managementSound synthesis with oscillatorsGravity and physics simulation

📝 Code Breakdown

preload()

preload() is called before setup() and is the ideal place to load resources that take time, like sound files or images. Here we create oscillators (software sound generators) instead of loading files. Each oscillator is stopped initially because browsers require a user gesture (click or touch) to start audio playback.

function preload() {
  // Setup simple oscillators for sound effects
  // These will be replaced with actual sound files if you have them
  airplaneEngineSound = new p5.Oscillator('sine');
  airplaneEngineSound.freq(220); // Low hum
  airplaneEngineSound.amp(0.1);
  airplaneEngineSound.stop(); // Stop initially

  dropSound = new p5.Oscillator('triangle');
  dropSound.freq(440); // Beep
  dropSound.amp(0.2);
  dropSound.stop(); // Stop initially

  collectSound = new p5.Oscillator('square');
  collectSound.freq(880); // Higher beep
  collectSound.amp(0.3);
  collectSound.stop(); // Stop initially
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

sound-initialization Airplane Engine Sound airplaneEngineSound = new p5.Oscillator('sine');

Creates a low-frequency sine wave oscillator that represents the airplane engine hum

sound-initialization Drop Sound Setup dropSound = new p5.Oscillator('triangle');

Creates a triangle wave oscillator for the sound played when a crate is dropped

sound-initialization Collect Sound Setup collectSound = new p5.Oscillator('square');

Creates a square wave oscillator for the sound played when a crate is collected

airplaneEngineSound = new p5.Oscillator('sine');
Creates a new oscillator using a sine wave shape, which produces a smooth, pure tone
airplaneEngineSound.freq(220);
Sets the frequency to 220 Hz, a low pitch suitable for an engine sound
airplaneEngineSound.amp(0.1);
Sets the volume (amplitude) to 0.1, fairly quiet so it doesn't overwhelm other sounds
airplaneEngineSound.stop();
Stops the oscillator initially; it will be started later in setup()
dropSound = new p5.Oscillator('triangle');
Creates a triangle wave oscillator with a slightly richer sound than sine
dropSound.freq(440);
Sets frequency to 440 Hz (the musical note A4), a mid-range pitch for the drop event
collectSound = new p5.Oscillator('square');
Creates a square wave oscillator, which produces a bright, buzzy tone
collectSound.freq(880);
Sets frequency to 880 Hz (double 440), a high pitch that signals successful collection

setup()

setup() runs once when the sketch starts. It initializes the canvas, creates the main game objects, and prepares the audio system. Notice that we start the airplane engine sound here because the game begins immediately; other sounds (drop and collect) are triggered by game events in the draw loop.

function setup() {
  createCanvas(windowWidth, windowHeight);
  airplane = new Airplane();
  foodCount = 0;

  // Start audio on user gesture (mandatory for browsers)
  // Audio will start playing after the first click/touch on the canvas
  userStartAudio();
  airplaneEngineSound.start();
  // airplaneEngineSound.loop(); // REMOVED: Oscillators are continuous by default

  // Initialize particles array (particles will be emitted by the airplane)
  particles = [];
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

initialization Canvas Creation createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire window

initialization Airplane Instantiation airplane = new Airplane();

Creates a single Airplane object that will fly across the screen

initialization Audio Initialization airplaneEngineSound.start();

Starts the engine sound oscillator to run continuously

initialization Particle Array Init particles = [];

Creates an empty array that will hold active particle objects

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that takes up the full width and height of the window
airplane = new Airplane();
Creates a new Airplane object and stores it in the global airplane variable
foodCount = 0;
Resets the food counter to 0 at the start of the game
userStartAudio();
Tells p5.sound that audio can now start playing after a user interaction (required by modern browsers)
airplaneEngineSound.start();
Starts the engine sound oscillator, which will run continuously in the background
particles = [];
Initializes an empty array that will store all active particle objects for the airplane trail

draw()

draw() is the heart of the sketch—it runs 60 times per second and orchestrates all game logic. Notice the three backwards for-loops: they safely remove items (supplyDrops and particles) while iterating, which prevents skipping or crashing. The background() call with low alpha creates the motion blur effect by layering semi-transparent frames on top of each other. The particle emission uses modulo (%) to trigger every N frames, a common pattern for timed events in games.

🔬 This entire block triggers a drop every dropInterval milliseconds. What happens if you remove the sound effect lines (dropSound.start() and the two amp() lines)? The drops will still fall—the game logic is separate from the audio.

  if (millis() - lastDropTime > dropInterval) {
    supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y));
    
    // Play drop sound with a quick fade-in and fade-out
    dropSound.start();
    dropSound.amp(0.2, 0.05); // Fade in to 0.2 amplitude in 0.05 seconds
    dropSound.amp(0, 0.5);   // Fade out to 0 amplitude in 0.5 seconds
    
    lastDropTime = millis();
  }

🔬 This if-statement removes drops when they're collected OR when they fall too far. What happens if you remove the second condition (drop.pos.y > height + drop.size) so only collected drops are removed? Drops that fall off-screen will persist in memory forever!

  // Remove collected drops or drops that have fallen off-screen
    if (drop.state === 'collected' || drop.pos.y > height + drop.size) {
      supplyDrops.splice(i, 1);
    }
function draw() {
  // Draw background with some transparency to create a trailing effect
  background(20, 20, 30, 25);

  // --- Airplane Logic ---
  airplane.update();
  airplane.display();

  // Emit particles from the airplane's rear as a trail
  if (frameCount % 5 === 0) { // Emit a particle every 5 frames
    let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1);
    particles.push(new Particle(particlePos.x, particlePos.y));
  }

  // Drop supply logic: Check if it's time to drop a new supply
  if (millis() - lastDropTime > dropInterval) {
    supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y));
    
    // Play drop sound with a quick fade-in and fade-out
    dropSound.start();
    dropSound.amp(0.2, 0.05); // Fade in to 0.2 amplitude in 0.05 seconds
    dropSound.amp(0, 0.5);   // Fade out to 0 amplitude in 0.5 seconds
    
    lastDropTime = millis();
  }

  // --- Supply Drops Logic ---
  // Loop through supply drops backwards to allow safe removal
  for (let i = supplyDrops.length - 1; i >= 0; i--) {
    let drop = supplyDrops[i];
    drop.update();
    drop.display();

    // Remove collected drops or drops that have fallen off-screen
    if (drop.state === 'collected' || drop.pos.y > height + drop.size) {
      supplyDrops.splice(i, 1);
    }
  }

  // --- Particle System Logic (Airplane Trail) ---
  // Loop through particles backwards to allow safe removal
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.update();
    p.display();
    if (!p.isAlive) {
      particles.splice(i, 1); // Remove dead particles
    }
  }

  // --- Food Counter Display ---
  fill(255); // White color for text
  textSize(24);
  textAlign(LEFT, TOP);
  text(`Food: ${foodCount}`, 10, 10);
}
Line-by-line explanation (26 lines)

🔧 Subcomponents:

visual Semi-Transparent Background background(20, 20, 30, 25);

Clears the canvas with a dark blue-gray color; the low alpha (25) creates a trailing/motion blur effect

update-and-display Airplane Update and Display airplane.update(); airplane.display();

Updates the airplane's position and redraws it at the new position each frame

conditional-loop Particle Emission if (frameCount % 5 === 0) { // Emit a particle every 5 frames let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1); particles.push(new Particle(particlePos.x, particlePos.y)); }

Creates new smoke particles behind the airplane every 5 frames to form a trail effect

conditional-and-sound Supply Drop Trigger if (millis() - lastDropTime > dropInterval) { supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y)); dropSound.start(); dropSound.amp(0.2, 0.05); dropSound.amp(0, 0.5); lastDropTime = millis(); }

Drops a new supply crate at the airplane's position every dropInterval milliseconds and plays a sound effect

for-loop Supply Drop Update Loop for (let i = supplyDrops.length - 1; i >= 0; i--) { let drop = supplyDrops[i]; drop.update(); drop.display(); if (drop.state === 'collected' || drop.pos.y > height + drop.size) { supplyDrops.splice(i, 1); } }

Updates and displays each supply drop; removes drops that are collected or fall off-screen

for-loop Particle Update Loop for (let i = particles.length - 1; i >= 0; i--) { let p = particles[i]; p.update(); p.display(); if (!p.isAlive) { particles.splice(i, 1); } }

Updates and displays each particle in the trail; removes particles that have faded out

text-rendering Food Counter Display fill(255); textSize(24); textAlign(LEFT, TOP); text(`Food: ${foodCount}`, 10, 10);

Renders the current food count as white text in the top-left corner

background(20, 20, 30, 25);
Clears the canvas with a very dark blue; the alpha value of 25 means only 10% opacity, so previous frames show through slightly, creating a motion blur trail effect
airplane.update();
Calls the Airplane's update method, which moves it by adding its velocity to its position and handles screen wrapping
airplane.display();
Calls the Airplane's display method, which draws the plane at its current position using shapes (ellipse, triangle, rect)
if (frameCount % 5 === 0) {
Checks if the current frame number is divisible by 5; this creates a particle every 5 frames (roughly every 83 milliseconds at 60 FPS)
let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1);
Calculates where to spawn the new particle: slightly behind and below the airplane's center to simulate a trail from the rear
particles.push(new Particle(particlePos.x, particlePos.y));
Creates a new Particle object at the calculated position and adds it to the particles array
if (millis() - lastDropTime > dropInterval) {
Checks if enough milliseconds have elapsed since the last drop; if true, it's time to drop a new supply crate
supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y));
Creates a new SupplyDrop object at the airplane's current position and adds it to the supplyDrops array
dropSound.start();
Starts playing the drop sound oscillator (the triangle wave beep)
dropSound.amp(0.2, 0.05);
Fades the sound in to 0.2 amplitude over 0.05 seconds (50 milliseconds)
dropSound.amp(0, 0.5);
Fades the sound out to 0 amplitude over 0.5 seconds (500 milliseconds), creating a beep sound
lastDropTime = millis();
Records the current time in milliseconds so the next drop timing check will use this as the reference point
for (let i = supplyDrops.length - 1; i >= 0; i--) {
Loops backwards through the supplyDrops array (from last to first) so items can be safely removed without skipping any
drop.update();
Updates the drop's velocity and position based on gravity; marks it as collected if the state has changed
drop.display();
Draws the supply drop (parachute, strings, box, and FOOD label) at its current position
if (drop.state === 'collected' || drop.pos.y > height + drop.size) {
Checks if the drop has been collected by the player or has fallen completely below the screen (height + its size)
supplyDrops.splice(i, 1);
Removes the drop from the array at index i, preventing memory leaks from accumulating off-screen objects
for (let i = particles.length - 1; i >= 0; i--) {
Loops backwards through the particles array to safely update and remove dead particles
p.update();
Updates the particle's position, velocity, and lifespan (fading it out over time)
p.display();
Draws the particle as a small circle with its current alpha (transparency) value
if (!p.isAlive) {
Checks if the particle's lifespan has dropped to zero, marking it as dead and ready for removal
particles.splice(i, 1);
Removes the dead particle from the array to free up memory
fill(255);
Sets the fill color to white for the text that follows
textSize(24);
Sets the font size to 24 pixels for the food counter display
textAlign(LEFT, TOP);
Aligns the text so the top-left corner of the text appears at the specified coordinates (10, 10)
text(`Food: ${foodCount}`, 10, 10);
Renders the food count at pixel (10, 10) using template literal syntax to insert the current foodCount value

mousePressed()

mousePressed() is a p5.js event function that fires when the mouse button is pressed. Here it delegates to handleTouchOrClick(), which centralizes the logic for both mouse and touch input. This keeps the code DRY (Don't Repeat Yourself).

function mousePressed() {
  handleTouchOrClick();
}
Line-by-line explanation (1 lines)
handleTouchOrClick();
Calls the shared touch/click handler function, which checks if the mouse click overlaps any falling supply crate

touchStarted()

touchStarted() fires when a user touches the screen on mobile or tablet devices. Returning false is important because it prevents the browser from treating touch as a scroll gesture, which would otherwise pan or zoom the page.

function touchStarted() {
  handleTouchOrClick();
  return false; // Prevent default touch behavior (like scrolling)
}
Line-by-line explanation (2 lines)
handleTouchOrClick();
Calls the shared touch/click handler function to detect supply crate collection on touch devices
return false; // Prevent default touch behavior (like scrolling)
Returning false tells the browser NOT to perform default touch actions like scrolling or zooming, keeping focus on the game

handleTouchOrClick()

This function is the core of the game's interactivity. It uses a for-of loop (for (let x of array)) to iterate through all drops and checks collision using the drop's isCollected() method. The break statement is crucial: it ensures only one drop is collected per interaction, which prevents accidental mass collection when multiple crates overlap. The sound feedback (collect sound) is essential for player satisfaction—immediate audio confirmation makes the game feel more responsive.

🔬 What happens if you comment out or remove the break statement? Without break, the loop continues and you could collect multiple drops with a single click if they all overlap your cursor position—try it and see!

  for (let drop of supplyDrops) {
    // Check if the drop is currently falling and if the mouse/touch is over it
    if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {
      drop.state = 'collected'; // Mark as collected
      foodCount++;             // Increment food count
      
      // Play collect sound with a quick fade-in and fade-out
      collectSound.start();
      collectSound.amp(0.3, 0.05); // Fade in to 0.3 amplitude in 0.05 seconds
      collectSound.amp(0, 0.5);    // Fade out to 0 amplitude in 0.5 seconds
      
      break; // Only collect one drop per click/touch
    }
  }
function handleTouchOrClick() {
  for (let drop of supplyDrops) {
    // Check if the drop is currently falling and if the mouse/touch is over it
    if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {
      drop.state = 'collected'; // Mark as collected
      foodCount++;             // Increment food count
      
      // Play collect sound with a quick fade-in and fade-out
      collectSound.start();
      collectSound.amp(0.3, 0.05); // Fade in to 0.3 amplitude in 0.05 seconds
      collectSound.amp(0, 0.5);    // Fade out to 0 amplitude in 0.5 seconds
      
      break; // Only collect one drop per click/touch
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Supply Drop Collision Loop for (let drop of supplyDrops) {

Iterates through every supply drop currently on screen to check for collisions with the mouse/touch position

conditional Collision and State Check if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {

Verifies that the drop is still falling (not already collected) and that the click/touch point overlaps the drop's box

sound-effect Collect Sound Playback collectSound.start(); collectSound.amp(0.3, 0.05); collectSound.amp(0, 0.5);

Plays a high-pitched beep with fade-in and fade-out to provide audio feedback for successful collection

loop-control Early Loop Exit break;

Exits the loop after collecting one drop so only one crate can be collected per click/touch

for (let drop of supplyDrops) {
Iterates through every supply drop in the array using for-of syntax, which automatically assigns each drop to the variable 'drop'
if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {
Checks two conditions: (1) the drop hasn't been collected yet (state is 'falling'), AND (2) the mouse/touch coordinates are inside the drop's collision box
drop.state = 'collected';
Changes the drop's state from 'falling' to 'collected', which prevents it from being drawn in the next frame and marks it for removal
foodCount++;
Increments the global food counter by 1, immediately updating the score display shown at the top-left
collectSound.start();
Starts playing the collect sound oscillator (the high-pitched square wave)
collectSound.amp(0.3, 0.05);
Fades the sound in to 0.3 amplitude over 50 milliseconds
collectSound.amp(0, 0.5);
Fades the sound out to 0 amplitude over 500 milliseconds, creating a celebratory beep effect
break;
Exits the for-loop immediately, so only one drop per click is collected (prevents collecting multiple drops from a single tap)

windowResized()

windowResized() is a p5.js event function that fires when the browser window is resized. Without it, the canvas wouldn't resize, and the game would look broken on smaller or larger screens. Resetting the airplane's position ensures it doesn't get stranded off-screen if it was far to the right when the resize happened.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Reset airplane position on resize to ensure it starts properly
  airplane.pos.x = -airplane.size;
  airplane.pos.y = height * 0.2;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

initialization Canvas Resize resizeCanvas(windowWidth, windowHeight);

Adjusts the p5.js canvas to match the new window dimensions when the browser is resized

state-reset Airplane Position Reset airplane.pos.x = -airplane.size; airplane.pos.y = height * 0.2;

Resets the airplane to the left edge of the newly resized canvas so it doesn't get stuck off-screen

resizeCanvas(windowWidth, windowHeight);
Tells p5.js to stretch or shrink the canvas to match the new window size
airplane.pos.x = -airplane.size;
Resets the airplane's x position to just off the left edge (negative value) so it enters from the left side after resizing
airplane.pos.y = height * 0.2;
Sets the airplane's y position to 20% down from the top, matching the initial flight height

Airplane

The Airplane class encapsulates all airplane behavior: position, velocity, and visual appearance. The key pattern here is separating update() (which handles logic and movement) from display() (which handles drawing). The use of push() and pop() in display() ensures transformations are local to the airplane and don't affect subsequent drawings. Notice that the airplane's size is stored as a variable and used to scale all body parts proportionally—changing this.size instantly rescales the entire plane, a great design pattern for flexible graphics.

🔬 This code draws the body, tail, and wings using shapes. What happens if you swap the rect() line to draw a much larger rectangle (change this.size * 1.5 to this.size * 4)? The wings will become enormous—you'll create a funny wide plane!

    // Body
    fill(this.color);
    noStroke();
    ellipse(0, 0, this.size, this.size * 0.4);

    // Tail fin
    triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);

    // Wings
    rectMode(CENTER);
    rect(0, 0, this.size * 1.5, this.size * 0.2);
class Airplane {
  constructor() {
    this.size = 60; // Size reference for drawing
    // Start off-screen left, flying at 20% of screen height
    this.pos = createVector(-this.size, height * 0.2);
    this.vel = createVector(3, 0); // Constant speed flying right
    this.color = color(150, 150, 150); // Gray plane
  }

  update() {
    this.pos.add(this.vel);
    // Wrap around screen: if plane goes off right, reset to off-screen left
    if (this.pos.x > width + this.size) {
      this.pos.x = -this.size;
      this.pos.y = height * random(0.1, 0.4); // Vary flight height slightly
    }
  }

  display() {
    push();
    translate(this.pos.x, this.pos.y);

    // Body
    fill(this.color);
    noStroke();
    ellipse(0, 0, this.size, this.size * 0.4);

    // Tail fin
    triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);

    // Wings
    rectMode(CENTER);
    rect(0, 0, this.size * 1.5, this.size * 0.2);

    // Cockpit
    fill(100, 100, 100);
    ellipse(this.size * 0.3, -this.size * 0.1, this.size * 0.3, this.size * 0.2);

    pop();
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

constructor Airplane Constructor constructor() { this.size = 60; this.pos = createVector(-this.size, height * 0.2); this.vel = createVector(3, 0); this.color = color(150, 150, 150); }

Initializes the airplane's size, starting position (off-screen left), velocity (constant rightward movement), and color

method Airplane Update Method update() { ... }

Moves the airplane each frame and wraps it back to the left edge when it exits the right side

method Airplane Display Method display() { ... }

Draws the airplane using p5.js shapes (ellipse, triangle, rect) positioned relative to the plane's center

conditional Screen Wrapping Logic if (this.pos.x > width + this.size) { ... }

Resets the airplane to the left side when it fully exits the right edge, with a varied flight height

this.size = 60;
Sets the airplane's size reference to 60 pixels, which is used to scale all the body parts proportionally
this.pos = createVector(-this.size, height * 0.2);
Creates a position vector starting off-screen to the left (negative x) at 20% down from the top (0.2 * height)
this.vel = createVector(3, 0);
Creates a velocity vector that moves the plane 3 pixels to the right each frame; no vertical movement (y = 0)
this.color = color(150, 150, 150);
Creates a gray color for the airplane body (RGB: 150, 150, 150)
this.pos.add(this.vel);
Adds the velocity vector to the position, moving the airplane 3 pixels to the right
if (this.pos.x > width + this.size) {
Checks if the airplane has moved far enough to the right that it's completely off-screen (past width + its own size)
this.pos.x = -this.size;
Resets the x position to off-screen left, ready to fly across again
this.pos.y = height * random(0.1, 0.4);
Randomizes the flight height between 10% and 40% down the screen, creating visual variety each loop
push();
Saves the current transformation matrix (translate, rotate, scale settings) so changes don't affect other objects
translate(this.pos.x, this.pos.y);
Moves the coordinate origin to the airplane's position, so all subsequent shapes are drawn relative to it
ellipse(0, 0, this.size, this.size * 0.4);
Draws the main body as an ellipse centered at (0,0) with width = size and height = 40% of size
triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);
Draws a triangle for the tail fin on the left rear of the plane, with three corner points defining its shape
rect(0, 0, this.size * 1.5, this.size * 0.2);
Draws the wings as a rectangle centered at (0,0) with width = 1.5x size (extending left and right) and height = 20% of size
fill(100, 100, 100);
Changes the fill color to dark gray for the cockpit (darker than the body)
ellipse(this.size * 0.3, -this.size * 0.1, this.size * 0.3, this.size * 0.2);
Draws the cockpit as a small dark gray ellipse on the right side of the body and slightly above center
pop();
Restores the previously saved transformation matrix, undoing the translate() so subsequent objects draw in the correct coordinate system

SupplyDrop

The SupplyDrop class demonstrates a key game design pattern: separating game logic (physics, state management) from rendering (display). The update() method handles all motion and state changes, while display() only draws the current state. Notice the state machine: 'falling' and 'collected' are two distinct states with different behaviors. The isCollected() method is a simple axis-aligned bounding box (AABB) collision check—very efficient for rectangular objects. The vel.limit() call is crucial for realistic physics; without it, the drop would accelerate infinitely and feel unrealistic.

🔬 This update() method applies gravity and limits velocity. What happens if you comment out the this.vel.limit(5) line? The crates will accelerate indefinitely and fall faster and faster—you'll see gravity working without air resistance!

    if (this.state === 'falling') {
      this.vel.add(this.gravity);
      this.pos.add(this.vel);

      // Limit velocity to prevent drops from falling too fast (simulating air resistance)
      this.vel.limit(5);
    }

🔬 This code draws the parachute and two strings. What if you add a third line to connect the parachute to the box at the center? Try adding a line like line(0, -this.size * 0.8, 0, 0) and the parachute will look more realistic with a center support cord!

      // Parachute
      fill(this.parachuteColor);
      noStroke();
      arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI); // Semi-circle

      // Strings connecting parachute to box
      stroke(0);
      strokeWeight(1);
      line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
      line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);
class SupplyDrop {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    this.gravity = createVector(0, 0.1); // Simple downward gravity
    this.size = 30; // Size of the box
    this.state = 'falling'; // States: 'falling', 'collected'
    this.parachuteColor = color(255, 100, 100); // Red parachute
    this.boxColor = color(150, 100, 50);   // Brown box
  }

  update() {
    if (this.state === 'falling') {
      this.vel.add(this.gravity);
      this.pos.add(this.vel);

      // Limit velocity to prevent drops from falling too fast (simulating air resistance)
      this.vel.limit(5);
    }
  }

  display() {
    if (this.state === 'falling') {
      push();
      translate(this.pos.x, this.pos.y);

      // Parachute
      fill(this.parachuteColor);
      noStroke();
      arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI); // Semi-circle

      // Strings connecting parachute to box
      stroke(0);
      strokeWeight(1);
      line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
      line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);

      // Box
      fill(this.boxColor);
      noStroke();
      rectMode(CENTER);
      rect(0, 0, this.size, this.size);

      // "FOOD" label on the box
      fill(255);
      textSize(this.size * 0.4);
      textAlign(CENTER, CENTER);
      text("FOOD", 0, 0);

      pop();
    }
  }

  isCollected(mx, my) {
    // Check if the given mouse/touch coordinates are within the box
    return mx > this.pos.x - this.size / 2 &&
           mx < this.pos.x + this.size / 2 &&
           my > this.pos.y - this.size / 2 &&
           my < this.pos.y + this.size / 2;
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

constructor SupplyDrop Constructor constructor(x, y) { ... }

Initializes a supply drop at position (x, y) with zero velocity, gravity, and a 'falling' state

method SupplyDrop Update Method update() { ... }

Applies gravity to velocity, updates position, and caps velocity to simulate air resistance

conditional-physics Gravity and Velocity Limiting if (this.state === 'falling') { this.vel.add(this.gravity); this.pos.add(this.vel); this.vel.limit(5); }

Only applies physics (gravity, movement, air resistance) while the crate is falling; stops once collected

method SupplyDrop Display Method display() { ... }

Draws the parachute, strings, and food box only if the state is 'falling'

method Collision Detection Method isCollected(mx, my) { ... }

Returns true if the given coordinates (mouse/touch) fall within the box's rectangular collision area

this.pos = createVector(x, y);
Stores the initial position where the drop starts (at the airplane's position when dropped)
this.vel = createVector(0, 0);
Initializes velocity to zero; it will increase downward due to gravity
this.gravity = createVector(0, 0.1);
Creates a gravity vector that pulls the drop downward with an acceleration of 0.1 pixels per frame per frame
this.size = 30;
Sets the box size to 30 pixels; used to scale the parachute, strings, and collision area proportionally
this.state = 'falling';
Initializes the state as 'falling'; when collected, it changes to 'collected' to stop rendering and prepare for removal
this.parachuteColor = color(255, 100, 100);
Creates a reddish color (RGB: 255, 100, 100) for the parachute
this.boxColor = color(150, 100, 50);
Creates a brownish color (RGB: 150, 100, 50) for the food crate
if (this.state === 'falling') {
Only applies physics when the drop hasn't been collected yet; prevents further movement after collection
this.vel.add(this.gravity);
Adds the gravity vector to velocity, increasing downward speed each frame (acceleration)
this.pos.add(this.vel);
Adds the velocity to position, moving the drop by the current speed
this.vel.limit(5);
Caps the velocity magnitude at 5 pixels per frame, simulating air resistance so the drop doesn't fall infinitely fast
if (this.state === 'falling') {
Only draws the drop if it's still falling; once collected, it becomes invisible and is removed from the array
arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI);
Draws a semi-circle (red parachute) above the box, from angle PI to TWO_PI (bottom half of a circle)
line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
Draws a line from the left edge of the parachute to the left edge of the box, simulating a support string
line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);
Draws a line from the right edge of the parachute to the right edge of the box, completing the parachute system
rect(0, 0, this.size, this.size);
Draws the brown food crate as a square centered at (0, 0) with width and height equal to this.size
text("FOOD", 0, 0);
Renders the white text 'FOOD' centered on the crate
return mx > this.pos.x - this.size / 2 && mx < this.pos.x + this.size / 2 && my > this.pos.y - this.size / 2 && my < this.pos.y + this.size / 2;
Checks if the mouse/touch point (mx, my) is inside the box's rectangular bounds by testing if it's within size/2 distance horizontally and vertically from the center

Particle

The Particle class is a classic game engine component for visual effects. The key insight is that each particle is independent: it has its own position, velocity, and lifespan. The random velocity makes the trail feel organic rather than mechanical. Using lifespan as an alpha value (transparency) is elegant—as the number decreases from 255 to 0, the particle naturally fades out. The isAlive flag is a simple lifecycle management system: when false, the particle is removed from the array in the draw loop, preventing memory leaks from accumulating thousands of dead particles.

🔬 This velocity makes particles drift left and up/down. What happens if you change the velocity to createVector(random(-2, -1), random(-0.5, 0.5))? Particles will move faster backward and less vertically, creating a longer horizontal trail!

    this.pos = createVector(x, y);
    // Particles move slightly backward and downward relative to the plane
    this.vel = createVector(random(-1, 0), random(-1, 1));

🔬 What happens if you remove the this.lifespan -= random(3, 7) line so particles never fade? Particles will never die and accumulate forever in memory, eventually crashing the sketch—don't actually try this!

  update() {
    this.pos.add(this.vel);
    this.lifespan -= random(3, 7); // Fade faster
    if (this.lifespan <= 0) {
      this.isAlive = false; // Mark particle for removal
    }
  }
class Particle {
  constructor(x, y) {
    this.pos = createVector(x, y);
    // Particles move slightly backward and downward relative to the plane
    this.vel = createVector(random(-1, 0), random(-1, 1));
    this.size = random(2, 5);
    this.color = color(200, 200, 200); // Light gray for smoke/trail
    this.lifespan = 255; // Initial alpha value, decreases over time
    this.isAlive = true;
  }

  update() {
    this.pos.add(this.vel);
    this.lifespan -= random(3, 7); // Fade faster
    if (this.lifespan <= 0) {
      this.isAlive = false; // Mark particle for removal
    }
  }

  display() {
    if (this.isAlive) {
      noStroke();
      // Apply lifespan as alpha value
      fill(red(this.color), green(this.color), blue(this.color), this.lifespan);
      ellipse(this.pos.x, this.pos.y, this.size);
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

constructor Particle Constructor constructor(x, y) { ... }

Initializes a particle at position (x, y) with randomized velocity, size, and a lifespan of 255 (fully opaque)

method Particle Update Method update() { ... }

Moves the particle and decreases its lifespan (opacity); marks it as dead when lifespan reaches zero

method Particle Display Method display() { ... }

Draws the particle as a circle with opacity determined by its current lifespan value

calculation Lifespan Decay this.lifespan -= random(3, 7);

Decreases the lifespan by a random amount each frame, causing the particle to fade out at a varying rate

this.pos = createVector(x, y);
Stores the initial position where the particle spawns (behind the airplane)
this.vel = createVector(random(-1, 0), random(-1, 1));
Creates a random velocity pointing backward (x from -1 to 0) and up or down (y from -1 to 1), making particles drift behind the plane
this.size = random(2, 5);
Randomizes each particle's diameter between 2 and 5 pixels for visual variety in the smoke trail
this.color = color(200, 200, 200);
Sets the particle color to light gray (RGB: 200, 200, 200), suitable for a smoke or exhaust effect
this.lifespan = 255;
Initializes the lifespan to 255, which is the maximum alpha (opacity) value—the particle starts fully visible
this.isAlive = true;
Marks the particle as alive; when lifespan drops to zero, this becomes false to signal removal
this.pos.add(this.vel);
Moves the particle by its velocity vector each frame, creating the drift effect
this.lifespan -= random(3, 7);
Decreases the lifespan by a random value between 3 and 7 each frame, making particles fade at slightly different rates for natural variation
if (this.lifespan <= 0) {
Checks if the particle has faded completely (lifespan at or below zero)
this.isAlive = false;
Sets isAlive to false, signaling to the draw loop that this particle should be removed from the array
if (this.isAlive) {
Only draws the particle if it hasn't died yet, preventing invisible particles from being rendered
fill(red(this.color), green(this.color), blue(this.color), this.lifespan);
Sets the fill color using the color's RGB values extracted with red(), green(), and blue() functions, and uses the current lifespan as the alpha (transparency)
ellipse(this.pos.x, this.pos.y, this.size);
Draws a circle at the particle's position with diameter equal to this.size; the fill color's alpha value creates the fading effect

📦 Key Variables

airplane object (Airplane class)

Stores the single airplane object that flies across the screen and drops supply crates

let airplane = new Airplane();
supplyDrops array

Stores all currently active supply crate objects; crates are added when dropped and removed when collected or fall off-screen

let supplyDrops = [];
foodCount number

Tracks the player's score—incremented each time a crate is collected

let foodCount = 0;
dropInterval number (milliseconds)

Defines how often the airplane drops a new crate (time between drops in milliseconds)

let dropInterval = 4000;
lastDropTime number (milliseconds)

Records the timestamp of the last drop so the sketch can check if dropInterval has elapsed

let lastDropTime = 0;
airplaneEngineSound object (p5.Oscillator)

A sound oscillator that plays the continuous airplane engine hum in the background

let airplaneEngineSound = new p5.Oscillator('sine');
dropSound object (p5.Oscillator)

A sound oscillator that plays a beep when a crate is dropped

let dropSound = new p5.Oscillator('triangle');
collectSound object (p5.Oscillator)

A sound oscillator that plays a high-pitched beep when the player collects a crate

let collectSound = new p5.Oscillator('square');
particles array

Stores all currently active particle objects that form the airplane's smoke trail; particles are removed when they fade completely

let particles = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - particle emission loop

Every 5 frames, a new particle is created. Over a long play session (hours), the particles array could theoretically hold thousands of dead particles if display/removal doesn't work perfectly, though the isAlive flag mitigates this.

💡 Add a safety cap: if (particles.length > 500) splice out the oldest dead particles proactively, or cap particle creation to prevent runaway growth.

BUG SupplyDrop.isCollected() collision detection

The collision box is based on the box size, but the parachute is drawn above and extends wider than the box. A player might miss clicking on the visible parachute and only hit if they click the box area.

💡 Expand the collision box to include the parachute radius: use this.size * 1.5 as the collision width instead of this.size, or calculate collision based on distance from center (circular collision).

STYLE Global scope

Many global variables (airplane, supplyDrops, particles, foodCount, dropInterval, etc.) pollute the global namespace. In larger projects, this can cause naming conflicts.

💡 Wrap the entire sketch in an object or use module pattern: create a Game object that holds all state variables and functions as methods, reducing global clutter.

FEATURE Game logic (handleTouchOrClick and draw)

The game has no end condition or difficulty progression—it runs indefinitely with the same drop rate and crate speed.

💡 Add a level system: every 10 crates collected, increase airplane speed or gravity, or decrease dropInterval. Add a game-over condition (e.g., let 5 crates fall without collecting them) for replayability.

BUG draw() - supply drop removal

Supply drops are removed when pos.y > height + size, but if a crate moves horizontally due to wind or other forces (not in current code), it could drift off-screen to the sides and never be cleaned up.

💡 Add boundary checks for all sides: check if pos.x < -size or pos.x > width + size in addition to the vertical check.

STYLE Airplane.display() and SupplyDrop.display()

Both classes use magic numbers (like this.size * 0.4, this.size * 0.8) scattered throughout the draw code, making the shapes hard to adjust or understand.

💡 Define constants for these proportions as properties: this.bodyHeight = this.size * 0.4, this.cockpitSize = this.size * 0.3, etc., then use these named values in display().

🔄 Code Flow

Code flow showing preload, setup, draw, mousepressed, touchstarted, handletouchorclick, windowresized, airplane, supplydrop, particle

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

graph TD start[Start] --> setup[setup] setup --> preload[preload] preload --> canvas-creation[canvas-creation] preload --> airplane-sound-setup[airplane-sound-setup] preload --> drop-sound-setup[drop-sound-setup] preload --> collect-sound-setup[collect-sound-setup] preload --> audio-start[audio-start] preload --> particles-array[particles-array] setup --> airplane-creation[airplane-creation] setup --> draw[draw loop] click preload href "#fn-preload" click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click airplane-sound-setup href "#sub-airplane-sound-setup" click drop-sound-setup href "#sub-drop-sound-setup" click collect-sound-setup href "#sub-collect-sound-setup" click audio-start href "#sub-audio-start" click particles-array href "#sub-particles-array" click airplane-creation href "#sub-airplane-creation" draw --> background-clear[background-clear] draw --> airplane-logic[airplane-logic] draw --> particle-emission[particle-emission] draw --> supply-drop-trigger[supply-drop-trigger] draw --> supply-drop-loop[supply-drop-loop] draw --> particle-loop[particle-loop] draw --> food-counter-display[food-counter-display] click draw href "#fn-draw" click background-clear href "#sub-background-clear" click airplane-logic href "#sub-airplane-logic" click particle-emission href "#sub-particle-emission" click supply-drop-trigger href "#sub-supply-drop-trigger" click supply-drop-loop href "#sub-supply-drop-loop" click particle-loop href "#sub-particle-loop" click food-counter-display href "#sub-food-counter-display" supply-drop-loop --> drop-loop[drop-loop] drop-loop --> collision-check[collision-check] drop-loop --> early-exit[early-exit] click drop-loop href "#sub-drop-loop" click collision-check href "#sub-collision-check" click early-exit href "#sub-early-exit" particle-loop --> particle-constructor[particle-constructor] particle-loop --> particle-update[particle-update] particle-loop --> particle-display[particle-display] click particle-constructor href "#sub-particle-constructor" click particle-update href "#sub-particle-update" click particle-display href "#sub-particle-display" airplane-logic --> airplane-update[airplane-update] airplane-logic --> airplane-display[airplane-display] airplane-logic --> airplane-wrapping[airplane-wrapping] click airplane-update href "#sub-airplane-update" click airplane-display href "#sub-airplane-display" click airplane-wrapping href "#sub-airplane-wrapping" supply-drop-trigger --> supplydrop-constructor[supplydrop-constructor] supplydrop-constructor --> supplydrop-update[supplydrop-update] supplydrop-update --> gravity-application[gravity-application] supplydrop-update --> supplydrop-display[supplydrop-display] click supplydrop-constructor href "#sub-supplydrop-constructor" click supplydrop-update href "#sub-supplydrop-update" click gravity-application href "#sub-gravity-application" click supplydrop-display href "#sub-supplydrop-display" windowresized[windowresized] --> canvas-resize[canvas-resize] canvas-resize --> airplane-reset[airplane-reset] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click airplane-reset href "#sub-airplane-reset" mousepressed[mousepressed] --> handletouchorclick[handletouchorclick] click mousepressed href "#fn-mousepressed" click handletouchorclick href "#fn-handletouchorclick" touchstarted[touchstarted] --> handletouchorclick click touchstarted href "#fn-touchstarted"

❓ Frequently Asked Questions

What visuals can users expect from the Airplane sketch?

The sketch creates a dynamic scene featuring an airplane flying across the canvas, leaving a trailing effect with emitted particles, while colorful supply drops are released at intervals.

How can users interact with the Airplane creative coding sketch?

Users can start the audio by clicking or touching the canvas, which also activates the airplane's engine sound and begins the visual simulation.

What creative coding techniques are showcased in this Airplane sketch?

This sketch demonstrates particle systems for visual effects, sound synthesis using p5.Oscillator, and timed events for dropping supplies, enhancing both interactivity and visual complexity.

Preview

Airplane - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Airplane - Code flow showing preload, setup, draw, mousepressed, touchstarted, handletouchorclick, windowresized, airplane, supplydrop, particle
Code Flow Diagram