Sketch 2026-05-17 05:06

This is a 3D first-person bird-shooting game built with p5.js where you fly through a procedurally generated forest, aim at colorful birds flying past, and earn points for accurate shots. As your score grows, you unlock increasingly powerful weapons and faster, more exotic birds—from ducks to dragons—each with unique models, colors, particle effects, and reactive audio.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make birds spawn closer — Birds spawn at random distances 500-2000 units away—change it to 200-800 and they'll surround you immediately, creating chaos.
  2. Slow down the music — The background loop plays a note every 0.15 seconds—increase it to 0.4 and the tempo drops to half speed, making it moody.
  3. Increase fire rate — Change pistol's rate from 15 frames between shots to 5, letting you rapid-fire without the rifle—test your aim.
  4. Make everything red — The sky is a blue RGB value (120, 190, 240)—change it to (255, 0, 0) for a red apocalypse.
  5. More particles on hit — Shots currently create 25 particles—boost to 60 for a massive explosion effect that will slow down performance.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full 3D first-person shooter game inside p5.js using WEBGL rendering, 3D camera control, procedural audio synthesis with p5.sound, and a progression system that unlocks new weapons and bird targets. The visual appeal comes from smooth first-person movement, realistic bird models that flap and spin, particle bursts on hit, dynamic muzzle flashes, and a vast procedurally generated forest. The code teaches you how to combine collision detection, 3D rendering, audio synthesis, and game state management into a cohesive interactive experience.

The sketch is organized into setup and draw loops, with helper functions for rendering the environment, gun models, and audio. You'll learn how to build a complete game loop: handling mouse-locked camera input, updating and drawing dozens of animated 3D objects, managing particle systems, detecting ray-casting collisions, triggering audio and visual feedback, and saving player progress through a shop UI. The code demonstrates professional game architecture patterns—object classes, collision logic, state-driven rendering, and audio-visual feedback synchronization—all within a few hundred lines.

⚙️ How It Works

  1. When you click START, the browser locks your mouse pointer and initializes audio synthesis. The canvas is created in WEBGL mode, 200 procedurally placed trees are scattered across a 4000×4000 unit terrain, and 15 bird targets spawn at random distances around you.
  2. Every frame, the draw loop clears the background, sets up 3D lighting and camera based on your mouse look direction, updates all bird and particle positions, detects collisions from gun rays to birds, and renders the forest, birds, particles, and gun model at different depths.
  3. Input is captured every frame: mouse movement rotates your view (yaw and pitch), WASD keys move you through the world, and mouse clicks fire the gun. The fire function casts a ray from your camera in the look direction and tests if it hits any living birds within a radius determined by the active gun's spread.
  4. When a shot hits a bird, it dies (starts spinning and rising), 25 particle bursts spawn at the bird's position in its color, your score increases by that bird's point value, and audio feedback plays. Particles update with gravity and rotation each frame, gradually fading until they disappear.
  5. Unlocking new guns and birds is managed through a shop UI: earning score lets you buy items, which unlock new weapon models (pistol to laser cannon) with different firing rates and spreads, and new bird targets with higher speeds and point values. The active gun and bird change the visual appearance, audio, and game difficulty instantly.
  6. The audio layer runs in parallel: a white-noise gunshot burst on every shot, a triangle-wave hit squeak when you score, and a fast background music loop that arpeggios through a 16-note bassline, all synthesized with p5.sound oscillators and envelopes.

🎓 Concepts You'll Learn

3D WEBGL rendering and camera controlRay-casting collision detectionFirst-person mouse-locked input handlingParticle systems with physicsProcedural audio synthesis with oscillators and envelopesGame state management and progressionObject-oriented design with classesAnimation blending and skeletal transforms

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, populates the world with static objects (trees) and dynamic ones (birds), sets up audio, and builds the UI. The WEBGL mode is critical—without it, 3D rendering with camera transforms and lighting won't work.

function setup() {
  cnv = createCanvas(windowWidth, windowHeight, WEBGL);
  
  document.getElementById('start-btn').addEventListener('click', async () => {
    cnv.elt.requestPointerLock();
    await userStartAudio(); // Start audio context on user gesture
    if (musicLoop && !musicLoop.isPlaying) {
      musicLoop.start(); // Start the background music
    }
  });

  // Generate realistic, massive forest
  for (let i = 0; i < 200; i++) {
    trees.push({ 
      x: random(-4000, 4000), 
      z: random(-4000, 4000), 
      size: random(1.5, 4.0) 
    });
  }
  
  for (let i = 0; i < 15; i++) ducks.push(new Target());

  setupAudio();
  buildShopUI(); 
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

event-listener Pointer Lock Request cnv.elt.requestPointerLock();

Hides the cursor and traps mouse input within the canvas for immersive first-person control

for-loop Forest Generation for (let i = 0; i < 200; i++) { trees.push({ x: random(-4000, 4000), z: random(-4000, 4000), size: random(1.5, 4.0) }); }

Randomly scatters 200 trees across a wide 4000×4000 unit area with varied sizes for depth perception

for-loop Initial Bird Spawning for (let i = 0; i < 15; i++) ducks.push(new Target());

Creates 15 bird targets ready to fly and be shot at

cnv = createCanvas(windowWidth, windowHeight, WEBGL);
Creates a full-screen WEBGL canvas—the third argument WEBGL enables 3D rendering with lighting, transforms, and depth
document.getElementById('start-btn').addEventListener('click', async () => {
Waits for the player to click START before locking the mouse and starting audio—browsers require user interaction to begin audio
cnv.elt.requestPointerLock();
Locks the mouse cursor to the canvas and hides it, so your view can be controlled by raw mouse movement
await userStartAudio();
Initializes the p5.sound context—browsers block audio until a user gesture like a click happens
if (musicLoop && !musicLoop.isPlaying) { musicLoop.start();
Checks if the background music loop exists and isn't already playing, then starts it
for (let i = 0; i < 200; i++) { trees.push({ x: random(-4000, 4000), z: random(-4000, 4000), size: random(1.5, 4.0) }); }
Loops 200 times, pushing a tree object with random position (x and z in a 4000-unit square) and random size (1.5 to 4 times normal) into the trees array
for (let i = 0; i < 15; i++) ducks.push(new Target());
Loops 15 times, creating a new Target instance (a bird) and adding it to the ducks array—these birds will spawn around the player
setupAudio();
Calls the audio initialization function to set up oscillators, envelopes, and the background music loop
buildShopUI();
Populates the HTML shop menu with all weapon and bird options dynamically

setupAudio()

This function sets up four independent audio generators: two one-shot envelopes (for gunshot and hit), one looping synthesizer (for background music). Each uses p5.sound's Envelope class to control amplitude over time, and the SoundLoop class to repeat a callback at a fixed tempo. The audio runs in a separate thread from the graphics, so a slow frame rate won't stutter the music.

🔬 This controls how fast the gunshot sound dies. What happens if you change the Decay from 0.1 to 0.5? Or change Range from 0.5 to 0.2?

  shootEnv.setADSR(0.01, 0.1, 0.0, 0.0); shootEnv.setRange(0.5, 0);
function setupAudio() {
  // Gunshot synth
  shootNoise = new p5.Noise('white'); shootEnv = new p5.Envelope();
  shootEnv.setADSR(0.01, 0.1, 0.0, 0.0); shootEnv.setRange(0.5, 0);
  shootNoise.amp(shootEnv); shootNoise.start();

  // Hit squeak synth
  hitOsc = new p5.Oscillator('triangle'); hitEnv = new p5.Envelope();
  hitEnv.setADSR(0.01, 0.2, 0.0, 0.0); hitEnv.setRange(1, 0);
  hitOsc.freq(800); hitOsc.amp(hitEnv); hitOsc.start();

  // Background Music Synth
  bgSynth = new p5.MonoSynth();
  bgSynth.amp(0.04); // Changed from setVolume() to amp() to fix the error
  
  // Creates a fast, driving bassline loop
  musicLoop = new p5.SoundLoop(function(timeFromNow) {
    let note = arpNotes[arpIndex];
    bgSynth.play(note, 0.5, timeFromNow, 0.1);
    arpIndex = (arpIndex + 1) % arpNotes.length;
  }, 0.15); // 0.15 seconds per note (fast tempo)
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

audio-initialization Gunshot Synthesizer shootNoise = new p5.Noise('white'); shootEnv = new p5.Envelope(); shootEnv.setADSR(0.01, 0.1, 0.0, 0.0); shootEnv.setRange(0.5, 0); shootNoise.amp(shootEnv); shootNoise.start();

Creates a white-noise burst that decays rapidly, mimicking a gun crack when triggered

audio-initialization Hit Squeak Synthesizer hitOsc = new p5.Oscillator('triangle'); hitEnv = new p5.Envelope(); hitEnv.setADSR(0.01, 0.2, 0.0, 0.0); hitEnv.setRange(1, 0); hitOsc.freq(800); hitOsc.amp(hitEnv); hitOsc.start();

Creates a high-pitched triangle wave that decays over 0.2 seconds, sounding like a satisfying impact squeak

audio-initialization Background Music Loop musicLoop = new p5.SoundLoop(function(timeFromNow) { let note = arpNotes[arpIndex]; bgSynth.play(note, 0.5, timeFromNow, 0.1); arpIndex = (arpIndex + 1) % arpNotes.length; }, 0.15);

Creates a repeating loop that plays each note in arpNotes sequentially every 0.15 seconds, cycling forever

shootNoise = new p5.Noise('white');
Creates a white-noise oscillator (all frequencies at equal volume)—the basis for the gunshot sound
shootEnv = new p5.Envelope();
Creates an amplitude envelope that will control how the gunshot sound fades in and out
shootEnv.setADSR(0.01, 0.1, 0.0, 0.0);
Sets Attack to 0.01s, Decay to 0.1s, Sustain to 0.0, and Release to 0.0—the sound pops quickly and decays fast
shootEnv.setRange(0.5, 0);
Sets the envelope peak to 0.5 (half volume) and the floor to 0—the sound goes from silence to 0.5 to silence
shootNoise.amp(shootEnv);
Connects the envelope to the noise's volume so the envelope controls when the sound plays
shootNoise.start();
Starts the white-noise oscillator—it will stay running in the background, silent until the envelope triggers
hitOsc = new p5.Oscillator('triangle');
Creates a triangle-wave oscillator (softer than sine, sharper than square)—the basis for the hit squeak
hitEnv.setADSR(0.01, 0.2, 0.0, 0.0);
Attack 0.01s, Decay 0.2s (twice as long as the gunshot), Sustain and Release 0.0—the squeak decays more slowly
hitOsc.freq(800);
Sets the triangle oscillator's frequency to 800 Hz—a high-pitched squeak in the treble range
bgSynth = new p5.MonoSynth();
Creates a monophonic synthesizer (plays one note at a time) for the background music
bgSynth.amp(0.04);
Sets the synth's amplitude to 0.04 (very quiet) so the music doesn't dominate the gunshot and hit sounds
let note = arpNotes[arpIndex];
Looks up the current note name (e.g., 'D2') from the arpNotes array using the current arpIndex
bgSynth.play(note, 0.5, timeFromNow, 0.1);
Plays that note at velocity 0.5, scheduled to play after timeFromNow, held for 0.1 seconds
arpIndex = (arpIndex + 1) % arpNotes.length;
Increments arpIndex and wraps it back to 0 when it reaches the end—cycles through all 16 notes forever
}, 0.15);
The second argument 0.15 sets the loop timing: a new note plays every 0.15 seconds (400 BPM in sixteenth notes)

draw()

draw() is the heartbeat of every p5.js sketch—it runs 60 times per second. This function demonstrates professional game architecture: checking game state (isLocked), processing input (handleInput), updating game objects (birds and particles), rendering the world (environment, targets, HUD), and managing the camera. The pointer lock check is crucial: it pauses the game when the menu is open and resumes when you click back into gameplay.

🔬 This loop updates and draws every bird each frame. What happens if you remove the if(isLocked) check and uncomment it—so birds move even when the menu is open?

  for (let d of ducks) {
    if (isLocked) d.update();
    d.draw();
  }
function draw() {
  let isLocked = document.pointerLockElement === cnv.elt;
  let gunData = items.guns[activeGun];

  if (isLocked) {
    document.getElementById('overlay').style.display = 'none';
    handleInput();
    if (gunData.auto && mouseIsPressed) fireGun();
  } else {
    document.getElementById('overlay').style.display = 'flex';
  }

  background(120, 190, 240); 
  ambientLight(120);
  directionalLight(255, 255, 240, 0.5, 1, -0.5); 

  let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);
  let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5;

  camera(
    player.x + shakeX, player.y + shakeY, player.z,
    player.x + lookX + shakeX, player.y + lookY + shakeY, player.z + lookZ,
    0, 1, 0
  );

  drawEnvironment();

  for (let d of ducks) {
    if (isLocked) d.update();
    d.draw();
  }

  for (let i = particles.length - 1; i >= 0; i--) {
    if (isLocked) particles[i].update();
    particles[i].draw();
    if (particles[i].life <= 0) particles.splice(i, 1);
  }

  recoil = lerp(recoil, 0, 0.1);

  camera(); // Reset camera matrix to screen space
  clearDepth(); // Guarantees the gun renders ON TOP of the environment, never clipping
  
  noLights(); 
  ambientLight(150); 
  directionalLight(255, 255, 255, -1, 1, -1);
  drawGun();
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

conditional Pointer Lock State Check let isLocked = document.pointerLockElement === cnv.elt;

Determines if the mouse is currently locked to the canvas, which controls whether gameplay updates run

conditional Input Handling and Auto-Fire if (isLocked) { document.getElementById('overlay').style.display = 'none'; handleInput(); if (gunData.auto && mouseIsPressed) fireGun(); } else { document.getElementById('overlay').style.display = 'flex'; }

If pointer is locked, hides the UI, processes movement input, and fires automatically if the gun has auto mode and the mouse is pressed

calculation Camera Direction Calculation let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);

Converts yaw and pitch angles into a 3D look direction vector using trigonometry

calculation Recoil Camera Shake let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5;

Adds random jitter to the camera position proportional to recoil, making shots feel impactful

for-loop Bird Update and Draw for (let d of ducks) { if (isLocked) d.update(); d.draw(); }

Iterates through all birds, updating their position/animation if gameplay is active, then drawing them

for-loop Particle System Update for (let i = particles.length - 1; i >= 0; i--) { if (isLocked) particles[i].update(); particles[i].draw(); if (particles[i].life <= 0) particles.splice(i, 1); }

Updates and draws all particles, removing dead ones—iterates backwards to safely splice while looping

calculation Recoil Decay recoil = lerp(recoil, 0, 0.1);

Smoothly interpolates recoil toward 0 each frame, so the camera shake fades away after a shot

let isLocked = document.pointerLockElement === cnv.elt;
Checks if the canvas currently owns the pointer lock (mouse is trapped in the game). This boolean controls gameplay updates.
let gunData = items.guns[activeGun];
Fetches the current gun's data object, which includes fire rate, spread radius, and whether it's automatic
if (isLocked) { document.getElementById('overlay').style.display = 'none';
If pointer is locked, hides the shop overlay so you can see the 3D world clearly
handleInput();
Processes keyboard input (WASD movement) and mouse look, updating player position and yaw/pitch
if (gunData.auto && mouseIsPressed) fireGun();
If the active gun is automatic AND the mouse button is held down, fires every frame
} else { document.getElementById('overlay').style.display = 'flex';
If pointer is not locked, shows the overlay so you can click buttons and access the shop
background(120, 190, 240);
Clears the canvas with a sky-blue color, erasing the previous frame's graphics
ambientLight(120);
Adds a uniform ambient light with intensity 120, illuminating all surfaces equally regardless of normal direction
directionalLight(255, 255, 240, 0.5, 1, -0.5);
Adds a directional light (like the sun) with warm white color, pointing toward (0.5, 1, -0.5) to create shading
let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);
Converts yaw (horizontal rotation) and pitch (vertical rotation) angles into a normalized look direction vector using 3D trigonometry
let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5;
Generates random camera jitter proportional to the recoil value—higher recoil means bigger shake
camera( player.x + shakeX, player.y + shakeY, player.z, player.x + lookX + shakeX, player.y + lookY + shakeY, player.z + lookZ, 0, 1, 0 );
Sets the 3D camera position (player location plus shake), look-at point (player plus look direction), and up vector (0, 1, 0 for standard Y-up)
drawEnvironment();
Renders the ground plane and all 200 trees
for (let d of ducks) { if (isLocked) d.update(); d.draw(); }
Loops through all birds: updates their position/animation if gameplay is running, then draws them to the screen
for (let i = particles.length - 1; i >= 0; i--) {
Loops backwards through the particles array—this allows safe removal of elements during iteration without skipping any
if (isLocked) particles[i].update(); particles[i].draw(); if (particles[i].life <= 0) particles.splice(i, 1);
Updates particle physics, draws it, and removes it if its life value has reached zero
recoil = lerp(recoil, 0, 0.1);
Smoothly interpolates recoil toward 0 with a factor of 0.1—recoil decays gradually, not instantly
camera();
Resets the camera to 2D screen-space mode (the p5.js default), so the gun renders as a 2D HUD element
clearDepth();
Clears the depth buffer, guaranteeing that subsequent 3D rendering (the gun) appears on top, never clipping behind the environment
noLights();
Disables all lighting calculations for the gun, so it renders at full brightness with no shading
drawGun();
Renders the gun model in screen-space, positioned in the lower-right of your view

handleInput()

handleInput() is where mouse-look and WASD movement happen—the two pillars of first-person controls. The key insight is direction vectors: walkDir and rightDir are computed from yaw every frame, so all movement is always relative to where you're looking. This is why strafing sideways while looking backward works intuitively.

🔬 These four if-statements are the WASD movement. What happens if you swap the positions of rightDir and walkDir—so W and A move in the same direction?

  if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
  if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
  if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
  if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z += rightDir.z * moveSpeed; }
function handleInput() {
  yaw += movedX * 0.002; pitch += movedY * 0.002;
  pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);

  let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize();
  let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();

  if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
  if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
  if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
  if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z += rightDir.z * moveSpeed; }
  
  player.x = constrain(player.x, -3500, 3500); player.z = constrain(player.z, -3500, 3500);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Mouse Look (Yaw and Pitch) yaw += movedX * 0.002; pitch += movedY * 0.002;

Accumulates horizontal and vertical rotation based on how far the mouse has moved since the last frame

conditional Pitch Clamp (Look Up/Down Limits) pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);

Prevents looking straight up or down (which would flip the camera), keeping gameplay natural

calculation Forward and Right Direction Vectors let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize(); let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();

Converts yaw into forward and rightward unit vectors, so movement is always relative to where you're looking

conditional WASD Movement Keys if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; } if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; } if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; } if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z += rightDir.z * moveSpeed; }

Checks four key codes (W=87, A=65, S=83, D=68) and moves the player forward, left, backward, or right accordingly

conditional World Boundary Clamping player.x = constrain(player.x, -3500, 3500); player.z = constrain(player.z, -3500, 3500);

Prevents the player from walking beyond the 7000×7000 unit forest boundary, keeping them in the generated terrain

yaw += movedX * 0.002;
movedX is p5.js's built-in variable tracking mouse movement since last frame—multiplying by 0.002 scales it to a usable rotation amount (0.002 radians per pixel)
pitch += movedY * 0.002;
movedY tracks vertical mouse movement—same scaling converts pixels into vertical rotation (pitch)
pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);
Clamps pitch between -π/2 + 0.05 and π/2 - 0.05 (roughly -90° to +90° with a tiny margin), preventing the camera from flipping
let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize();
Creates a unit vector pointing forward based on yaw—the 0 in the middle means no Y movement (you walk on the XZ plane)
let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();
Creates a unit vector perpendicular to walkDir by adding π/2 to yaw, pointing rightward relative to where you're looking
if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
If W (keyCode 87) is held, moves the player forward by moveSpeed units in the X and Z components of walkDir
if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
If S (keyCode 83) is held, moves backward by subtracting walkDir
if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
If A (keyCode 65) is held, strafes left by subtracting rightDir
if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z += rightDir.z * moveSpeed; }
If D (keyCode 68) is held, strafes right by adding rightDir
player.x = constrain(player.x, -3500, 3500);
Clamps player.x between -3500 and 3500 units, preventing escape from the forest boundary
player.z = constrain(player.z, -3500, 3500);
Clamps player.z between -3500 and 3500 units, matching the forest boundary

fireGun()

fireGun() combines four game mechanics: fire-rate limiting (preventing overfiring), ray-casting collision detection (checking if the look ray hits a bird), feedback (audio and particles), and score tracking. The collision math is the star: dot product finds distance along the ray, subtraction + magnitude finds perpendicular distance, and comparing to a radius determines a hit. This is the foundation of hitscan weapons in all games.

🔬 This creates colored particles matching the bird type. What happens if you replace cols.body with a fixed color like [255, 0, 0] (red)? All birds would burst the same color!

      let cols = getBirdColors(activeBird);
        for(let i = 0; i < 25; i++) particles.push(new Particle(d.x, d.y, d.z, cols.body));
function fireGun() {
  let gunData = items.guns[activeGun];
  if (frameCount - lastFired < gunData.rate) return; 
  lastFired = frameCount;

  shootEnv.play();
  recoil = 3;

  let lookDir = createVector(cos(yaw) * cos(pitch), sin(pitch), sin(yaw) * cos(pitch));
  
  let birdScale = 1.0;
  if (activeBird === 'phoenix') birdScale = 4.0;
  if (activeBird === 'dragon') birdScale = 7.0; 

  for (let d of ducks) {
    if (d.dead) continue;
    let vectorToDuck = createVector(d.x - player.x, d.y - player.y, d.z - player.z);
    let t = vectorToDuck.dot(lookDir); 
    
    if (t > 0) { 
      let projection = p5.Vector.mult(lookDir, t);
      let distToLine = p5.Vector.sub(vectorToDuck, projection).mag();
      
      let effectiveRadius = gunData.radius + (d.size * birdScale * 0.6);
      
      if (distToLine < effectiveRadius) {
        d.die();
        score += items.birds[activeBird].pts;
        document.getElementById('score').innerText = score;
        hitEnv.play();
        
        let cols = getBirdColors(activeBird);
        for(let i = 0; i < 25; i++) particles.push(new Particle(d.x, d.y, d.z, cols.body));
        
        if (activeGun !== 'sniper') break; 
      }
    }
  }
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

conditional Fire Rate Limiter if (frameCount - lastFired < gunData.rate) return;

Prevents firing faster than the gun's rate allows by exiting early if not enough frames have passed

for-loop Ray-Bird Collision Detection for (let d of ducks) { if (d.dead) continue; let vectorToDuck = createVector(d.x - player.x, d.y - player.y, d.z - player.z); let t = vectorToDuck.dot(lookDir); if (t > 0) { let projection = p5.Vector.mult(lookDir, t); let distToLine = p5.Vector.sub(vectorToDuck, projection).mag(); let effectiveRadius = gunData.radius + (d.size * birdScale * 0.6); if (distToLine < effectiveRadius) { d.die(); score += items.birds[activeBird].pts; document.getElementById('score').innerText = score; hitEnv.play(); let cols = getBirdColors(activeBird); for(let i = 0; i < 25; i++) particles.push(new Particle(d.x, d.y, d.z, cols.body)); if (activeGun !== 'sniper') break; } } }

Iterates through all birds, performs ray-casting to test if the gun's ray passes within the bird's collision radius, and triggers hit effects if so

let gunData = items.guns[activeGun];
Looks up the active gun's stats (rate, radius, auto) from the items object
if (frameCount - lastFired < gunData.rate) return;
If fewer frames have passed than the gun's rate allows, exit early (do not fire). This prevents bursts faster than intended.
lastFired = frameCount;
Records the current frame number, so we can check against it on the next fire attempt
shootEnv.play();
Triggers the gunshot audio envelope, playing the white-noise burst sound
recoil = 3;
Sets recoil to 3, which will cause camera shake and gun knockback animation this frame and fade away over subsequent frames
let lookDir = createVector(cos(yaw) * cos(pitch), sin(pitch), sin(yaw) * cos(pitch));
Converts yaw and pitch into a 3D unit vector pointing in the direction you're aiming
let birdScale = 1.0; if (activeBird === 'phoenix') birdScale = 4.0; if (activeBird === 'dragon') birdScale = 7.0;
Sets a scale multiplier for the bird's visual size—larger birds should have larger collision radii
for (let d of ducks) {
Loops through all bird targets
if (d.dead) continue;
Skips dead birds—they can't be shot again
let vectorToDuck = createVector(d.x - player.x, d.y - player.y, d.z - player.z);
Calculates the vector from the player's position to the bird's position
let t = vectorToDuck.dot(lookDir);
Computes the dot product: the distance along the look ray toward the bird. If t <= 0, the bird is behind you.
if (t > 0) {
Only test collision if the bird is ahead of you (positive t)
let projection = p5.Vector.mult(lookDir, t); let distToLine = p5.Vector.sub(vectorToDuck, projection).mag();
Projects the bird onto the look ray (projection), then calculates the perpendicular distance from the bird to the ray (distToLine)
let effectiveRadius = gunData.radius + (d.size * birdScale * 0.6);
Calculates the bird's collision radius: the gun's spread radius plus a portion of the bird's visual size, so bigger birds are easier to hit
if (distToLine < effectiveRadius) {
If the bird is within the collision radius, it's a hit
d.die();
Sets the bird's dead flag to true, stops its horizontal movement, and makes it spin upward
score += items.birds[activeBird].pts;
Adds the bird's point value (e.g., 100 for duck, 5000 for dragon) to the player's score
document.getElementById('score').innerText = score;
Updates the on-screen score display immediately
hitEnv.play();
Triggers the hit-squeak audio sound
let cols = getBirdColors(activeBird); for(let i = 0; i < 25; i++) particles.push(new Particle(d.x, d.y, d.z, cols.body));
Gets the bird's color and creates 25 particle burst effects at the bird's location in that color
if (activeGun !== 'sniper') break;
If the gun is not a sniper rifle, stop checking more birds (single-target guns stop after one hit). Sniper rifles can pierce through.

drawGun()

drawGun() is pure rendering—it builds a 3D gun model from primitives (boxes, spheres, cylinders) and displays it in screen space. The clever part is the recoil animation: recoil is added to the Y position and the shotgun's pump, making the gun kick visibly when you fire. Each gun is a different set of boxes to create unique silhouettes.

🔬 This draws the pistol using three boxes. What happens if you change the numbers in the first box(8, 12, 50) to much larger values like (20, 30, 120)?

  if (activeGun === 'pistol') {
    fill(60); box(8, 12, 50); 
    push(); translate(0, 15, 15); fill(30); box(8, 25, 15); pop(); 
    push(); translate(0, 8, 5); fill(20); box(2, 8, 10); pop(); 
  }
function drawGun() {
  push();
  translate(width * 0.15, height * 0.25 + recoil * 10, 250);
  scale(1.2); 
  rotateY(-PI / 9); rotateX(PI / 20);
  
  let skin = color(255, 204, 153);
  
  push(); 
  if (activeGun === 'laser') translate(0, 25, 45);
  else translate(0, 15, 25); 
  fill(skin); sphere(9); 
  pop();
  
  push();
  if(activeGun === 'shotgun') translate(0, 5, -20 + recoil * 10);
  else if(activeGun === 'pistol') translate(0, 18, 15);
  else if(activeGun === 'laser') translate(-18, 15, -40); 
  else if(activeGun === 'minigun') translate(22, 5, -10); 
  else translate(0, 10, -20);
  fill(skin); sphere(9);
  pop();

  if (activeGun === 'pistol') {
    fill(60); box(8, 12, 50); 
    push(); translate(0, 15, 15); fill(30); box(8, 25, 15); pop(); 
    push(); translate(0, 8, 5); fill(20); box(2, 8, 10); pop(); 
  } 
  else if (activeGun === 'shotgun') {
    fill(40); box(10, 10, 140); 
    push(); translate(0, 5, -20 + recoil * 10); fill(100, 50, 20); box(14, 14, 35); pop(); 
    translate(0, 10, 60); fill(80, 50, 20); box(12, 25, 60); 
  } 
  else if (activeGun === 'ar') {
    fill(30); box(10, 15, 120); 
    push(); translate(0, -10, -20); fill(10); box(2, 5, 20); pop(); 
    push(); translate(0, -10, 30); fill(10); box(2, 5, 10); pop(); 
    push(); translate(0, 20, -5); fill(20); box(8, 30, 15); pop(); 
    push(); translate(0, 15, 25); fill(20); box(10, 25, 15); pop(); 
    translate(0, 10, 55); fill(20); box(10, 20, 50); 
  }
  else if (activeGun === 'sniper') {
    fill(30, 40, 30); box(6, 6, 200); 
    push(); translate(0, -12, 10); rotateX(HALF_PI); fill(10); cylinder(5, 50); pop(); 
    push(); translate(0, 15, 30); fill(20); box(10, 25, 15); pop(); 
    translate(0, 10, 70); fill(20); box(10, 25, 60); 
  } 
  else if (activeGun === 'minigun') {
    let spin = mouseIsPressed ? frameCount * 0.5 : 0;
    push();
    translate(0, 0, -50); rotateZ(spin);
    for (let i=0; i<4; i++) { 
      push(); rotateZ(i * HALF_PI); translate(14, 0, 0); fill(70); box(8, 8, 160); pop();
    }
    pop();
    translate(0, 15, 40); fill(30); box(35, 35, 100); 
  }
  else if (activeGun === 'laser') {
    fill(200, 200, 255); box(36, 45, 200); 
    push(); translate(0, 0, -80); rotateX(HALF_PI); fill(0, 255, 255); cylinder(12, 260); pop(); 
    push(); translate(0, 0, -120); fill(0, 255, 255); box(45, 55, 20); pop(); 
    push(); translate(0, 25, 45); fill(80); box(12, 35, 25); pop(); 
  }

  // --- MUZZLE FLASH ---
  if (recoil > 2.5 && activeGun !== 'sniper') {
    push();
    let flashOffset = -60;
    if (activeGun === 'minigun') flashOffset = -140; 
    else if (activeGun === 'laser') flashOffset = -220; 
    
    translate(0, 0, flashOffset); rotateX(-HALF_PI); 
    noStroke(); 
    
    if (activeGun === 'laser') {
      fill(0, 255, 255, 200); cone(35, 90); 
      translate(0, -15, 0); fill(255, 255, 200); sphere(20);
    } else if (activeGun === 'minigun') {
      fill(255, 150, 0, 200); cone(25, 60); 
      translate(0, -10, 0); fill(255, 255, 200); sphere(15);
    } else {
      fill(255, 150, 0, 200); cone(12, 30); 
      translate(0, -8, 0); fill(255, 255, 200); sphere(8);
    }
    pop();
  }
  pop();
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

transformation Gun Screen Position translate(width * 0.15, height * 0.25 + recoil * 10, 250); scale(1.2); rotateY(-PI / 9); rotateX(PI / 20);

Positions the gun in the lower-right corner of the screen (15% from left), kicks it up slightly on recoil, tilts it for a natural grip angle

geometry Hand Geometry push(); if (activeGun === 'laser') translate(0, 25, 45); else translate(0, 15, 25); fill(skin); sphere(9); pop(); push(); if(activeGun === 'shotgun') translate(0, 5, -20 + recoil * 10); else if(activeGun === 'pistol') translate(0, 18, 15); else if(activeGun === 'laser') translate(-18, 15, -40); else if(activeGun === 'minigun') translate(22, 5, -10); else translate(0, 10, -20); fill(skin); sphere(9); pop();

Draws two skin-colored spheres representing hands gripping the gun, positioned differently for each gun type

switch-case Gun-Specific Models if (activeGun === 'pistol') { ... } else if (activeGun === 'shotgun') { ... } else if (activeGun === 'ar') { ... } else if (activeGun === 'sniper') { ... } else if (activeGun === 'minigun') { ... } else if (activeGun === 'laser') { ... }

Each gun has unique geometry—pistol is compact, sniper is long, minigun has spinning barrels, laser is sci-fi

conditional Muzzle Flash Effect if (recoil > 2.5 && activeGun !== 'sniper') { push(); let flashOffset = -60; if (activeGun === 'minigun') flashOffset = -140; else if (activeGun === 'laser') flashOffset = -220; translate(0, 0, flashOffset); rotateX(-HALF_PI); noStroke(); if (activeGun === 'laser') { fill(0, 255, 255, 200); cone(35, 90); translate(0, -15, 0); fill(255, 255, 200); sphere(20); } else if (activeGun === 'minigun') { fill(255, 150, 0, 200); cone(25, 60); translate(0, -10, 0); fill(255, 255, 200); sphere(15); } else { fill(255, 150, 0, 200); cone(12, 30); translate(0, -8, 0); fill(255, 255, 200); sphere(8); } pop(); }

Renders a cone and sphere burst at the muzzle when recoil is high, creating a flashy hit-feedback visual

push();
Saves the current transformation state so all gun geometry is grouped together
translate(width * 0.15, height * 0.25 + recoil * 10, 250);
Positions the gun at 15% from the left edge, 25% from the top (lower-right screen corner), and 250 units into the screen. The + recoil * 10 makes it kick upward on firing.
scale(1.2);
Makes the gun 20% larger so it's clearly visible in the corner
rotateY(-PI / 9); rotateX(PI / 20);
Tilts the gun -20° horizontally and +9° vertically so it looks naturally gripped at an angle
let skin = color(255, 204, 153);
Defines a skin-tone color (peachy orange) for the hands
if (activeGun === 'laser') translate(0, 25, 45); else translate(0, 15, 25);
Positions the first hand (grip) differently depending on the gun—laser hands are higher and farther forward
fill(skin); sphere(9);
Draws a sphere (size 9) in skin tone at the translated position—one hand/finger
if (activeGun === 'pistol') { fill(60); box(8, 12, 50);
If pistol is active, draw a dark gray box (8×12×50) as the barrel—compact and simple
push(); translate(0, 15, 15); fill(30); box(8, 25, 15); pop();
Adds a darker box (the grip/frame) offset from the barrel
push(); translate(0, 8, 5); fill(20); box(2, 8, 10); pop();
Adds a tiny slide on top of the barrel for detail
} else if (activeGun === 'shotgun') { fill(40); box(10, 10, 140);
Shotgun barrel is wider (10×10) but same length (140) as pistol
push(); translate(0, 5, -20 + recoil * 10); fill(100, 50, 20); box(14, 14, 35); pop();
Shotgun pump head moves forward on recoil (+ recoil * 10), simulating recoil animation
} else if (activeGun === 'minigun') { let spin = mouseIsPressed ? frameCount * 0.5 : 0;
If mouse is pressed, spin the barrels (frameCount * 0.5 = accelerating rotation); else no spin
for (let i=0; i<4; i++) { push(); rotateZ(i * HALF_PI); translate(14, 0, 0); fill(70); box(8, 8, 160); pop(); }
Loop 4 times, each time rotate by 90° and draw a barrel at distance 14—creates a Gatling gun look
} else if (activeGun === 'laser') { fill(200, 200, 255); box(36, 45, 200);
Laser gun is huge (36×45×200), light blue—sci-fi aesthetic
push(); translate(0, 0, -80); rotateX(HALF_PI); fill(0, 255, 255); cylinder(12, 260); pop();
Adds a bright cyan cylinder (rotated to point forward) as the laser barrel
if (recoil > 2.5 && activeGun !== 'sniper') {
If recoil is high enough (just fired) AND gun is not sniper (snipers have no flash), draw muzzle flash
let flashOffset = -60; if (activeGun === 'minigun') flashOffset = -140; else if (activeGun === 'laser') flashOffset = -220;
Determines where the muzzle flash appears (at the barrel tip), adjusted for each gun's barrel length
translate(0, 0, flashOffset); rotateX(-HALF_PI);
Moves to the muzzle and rotates the flash so it points outward from the gun
if (activeGun === 'laser') { fill(0, 255, 255, 200); cone(35, 90);
Laser flash is bright cyan, a wide cone
} else { fill(255, 150, 0, 200); cone(12, 30);
Normal guns have orange/yellow flash (fire colors)
translate(0, -8, 0); fill(255, 255, 200); sphere(8);
Adds a white glow sphere behind the cone for brightness

drawEnvironment()

drawEnvironment() renders static scenery—the ground and trees. It demonstrates two rendering patterns: the ground is a single large plane, and trees are instanced geometry (the same cylinder+cone shape drawn 200 times in different positions and scales). This is efficient because p5.js doesn't redraw geometry at all—it just transforms the same primitives.

🔬 Each tree is made of a cylinder (trunk) and cone (canopy). What happens if you swap the colors—make the trunk green and canopy brown?

  for (let t of trees) {
    push(); 
    translate(t.x, 0, t.z); 
    scale(t.size);
    translate(0, -50, 0); fill(60, 40, 20); cylinder(15, 100); 
    translate(0, -100, 0); fill(34, 100, 34); cone(70, 200); 
    pop();
  }
function drawEnvironment() {
  push(); 
  rotateX(HALF_PI); 
  fill(40, 100, 40); 
  plane(15000, 15000); 
  pop();
  
  for (let t of trees) {
    push(); 
    translate(t.x, 0, t.z); 
    scale(t.size);
    translate(0, -50, 0); fill(60, 40, 20); cylinder(15, 100); 
    translate(0, -100, 0); fill(34, 100, 34); cone(70, 200); 
    pop();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

geometry Ground Plane push(); rotateX(HALF_PI); fill(40, 100, 40); plane(15000, 15000); pop();

Draws a 15000×15000 unit grass plane that forms the forest floor

for-loop Tree Rendering Loop for (let t of trees) { push(); translate(t.x, 0, t.z); scale(t.size); translate(0, -50, 0); fill(60, 40, 20); cylinder(15, 100); translate(0, -100, 0); fill(34, 100, 34); cone(70, 200); pop(); }

Loops through all 200 trees and draws each as a trunk (cylinder) and canopy (cone) scaled and positioned uniquely

push();
Saves transformation state for the ground plane
rotateX(HALF_PI);
Rotates the plane 90° so it lies flat on the XZ plane instead of vertical
fill(40, 100, 40);
Sets ground color to a grass green (darker green values)
plane(15000, 15000);
Draws a square plane 15000 units in each direction—huge floor for the forest
pop();
Restores transformation state so ground rotation doesn't affect trees
for (let t of trees) {
Loops through all tree objects in the trees array
push();
Saves state for this individual tree
translate(t.x, 0, t.z);
Moves to the tree's X and Z position (Y is always 0, ground level)
scale(t.size);
Scales the tree by its size value (1.5 to 4.0), making some trees much larger
translate(0, -50, 0); fill(60, 40, 20); cylinder(15, 100);
Moves down 50 units and draws a dark brown cylinder (radius 15, height 100) as the tree trunk
translate(0, -100, 0); fill(34, 100, 34); cone(70, 200);
Moves down another 100 units and draws a dark green cone (radius 70, height 200) as the tree canopy
pop();
Restores transformation state, ready for the next tree

class Target

The Target class is the core game object—every bird is an instance of it. It demonstrates object-oriented design: encapsulating state (position, velocity, size), behavior (movement, collision), and rendering. The clever parts are: reset() respawning dead birds invisibly, update() managing different physics for alive vs. dead birds, and draw() switching between six entirely different 3D models based on activeBird. This flexibility lets the same class represent a duck, dragon, or anything in between.

🔬 These three lines control bird motion: bobbing, vertical bounce, and boundary bounce. What happens if you comment out the boundary check—so birds can fly infinitely far away?

      this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5;
      if (this.y < -800 || this.y > -100) this.vy *= -1;
      if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; }
class Target {
  constructor() { this.reset(); }
  reset() {
    let angle = random(TWO_PI); 
    let dist = random(500, 2000); 
    this.x = player.x + cos(angle) * dist; 
    this.z = player.z + sin(angle) * dist;
    this.y = random(-600, -150);
    this.vx = random(-6, 6); 
    this.vz = random(-6, 6); 
    this.vy = random(-1.0, 1.0);
    this.size = random(18, 28);
    this.dead = false; this.deathSpin = 0;
  }
  die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }
  
  update() {
    let spdMult = items.birds[activeBird].speed;
    this.x += this.vx * (this.dead ? 1 : spdMult);
    this.y += this.vy * (this.dead ? 1 : spdMult);
    this.z += this.vz * (this.dead ? 1 : spdMult);
    
    if (this.dead) {
      this.deathSpin += 0.3; if (this.y > 0) this.reset();
    } else {
      this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5;
      if (this.y < -800 || this.y > -100) this.vy *= -1;
      if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; }
    }
  }
  
  draw() {
    push(); translate(this.x, this.y, this.z);
    if (!this.dead) rotateY(atan2(this.vx, this.vz)); else { rotateZ(this.deathSpin); rotateX(this.deathSpin); }
    noStroke();
    
    let cols = getBirdColors(activeBird);
    let scaleMult = 1.0;
    if (activeBird === 'phoenix') scaleMult = 4.0;
    if (activeBird === 'dragon') scaleMult = 7.0; 
    let s = this.size * scaleMult;
    
    let flap = this.dead ? 0 : sin(frameCount * items.birds[activeBird].speed * 0.4 + s) * 0.6;

    fill(cols.body[0], cols.body[1], cols.body[2]);

    if (activeBird === 'duck') {
      ellipsoid(s, s * 0.8, s * 1.3);
      push(); translate(0, -s * 0.8, s); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.7); 
      translate(0, 0, s * 0.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.5, s * 0.2, s * 0.8); pop(); 
      push(); translate(-s * 1.3, 0, 0); rotateZ(flap); box(s * 1.5, s * 0.1, s); pop(); 
      push(); translate(s * 1.3, 0, 0); rotateZ(-flap); box(s * 1.5, s * 0.1, s); pop(); 
    } 
    else if (activeBird === 'pigeon') {
      ellipsoid(s * 1.2, s * 1.1, s * 1.3);
      push(); translate(0, -s * 0.7, s * 0.8); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.5); 
      translate(0, 0, s * 0.5); rotateX(HALF_PI); fill(cols.beak[0], cols.beak[1], cols.beak[2]); cone(s * 0.2, s * 0.4); pop(); 
      push(); translate(-s * 1.3, 0, 0); rotateZ(flap); box(s * 1.5, s * 0.1, s); pop();
      push(); translate(s * 1.3, 0, 0); rotateZ(-flap); box(s * 1.5, s * 0.1, s); pop();
    }
    else if (activeBird === 'crow') {
      ellipsoid(s * 0.8, s * 0.7, s * 1.5);
      push(); translate(0, -s * 0.6, s * 1.2); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.6);
      translate(0, 0, s * 0.6); rotateX(HALF_PI); fill(cols.beak[0], cols.beak[1], cols.beak[2]); cone(s * 0.2, s * 1.0); pop(); 
      push(); translate(-s * 1.2, 0, -s * 0.5); rotateZ(flap); rotateY(PI/6); box(s * 1.8, s * 0.1, s); pop(); 
      push(); translate(s * 1.2, 0, -s * 0.5); rotateZ(-flap); rotateY(-PI/6); box(s * 1.8, s * 0.1, s); pop();
    }
    else if (activeBird === 'goose') {
      ellipsoid(s * 1.2, s, s * 1.5);
      push(); 
      translate(0, -s * 1.2, s * 1.2); rotateX(PI/8); cylinder(s * 0.3, s * 2); 
      translate(0, -s, s * 0.5); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.6); 
      translate(0, 0, s * 0.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.6, s * 0.3, s * 1.2); 
      pop();
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); box(s * 2, s * 0.1, s * 1.5); pop(); 
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); box(s * 2, s * 0.1, s * 1.5); pop();
    }
    else if (activeBird === 'phoenix') {
      ellipsoid(s, s * 0.8, s * 1.3);
      push(); translate(0, -s * 0.8, s); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.7);
      translate(0, -s * 0.6, -s * 0.2); rotateX(-PI/4); fill(255, 200, 0); cone(s * 0.3, s * 1.5); pop(); 
      push(); translate(0, -s * 0.8, s * 1.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.4, s * 0.2, s * 0.6); pop();
      push(); translate(0, 0, -s * 1.5); rotateX(-HALF_PI); fill(255, 100, 0); cone(s * 0.6, s * 2.5); pop(); 
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); fill(255, 150, 0); box(s * 2, s * 0.1, s * 1.2); pop(); 
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); fill(255, 150, 0); box(s * 2, s * 0.1, s * 1.2); pop();
    }
    else if (activeBird === 'dragon') {
      ellipsoid(s * 0.8, s * 0.8, s * 2.2);
      push(); translate(0, -s * 0.8, s * 1.8); fill(cols.head[0], cols.head[1], cols.head[2]); box(s * 1.2, s, s * 1.2); 
      translate(0, 0, s * 0.8); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.8, s * 0.5, s); pop(); 
      push(); translate(-s * 0.4, -s * 1.5, s * 1.5); rotateX(-PI/4); fill(200); cone(s * 0.15, s * 1.2); pop(); 
      push(); translate(s * 0.4, -s * 1.5, s * 1.5); rotateX(-PI/4); fill(200); cone(s * 0.15, s * 1.2); pop();
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); box(s * 2, s * 0.1, s * 1.5); translate(-s, 0, -s * 0.5); box(s, s * 0.1, s * 2); pop();
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); box(s * 2, s * 0.1, s * 1.5); translate(s, 0, -s * 0.5); box(s, s * 0.1, s * 2); pop();
    }
    pop();
  }
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

method constructor() constructor() { this.reset(); }

Initializes a new bird by calling reset() to randomize its position and properties

method reset() reset() { let angle = random(TWO_PI); let dist = random(500, 2000); this.x = player.x + cos(angle) * dist; this.z = player.z + sin(angle) * dist; this.y = random(-600, -150); this.vx = random(-6, 6); this.vz = random(-6, 6); this.vy = random(-1.0, 1.0); this.size = random(18, 28); this.dead = false; this.deathSpin = 0; }

Respawns a bird at a random location around the player with random velocity and size

method die() die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }

Called when a bird is shot—sets dead flag, stops horizontal movement, and makes it fly upward before resetting

method update() update() { let spdMult = items.birds[activeBird].speed; this.x += this.vx * (this.dead ? 1 : spdMult); this.y += this.vy * (this.dead ? 1 : spdMult); this.z += this.vz * (this.dead ? 1 : spdMult); if (this.dead) { this.deathSpin += 0.3; if (this.y > 0) this.reset(); } else { this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5; if (this.y < -800 || this.y > -100) this.vy *= -1; if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; } } }

Updates the bird's position each frame: applies speed multiplier, adds bobbing motion, handles boundaries, and resets when dead bird flies off-screen

method draw() draw() { ... bird rendering logic ... }

Renders the bird's 3D model with species-specific geometry, wing flapping animation, and rotation

constructor() { this.reset(); }
When a new Target is created, immediately call reset() to set up its starting position, velocity, and appearance
let angle = random(TWO_PI);
Picks a random angle (0 to 2π) for spawn direction around the player
let dist = random(500, 2000);
Picks a random distance (500 to 2000 units) to spawn the bird away from the player
this.x = player.x + cos(angle) * dist;
Uses trigonometry to convert angle and distance into an X coordinate, placing the bird in a circle around the player
this.z = player.z + sin(angle) * dist;
Matches the Z coordinate using sine, completing the circle placement
this.y = random(-600, -150);
Spawns the bird at a random height between -600 and -150 (negative Y is above ground at eye level)
this.vx = random(-6, 6); this.vz = random(-6, 6);
Gives the bird random horizontal velocities so it moves unpredictably across the sky
this.vy = random(-1.0, 1.0);
Gives the bird small random vertical velocity—usually moves mostly horizontally
this.size = random(18, 28);
Randomizes bird size between 18 and 28 units, making targets of varying difficulty
this.dead = false; this.deathSpin = 0;
Resets death state—bird is alive and not spinning
die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }
Sets dead flag, zeroes out horizontal velocity (stops flying away), and sets upward velocity so dead birds float upward
let spdMult = items.birds[activeBird].speed;
Fetches the speed multiplier of the active bird species (e.g., dragon = 3.0x, duck = 1.0x)
this.x += this.vx * (this.dead ? 1 : spdMult);
Updates X position: if dead, use normal velocity; if alive, multiply by bird speed to make harder birds faster
if (this.dead) { this.deathSpin += 0.3; if (this.y > 0) this.reset();
If dead, rotate faster each frame, and when the bird drifts above ground (y > 0), reset it to spawn a new bird
} else { this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5;
If alive, add a sine-wave bobbing motion to the Y position—makes birds flutter up and down naturally
if (this.y < -800 || this.y > -100) this.vy *= -1;
If the bird flies above -100 or below -800, reverse its vertical velocity (bounce off invisible ceiling/floor)
if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; }
If the bird drifts farther than 2500 units away (beyond the forest), reverse its horizontal velocity (bounce back)
if (!this.dead) rotateY(atan2(this.vx, this.vz)); else { rotateZ(this.deathSpin); rotateX(this.deathSpin); }
If alive, rotate the bird to face its movement direction; if dead, spin it around both X and Z axes for a tumble effect
let cols = getBirdColors(activeBird);
Fetches the color scheme (body, beak, head) for the active bird species
let scaleMult = 1.0; if (activeBird === 'phoenix') scaleMult = 4.0; if (activeBird === 'dragon') scaleMult = 7.0;
Some birds are much larger visually—scale them up so they're intimidating targets
let flap = this.dead ? 0 : sin(frameCount * items.birds[activeBird].speed * 0.4 + s) * 0.6;
Calculates wing flapping angle: if dead, no flap; if alive, sine wave animation synchronized to speed and bird size
if (activeBird === 'duck') { ellipsoid(s, s * 0.8, s * 1.3);
Duck body is an ellipsoid (wider front-to-back than tall, longer side-to-side)
push(); translate(0, -s * 0.8, s); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.7);
Duck head is a sphere positioned forward and up from the body, colored with the head color
translate(0, 0, s * 0.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.5, s * 0.2, s * 0.8); pop();
Duck beak is a small box extending forward from the head
push(); translate(-s * 1.3, 0, 0); rotateZ(flap); box(s * 1.5, s * 0.1, s); pop();
Left wing is a thin box on the left side, rotated by the flap angle for flapping animation

class Particle

The Particle class is a simple physics system: each particle stores position, velocity, rotation, and lifespan. The update() method applies gravity (vel.y += 0.3) and rotation, and the draw() method fades the particle based on remaining life using alpha transparency. This creates the satisfying visual feedback when you shoot a bird—dozens of colored rectangles tumbling through the air and fading away.

🔬 First line gives particles random 3D velocity; second subtracts downward velocity (gravity bias). What if you remove the second line so particles fly purely randomly without gravity bias?

    this.vel = p5.Vector.random3D().mult(random(4, 12));
    this.vel.y -= random(3, 8);
class Particle {
  constructor(x, y, z, colArr) {
    this.pos = createVector(x, y, z);
    this.vel = p5.Vector.random3D().mult(random(4, 12));
    this.vel.y -= random(3, 8);
    this.life = 255;
    this.rot = createVector(random(TWO_PI), random(TWO_PI), random(TWO_PI));
    this.rotSpeed = p5.Vector.random3D().mult(0.15);
    this.col = colArr; 
  }
  update() {
    this.vel.y += 0.3; this.pos.add(this.vel);
    this.rot.add(this.rotSpeed); this.life -= 4; 
  }
  draw() {
    push(); translate(this.pos.x, this.pos.y, this.pos.z);
    rotateX(this.rot.x); rotateY(this.rot.y); rotateZ(this.rot.z);
    noStroke();
    fill(this.col[0], this.col[1], this.col[2], this.life); 
    plane(8, 16);
    pop();
  }
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

method constructor() constructor(x, y, z, colArr) { this.pos = createVector(x, y, z); this.vel = p5.Vector.random3D().mult(random(4, 12)); this.vel.y -= random(3, 8); this.life = 255; this.rot = createVector(random(TWO_PI), random(TWO_PI), random(TWO_PI)); this.rotSpeed = p5.Vector.random3D().mult(0.15); this.col = colArr; }

Creates a new particle at a position with random velocity (biased downward), lifespan, rotation, and color

method update() update() { this.vel.y += 0.3; this.pos.add(this.vel); this.rot.add(this.rotSpeed); this.life -= 4; }

Updates particle physics: applies gravity, moves the particle, spins it, and decreases its lifespan

method draw() draw() { push(); translate(this.pos.x, this.pos.y, this.pos.z); rotateX(this.rot.x); rotateY(this.rot.y); rotateZ(this.rot.z); noStroke(); fill(this.col[0], this.col[1], this.col[2], this.life); plane(8, 16); pop(); }

Renders the particle as a small plane at its position, rotated, with color and alpha controlled by lifespan

constructor(x, y, z, colArr) {
Creates a new particle at position (x, y, z) with a color array [R, G, B]
this.pos = createVector(x, y, z);
Stores the starting position as a p5.Vector
this.vel = p5.Vector.random3D().mult(random(4, 12));
Creates a random 3D velocity direction and scales it by a random speed (4 to 12 units per frame)
this.vel.y -= random(3, 8);
Subtracts additional downward velocity (3 to 8 units), so particles always drift downward initially (gravity bias)
this.life = 255;
Sets lifespan to 255—particles fade over time as this decreases, and die when it reaches 0
this.rot = createVector(random(TWO_PI), random(TWO_PI), random(TWO_PI));
Initializes random rotation angles around all three axes (0 to 2π)
this.rotSpeed = p5.Vector.random3D().mult(0.15);
Creates a random rotation speed for each axis, multiplied by 0.15 to keep spin moderate
this.col = colArr;
Stores the color array (passed from the bird when it dies)
this.vel.y += 0.3;
Applies gravity each frame, accelerating the particle downward
this.pos.add(this.vel);
Adds velocity to position, moving the particle
this.rot.add(this.rotSpeed);
Adds rotSpeed to rotation angles, making the particle spin
this.life -= 4;
Decreases lifespan by 4 each frame—at 60 FPS, the particle lives about 4 seconds (255 / 4 / 60 ≈ 1 second)
translate(this.pos.x, this.pos.y, this.pos.z);
Positions the particle in 3D space
rotateX(this.rot.x); rotateY(this.rot.y); rotateZ(this.rot.z);
Applies all three rotation angles, making the particle tumble in 3D
fill(this.col[0], this.col[1], this.col[2], this.life);
Sets the color to the stored bird color, with alpha (transparency) equal to life—as life decreases, alpha decreases and particles fade
plane(8, 16);
Draws a small rectangular plane (8×16 units) as the particle visual

📦 Key Variables

player object

Stores the player's 3D position {x, y, z}—the camera origin from which all rays are cast

let player = { x: 0, y: -150, z: 0 };
yaw number

Horizontal rotation angle (left/right look direction) in radians

let yaw = -Math.PI / 2;
pitch number

Vertical rotation angle (up/down look direction) in radians

let pitch = 0;
moveSpeed number

How many pixels the player moves per frame when pressing WASD

let moveSpeed = 6;
ducks array

Array of all Target instances (birds)—updated and drawn every frame

let ducks = [];
trees array

Array of tree objects {x, z, size}—the static forest scenery

let trees = [];
particles array

Array of all active Particle instances—burst effects from hits

let particles = [];
score number

Player's cumulative score earned by shooting birds—used to unlock upgrades

let score = 0;
recoil number

Current recoil value affecting camera shake and gun knockback—decays to 0 each frame

let recoil = 0;
lastFired number

Frame count when the gun was last fired—used to enforce fire rate limits

let lastFired = 0;
shootEnv p5.Envelope

Audio envelope for the gunshot sound—controls attack, decay, sustain, release

let shootEnv;
hitOsc p5.Oscillator

Triangle-wave oscillator for the hit squeak sound

let hitOsc;
bgSynth p5.MonoSynth

Monophonic synthesizer that plays the background music notes

let bgSynth;
musicLoop p5.SoundLoop

Repeating loop that plays background music notes at a fixed tempo

let musicLoop;
arpIndex number

Current index in the arpNotes array—cycles through the bassline

let arpIndex = 0;
arpNotes array

Array of 16 note names forming the background music loop

let arpNotes = ['D2', 'D3', 'F2', 'D3', ...];
activeGun string

Key of the currently selected weapon (e.g., 'pistol', 'sniper', 'laser')

let activeGun = 'pistol';
activeBird string

Key of the currently selected bird target (e.g., 'duck', 'dragon', 'phoenix')

let activeBird = 'duck';
items object

Shop data structure storing all guns and birds with their stats (cost, speed, points, unlocked status)

let items = { guns: {...}, birds: {...} };

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG fireGun() collision detection

The ray-casting uses dot product and perpendicular distance, which can miss birds directly in front of the camera at very close range due to numerical precision

💡 Add a distance check: test if the bird is within a maximum range (e.g., 5000 units) before ray-casting, as birds can only exist within the forest bounds

PERFORMANCE draw()

The particles array is searched backwards every frame (for (let i = particles.length - 1; i >= 0; i--)), which is O(n) even if particles are sparse

💡 Use a pool-based approach: maintain a fixed array of particles and reuse dead ones instead of splicing, avoiding array reallocation

PERFORMANCE Target.draw()

Each bird's draw() contains massive switch/if-else chains with duplicate geometry code (six bird types × multiple push/pop blocks = hundreds of lines)

💡 Extract bird-specific geometry into separate methods or data structures: e.g., birdGeometry['duck'] = {...}, then call a generic render function to apply it

STYLE Global scope

Many global variables (shootNoise, hitOsc, bgSynth, musicLoop, etc.) pollute the namespace and make it hard to reset or isolate the game state

💡 Wrap audio initialization and game state into a Game class: game.audio.shootEnv, game.world.ducks, etc., for better organization

FEATURE fireGun()

Sniper rifle is the only gun that can multi-hit (break doesn't execute); other guns stop after one target per shot

💡 Add a gun property penetration: true/false, and change the logic to: if (!gunData.penetration) break; instead of hardcoding sniper

BUG handleInput()

Player can move backward faster than forward because velocity is applied naively—no acceleration/friction model

💡 Implement damping: player.vel *= 0.9; each frame, then apply acceleration to player.vel instead of directly modifying player position

🔄 Code Flow

Code flow showing setup, setupaudio, draw, handleinput, firegun, drawgun, drawenvironment, target, particle

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

graph TD start[Start] --> setup[setup] setup --> setupaudio[setupaudio] setup --> draw[draw loop] setup --> pointer-lock-setup[pointer-lock-setup] setup --> forest-generation[forest-generation] setup --> bird-spawning[bird-spawning] setupaudio --> gunshot-setup[gunshot-setup] setupaudio --> hit-squeak-setup[hit-squeak-setup] setupaudio --> music-loop-setup[music-loop-setup] click setup href "#fn-setup" click setupaudio href "#fn-setupaudio" click pointer-lock-setup href "#sub-pointer-lock-setup" click forest-generation href "#sub-forest-generation" click bird-spawning href "#sub-bird-spawning" click gunshot-setup href "#sub-gunshot-setup" click hit-squeak-setup href "#sub-hit-squeak-setup" click music-loop-setup href "#sub-music-loop-setup" draw --> pointer-lock-check[pointer-lock-check] pointer-lock-check -->|Locked| input-handling[input-handling] pointer-lock-check -->|Unlocked| drawenvironment[drawenvironment] pointer-lock-check -->|Unlocked| bird-update-loop[bird-update-loop] pointer-lock-check -->|Unlocked| particle-update-loop[particle-update-loop] pointer-lock-check -->|Unlocked| recoil-decay[recoil-decay] click pointer-lock-check href "#sub-pointer-lock-check" click input-handling href "#sub-input-handling" click drawenvironment href "#fn-drawenvironment" click bird-update-loop href "#sub-bird-update-loop" click particle-update-loop href "#sub-particle-update-loop" click recoil-decay href "#sub-recoil-decay" input-handling --> mouse-look[mouse-look] input-handling --> pitch-clamping[pitch-clamping] input-handling --> direction-vectors[direction-vectors] input-handling --> wasd-movement[wasd-movement] input-handling --> boundary-clamping[boundary-clamping] click mouse-look href "#sub-mouse-look" click pitch-clamping href "#sub-pitch-clamping" click direction-vectors href "#sub-direction-vectors" click wasd-movement href "#sub-wasd-movement" click boundary-clamping href "#sub-boundary-clamping" bird-update-loop --> bird-collision-loop[bird-collision-loop] bird-update-loop -->|Update| update-method[update-method] bird-update-loop -->|Draw| draw-method[draw-method] click bird-collision-loop href "#sub-bird-collision-loop" click update-method href "#sub-update-method" click draw-method href "#sub-draw-method" particle-update-loop --> particle-update[particle-update] particle-update-loop --> particle-draw[particle-draw] click particle-update href "#sub-particle-update" click particle-draw href "#sub-particle-draw" drawenvironment --> ground-plane[ground-plane] drawenvironment --> tree-loop[tree-loop] click ground-plane href "#sub-ground-plane" click tree-loop href "#sub-tree-loop" firegun --> fire-rate-check[fire-rate-check] firegun --> bird-collision-loop firegun --> muzzle-flash[muzzle-flash] click fire-rate-check href "#sub-fire-rate-check" click muzzle-flash href "#sub-muzzle-flash" drawgun --> gun-positioning[gun-positioning] drawgun --> hand-parts[hand-parts] drawgun --> gun-model-branches[gun-model-branches] click gun-positioning href "#sub-gun-positioning" click hand-parts href "#sub-hand-parts" click gun-model-branches href "#sub-gun-model-branches"

❓ Frequently Asked Questions

What visual experience does the p5.js Sketch 2026-05-17 05:06 provide?

The sketch creates a vibrant 3D forest environment where users fly through and encounter colorful birds, accompanied by dynamic particle bursts and reactive sound effects.

How can users interact with the Sketch 2026-05-17 05:06?

Users can aim and shoot at flying birds to score points, unlock new guns, and increase the speed of the birds for a more challenging experience.

What creative coding techniques does Sketch 2026-05-17 05:06 demonstrate?

This sketch showcases 3D rendering, particle systems, and interactive sound design, combining visual and audio elements to enhance user engagement.

Preview

Sketch 2026-05-17 05:06 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-05-17 05:06 - Code flow showing setup, setupaudio, draw, handleinput, firegun, drawgun, drawenvironment, target, particle
Code Flow Diagram