This interactive game lets players control a brown dog character using arrow keys and drag colorful food items to feed it. As the dog eats, a 'Poo Power' meter fills up, and pressing 'P' triggers an animated poo release with synthesized sound effects—a quirky, playful experience combining movement, drag-and-drop interaction, and Web Audio API synthesis.
This sketch is a complete, playable game where you control a dog with arrow keys and drag food items across the screen to feed it. As the dog eats, a purple power bar fills up; when it's full, press 'P' to release 'Poo Power'—an animated brown blob appears at the dog's position with a synthesized 'plop' sound. The sketch demonstrates several advanced p5.js techniques: object-oriented design with custom Dog and Food classes, mouse interaction (click-and-drag), keyboard input, collision detection, animation state management, and Web Audio API sound synthesis using p5.sound's oscillators and noise generators.
The code is organized into three main classes—Dog, Food, and global game state variables—plus helper methods for sound playback. By studying it, you will learn how to structure a complete interactive game in p5.js: how to handle multiple moving objects, detect when objects collide, synthesize sound procedurally instead of loading audio files, manage animation state with flags and timers, and respond to both mouse and keyboard input simultaneously. The sketch also shows responsive canvas sizing and introduces p5.sound's oscillators, noise generators, and amplitude control for dynamic audio creation.
⚙️ How It Works
When the sketch loads, preload() initializes four sound generators (oscillators and noise nodes) and sets them to silent, and setup() creates a responsive canvas, spawns a dog in the center, and fills the screen with five randomly-positioned and randomly-colored food items.
Every frame, draw() clears the background and redraws the dog at its current position, all food items, and displays game instructions at the bottom of the screen. If the dog is currently pooing, a brown ellipse is drawn and a low-frequency sine tone plays for two seconds, then fades out.
When you press an arrow key, the dog.move() method updates its x or y position and constrains it to stay within the canvas boundaries using constrain().
When you click on a food item, mousePressed() detects which food is under the cursor using dist() and distance-based collision, calls startDrag() to begin tracking the food's offset from the mouse, and plays a soft synthesized bark using the triangle oscillator.
As you drag the food, updateDrag() continuously repositions it to follow your mouse while respecting the offset you started with, keeping the canvas bounds tight using constrain().
When you release the mouse, mouseReleased() checks if the food is now close enough to the dog (another dist() check in collectFood()); if so, the food disappears, pooAmount increases by 25, a crunch sound plays using pink noise, and a new food item spawns randomly. If pooAmount reaches maxPooAmount (100), the 'Press P to POO!' instruction appears.
Pressing 'P' calls dog.poo(), which resets pooAmount to zero, sets isPooing to true, stores the dog's current position in pooX and pooY, starts a 120-frame animation timer, plays a bark sound, and increments the poo counter. The next 120 frames display a brown blob and play a sine-wave 'plop' sound that fades out.
🎓 Concepts You'll Learn
Object-Oriented Programming (Classes and Methods)Mouse Interaction (Click, Drag, Release Detection)Keyboard Input (keyPressed, keyIsDown with Arrow Keys)Collision Detection (dist() function)Animation State Management (Flags and Timers)Web Audio Synthesis (Oscillators, Noise, Amplitude Envelopes)Canvas Responsiveness (windowResized)Game Loop Architecture
📝 Code Breakdown
preload()
preload() is a special p5.js function that runs once at startup, before setup(). It's the right place to initialize expensive resources (like sound generators and images) so they're ready to use instantly. All four sound generators are created, configured, and started here in silent mode (amp 0) so that later methods like playDogBark() can instantly change the amplitude and frequency without waiting for them to initialize.
function preload() {
dogBarkOsc = new p5.Oscillator();
dogBarkOsc.setType('triangle');
dogBarkOsc.freq(200);
dogBarkOsc.amp(0);
dogBarkOsc.start();
dogBarkNoise = new p5.Noise('white');
dogBarkNoise.amp(0);
dogBarkNoise.start();
eatSoundNoise = new p5.Noise('pink');
eatSoundNoise.amp(0);
eatSoundNoise.start();
pooSoundOsc = new p5.Oscillator();
pooSoundOsc.setType('sine');
pooSoundOsc.freq(100);
pooSoundOsc.amp(0);
pooSoundOsc.start();
}
Creates a triangle-wave oscillator at 200 Hz for the dog's bark pitch; starts silent (amp 0) and begins oscillating in the background so it's ready to play instantly when needed
calculationDog Bark Noise Setup dogBarkNoise = new p5.Noise('white');
dogBarkNoise.amp(0);
dogBarkNoise.start();
Creates white noise for bark texture and starts it silent—when mixed with the oscillator, creates a more realistic 'growly' bark sound
calculationEat Sound Noise Setup eatSoundNoise = new p5.Noise('pink');
eatSoundNoise.amp(0);
eatSoundNoise.start();
Initializes pink noise (which sounds more crunchy than white noise) for the food-eating sound effect
Creates a low-frequency sine wave (100 Hz) for the poo 'plop' sound; starts silent and running so it plays instantly on demand
function preload() {
preload() runs once before setup(), making it the perfect place to initialize sound generators that must be ready before the user interacts
dogBarkOsc = new p5.Oscillator();
Creates a new oscillator object that will generate a continuous sine wave by default (we'll change it to triangle next)
dogBarkOsc.setType('triangle');
Changes the waveform from sine to triangle, which sounds slightly more 'bark-like' than a pure sine wave
dogBarkOsc.freq(200);
Sets the oscillator's base frequency to 200 Hz—a reasonable pitch for a dog bark (lower than human speech, higher than a rumble)
dogBarkOsc.amp(0);
Sets amplitude to 0, making the oscillator silent—this is essential because the oscillator is already running in the background
dogBarkOsc.start();
Starts the oscillator running in the background at zero volume—when we call amp(0.3) later, the sound plays immediately without a startup delay
dogBarkNoise = new p5.Noise('white');
Creates a white noise generator (random static-like sound) that will be layered with the oscillator to add texture to the bark
eatSoundNoise = new p5.Noise('pink');
Creates pink noise (which emphasizes lower frequencies and sounds more like rustling or crunching than white noise)
pooSoundOsc = new p5.Oscillator();
Creates another oscillator, this time for the poo sound
pooSoundOsc.setType('sine');
Keeps the sine wave (default)—a pure sine at low frequency will sound like a 'plop' or 'thud'
pooSoundOsc.freq(100);
Sets the poo sound to an even lower frequency (100 Hz) than the bark (200 Hz), making it sound deep and resonant
setup()
setup() initializes your entire game world. Here we create the canvas, spawn the main character (dog), populate the world with food objects, and prepare audio. Every global variable that needs a starting value should be initialized here.
function setup() {
createCanvas(windowWidth, windowHeight);
dog = new Dog();
for (let i = 0; i < 5; i++) {
foodItems.push(new Food());
}
textSize(16);
textAlign(CENTER, CENTER);
userStartAudio();
}
Creates a canvas that fills the entire browser window—later, windowResized() will keep it responsive when the window is resized
calculationDog Object Creation dog = new Dog();
Instantiates the Dog class, which initializes the dog at the center of the canvas with starting values for position, size, speed, and poo power
for-loopInitial Food Spawning for (let i = 0; i < 5; i++) {
foodItems.push(new Food());
}
Loops 5 times, creating 5 Food objects at random positions and colors and adding them to the foodItems array
function setup() {
setup() runs once when the sketch first loads—it's where you initialize the game state
createCanvas(windowWidth, windowHeight);
Creates a canvas that matches your browser window size; windowWidth and windowHeight are p5.js built-in variables
dog = new Dog();
Creates a new Dog instance and stores it in the global 'dog' variable so draw() can access it every frame
for (let i = 0; i < 5; i++) {
Loops 5 times (i goes from 0 to 4); this is how we spawn multiple food items at startup
foodItems.push(new Food());
Creates a new Food object and adds it to the foodItems array; each Food gets a random position and color
textSize(16);
Sets the text size to 16 pixels for all text drawn later (instructions, poo counter, poo power bar)
textAlign(CENTER, CENTER);
Aligns text to the center horizontally and vertically—without this, text would draw from its top-left corner
userStartAudio();
Required by modern browsers to enable Web Audio playback; must be called after a user gesture (click/tap). p5.js automatically treats the canvas load as a gesture in most cases
draw()
draw() is the heartbeat of every p5.js sketch. It runs every frame and contains the game logic: updating positions, checking collisions, rendering visuals, and managing state transitions. Notice how we check isPooing as a state flag to control which code runs—this is a common pattern in games. Also notice the backward loop for food items; this prevents array index errors if we removed items during iteration.
function draw() {
background(173, 216, 230);
dog.draw();
dog.move();
for (let i = foodItems.length - 1; i >= 0; i--) {
foodItems[i].draw();
foodItems[i].updateDrag();
}
if (isPooing) {
fill(101, 67, 33);
ellipse(pooX, pooY, dog.size * 0.6, dog.size * 0.4);
pooSoundOsc.amp(0.5, 0.1);
pooTimer--;
if (pooTimer <= 0) {
isPooing = false;
pooSoundOsc.amp(0, 0.5);
}
} else {
pooSoundOsc.amp(0);
}
fill(0);
text("Use ARROW KEYS to move the dog.", width / 2, height - 80);
text("CLICK AND DRAG food to the dog.", width / 2, height - 60);
text("Collect food to fill Poo Power.", width / 2, height - 40);
if (dog.pooAmount >= dog.maxPooAmount) {
text("Press 'P' to POO!", width / 2, height - 20);
}
text("Successful Poos: " + pooCounter, width / 2, 30);
}
Clears the canvas and fills it with light blue (RGB 173, 216, 230) every frame, erasing the previous frame's drawings
calculationDog Update dog.draw();
dog.move();
Redraws the dog at its current position, then updates its position based on held arrow keys
for-loopFood Item Loop for (let i = foodItems.length - 1; i >= 0; i--) {
foodItems[i].draw();
foodItems[i].updateDrag();
}
Loops backward through all food items, drawing each one and updating its position if it's being dragged; looping backward allows safe removal during iteration
Displays the poo counter at the top of the screen (30 pixels down), showing how many times the player has successfully released poo
mousePressed()
mousePressed() is called once per mouse click. It's perfect for detecting when the user clicks on something (like a draggable object). Notice how we loop backward through the array and break immediately after finding a match—this ensures only one food item responds to the click, even if they overlap visually. The bark sound happens instantly, giving audio feedback for the click.
function mousePressed() {
for (let i = foodItems.length - 1; i >= 0; i--) {
if (foodItems[i].isMouseOver()) {
foodItems[i].startDrag();
dogBarkOsc.freq(random(150, 180), 0.05);
dogBarkOsc.amp(0.2, 0.05);
setTimeout(() => {
dogBarkOsc.amp(0, 0.3);
}, 100);
break;
}
}
}
Line-by-line explanation (10 lines)
🔧 Subcomponents:
for-loopFood Click Detection Loop for (let i = foodItems.length - 1; i >= 0; i--) {
if (foodItems[i].isMouseOver()) {
foodItems[i].startDrag();
break;
}
}
Loops through food items backward to find which (if any) has the mouse over it; when one is found, starts dragging it and breaks out of the loop so only one food is dragged at a time
Randomizes the bark pitch slightly, fades in the amplitude, then uses setTimeout to fade it back out after 100 milliseconds
function mousePressed() {
mousePressed() is a built-in p5.js function that runs once every time the mouse button is pressed
for (let i = foodItems.length - 1; i >= 0; i--) {
Loops backward through all food items looking for one under the mouse cursor
if (foodItems[i].isMouseOver()) {
Calls the Food object's isMouseOver() method, which uses dist() to check if the mouse is within the food's radius
foodItems[i].startDrag();
Calls the Food object's startDrag() method, which sets isDragging to true and stores the offset between the mouse and the food's center
dogBarkOsc.freq(random(150, 180), 0.05);
Changes the bark oscillator's frequency to a random value between 150–180 Hz with a 0.05-second fade-in time—randomizing the pitch makes it sound more natural
dogBarkOsc.amp(0.2, 0.05);
Sets the bark oscillator's volume to 0.2 (20% of max) with a 0.05-second fade-in, making the bark audible
setTimeout(() => {
Schedules a JavaScript callback to run after a delay (in milliseconds)
dogBarkOsc.amp(0, 0.3);
Fades out the bark sound to silence over 0.3 seconds, creating a natural tail-off instead of an abrupt stop
}, 100);
The callback runs after 100 milliseconds (0.1 seconds), so the bark plays for about 100ms then fades out over the next 300ms
break;
Breaks out of the loop so only one food item is selected; without this, the last food in the array under the cursor could also be dragged
mouseReleased()
mouseReleased() is called once per mouse button release. Here, it detects which food was being dragged, stops the drag, and checks if the dog ate it. If the dog is close enough, the food is removed and replaced with a new random one. Notice the backward loop and break statement again—this pattern (backward loop + break) appears throughout the code to handle one action per event safely.
function mouseReleased() {
for (let i = foodItems.length - 1; i >= 0; i--) {
if (foodItems[i].isDragging) {
foodItems[i].stopDrag();
if (dog.collectFood(foodItems[i])) {
foodItems.splice(i, 1);
foodItems.push(new Food());
}
break;
}
}
}
Line-by-line explanation (8 lines)
🔧 Subcomponents:
for-loopDragging Food Detection for (let i = foodItems.length - 1; i >= 0; i--) {
if (foodItems[i].isDragging) {
foodItems[i].stopDrag();
break;
}
}
Loops backward to find the food currently being dragged, stops dragging it, and breaks so only one is processed
conditionalFood Collection Check and Respawn if (dog.collectFood(foodItems[i])) {
foodItems.splice(i, 1);
foodItems.push(new Food());
}
Calls the dog's collectFood() method (which checks distance and gives the dog poo power); if true, removes the collected food from the array and spawns a new one
function mouseReleased() {
mouseReleased() is a built-in p5.js function that runs once every time the mouse button is released
for (let i = foodItems.length - 1; i >= 0; i--) {
Loops backward through food items to find the one currently being dragged
if (foodItems[i].isDragging) {
Checks if this food's isDragging flag is true (meaning the user was holding this food)
foodItems[i].stopDrag();
Calls the Food's stopDrag() method, which sets isDragging to false so it no longer follows the mouse
if (dog.collectFood(foodItems[i])) {
Calls the dog's collectFood() method, which checks if the food is close enough to the dog; returns true if collected, false otherwise
foodItems.splice(i, 1);
If the food was collected, removes it from the foodItems array at index i (splice removes 1 element starting at i)
foodItems.push(new Food());
Adds a new Food object (at a random position with random color) to the end of the foodItems array, replacing the collected one
break;
Breaks out of the loop so only one food item's release is processed per mouse release
keyPressed()
keyPressed() runs once per key press. This is where we detect the 'p' key and check if pooing is allowed (not already animating and power is full). The three-condition AND logic ensures the player can't spam poos or poo while an animation is running. For continuous key detection (like holding a key), use keyIsDown() instead, which is what dog.move() uses for arrow keys.
function keyPressed() {
if (key === 'p' && !isPooing && dog.pooAmount >= dog.maxPooAmount) {
dog.poo();
}
}
Checks three conditions: the 'p' key was pressed, the dog is not already pooing, and the poo power meter is full; only if all three are true does the poo happen
function keyPressed() {
keyPressed() is a built-in p5.js function that runs once every time any key is pressed
Checks three conditions with AND logic (&&): the key pressed is 'p', isPooing is false (! means NOT), and the dog's poo power is at or above maxPooAmount
dog.poo();
Calls the dog's poo() method, which resets poo power to 0, starts the animation, stores the current position, and plays a bark sound
windowResized()
windowResized() ensures the sketch stays responsive when the window is resized. By default, if you don't define this function, the canvas stays at its original size. Here, we simply resize it to match the current window. (The sketch doesn't reposition the dog, but you could add 'dog.x = width / 2' here to center it again.)
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (2 lines)
function windowResized() {
windowResized() is a built-in p5.js function that runs automatically whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions, keeping the sketch responsive
Dog constructor()
The constructor() method initializes a new Dog object with starting values for all its properties. Every time you write 'new Dog()', this constructor runs. By storing values like this.x, this.y, this.size, etc., we keep all dog-related data grouped together, making the code organized and easy to manage.
Defines a Dog class, which is a blueprint for creating dog objects with properties and methods
constructor() {
The constructor() method runs automatically when a new Dog is created (like 'new Dog()' in setup())
this.x = width / 2;
Sets the dog's starting x position to the center of the canvas
this.y = height / 2;
Sets the dog's starting y position to the center of the canvas
this.size = 50;
Sets the dog's visual size to 50 pixels—used for drawing body parts and collision detection
this.speed = 5;
Sets how many pixels the dog moves per frame when arrow keys are held
this.pooAmount = 0;
Initializes the poo power meter to 0 (empty)
this.maxPooAmount = 100;
Sets the maximum poo power to 100; the dog can only poo when pooAmount reaches this value
Dog.move()
move() is called every frame by draw(). It uses keyIsDown() (not keyPressed()) because we want continuous movement while a key is held, not just one movement per keypress. The four separate if statements allow the dog to move diagonally when two arrow keys are pressed simultaneously. The constrain() calls at the end keep the dog from leaving the screen—a common and essential practice in games.
🔬 These four lines handle the four arrow keys separately using keyIsDown(). What happens if you comment out (delete) the UP_ARROW and DOWN_ARROW lines? The dog won't move vertically anymore—try it!
if (keyIsDown(LEFT_ARROW)) this.x -= this.speed;
if (keyIsDown(RIGHT_ARROW)) this.x += this.speed;
if (keyIsDown(UP_ARROW)) this.y -= this.speed;
if (keyIsDown(DOWN_ARROW)) this.y += this.speed;
conditionalArrow Key Movement if (keyIsDown(LEFT_ARROW)) this.x -= this.speed;
if (keyIsDown(RIGHT_ARROW)) this.x += this.speed;
if (keyIsDown(UP_ARROW)) this.y -= this.speed;
if (keyIsDown(DOWN_ARROW)) this.y += this.speed;
Four separate if statements check which arrow keys are currently held and move the dog in that direction; four separate ifs allow diagonal movement when two keys are held at once
constrain() clamps y between a minimum (size/2) and maximum (height - size/2), preventing the dog from leaving the top or bottom edge
Dog.draw()
Dog.draw() is called every frame from the main draw() function. It renders the dog's body, head, legs, and a wagging tail using basic shapes. The wagging tail demonstrates push/pop (saving/restoring state) and rotate() with a sin-based oscillation—a classic animation pattern. The power bar at the top-left shows the player's progress toward being able to poo, providing clear visual feedback.
Uses push/pop to save the drawing state, translates to the dog's left side, rotates a tail rectangle back and forth using sin(frameCount) for oscillation, then restores the state
Draws the dog's legs as a wide, short brown rectangle below the body
push();
Saves the current drawing state (position, rotation, fill color, etc.) so we can restore it after drawing the tail
translate(this.x - this.size / 2, this.y);
Moves the coordinate system to the dog's left side, which becomes the rotation pivot point for the tail
rotate(sin(frameCount * 0.1) * PI / 8);
Rotates around the pivot point by an angle that oscillates: sin(frameCount * 0.1) produces a value between -1 and 1, multiplied by PI/8 (about 22 degrees), creating a wagging motion
rect(0, 0, this.size / 3, this.size / 8);
Draws a small brown rectangle (the tail) starting at the new origin (0,0), which is the dog's left side after translate()
pop();
Restores the drawing state to what it was before push(), so the rotation doesn't affect the power bar
fill(50);
Sets fill color to dark gray (RGB 50, 50, 50) for the power bar background
rect(10, 10, this.maxPooAmount * 2, 20);
Draws the power bar background: a dark gray rectangle at (10, 10) with width = maxPooAmount * 2 (200 pixels) and height 20
fill(72, 61, 139);
Sets fill color to dark purple (RGB 72, 61, 139) for the power bar foreground
rect(10, 10, this.pooAmount * 2, 20);
Draws the power bar foreground on top, with the same position and height but width = pooAmount * 2, so it grows as pooAmount increases
fill(255);
Sets fill color to white for the text label
textAlign(LEFT, CENTER);
Aligns text to the left horizontally and center vertically
Draws the label text at (15, 20) showing current poo power and max; floor() rounds down pooAmount to remove decimals
Dog.collectFood()
collectFood() is called from mouseReleased() when the player releases a dragged food item. It uses dist() to check if the food is within the dog's collision radius. If yes, it increases pooAmount (clamped to stay within bounds), plays a sound, and returns true so mouseReleased() knows to remove the food and spawn a new one. This is the core of the game loop: drag food to dog → dog eats → poo power increases.
Adds 25 to pooAmount, then uses constrain() to clamp it between 0 and maxPooAmount so it stays in bounds
this.playEatSound();
Calls the playEatSound() method to play the eating sound effect
return true;
Returns true, signaling that the food was successfully collected
return false;
If the distance is too large (no collision), returns false without collecting the food
Dog.poo()
poo() is called from keyPressed() and manages the poo action. It checks if the dog has enough power, then sets up the global animation state (isPooing flag, pooTimer, pooX, pooY) so that the main draw() function can animate the poo each frame. By storing the position before the animation starts, the poo stays in one place rather than following the moving dog—a nice visual detail. The method returns true/false to indicate success, a common pattern for action methods.
Resets pooAmount to 0, sets the isPooing flag to trigger the animation, initializes the animation timer, and stores the poo position (offset down from the dog's center)
poo() {
poo() is called from keyPressed() when the player presses 'P' and meets the conditions
if (this.pooAmount >= this.maxPooAmount) {
Checks if the dog has enough poo power (at or above maxPooAmount); if not, nothing happens and the function returns false
this.pooAmount = 0;
Resets pooAmount to 0 so the player must collect more food to poo again
isPooing = true;
Sets the global isPooing flag to true, which tells draw() to render the poo animation next frame
pooTimer = 120;
Sets the animation timer to 120 frames; draw() will decrement this each frame and end the animation when it reaches 0
pooX = this.x;
Stores the dog's current x position as the poo's x position (the poo appears at the dog's location)
pooY = this.y + this.size / 2;
Stores the dog's y position offset down by size/2, so the poo appears below the dog's center
this.playDogBark();
Calls playDogBark() to play a bark sound, giving audio feedback for the poo action
pooCounter++;
Increments the global pooCounter, tracking successful poos for the score display
return true;
Returns true, indicating the poo was successful
return false;
If pooAmount is not full, returns false and doesn't poo
Dog.playDogBark()
playDogBark() is a helper method that plays a realistic bark sound by layering an oscillator (pitch) with noise (texture). The randomized frequency makes barks sound more natural—identical barks would sound robotic. setTimeout() is a JavaScript timer that schedules the fade-out after a delay, allowing the bark to ring out before diminishing. This is more sophisticated than simply playing and stopping a pre-recorded sound.
calculationBark Frequency Randomization dogBarkOsc.freq(random(180, 220), 0.05);
Randomizes the oscillator's frequency between 180–220 Hz with a 0.05-second fade-in, making the pitch vary slightly for a more natural sound
calculationBark Amplitude Control with setTimeout setTimeout(() => {
dogBarkOsc.amp(0, 0.5);
dogBarkNoise.amp(0, 0.5);
}, 150);
Uses setTimeout to schedule a callback 150 milliseconds later; the callback fades both the oscillator and noise amplitude to 0 over 0.5 seconds
playDogBark() {
playDogBark() is a helper method that synthesizes a quick bark sound for the poo action
dogBarkOsc.freq(random(180, 220), 0.05);
Changes the bark oscillator's frequency to a random value between 180–220 Hz, with the frequency change smoothed over 0.05 seconds (fade-in time for the frequency sweep)
dogBarkOsc.amp(0.3, 0.05);
Sets the oscillator's amplitude (volume) to 0.3 (30% of max) with a 0.05-second fade-in, making it audible smoothly
dogBarkNoise.amp(0.2, 0.05);
Sets the noise generator's amplitude to 0.2 (20% of max) with a 0.05-second fade-in, adding texture to the bark
setTimeout(() => {
setTimeout is a JavaScript function that schedules a callback function to run after a specified delay (in milliseconds)
dogBarkOsc.amp(0, 0.5);
Fades the oscillator's amplitude to 0 over 0.5 seconds, creating a natural tail-off
dogBarkNoise.amp(0, 0.5);
Fades the noise amplitude to 0 over 0.5 seconds
}, 150);
The callback runs 150 milliseconds (0.15 seconds) after playDogBark() is called, so the bark plays for roughly 150ms before fading out over the next 500ms
Dog.playEatSound()
playEatSound() creates a crunching sound using pink noise (which is grainier and lower-frequency than white noise, mimicking food sounds). The pan() method adds stereo variety—each crunch sound comes from a slightly different speaker position, making multiple eating sounds less repetitive. Both playEatSound() and playDogBark() follow the same pattern: fade in immediately, then use setTimeout to schedule a fade-out later.
Randomizes the stereo pan position from left (-0.5) to right (0.5), adding a subtle spatial variety to eating sounds
playEatSound() {
playEatSound() is called from collectFood() when the dog eats food
eatSoundNoise.amp(0.4, 0.05);
Sets the pink noise amplitude to 0.4 (40% of max) with a 0.05-second fade-in
eatSoundNoise.pan(random(-0.5, 0.5));
Randomizes the stereo panning: -1 is hard left, 0 is center, 1 is hard right; here it ranges from -0.5 to 0.5, a subtle effect
setTimeout(() => {
Schedules a callback 100 milliseconds later
eatSoundNoise.amp(0, 0.3);
Fades the noise amplitude to 0 over 0.3 seconds, creating a natural crunch tail-off
}, 100);
The callback runs after 100ms, so the crunch sound plays for 100ms then fades out over 300ms
Food constructor()
The Food constructor creates a new food item at a random position with a random warm color. Each food object is independent, storing its own position, size, color, and drag state. By initializing offsetX and offsetY to 0, we're ready to calculate them when dragging starts; this prevents the food from jumping to the cursor.
Creates a random color for the food: red 200–255, green 100–150, blue 50 (always low), resulting in warm, varied colors
this.isDragging = false;
Initializes the drag flag to false; becomes true when the player clicks and drags this food
this.offsetX = 0;
Initializes the offset from the mouse; calculated when dragging starts to prevent the food from snapping to the cursor
this.offsetY = 0;
Initializes the vertical offset from the mouse
Food.draw()
Food.draw() renders each food item with a shadow effect for depth. The shadow (a semi-transparent black ellipse offset slightly) is drawn first, then the colorful food circle is drawn on top with a black outline. Finally, the word "Food" is drawn in the center. This layering technique is common in 2D graphics: draw background elements first, then foreground elements on top.
Draws a shadow ellipse offset 2 pixels right and down from the food's position
fill(this.color);
Resets the fill color to the food's original color
stroke(0);
Enables a black stroke (outline) with the default color
strokeWeight(1);
Sets the stroke thickness to 1 pixel
ellipse(this.x, this.y, this.size, this.size);
Draws the main food circle with the random color and black outline
fill(0);
Sets the fill color to black for the text
noStroke();
Disables stroke for the text
textSize(10);
Sets the text size to 10 pixels
textAlign(CENTER, CENTER);
Centers the text horizontally and vertically
text("Food", this.x, this.y);
Draws the label "Food" at the center of this food item
Food.isMouseOver()
isMouseOver() is a simple boolean check using distance-based collision. It returns true if the mouse is within the food's circular bounds, false otherwise. This is called from mousePressed() to detect when the player clicks on a food item.
Uses dist() to calculate the distance from the mouse to the food's center; returns true if the distance is less than the food's radius (size/2)
Food.startDrag()
startDrag() initializes the drag state by setting the flag to true and calculating the offset. The offset is crucial: if the player clicks on the edge of the food, the offset will be large; if they click the center, the offset will be small. This keeps the food from jumping to the cursor during drag.
startDrag() is called from mousePressed() when the player clicks on this food
this.isDragging = true;
Sets the isDragging flag to true, signaling that this food is being dragged
this.offsetX = this.x - mouseX;
Calculates and stores the horizontal offset between the food's position and the cursor; this prevents the food from snapping to the cursor when dragging starts
this.offsetY = this.y - mouseY;
Calculates and stores the vertical offset
Food.updateDrag()
updateDrag() runs every frame and moves the dragged food to follow the mouse. The offset ensures the food maintains its grab point (the point where the player clicked) rather than jumping to the cursor. The constrain() calls prevent the food from being dragged off-screen, keeping it always visible.
Clamps the food's y position to stay within the canvas
Food.stopDrag()
stopDrag() is the simplest method: just set the isDragging flag to false. After this, updateDrag() won't move the food anymore (it checks isDragging before updating position). Simple but essential.
stopDrag() {
this.isDragging = false;
}
Line-by-line explanation (2 lines)
stopDrag() {
stopDrag() is called from mouseReleased() when the player releases the food
this.isDragging = false;
Sets isDragging to false, stopping the food from following the mouse
📦 Key Variables
dogobject
Stores the Dog instance; contains all dog properties (position, size, poo power) and methods (move, draw, poo, etc.)
let dog;
foodItemsarray
An array of Food objects currently on the canvas; looped through each frame to draw and update each food item
let foodItems = [];
isPooingboolean
Flag that tracks whether the dog is currently in a poo animation; controls whether the poo blob is drawn and the sound plays
let isPooing = false;
pooTimernumber
Countdown timer (in frames) for the poo animation; decremented each frame, animation ends when it reaches 0
let pooTimer = 0;
pooCounternumber
Tracks the total number of successful poos; displayed at the top of the screen
let pooCounter = 0;
pooXnumber
Stores the x position where the poo blob is drawn; fixed at the moment poo() is called so the blob doesn't follow the moving dog
let pooX;
pooYnumber
Stores the y position where the poo blob is drawn
let pooY;
dogBarkOscobject
A p5.sound Oscillator that generates the bark pitch; started in preload() and controlled by playDogBark()
let dogBarkOsc;
dogBarkNoiseobject
A p5.sound Noise generator (white noise) that adds texture to the bark sound
let dogBarkNoise;
eatSoundNoiseobject
A p5.sound Noise generator (pink noise) that creates the crunchy eating sound
let eatSoundNoise;
pooSoundOscobject
A p5.sound Oscillator that generates the poo 'plop' sound; a low-frequency sine wave
let pooSoundOsc;
🔧 Potential Improvements (5)
Here are some ways this code could be enhanced:
STYLEglobal audio initialization
Sound objects are created in preload() but many are never reassigned once initialized; consider storing them as class properties or using an audio manager
💡 Create a Sound class or manager object to encapsulate all audio generators, making the code more modular and easier to extend with new sounds
FEATUREmouseReleased() and collectFood()
Food collection is based on distance but doesn't account for the dog's mouth or specific body part; feels abstract
💡 Track the dog's head position separately (e.g., headX, headY) and check distance to the head for more intuitive collision; or add visual feedback (like a munching animation) when food is collected
BUGDog.move()
The boundary constraints keep the dog's center within bounds but don't account for the dog's visual size fully when it's stretched (e.g., by the head and tail)
💡 Increase the buffer in constrain() to use a larger radius, e.g., constrain(this.x, this.size, width - this.size) to account for the head extending further
PERFORMANCEdraw() loop and food iteration
The food loop uses 'foodItems.length - 1' backward iteration every frame; while safe, it's slightly less efficient than a forward loop with careful splice handling
💡 For a small array like foodItems (usually <10 items), this is negligible, but consider using filter() or a more functional approach if the array grows
STYLEkeyPressed()
Checking 'key === "p"' is case-sensitive, so pressing 'P' (shift + p) won't work; players might expect either to work
💡 Change to 'key.toLowerCase() === "p"' so both 'p' and 'P' trigger poo