Sketch 2026-06-07 16:54

AI Crab Hunter is an action game where you control a cyan character that shoots bullets to eliminate crabs spawning from the screen edges. Move with WASD or arrow keys, aim with your mouse, and fire with clicks or spacebar to rack up kills and collect drops.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make crabs spawn faster — Lowering maxCrabs will spawn a new one every frame until you hit the limit, making the game busier.
  2. Double the bullet speed — Changing the setMag value in handleFire makes bullets zip across the screen faster.
  3. One-shot kill crabs — Setting maxHP to 1 makes every bullet instantly defeat a crab—much easier gameplay.
  4. Change the background to a blue night sky — The background RGB triplet controls the canvas color; blue looks nice for a night game.
  5. Make the player move twice as fast — Increasing the Player speed value lets you zip around the canvas more easily.
Prefer the full editor? Open it there →

📖 About This Sketch

AI Crab Hunter is a fast-paced action game built entirely in p5.js. You control a cyan character armed with a gun barrel that aims at your mouse cursor, fire bullets with clicks or the spacebar, and dodge or eliminate crabs that swarm toward you from all edges of the screen. Defeated crabs drop collectible items that increment your score. The sketch demonstrates collision detection, game entity management, and event-driven input handling—three pillars of interactive games.

The code is organized into a main game loop in draw() that updates all entities (player, crabs, bullets, drops), detects collisions, and renders everything in the correct layer order. Four custom classes—Player, Crab, Bullet, and Drop—encapsulate the behavior and appearance of each entity type. By studying this sketch, you will learn how to spawn and track multiple moving objects, manage game state across frames, and build a complete working game in under 400 lines of code.

⚙️ How It Works

  1. When the sketch starts, setup() creates a full-window canvas, places the player character near the bottom center, and spawns 8 crabs at random points outside the screen edges.
  2. Every frame, draw() clears the background and updates the player's position based on pressed keys, then moves all crabs toward the player using simple AI pathfinding.
  3. Bullets fire either toward the mouse cursor (on click) or along the player's current aim direction (on spacebar), with a 150ms cooldown between shots to prevent spam.
  4. Each frame, collision detection checks whether any bullet has hit a crab and whether the player has touched any drop item.
  5. When a bullet hits a crab, the crab takes damage and enters a 'dying' state where it shrinks over 30 frames; once a crab dies, a drop spawns and the kill counter increments.
  6. As the player collects drops, they fade out and expand before disappearing, and the loot counter increases.
  7. The game spawns new crabs each frame if fewer than 12 are alive, creating continuous waves of enemies to fight.

🎓 Concepts You'll Learn

Game loops and entity updatesCollision detection with distance calculationsGame state machines (alive/dying/collected)Class-based architecture for game objectsInput handling (keyboard, mouse, touch)Vector math for aiming and movementSprite animation and visual effects

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. Use it to initialize the canvas, create your main objects, and load assets. Since loadImage is asynchronous, we pass callback functions (arrow functions with =>) that run when the image finishes loading or fails.

function setup() {
  createCanvas(windowWidth, windowHeight);

  player = new Player(width / 2, height - 80);

  // Start with a few crabs
  for (let i = 0; i < 8; i++) {
    spawnCrab();
  }

  // Try to load the drop image (safe: not in preload)
  loadImage(
    "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSdhQ-HYlAt8UQ83y38OKoFZmRzfqB9qgsTKg&s",
    img => {
      dropImg = img;
    },
    err => {
      // If it fails (CORS/404), we just use a drawn gem instead
      console.warn("Drop image failed to load, using fallback shape.", err);
    }
  );
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Canvas creation createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire window for full-screen gameplay

object-creation Player instantiation player = new Player(width / 2, height - 80);

Creates the player character at the bottom center of the canvas

for-loop Initial crab spawning for (let i = 0; i < 8; i++) { spawnCrab(); }

Populates the game with 8 starting crabs to challenge the player immediately

async-function-call Drop image loading loadImage("https://...", img => { dropImg = img; }, err => { console.warn(...); });

Asynchronously loads a gem image for drops; uses a fallback shape if it fails

createCanvas(windowWidth, windowHeight);
Creates a canvas that matches your browser window size; this makes the game fullscreen and responsive
player = new Player(width / 2, height - 80);
Creates a new Player object at the horizontal center and near the bottom of the canvas (height - 80 pixels)
for (let i = 0; i < 8; i++) {
Loops 8 times to populate the starting wave of enemies
spawnCrab();
Calls spawnCrab() to add one new crab at a random edge of the screen
loadImage("https://...", img => { dropImg = img; }, ...
Attempts to load an image from a URL and stores it in dropImg if successful; the arrow function runs only when the image finishes loading
console.warn("Drop image failed to load, using fallback shape.", err);
If the image fails to load (due to network or CORS issues), this message prints to the console and the game uses a drawn cyan diamond instead

draw()

draw() runs 60 times per second and is the heartbeat of the game. Every frame follows the same pattern: clear the screen, update all entities, check collisions, remove dead entities, and redraw everything in the correct order. This is the core game loop that powers almost every interactive program in p5.js.

🔬 These three lines remove dead entities from their arrays to save memory. What happens if you comment out the bullets filter line with //? Will old bullets ever disappear, or will they keep flying forever?

  // Clean up entities that are gone
  bullets = bullets.filter(b => b.alive);
  crabs = crabs.filter(c => !c.isGone());
  drops = drops.filter(d => !d.isGone());

🔬 The order of these draw loops determines what appears on top. What happens if you move player.draw() to before the crab loop, so the player draws first?

  // Draw order: drops → crabs → bullets → player → HUD
  for (let drop of drops) {
    drop.draw();
  }
  for (let crab of crabs) {
    crab.draw();
  }
  for (let bullet of bullets) {
    bullet.draw();
  }
  player.draw();
function draw() {
  background(15, 20, 35);

  // Update player (movement + aim)
  player.update();

  // Maintain a bunch of crabs
  if (crabs.length < maxCrabs && crabs.length < 200) {
    spawnCrab();
  }

  // Update entities
  for (let crab of crabs) {
    crab.update(player);
  }
  for (let bullet of bullets) {
    bullet.update();
  }
  for (let drop of drops) {
    drop.update();
  }

  // Handle collisions
  handleBulletCrabCollisions();
  handlePlayerDropCollisions();

  // Clean up entities that are gone
  bullets = bullets.filter(b => b.alive);
  crabs = crabs.filter(c => !c.isGone());
  drops = drops.filter(d => !d.isGone());

  // Draw order: drops → crabs → bullets → player → HUD
  for (let drop of drops) {
    drop.draw();
  }
  for (let crab of crabs) {
    crab.draw();
  }
  for (let bullet of bullets) {
    bullet.draw();
  }
  player.draw();

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

🔧 Subcomponents:

conditional Continuous spawning if (crabs.length < maxCrabs && crabs.length < 200) { spawnCrab(); }

Checks if there are fewer than maxCrabs alive and spawns a new one to keep waves continuous

for-loop Entity updates for (let crab of crabs) { crab.update(player); }

Calls update() on every crab, bullet, and drop each frame to move them and handle animations

function-call Collision detection handleBulletCrabCollisions(); handlePlayerDropCollisions();

Checks for bullet-crab hits and player-drop pickups every frame

function-call Dead entity removal bullets = bullets.filter(b => b.alive); crabs = crabs.filter(c => !c.isGone()); drops = drops.filter(d => !d.isGone());

Removes bullets that left the screen, crabs that finished dying, and drops that faded out—keeps arrays from growing infinitely

for-loop Layered rendering for (let drop of drops) { drop.draw(); } for (let crab of crabs) { crab.draw(); }

Draws entities in a specific order so drops appear behind crabs, which appear behind bullets, which appear behind the player—correct layering prevents visual confusion

background(15, 20, 35);
Clears the canvas every frame with a dark navy blue color (15, 20, 35 in RGB), erasing the previous frame so entities don't trail
player.update();
Calls the player's update method to process keyboard input and move the character, also updates aim direction based on mouse position
if (crabs.length < maxCrabs && crabs.length < 200) {
Checks if there are fewer than maxCrabs (default 12) to spawn a new one; the second condition prevents spawning if something goes wrong and the array grows huge
for (let crab of crabs) { crab.update(player); }
Loops through every crab and calls its update method, which moves it toward the player and handles the dying animation
for (let bullet of bullets) { bullet.update(); }
Loops through every bullet and updates its position; bullets move in a straight line until they leave the screen
for (let drop of drops) { drop.update(); }
Updates each drop's bobbing animation and fade effect
handleBulletCrabCollisions();
Checks every bullet against every crab to detect hits; if a collision is found, the bullet dies and the crab takes damage
handlePlayerDropCollisions();
Checks the player's position against every drop to detect pickups; collected drops increment the loot counter
bullets = bullets.filter(b => b.alive);
Removes all bullets from the array that have alive === false (either hit a crab or left the screen)
crabs = crabs.filter(c => !c.isGone());
Removes all crabs that have finished their dying animation and are no longer needed
drops = drops.filter(d => !d.isGone());
Removes all drops that have faded out and been collected
for (let drop of drops) { drop.draw(); }
Draws all drops first so they appear in the background layer
for (let crab of crabs) { crab.draw(); }
Draws all crabs on top of drops so the player can see them clearly
for (let bullet of bullets) { bullet.draw(); }
Draws all bullets on top of crabs so they appear to pass in front
player.draw();
Draws the player on top of everything so the gun barrel is always visible
drawHUD();
Renders the kill count and loot count text in the top-left corner

spawnCrab()

spawnCrab() is called every frame (if crabs.length < maxCrabs) to continuously spawn new enemies. By spawning just off-screen, crabs appear to slide into view smoothly rather than blinking into existence. This function demonstrates conditional logic (if/else if) and random number generation.

🔬 This spawns a crab at the top edge with a random x position. What happens if you change y = -margin to y = 0 (or y = height / 2)? Where will crabs appear instead?

  // Spawn from one of the 4 edges just outside the screen
  if (edge === 0) {
    // Top
    x = random(width);
    y = -margin;
function spawnCrab() {
  let x, y;
  const edge = floor(random(4));
  const margin = 40;

  // Spawn from one of the 4 edges just outside the screen
  if (edge === 0) {
    // Top
    x = random(width);
    y = -margin;
  } else if (edge === 1) {
    // Right
    x = width + margin;
    y = random(height);
  } else if (edge === 2) {
    // Bottom
    x = random(width);
    y = height + margin;
  } else {
    // Left
    x = -margin;
    y = random(height);
  }

  crabs.push(new Crab(x, y));
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

variable-assignment Random edge selection const edge = floor(random(4));

Randomly picks a number 0–3 representing top, right, bottom, or left edge

conditional Position calculation if (edge === 0) { ... } else if (edge === 1) { ... } else if (edge === 2) { ... } else { ... }

Sets x and y coordinates based on which edge was selected, placing the crab just outside the visible canvas

object-creation Crab instantiation crabs.push(new Crab(x, y));

Creates a new Crab object at the calculated position and adds it to the crabs array

const edge = floor(random(4));
random(4) generates a decimal between 0 and 4; floor() rounds it down to 0, 1, 2, or 3 with equal probability
const margin = 40;
Margin is the pixel distance outside the canvas where crabs spawn—40 pixels means they appear just out of view and slide into the screen
if (edge === 0) {
If edge is 0, spawn from the top edge
x = random(width);
Picks a random x position anywhere across the width of the canvas
y = -margin;
Places the crab 40 pixels above the top (negative y), so it's off-screen but will animate into view
} else if (edge === 1) {
If edge is 1, spawn from the right edge
x = width + margin;
Places the crab 40 pixels to the right of the right edge, off-screen
y = random(height);
Picks a random y position anywhere down the height of the canvas
} else if (edge === 2) {
If edge is 2, spawn from the bottom edge (same pattern as top)
} else {
If edge is 3, spawn from the left edge (same pattern as right)
crabs.push(new Crab(x, y));
Creates a new Crab object at the calculated (x, y) position and adds it to the crabs array so it will be updated and drawn every frame

handleBulletCrabCollisions()

This is the core collision detection logic that makes the game work. It uses nested loops (a bullet-checking loop inside a crab-checking loop) and distance calculation to test whether two circles have touched. The wasAlive flag is a clever pattern to detect state transitions—it ensures drops spawn only when a crab transitions from alive to dying, not every frame the crab is dying.

🔬 This calculates distance and deals damage. What happens if you change the comparison from d < to d > ? Will bullets now only hit crabs that are FAR away?

      const d = p5.Vector.dist(bullet.pos, crab.pos);
      if (d < bullet.radius + crab.collisionRadius()) {
        bullet.alive = false;

        const wasAlive = crab.state === "alive";
        crab.takeDamage(1);
function handleBulletCrabCollisions() {
  for (let bullet of bullets) {
    if (!bullet.alive) continue;

    for (let crab of crabs) {
      if (!crab.isHittable()) continue;

      const d = p5.Vector.dist(bullet.pos, crab.pos);
      if (d < bullet.radius + crab.collisionRadius()) {
        bullet.alive = false;

        const wasAlive = crab.state === "alive";
        crab.takeDamage(1);

        // On KO, spawn drop once
        if (wasAlive && crab.state === "dying" && !crab.dropSpawned) {
          crab.dropSpawned = true;
          killCount++;
          drops.push(new Drop(crab.pos.x, crab.pos.y));
        }

        break; // this bullet is done
      }
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Iterate bullets for (let bullet of bullets) { if (!bullet.alive) continue;

Loops through every bullet and skips dead ones to avoid unnecessary collision checks

for-loop Iterate crabs for (let crab of crabs) { if (!crab.isHittable()) continue;

For each bullet, loops through every crab and skips those that can't be hit (dying or dead)

calculation Distance measurement const d = p5.Vector.dist(bullet.pos, crab.pos);

Calculates the straight-line distance between bullet and crab positions

conditional Collision test if (d < bullet.radius + crab.collisionRadius()) {

If distance is less than the sum of their radii, they overlap and a collision happens

conditional Kill and drop spawning if (wasAlive && crab.state === "dying" && !crab.dropSpawned) { crab.dropSpawned = true; killCount++; drops.push(new Drop(crab.pos.x, crab.pos.y)); }

Ensures a drop spawns exactly once per crab death and increments the kill counter

for (let bullet of bullets) {
Loops through every bullet in the bullets array
if (!bullet.alive) continue;
Skips this bullet if it's already dead (the ! means 'not', so this says 'if not alive, skip to the next bullet')
for (let crab of crabs) {
For each alive bullet, loops through every crab
if (!crab.isHittable()) continue;
Skips crabs that cannot be hit right now (those in the dying state)
const d = p5.Vector.dist(bullet.pos, crab.pos);
Calculates the distance between the bullet's position and crab's position using the distance formula
if (d < bullet.radius + crab.collisionRadius()) {
Tests if the distance is smaller than the sum of their collision radii—if so, they overlap and have collided
bullet.alive = false;
Marks the bullet as dead so it will be removed from the array on the next frame
const wasAlive = crab.state === "alive";
Saves whether the crab was alive BEFORE taking damage, so we can detect the moment it dies
crab.takeDamage(1);
Subtracts 1 HP from the crab; if HP reaches 0, the crab transitions to the dying state
if (wasAlive && crab.state === "dying" && !crab.dropSpawned) {
Checks if the crab JUST died (was alive before, is dying now) and hasn't spawned a drop yet
crab.dropSpawned = true;
Sets a flag on the crab so this drop-spawning code never runs again for this crab
killCount++;
Increments the kill counter by 1 to update the HUD score
drops.push(new Drop(crab.pos.x, crab.pos.y));
Creates a new Drop object at the crab's position and adds it to the drops array
break;
Exits the inner loop (the crab loop) because this bullet has hit a crab and is now dead—no need to check other crabs

handlePlayerDropCollisions()

This function mirrors the bullet-crab collision detection but is much simpler because we only check the player against drops, not drops against each other. The state check prevents double-counting; once a drop is collected and fading, we ignore it.

function handlePlayerDropCollisions() {
  for (let drop of drops) {
    if (drop.state !== "idle") continue;

    const d = p5.Vector.dist(drop.pos, player.pos);
    if (d < drop.size * 0.6 + player.radius) {
      drop.collect();
      lootCount++;
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Iterate drops for (let drop of drops) { if (drop.state !== "idle") continue;

Loops through every drop and skips those already collected (fading out)

calculation Distance to player const d = p5.Vector.dist(drop.pos, player.pos);

Measures how far the drop is from the player's position

conditional Pickup detection if (d < drop.size * 0.6 + player.radius) {

If the distance is less than the collision radius, the player has touched the drop

for (let drop of drops) {
Loops through every drop object in the drops array
if (drop.state !== "idle") continue;
Skips drops that are already collected (state === 'collected'), so we don't count them twice
const d = p5.Vector.dist(drop.pos, player.pos);
Calculates the distance from the drop to the player using the distance formula
if (d < drop.size * 0.6 + player.radius) {
Tests if the distance is smaller than the sum of their collision radii (drop size scaled by 0.6 plus player radius)
drop.collect();
Calls the drop's collect method, which transitions it to the 'collected' state and starts its fade-out animation
lootCount++;
Increments the loot counter to update the HUD display

drawHUD()

drawHUD() renders the on-screen score display. It uses push() and pop() to isolate text styling changes—a best practice that prevents accidental visual side effects. The concatenation (killCount + 0 = "0", etc.) converts numbers to strings for display.

function drawHUD() {
  push();
  noStroke();
  fill(255);
  textSize(16);
  textAlign(LEFT, TOP);
  text("Crabs eliminated: " + killCount, 12, 12);
  text("Drops collected: " + lootCount, 12, 32);
  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Text styling push(); noStroke(); fill(255); textSize(16); textAlign(LEFT, TOP);

Sets up text appearance: white color, 16-pixel font, left-aligned at the top

function-call HUD display text("Crabs eliminated: " + killCount, 12, 12); text("Drops collected: " + lootCount, 12, 32);

Renders two lines of text showing the current scores at pixel positions (12, 12) and (12, 32)

push();
Saves the current drawing settings (color, stroke, font, etc.) so changes made here don't affect other parts of the sketch
noStroke();
Disables outlines for text (text can have strokes in p5.js, but we want solid letters)
fill(255);
Sets the text color to white (255 is full brightness in RGB)
textSize(16);
Sets the font size to 16 pixels
textAlign(LEFT, TOP);
Aligns text to the left horizontally and the top vertically, so coordinates (12, 12) place text at the top-left corner
text("Crabs eliminated: " + killCount, 12, 12);
Draws the first line of text at position (12, 12), concatenating the string with the current killCount value
text("Drops collected: " + lootCount, 12, 32);
Draws the second line of text at position (12, 32), 20 pixels below the first (enough vertical space for the text height)
pop();
Restores the drawing settings that were saved by push(), so the game doesn't use HUD-style text for other drawing

mousePressed()

mousePressed() is a built-in p5.js event handler that runs once every time the player clicks the mouse. It's the simplest way to detect clicks; we immediately delegate to handleFire() to process the shot.

function mousePressed() {
  handleFire(mouseX, mouseY);
}
Line-by-line explanation (1 lines)
handleFire(mouseX, mouseY);
Calls handleFire with the current mouse position, firing a bullet toward wherever the player clicked

touchStarted()

touchStarted() is the mobile equivalent of mousePressed(). It fires when the player first touches the screen. By returning false, we prevent the browser from zooming or scrolling, keeping the game responsive on phones and tablets.

function touchStarted() {
  if (touches.length > 0) {
    handleFire(touches[0].x, touches[0].y);
  }
  // Prevent default browser behavior (scroll, zoom)
  return false;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Touch detection if (touches.length > 0) {

Checks if there is at least one active touch on the screen

if (touches.length > 0) {
p5.js stores active touches in the touches array; this checks if at least one touch exists
handleFire(touches[0].x, touches[0].y);
Fires a bullet toward the first touch point (touches[0]) if one exists
return false;
Tells the browser NOT to perform its default touch behaviors (scroll, pinch-zoom), so the game handles all touch input itself

keyPressed()

keyPressed() runs once per key press. We use it to fire when spacebar is pressed. Note that this doesn't continuously fire while holding space—for that, we'd use keyIsDown() in the Player.update() method (like we do for movement).

function keyPressed() {
  // Space bar to fire in current aim direction
  if (key === " ") {
    handleFire(); // no target → uses aimDir
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Spacebar detection if (key === " ") {

Detects when the player presses the spacebar

if (key === " ") {
p5.js stores the last pressed key in the key variable; this checks if it equals a space character
handleFire();
Calls handleFire() with no arguments, which triggers firing along the player's current aimDir instead of toward the mouse

handleFire()

handleFire() is a utility function that consolidates all firing logic. It handles three input sources (mouse clicks, touches, spacebar), enforces a cooldown to prevent spam, and validates the direction before creating a bullet. The typeof check is a JavaScript pattern for checking data types.

🔬 This is the fire rate limiter. It prevents shots if less than FIRE_COOLDOWN (150ms) has passed. What happens if you remove this entire block (delete the 4 lines)? Can you fire unlimited bullets per frame?

  const now = millis();
  if (now - lastShotTime < FIRE_COOLDOWN) {
    return;
  }
  lastShotTime = now;
function handleFire(targetX, targetY) {
  const now = millis();
  if (now - lastShotTime < FIRE_COOLDOWN) {
    return;
  }
  lastShotTime = now;

  let dir;

  if (typeof targetX === "number" && typeof targetY === "number") {
    dir = createVector(targetX - player.pos.x, targetY - player.pos.y);
  } else {
    // No target → use current aim direction
    dir = player.aimDir.copy();
  }

  if (dir.magSq() < 1) {
    // If direction is too small, default to right
    dir.set(1, 0);
  }

  dir.setMag(9); // bullet speed

  bullets.push(new Bullet(player.pos.x, player.pos.y, dir.x, dir.y));
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Fire rate limiting const now = millis(); if (now - lastShotTime < FIRE_COOLDOWN) { return; }

Prevents firing too rapidly by checking elapsed time since the last shot

conditional Direction determination if (typeof targetX === "number" && typeof targetY === "number") { dir = createVector(targetX - player.pos.x, targetY - player.pos.y); } else { dir = player.aimDir.copy(); }

Calculates bullet direction either toward a target or along the player's current aim direction

conditional Direction validation if (dir.magSq() < 1) { dir.set(1, 0); }

Ensures the direction vector has meaningful length; if too small (e.g., aiming at yourself), defaults to right

object-creation Bullet spawning dir.setMag(9); // bullet speed bullets.push(new Bullet(player.pos.x, player.pos.y, dir.x, dir.y));

Sets bullet speed to 9 pixels/frame and creates a new Bullet object at the player's position

const now = millis();
Stores the current time in milliseconds since the sketch started
if (now - lastShotTime < FIRE_COOLDOWN) {
Calculates how much time has passed since the last shot; if it's less than FIRE_COOLDOWN (150ms), the function returns early (prevents firing)
return;
Exits the function immediately, preventing a new bullet from being created
lastShotTime = now;
Updates lastShotTime to now, resetting the cooldown timer for the next shot
if (typeof targetX === "number" && typeof targetY === "number") {
Checks if handleFire was called with numeric target coordinates (from a click or touch)
dir = createVector(targetX - player.pos.x, targetY - player.pos.y);
Creates a direction vector pointing from the player toward the target position
} else {
If no target was provided (spacebar fire), use the player's current aim direction
dir = player.aimDir.copy();
Copies the player's aimDir vector (we copy it to avoid modifying the original)
if (dir.magSq() < 1) {
Checks if the direction vector's length-squared is extremely small (meaning it's almost zero length)
dir.set(1, 0);
If direction is too small or zero, defaults to pointing right (1, 0)
dir.setMag(9);
Normalizes the direction and scales it to exactly 9, so every bullet travels at 9 pixels per frame regardless of aiming angle
bullets.push(new Bullet(player.pos.x, player.pos.y, dir.x, dir.y));
Creates a new Bullet object at the player's current position with velocity components dir.x and dir.y, and adds it to the bullets array

windowResized()

windowResized() is called automatically whenever the player resizes their browser window. Without it, the canvas would stay its original size. This ensures the game remains fullscreen and responsive on different devices.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window width and height whenever the player resizes their browser

Player (class)

The Player class encapsulates all player behavior: keyboard input, position clamping, mouse aiming, and rendering. The update() method runs every frame to handle input and movement, while draw() renders the character's appearance. Note the use of translate() and pop()/push() to isolate transformations—a pattern that makes local coordinate systems clean and readable.

🔬 These lines let you move left and right. What happens if you change move.x -= 1 to move.y -= 1? Will your character move up instead of left?

    if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) {   // A or Left
      move.x -= 1;
    }
    if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) {  // D or Right
      move.x += 1;
    }
class Player {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.speed = 3;
    this.radius = 18;
    this.aimDir = createVector(1, 0);
  }

  update() {
    // Movement with WASD / Arrow keys
    let move = createVector(0, 0);

    if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) {   // A or Left
      move.x -= 1;
    }
    if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) {  // D or Right
      move.x += 1;
    }
    if (keyIsDown(87) || keyIsDown(UP_ARROW)) {     // W or Up
      move.y -= 1;
    }
    if (keyIsDown(83) || keyIsDown(DOWN_ARROW)) {   // S or Down
      move.y += 1;
    }

    if (move.magSq() > 0) {
      move.setMag(this.speed);
      this.pos.add(move);
    }

    // Stay inside canvas
    this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
    this.pos.y = constrain(this.pos.y, this.radius, height - this.radius);

    // Aim toward mouse if cursor is inside canvas
    if (
      mouseX >= 0 && mouseX <= width &&
      mouseY >= 0 && mouseY <= height
    ) {
      const m = createVector(mouseX - this.pos.x, mouseY - this.pos.y);
      if (m.magSq() > 25) {
        this.aimDir = m.normalize();
      }
    }
  }

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

    // Body
    noStroke();
    fill(60, 200, 255);
    circle(0, 0, this.radius * 2);

    // Gun barrel
    stroke(255);
    strokeWeight(4);
    line(0, 0, this.aimDir.x * (this.radius + 12), this.aimDir.y * (this.radius + 12));

    // Eye
    noStroke();
    fill(0);
    circle(this.aimDir.x * 8, this.aimDir.y * 8, 6);

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

🔧 Subcomponents:

function Player constructor constructor(x, y) { this.pos = createVector(x, y); this.speed = 3; this.radius = 18; this.aimDir = createVector(1, 0); }

Initializes a player object with starting position, speed, collision radius, and aim direction

for-loop WASD/arrow key input if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) { move.x -= 1; }

Checks if A or Left arrow is pressed and sets move vector accordingly

function-call Canvas boundary enforcement this.pos.x = constrain(this.pos.x, this.radius, width - this.radius); this.pos.y = constrain(this.pos.y, this.radius, height - this.radius);

Prevents the player from moving outside the canvas boundaries by constraining position

conditional Mouse aiming if ( mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height ) { const m = createVector(mouseX - this.pos.x, mouseY - this.pos.y); if (m.magSq() > 25) { this.aimDir = m.normalize(); } }

Updates aim direction to point toward the mouse if it's inside the canvas

constructor(x, y) {
Defines the constructor method, which runs once when a new Player is created with new Player(x, y)
this.pos = createVector(x, y);
Creates a p5.Vector at the starting position and stores it in this.pos
this.speed = 3;
Sets the player's movement speed to 3 pixels per frame
this.radius = 18;
Sets the player's collision radius to 18 pixels (the circular body has diameter 2 * radius = 36)
this.aimDir = createVector(1, 0);
Initializes aim direction to (1, 0)—pointing right—in case the mouse isn't moved
if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) {
Checks if either A key (keyCode 65) or the Left arrow key is currently pressed
move.x -= 1;
Subtracts 1 from move.x (left direction) if left movement is pressed
if (move.magSq() > 0) {
Only applies movement if the move vector has a non-zero length (some key was pressed)
move.setMag(this.speed);
Normalizes the move vector and scales it to exactly this.speed (3), ensuring consistent movement regardless of diagonal vs cardinal directions
this.pos.add(move);
Adds the move vector to the player's position, translating the character
this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
Clamps x position between this.radius and width - this.radius so the player's circle doesn't extend past the left or right edge
this.pos.y = constrain(this.pos.y, this.radius, height - this.radius);
Clamps y position to keep the player inside the top and bottom edges
if ( mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height ) {
Only updates aim if the mouse is currently inside the canvas (prevents aiming when cursor is outside the window)
const m = createVector(mouseX - this.pos.x, mouseY - this.pos.y);
Creates a vector pointing from the player's position toward the mouse cursor
if (m.magSq() > 25) {
Only updates aimDir if the mouse is at least 5 pixels away (magSq > 25 means magnitude > 5); avoids aiming at zero-length vectors
this.aimDir = m.normalize();
Normalizes the mouse direction vector to unit length (magnitude 1) and stores it in aimDir; this makes aiming consistent regardless of mouse distance
push();
Saves the current coordinate system before translating
translate(this.pos.x, this.pos.y);
Moves the origin (0, 0) to the player's position, so all subsequent drawing is relative to the player
fill(60, 200, 255);
Sets color to cyan (RGB: 60, 200, 255)
circle(0, 0, this.radius * 2);
Draws a cyan circle at the origin (now at the player's pos) with diameter 2 * radius = 36
stroke(255);
Changes stroke color to white for the gun barrel
strokeWeight(4);
Sets stroke thickness to 4 pixels
line(0, 0, this.aimDir.x * (this.radius + 12), this.aimDir.y * (this.radius + 12));
Draws a line from the player's center toward the aim direction; length is (radius + 12) pixels, extending past the body to look like a gun barrel
fill(0);
Changes fill color to black for the eye
circle(this.aimDir.x * 8, this.aimDir.y * 8, 6);
Draws a small black circle (diameter 6) in the direction the player is aiming, creating a cyclopean eye effect
pop();
Restores the original coordinate system

Crab (class)

The Crab class demonstrates state machines, animation timing, and simple AI. The state property ('alive' vs 'dying') controls behavior, while dyingTimer and dyingDuration implement frame-based animations. The lerp() call is a classic smoothing technique that makes movement feel natural. The hitFlash counter implements visual feedback (white flash) on impact.

🔬 This is the chase AI. The lerp(..., 0.2) means the crab blends 20% of the desired direction each frame. What happens if you change 0.2 to 1.0? Will the crab turn more sharply and instantly?

      if (player) {
        const desired = p5.Vector.sub(player.pos, this.pos);
        if (desired.magSq() > 1) {
          desired.setMag(this.speed);
          this.vel.lerp(desired, 0.2); // smooth turning
        }
      }
class Crab {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(1.0);
    this.speed = 1.4;
    this.size = 40;
    this.maxHP = 3;
    this.hp = this.maxHP;

    this.state = "alive"; // 'alive' | 'dying'
    this.dyingTimer = 0;
    this.dyingDuration = 30;

    this.hitFlash = 0; // frames to flash when hit
    this.dropSpawned = false;
  }

  update(player) {
    if (this.state === "alive") {
      // Simple AI: move toward the player with a bit of smoothing
      if (player) {
        const desired = p5.Vector.sub(player.pos, this.pos);
        if (desired.magSq() > 1) {
          desired.setMag(this.speed);
          this.vel.lerp(desired, 0.2); // smooth turning
        }
      }

      this.pos.add(this.vel);
    } else if (this.state === "dying") {
      this.dyingTimer++;
    }
  }

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

    // Shrink a bit while dying
    let s = this.size;
    if (this.state === "dying") {
      const t = this.dyingTimer / this.dyingDuration;
      s *= max(0, 1 - 0.5 * t);
    }
    const scaleFactor = s / 40;
    scale(scaleFactor);

    // Body color & hit flash
    let bodyCol = color(255, 100, 80);
    if (this.hitFlash > 0) {
      bodyCol = color(255, 220, 220);
      this.hitFlash--;
    }

    // Legs
    stroke(bodyCol);
    strokeWeight(3);
    line(-18, 8, -28, 16);
    line(-12, 8, -22, 18);
    line(18, 8, 28, 16);
    line(12, 8, 22, 18);

    // Claws
    line(-20, 0, -32, -6);
    line(20, 0, 32, -6);

    // Body
    noStroke();
    fill(bodyCol);
    ellipse(0, 0, 40, 24);

    // Eyes
    fill(255);
    ellipse(-8, -10, 8, 10);
    ellipse(8, -10, 8, 10);
    fill(0);
    ellipse(-8, -10, 4, 6);
    ellipse(8, -10, 4, 6);

    pop();
  }

  takeDamage(amount) {
    if (this.state !== "alive") return;
    this.hp -= amount;
    this.hitFlash = 5;

    if (this.hp <= 0) {
      this.state = "dying";
      this.dyingTimer = 0;
    }
  }

  isHittable() {
    return this.state === "alive";
  }

  collisionRadius() {
    return this.size * 0.5;
  }

  isGone() {
    return this.state === "dying" && this.dyingTimer > this.dyingDuration;
  }
}
Line-by-line explanation (32 lines)

🔧 Subcomponents:

function Crab initialization constructor(x, y) { this.pos = createVector(x, y); this.vel = p5.Vector.random2D().mult(1.0); this.speed = 1.4; this.size = 40; this.maxHP = 3; this.hp = this.maxHP; this.state = "alive"; this.dyingTimer = 0; this.dyingDuration = 30; this.hitFlash = 0; this.dropSpawned = false; }

Initializes a crab with position, velocity, health, animation state, and visual properties

conditional Player-seeking AI if (this.state === "alive") { if (player) { const desired = p5.Vector.sub(player.pos, this.pos); if (desired.magSq() > 1) { desired.setMag(this.speed); this.vel.lerp(desired, 0.2); } } this.pos.add(this.vel); }

Implements simple chase AI: moves toward player with smooth turning via lerp

calculation Dying shrink effect let s = this.size; if (this.state === "dying") { const t = this.dyingTimer / this.dyingDuration; s *= max(0, 1 - 0.5 * t); }

Scales the crab smaller as dyingTimer increases, creating a fade-out shrink animation

this.pos = createVector(x, y);
Creates the crab's position at the spawn point
this.vel = p5.Vector.random2D().mult(1.0);
Creates a random 2D unit vector and multiplies it by 1.0; this gives the crab an initial random direction
this.speed = 1.4;
Sets the crab's movement speed to 1.4 pixels per frame when pursuing the player
this.size = 40;
Sets the visual size of the crab to 40 pixels
this.hp = this.maxHP;
Initializes health points to maxHP (3 hits required to die)
this.state = "alive";
Sets the crab's state machine to 'alive'; it will transition to 'dying' when HP <= 0
this.dyingDuration = 30;
The dying animation lasts 30 frames (about 0.5 seconds at 60 FPS)
const desired = p5.Vector.sub(player.pos, this.pos);
Creates a vector pointing from the crab toward the player
if (desired.magSq() > 1) {
Only updates AI if the player is at least 1 unit away (avoids degenerate zero-length vectors)
desired.setMag(this.speed);
Normalizes the desired direction and scales it to this.speed (1.4), so the crab always moves at consistent speed
this.vel.lerp(desired, 0.2);
Smoothly blends the current velocity toward the desired direction with a 20% weight—this creates smooth turning instead of jerky instant direction changes
this.pos.add(this.vel);
Moves the crab by adding its velocity vector to its position
const t = this.dyingTimer / this.dyingDuration;
Calculates a normalized time value from 0 to 1 representing progress through the 30-frame death animation
s *= max(0, 1 - 0.5 * t);
Shrinks the size: at t=0, size stays 100%; at t=1, size becomes 50%; max(0, ...) prevents negative size
const scaleFactor = s / 40;
Calculates how much to scale the drawn crab relative to its baseline size of 40
scale(scaleFactor);
Applies the scale transformation to all subsequent drawing, shrinking the crab's visual size
let bodyCol = color(255, 100, 80);
Sets default body color to coral red (RGB: 255, 100, 80)
if (this.hitFlash > 0) { bodyCol = color(255, 220, 220); this.hitFlash--; }
If hitFlash is still counting down, flashes the crab white instead, then decrements the counter
stroke(bodyCol);
Sets the stroke color for the legs and claws
ellipse(0, 0, 40, 24);
Draws the main body as an ellipse (wider than tall)
fill(255);
Changes fill color to white for the eye whites
ellipse(-8, -10, 8, 10);
Draws the left eye white (8 pixels wide, 10 tall)
fill(0);
Changes fill to black for the pupils
ellipse(-8, -10, 4, 6);
Draws a black pupil inside the white eye
if (this.state !== "alive") return;
takeDamage() exits early if the crab is already dying, preventing stacked damage
this.hp -= amount;
Subtracts damage from health
this.hitFlash = 5;
Sets hitFlash to 5 frames, triggering a white flash on the next draw
if (this.hp <= 0) {
When health reaches 0 or below, transition to dying state
this.state = "dying";
Changes state so update() and draw() behave differently
return this.state === "alive";
isHittable() returns true only while alive; bullets can't hit dying crabs
return this.size * 0.5;
collisionRadius() returns half the size, used for bullet collision detection
return this.state === "dying" && this.dyingTimer > this.dyingDuration;
isGone() returns true once the dying animation finishes, signaling that the crab should be removed from the array

Bullet (class)

The Bullet class is intentionally minimal: position, velocity, and a simple alive flag. It updates by adding velocity to position each frame (constant linear motion) and removes itself if it leaves the screen. This design is clean and reusable—the actual firing logic happens in handleFire().

class Bullet {
  constructor(x, y, vx, vy) {
    this.pos = createVector(x, y);
    this.vel = createVector(vx, vy);
    this.radius = 5;
    this.alive = true;
  }

  update() {
    this.pos.add(this.vel);

    // Remove if far off-screen
    if (
      this.pos.x < -50 || this.pos.x > width + 50 ||
      this.pos.y < -50 || this.pos.y > height + 50
    ) {
      this.alive = false;
    }
  }

  draw() {
    push();
    noStroke();
    fill(255, 230, 120);
    circle(this.pos.x, this.pos.y, this.radius * 2);
    pop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function Bullet initialization constructor(x, y, vx, vy) { this.pos = createVector(x, y); this.vel = createVector(vx, vy); this.radius = 5; this.alive = true; }

Creates a bullet at the given position with a velocity vector

conditional Off-screen cleanup if ( this.pos.x < -50 || this.pos.x > width + 50 || this.pos.y < -50 || this.pos.y > height + 50 ) { this.alive = false; }

Marks bullet as dead if it travels too far off-screen to prevent infinite array growth

this.pos = createVector(x, y);
Stores the bullet's starting position
this.vel = createVector(vx, vy);
Stores the bullet's velocity (vx and vy passed from handleFire)
this.radius = 5;
Sets the bullet's collision radius to 5 pixels
this.alive = true;
Marks the bullet as active; set to false when it hits something or leaves the screen
this.pos.add(this.vel);
Moves the bullet forward by adding velocity to position
if ( this.pos.x < -50 || this.pos.x > width + 50 || this.pos.y < -50 || this.pos.y > height + 50 ) {
Checks if the bullet has traveled more than 50 pixels beyond any edge of the canvas
this.alive = false;
Marks the bullet as dead so it will be filtered out of the array
fill(255, 230, 120);
Sets the bullet color to warm yellow (RGB: 255, 230, 120)
circle(this.pos.x, this.pos.y, this.radius * 2);
Draws the bullet as a circle with diameter 2 * radius = 10 pixels at its current position

Drop (class)

The Drop class demonstrates animation techniques: sine-wave bobbing for idle motion and linear fade+scale for collection effects. The fallback diamond shape (using beginShape/vertex/endShape) is a perfect example of defensive programming—if the external image fails to load (due to CORS or network issues), the game still looks good. The state machine ('idle' vs 'collected') controls which animation plays.

🔬 The bobPhase increment (0.1) controls how fast the drop bobs. What happens if you change 0.1 to 0.05 or 0.3? Will the drop bob slower or faster?

    if (this.state === "idle") {
      this.bobPhase += 0.1;
      this.offsetY = sin(this.bobPhase) * 2;
    } else if (this.state === "collected") {
      this.fade -= 18;
      this.size *= 1.02;
    }
class Drop {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.size = 32;

    this.state = "idle"; // 'idle' | 'collected'
    this.fade = 255;

    this.bobPhase = random(TWO_PI);
    this.offsetY = 0;
  }

  update() {
    if (this.state === "idle") {
      this.bobPhase += 0.1;
      this.offsetY = sin(this.bobPhase) * 2;
    } else if (this.state === "collected") {
      this.fade -= 18;
      this.size *= 1.02;
    }
  }

  draw() {
    push();
    translate(this.pos.x, this.pos.y + this.offsetY);

    if (dropImg) {
      imageMode(CENTER);
      if (this.state === "collected") {
        tint(255, this.fade);
      } else {
        noTint();
      }
      image(dropImg, 0, 0, this.size, this.size);
    } else {
      // Fallback: glowing diamond
      noStroke();
      const alpha = this.state === "collected" ? this.fade : 255;
      fill(0, 255, 200, alpha);
      beginShape();
      vertex(0, -this.size / 2);
      vertex(this.size / 2, 0);
      vertex(0, this.size / 2);
      vertex(-this.size / 2, 0);
      endShape(CLOSE);
    }

    pop();
  }

  collect() {
    if (this.state !== "idle") return;
    this.state = "collected";
    this.fade = 255;
  }

  isGone() {
    return this.state === "collected" && this.fade <= 0;
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

function Drop initialization constructor(x, y) { this.pos = createVector(x, y); this.size = 32; this.state = "idle"; this.fade = 255; this.bobPhase = random(TWO_PI); this.offsetY = 0; }

Creates a collectible drop with position, size, animation state, and bobbing phase

conditional Idle bobbing if (this.state === "idle") { this.bobPhase += 0.1; this.offsetY = sin(this.bobPhase) * 2; }

Animates the drop bobbing up and down using a sine wave

conditional Collection fade-out } else if (this.state === "collected") { this.fade -= 18; this.size *= 1.02; }

Fades the drop out and expands it over several frames when collected

conditional Fallback shape } else { // Fallback: glowing diamond noStroke(); const alpha = this.state === "collected" ? this.fade : 255; fill(0, 255, 200, alpha); beginShape(); vertex(0, -this.size / 2); vertex(this.size / 2, 0); vertex(0, this.size / 2); vertex(-this.size / 2, 0); endShape(CLOSE); }

If the external image fails to load, draws a cyan diamond shape as a fallback

this.bobPhase = random(TWO_PI);
Initializes bobPhase to a random angle (0 to 2π), so drops bob at different times instead of all in sync
this.bobPhase += 0.1;
Increments the phase each frame by 0.1 radians, causing the sine wave to cycle
this.offsetY = sin(this.bobPhase) * 2;
Calculates vertical offset using sine; ranges from -2 to 2 pixels, creating a gentle up-and-down bob
this.fade -= 18;
Decreases alpha (opacity) by 18 per frame; starting at 255, it becomes 0 after 14 frames (~0.23 seconds)
this.size *= 1.02;
Multiplies size by 1.02 each frame, causing the drop to grow by 2% per frame—a visible expanding effect
translate(this.pos.x, this.pos.y + this.offsetY);
Moves the origin to the drop's position plus its bobbing offset, so the drop visually bobs up and down
imageMode(CENTER);
Sets image drawing to use CENTER mode, so the image draws from its center point
if (this.state === "collected") { tint(255, this.fade); } else { noTint(); }
During collection, applies a tint with the fade alpha value to fade the image out; otherwise, no tint
image(dropImg, 0, 0, this.size, this.size);
Draws the loaded image at the origin, scaled to this.size × this.size
fill(0, 255, 200, alpha);
Sets fill to cyan with alpha transparency (opaque when idle, fading when collected)
beginShape(); vertex(0, -this.size / 2); vertex(this.size / 2, 0); vertex(0, this.size / 2); vertex(-this.size / 2, 0); endShape(CLOSE);
Draws a diamond shape by specifying 4 vertices (top, right, bottom, left) and closing the path
if (this.state !== "idle") return;
collect() exits early if the drop is already collected, preventing double-collection
this.state = "collected";
Transitions the drop to collected state, triggering the fade-out animation
this.fade = 255;
Resets fade to 255 (fully opaque) when collection begins, ensuring a clean animation start
return this.state === "collected" && this.fade <= 0;
isGone() returns true once the drop is fully faded out, signaling it should be removed

📦 Key Variables

player Player object

Stores the single Player instance; accessed globally to update and draw the character

let player;
crabs array

Holds all active Crab objects; updated each frame to move them and check collisions

let crabs = [];
bullets array

Holds all active Bullet objects; updated each frame to move them and check hits

let bullets = [];
drops array

Holds all active Drop objects; updated each frame to animate and collect them

let drops = [];
killCount number

Tracks the total number of crabs eliminated; displayed in the HUD

let killCount = 0;
lootCount number

Tracks the total number of drops collected; displayed in the HUD

let lootCount = 0;
maxCrabs number

The maximum number of crabs that should be alive at once; controls game difficulty

let maxCrabs = 12;
dropImg p5.Image or null

Stores the loaded image for drops; used if available, otherwise falls back to a drawn diamond

let dropImg = null;
lastShotTime number (milliseconds)

Records the time of the last bullet fired; used to enforce fire rate cooldown

let lastShotTime = 0;
FIRE_COOLDOWN number (milliseconds)

The minimum milliseconds between shots; prevents bullet spam

const FIRE_COOLDOWN = 150;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Crab.update()

Crabs don't wrap off-screen; they travel infinitely far off-canvas if the player moves away, wasting CPU on invisible entities.

💡 Add an isGone() check similar to the bullet's screen-bounds check. Remove crabs that travel beyond a threshold distance from the canvas.

PERFORMANCE handleBulletCrabCollisions()

Nested loop over all bullets × all crabs is O(n²) and will slow down if counts grow large. At 12 crabs × 20 bullets = 240 checks per frame.

💡 Implement spatial partitioning (grid-based collision zones) or use p5.js QuadTree libraries to reduce unnecessary distance checks.

STYLE keyPressed()

Only spacebar is handled in keyPressed(); WASD/arrow keys use keyIsDown() in Player.update(). Inconsistent input patterns make the code harder to follow.

💡 Consolidate all input handling into a single method or comment clearly why different keys use different detection methods.

FEATURE Global scope

No pause, restart, or game-over state; the game runs forever and never ends.

💡 Add a global gameState variable ('playing', 'paused', 'gameOver') and implement restart logic. Perhaps add a loss condition (player hit by crab?).

BUG Drop.draw()

If the external image URL returns a 404 or has CORS issues, dropImg remains null silently. The fallback diamond is good, but no visual indication that the image failed.

💡 Log to console.log when the fallback is used (already done as console.warn), or add a small on-screen indicator for debugging.

🔄 Code Flow

Code flow showing setup, draw, spawnCrab, handlebulletcrabcollisions, handleplayerdroplcollisions, drawhud, mousepressed, touchstarted, keypressed, handlefire, windowresized, player, crab, bullet, drop

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

graph TD start[Start] --> setup[setup] setup --> canvas-init[canvas-init] canvas-init --> player-spawn[player-spawn] player-spawn --> initial-crabs[initial-crabs] initial-crabs --> draw[draw loop] click setup href "#fn-setup" click canvas-init href "#sub-canvas-init" click player-spawn href "#sub-player-spawn" click initial-crabs href "#sub-initial-crabs" draw --> crab-spawn-check[crab-spawn-check] crab-spawn-check -->|if crabs.length < maxCrabs| spawnCrab[spawnCrab] spawnCrab --> edge-pick[edge-pick] edge-pick --> spawn-conditional[spawn-conditional] spawn-conditional --> crab-creation[crab-creation] crab-creation --> update-loop-all[update-loop-all] update-loop-all --> bullet-loop[bullet-loop] bullet-loop --> crab-loop[crab-loop] crab-loop --> distance-calc[distance-calc] distance-calc --> collision-check[collision-check] collision-check -->|if collision| handlebulletcrabcollisions[handlebulletcrabcollisions] handlebulletcrabcollisions --> damage-and-drop[damage-and-drop] damage-and-drop --> cleanup-filter[cleanup-filter] cleanup-filter --> draw-order[draw-order] draw-order --> collision-checks[collision-checks] collision-checks --> handleplayerdroplcollisions[handleplayerdroplcollisions] handleplayerdroplcollisions --> drop-loop[drop-loop] drop-loop --> pickup-distance[pickup-distance] pickup-distance --> pickup-collision[pickup-collision] pickup-collision --> drawhud[drawhud] drawhud --> draw[draw loop] click draw href "#fn-draw" click crab-spawn-check href "#sub-crab-spawn-check" click spawnCrab href "#fn-spawnCrab" click edge-pick href "#sub-edge-pick" click spawn-conditional href "#sub-spawn-conditional" click crab-creation href "#sub-crab-creation" click update-loop-all href "#sub-update-loop-all" click bullet-loop href "#sub-bullet-loop" click crab-loop href "#sub-crab-loop" click distance-calc href "#sub-distance-calc" click collision-check href "#sub-collision-check" click handlebulletcrabcollisions href "#fn-handlebulletcrabcollisions" click damage-and-drop href "#sub-damage-and-drop" click cleanup-filter href "#sub-cleanup-filter" click draw-order href "#sub-draw-order" click collision-checks href "#sub-collision-checks" click handleplayerdroplcollisions href "#fn-handleplayerdroplcollisions" click drop-loop href "#sub-drop-loop" click pickup-distance href "#sub-pickup-distance" click pickup-collision href "#sub-pickup-collision" click drawhud href "#fn-drawhud"

❓ Frequently Asked Questions

What visual elements can I expect to see in the AI Crab Hunter sketch?

The sketch features a player character, animated crabs, and collectible loot drops, all set against a dark, starry background.

How can users interact with the AI Crab Hunter game?

Users can control the player character's movement using the WASD or arrow keys, and shoot at crabs using the mouse, tap, or spacebar.

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

This sketch demonstrates concepts such as entity management, collision detection, and dynamic gameplay elements through the spawning and interaction of crabs, bullets, and loot drops.

Preview

Sketch 2026-06-07 16:54 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-06-07 16:54 - Code flow showing setup, draw, spawnCrab, handlebulletcrabcollisions, handleplayerdroplcollisions, drawhud, mousepressed, touchstarted, keypressed, handlefire, windowresized, player, crab, bullet, drop
Code Flow Diagram