🔬 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
🔬 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
🔬 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
🔬 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