spider man find home p5ai

This is a Spider-Man themed side-scrolling swinging game where you hold down to swing upward through a procedurally generated city while dodging buildings. The game combines gravity physics, web-swinging mechanics, parallax scrolling, and progressive difficulty to create an engaging arcade-style experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make gravity weaker — Lower gravity makes Spider-Man fall more slowly, giving you more time to react. Try 0.2 or 0.3
  2. Boost the swing power — Make lift more negative (like -2.0) so swings propel you higher and faster
  3. Widen the building gaps — Larger gaps make the game easier and less punishing. Change the random range in addFgBuilding()
  4. Speed up score gain — Earn points faster by checking every 5 frames instead of 10. Higher scores = faster difficulty progression
  5. Change the night sky color — Modify the background RGB to create a different atmosphere—try purples, deep blues, or even oranges
  6. Increase Spider-Man's size — Draw Spider-Man bigger by increasing the size parameter when calling drawSpiderman()
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable side-scrolling game where you control Spider-Man swinging upward through an endless procedurally generated city. What makes it visually engaging is the parallax scrolling backgrounds, the detailed chibi Spider-Man sprite with dynamic rotation, glowing building windows, and a starry night sky. Under the hood, it uses gravity and velocity physics, collision detection between the player circle and building rectangles, web-swinging mechanics that anchor to random points above screen, and progressive difficulty that speeds up the game every 100 points.

The code is organized into clear sections: game state management (START/PLAY/GAMEOVER), physics and collision logic in updateGameLogic(), a world generation system that creates buildings procedurally as they scroll off-screen, and separate drawing functions for the city, player, and UI. By studying this sketch you will learn how to build a complete game loop with state transitions, how to implement gravity-based physics with drag, how procedural generation keeps memory efficient, and how to layer parallax backgrounds for visual depth.

⚙️ How It Works

  1. When you load the sketch, setup() creates an 800×600 canvas and generates 80 random stars that stay fixed in the night sky. The game begins in the START state showing instructions.
  2. When you click or press SPACE, handleInputStart() switches the game to PLAY mode and resets all variables. Spider-Man appears at the center of the screen above a scrolling city.
  3. Every frame, updateGameLogic() applies gravity (pulling Spider-Man downward at 0.5 pixels/frame²) and air friction (multiplying velocity by 0.95 each frame). You move left or right on your own, but gravity pulls you down constantly.
  4. When you hold down the mouse or SPACE key, isSwinging becomes true. This flips gravity to a negative lift value (upward acceleration of -1.2), creating an upward swing. Simultaneously, a web anchor point is placed forward and above Spider-Man, and a visual line draws from player to anchor.
  5. The foreground buildings scroll left at gameSpeed pixels per frame. Every 10 frames, score increments by 1. Every 100 points scored, gameSpeed increases by 0.2, making the game harder. Background buildings move slower (0.4× gameSpeed), creating parallax depth.
  6. Collision detection compares Spider-Man's circular hitbox against each foreground building's rectangular bounds. If the circles overlap with the building's bottom edge (the roofline), triggerGameOver() switches to GAMEOVER state. New buildings spawn procedurally with random gaps so you always have a swingable route upward.
  7. In GAMEOVER state, you see your final score and high score. Clicking or pressing SPACE resets everything and returns to PLAY.

🎓 Concepts You'll Learn

Game State ManagementGravity and Physics SimulationCollision Detection (Circle vs Rectangle)Procedural GenerationParallax ScrollingInput Handling (Mouse and Keyboard)Score and Difficulty ProgressionSprite Drawing and Animation

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to create your canvas, pre-generate static visual elements (like these stars), and call initialization functions. Notice how the stars are pre-generated here once, not every frame in draw()—this is a performance pattern worth remembering.

function setup() {
  createCanvas(800, 600);
  textAlign(CENTER, CENTER);
  
  // Generate static stars once
  for (let i = 0; i < 80; i++) {
    stars.push({ x: random(width), y: random(height * 0.6), r: random(1, 3) });
  }
  
  resetGame();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Star Generation Loop for (let i = 0; i < 80; i++) { stars.push({ x: random(width), y: random(height * 0.6), r: random(1, 3) }); }

Creates 80 randomly positioned stars that stay fixed in the night sky backdrop

createCanvas(800, 600);
Creates a landscape-oriented 800×600 pixel canvas—wide enough for side-scrolling gameplay
textAlign(CENTER, CENTER);
Sets all text to draw centered horizontally and vertically, making UI text positioning easier
for (let i = 0; i < 80; i++) {
Loop runs 80 times to create an array of stars
stars.push({ x: random(width), y: random(height * 0.6), r: random(1, 3) });
Adds a star object with random x position (anywhere on canvas), y position (in upper 60% of screen), and radius (1-3 pixels). These stars are created once and never move.
resetGame();
Calls the resetGame() function to initialize all game variables to their starting values

draw()

draw() runs 60 times per second and is where all animation happens. Notice how it's organized in three clear sections: (1) render static background elements like the moon and stars, (2) conditionally update game logic and render dynamic elements, (3) draw UI on top. This layering order is crucial—draw the background first, then game elements, then UI.

🔬 This loop draws all the stars using their stored radius (s.r). What happens if you change s.r to a fixed value like 2, or multiply it like s.r * 3?

  for (let s of stars) {
    ellipse(s.x, s.y, s.r);
  }
function draw() {
  // 1. Draw static background
  background(20, 25, 50);
  fill(255, 250, 210);
  noStroke();
  ellipse(width * 0.8, height * 0.2, 120, 120); // Moon
  
  fill(255, 200);
  for (let s of stars) {
    ellipse(s.x, s.y, s.r);
  }

  // 2. Update and draw game elements
  if (gameState === "PLAY") {
    updateGameLogic();
  }
  
  drawCity();
  
  if (gameState === "PLAY" || gameState === "GAMEOVER") {
    drawPlayer();
  }

  // 3. Draw UI
  drawUI();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Star Rendering Loop for (let s of stars) { ellipse(s.x, s.y, s.r); }

Draws all 80 pre-generated stars as small circles, creating a fixed starfield

conditional Game State Logic if (gameState === "PLAY") { updateGameLogic(); }

Only updates physics and collision when actively playing—paused during START and GAMEOVER screens

conditional Player Render Gate if (gameState === "PLAY" || gameState === "GAMEOVER") { drawPlayer(); }

Draws Spider-Man only during PLAY and GAMEOVER states, not on the START screen

background(20, 25, 50);
Clears the canvas with a dark blue-black color every frame, creating the night sky
ellipse(width * 0.8, height * 0.2, 120, 120);
Draws a moon at 80% across and 20% down the canvas, using warm cream color (255, 250, 210)
for (let s of stars) { ellipse(s.x, s.y, s.r); }
Loops through the stars array created in setup() and draws each star as a small circle at its stored position
if (gameState === "PLAY") { updateGameLogic(); }
Only calls updateGameLogic() when actively in PLAY state—physics and collision detection don't run on START or GAMEOVER screens
drawCity();
Calls the function that renders all buildings (both background and foreground layers)
if (gameState === "PLAY" || gameState === "GAMEOVER") { drawPlayer(); }
Draws Spider-Man during PLAY (to show active gameplay) and GAMEOVER (to show final position), but not on START screen
drawUI();
Calls the function that renders score, instructions, or game-over text depending on gameState

updateGameLogic()

updateGameLogic() is the game's engine. It handles scoring, physics simulation, parallax scrolling, memory management (recycling buildings), and collision detection. This is where the game's behavior lives. Notice how it only runs when gameState === 'PLAY'—this architecture allows you to pause/unpause or show menus without rewriting the physics system.

🔬 This is the physics heart of the game. When swinging, vy gets more negative (upward), and when falling, vy gets more positive (downward). What happens if you swap the two values—making lift positive and gravity negative?

  if (isSwinging) {
    vy += lift; // Accelerate up
    webAnchorX -= gameSpeed; // Web moves left with the world
  } else {
    vy += gravity; // Accelerate down
  }

🔬 This collision detection checks if a circle overlaps a building. The first if checks horizontal overlap, the second checks vertical. What happens if you remove the second if statement (the vertical check)? Spider-Man would collide with buildings earlier—notice how game design is really just conditional geometry.

  if (playerX + playerRadius > b.x && playerX - playerRadius < b.x + b.w) {
      if (playerY + playerRadius > height - b.h) {
        triggerGameOver();
      }
    }
function updateGameLogic() {
  // Add score
  if (frameCount % 10 === 0) score++;
  if (score > 0 && score % 100 === 0) gameSpeed += 0.2; // Increase difficulty

  // Physics
  if (isSwinging) {
    vy += lift; // Accelerate up
    webAnchorX -= gameSpeed; // Web moves left with the world
  } else {
    vy += gravity; // Accelerate down
  }
  
  vy *= 0.95; // Air friction / terminal velocity
  vy = constrain(vy, -12, 12);
  playerY += vy;

  // Screen constraints & Game Over conditions
  if (playerY < 0) {
    playerY = 0;
    vy = 0;
  }
  if (playerY + playerRadius > height) {
    triggerGameOver();
  }

  // Move and manage background buildings (Parallax speed)
  for (let b of bgBuildings) b.x -= gameSpeed * 0.4;
  if (bgBuildings[0].x + bgBuildings[0].w < 0) {
    bgBuildings.shift();
    addBgBuilding();
  }

  // Move and manage foreground buildings (Game speed)
  for (let b of fgBuildings) {
    b.x -= gameSpeed;
    
    // COLLISION DETECTION (Player vs Foreground Building)
    // Simple AABB vs Circle collision check
    if (playerX + playerRadius > b.x && playerX - playerRadius < b.x + b.w) {
      if (playerY + playerRadius > height - b.h) {
        triggerGameOver();
      }
    }
  }
  
  if (fgBuildings[0].x + fgBuildings[0].w < 0) {
    fgBuildings.shift();
    addFgBuilding();
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Score Increment if (frameCount % 10 === 0) score++;

Increases score by 1 every 10 frames (6 times per second), rewarding survival time

conditional Difficulty Progression if (score > 0 && score % 100 === 0) gameSpeed += 0.2;

Every 100 points, increases gameSpeed by 0.2, making buildings scroll faster and the game harder

conditional Physics System if (isSwinging) { vy += lift; webAnchorX -= gameSpeed; } else { vy += gravity; }

Applies upward acceleration when swinging, downward acceleration when falling, simulating gravity and web mechanics

calculation Air Friction vy *= 0.95;

Multiplies vertical velocity by 0.95 each frame, simulating air resistance and preventing infinite acceleration

calculation Terminal Velocity Clamp vy = constrain(vy, -12, 12);

Prevents velocity from exceeding ±12 pixels/frame, creating a max fall/rise speed

calculation Position Update playerY += vy;

Updates Spider-Man's vertical position by the velocity amount, creating smooth motion

conditional Top Screen Boundary if (playerY < 0) { playerY = 0; vy = 0; }

Prevents Spider-Man from leaving the top of the canvas and stops upward velocity

conditional Bottom Boundary Game Over if (playerY + playerRadius > height) { triggerGameOver(); }

Ends the game if Spider-Man falls below the bottom of the canvas

for-loop Background Building Parallax for (let b of bgBuildings) b.x -= gameSpeed * 0.4;

Scrolls background buildings at 40% of foreground speed, creating depth illusion

conditional Background Building Recycling if (bgBuildings[0].x + bgBuildings[0].w < 0) { bgBuildings.shift(); addBgBuilding(); }

When a background building scrolls off-screen left, removes it and generates a new one on the right to save memory

for-loop Foreground Building Processing for (let b of fgBuildings) { b.x -= gameSpeed; if (playerX + playerRadius > b.x && playerX - playerRadius < b.x + b.w) { if (playerY + playerRadius > height - b.h) { triggerGameOver(); } } }

Scrolls foreground buildings at game speed and checks collision between Spider-Man's circular hitbox and each building's rectangular area

if (frameCount % 10 === 0) score++;
frameCount is p5's built-in frame counter. The modulo operator % checks if frameCount is divisible by 10. When true (every 10 frames), score increases by 1
if (score > 0 && score % 100 === 0) gameSpeed += 0.2;
Every time score reaches a multiple of 100 (100, 200, 300…), gameSpeed increases by 0.2, making the game progressively harder
if (isSwinging) {
isSwinging becomes true when you hold down the mouse or SPACE key, triggering upward acceleration
vy += lift;
Adds the lift value (-1.2) to vertical velocity, accelerating Spider-Man upward. The negative value means upward movement
webAnchorX -= gameSpeed;
Moves the web anchor point left at game speed so it scrolls with the world, keeping the web line visually connected
} else { vy += gravity;
When not swinging, adds gravity (0.5) to velocity, accelerating Spider-Man downward. Positive values mean downward
vy *= 0.95;
Multiplies velocity by 0.95 each frame (5% loss). This simulates air friction and prevents unlimited acceleration
vy = constrain(vy, -12, 12);
Clamps velocity between -12 and 12. This creates a terminal velocity—the fastest you can fall or rise
playerY += vy;
Adds the velocity to playerY position, moving Spider-Man. This happens every frame, creating smooth animation
if (playerY < 0) {
Checks if Spider-Man has moved above the top of the canvas (y < 0)
if (playerY + playerRadius > height) {
Checks if the bottom edge of Spider-Man's hitbox circle has crossed the bottom of the canvas, triggering game over
for (let b of bgBuildings) b.x -= gameSpeed * 0.4;
Loops through all background buildings and scrolls them left at 40% of gameSpeed. This slower speed creates depth—distant buildings move slower
if (bgBuildings[0].x + bgBuildings[0].w < 0) {
Checks if the first (oldest) background building has completely scrolled off the left side of the canvas
if (playerX + playerRadius > b.x && playerX - playerRadius < b.x + b.w) {
Checks horizontal overlap: is Spider-Man's circle touching this building's left or right edge? This is the first part of circle-vs-rectangle collision
if (playerY + playerRadius > height - b.h) {
Checks vertical overlap: is Spider-Man's bottom edge touching the building's top edge (roofline)? If both horizontal AND this vertical check pass, it's a collision

handleInputStart()

handleInputStart() is called when you press mouse down or SPACE key. It handles the two critical input transitions: (1) starting a new game from menus, and (2) initiating a swing during gameplay. Notice how it branches based on gameState—this keeps input logic clean and prevents bugs like restarting mid-swing.

function handleInputStart() {
  if (gameState === "START" || gameState === "GAMEOVER") {
    resetGame();
    gameState = "PLAY";
  } else if (gameState === "PLAY") {
    isSwinging = true;
    // Attach web forward and upward
    webAnchorX = playerX + random(150, 300);
    webAnchorY = -50; // Attach off-screen top
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Game Start Transition if (gameState === "START" || gameState === "GAMEOVER") { resetGame(); gameState = "PLAY"; }

When on START or GAMEOVER screens, clicking resets the game and switches to PLAY state

conditional Swing Initiation } else if (gameState === "PLAY") { isSwinging = true; webAnchorX = playerX + random(150, 300); webAnchorY = -50; }

During PLAY, clicking sets isSwinging true and anchors the web at a random point forward and above Spider-Man

if (gameState === "START" || gameState === "GAMEOVER") {
Checks if the game is on the START screen or has ended in GAMEOVER
resetGame();
Calls resetGame() to reinitialize all variables—score resets to 0, speed resets to 5, buildings clear, Spider-Man repositions
gameState = "PLAY";
Transitions the game to PLAY state, which enables physics updates and gameplay rendering
} else if (gameState === "PLAY") {
If already in PLAY state and you click/press SPACE, enter swing mode instead of restarting
isSwinging = true;
Sets the flag that tells updateGameLogic() to apply lift (upward acceleration) instead of gravity
webAnchorX = playerX + random(150, 300);
Places the web anchor horizontally 150–300 pixels forward (to the right) of Spider-Man. random() picks a random value in that range
webAnchorY = -50;
Places the web anchor 50 pixels above the top of the screen (negative y). This creates the illusion of swinging from buildings above

handleInputStop()

handleInputStop() is called when you release the mouse button or SPACE key. It's a simple one-liner that ends the swing and lets gravity take over. This responsive input is what makes the game feel responsive to player control.

function handleInputStop() {
  isSwinging = false;
}
Line-by-line explanation (1 lines)
isSwinging = false;
Sets isSwinging to false, causing updateGameLogic() to switch from lift (upward) back to gravity (downward) on the next frame

resetGame()

resetGame() is called when you click/press SPACE on a menu screen, or inside handleInputStart() when starting fresh. It's responsible for zeroing out all state variables and pre-generating the initial world. The while loops at the end are clever—they generate exactly as many buildings as needed to fill the viewport, avoiding waste.

function resetGame() {
  playerY = height / 2;
  vy = 0;
  score = 0;
  isSwinging = false;
  gameSpeed = 5;
  
  bgBuildings = [];
  fgBuildings = [];
  nextFgX = 0;
  nextBgX = -50;
  
  // Pre-fill buildings
  while (nextBgX < width + 100) addBgBuilding();
  while (nextFgX < width + 200) addFgBuilding();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

while-loop Building Pre-generation while (nextBgX < width + 100) addBgBuilding(); while (nextFgX < width + 200) addFgBuilding();

Generates enough buildings to fill the initial viewport so the player sees a populated city at game start

playerY = height / 2;
Resets Spider-Man to the vertical center of the canvas
vy = 0;
Resets vertical velocity to zero—Spider-Man starts at rest
score = 0;
Resets score to zero for a new game
isSwinging = false;
Ensures Spider-Man is not swinging at game start
gameSpeed = 5;
Resets game speed to the base speed (5 pixels/frame). This removes any difficulty scaling from previous games
bgBuildings = [];
Clears the background buildings array, preparing for fresh generation
fgBuildings = [];
Clears the foreground buildings array
nextFgX = 0; nextBgX = -50;
Resets the position counters. nextBgX starts at -50 (off-screen left) and nextFgX at 0, telling addBgBuilding() and addFgBuilding() where to place the first buildings
while (nextBgX < width + 100) addBgBuilding();
Fills the background layer with buildings until they cover the entire initial canvas width plus 100 pixels (so the player sees a full scene at start)
while (nextFgX < width + 200) addFgBuilding();
Pre-generates enough foreground buildings for the player to swing through without them running out

triggerGameOver()

triggerGameOver() is called whenever you hit a building or fall off-screen. It immediately stops gameplay, freezes physics, and updates the high score if needed. This simple function is the game's fail state handler—notice how it's called from updateGameLogic() when collisions occur.

function triggerGameOver() {
  gameState = "GAMEOVER";
  isSwinging = false;
  if (score > highScore) highScore = score;
}
Line-by-line explanation (3 lines)
gameState = "GAMEOVER";
Switches the game state, which pauses physics updates and signals drawUI() to show the game-over screen
isSwinging = false;
Ends any active swing immediately
if (score > highScore) highScore = score;
Compares the current score to the high score. If higher, updates highScore so it persists on the game-over screen

addBgBuilding()

addBgBuilding() is a procedural generation helper that creates background buildings with random widths and heights. Each building is placed at nextBgX, which advances by the building's width. This ensures no gaps and creates an endless city. Background buildings are generated on-demand as the camera scrolls, keeping memory efficient.

function addBgBuilding() {
  let w = 90 + random(20);
  bgBuildings.push({
    x: nextBgX,
    w: w,
    h: random(250, 500)
  });
  nextBgX += w;
}
Line-by-line explanation (6 lines)
let w = 90 + random(20);
Calculates a random width between 90 and 110 pixels for this building
bgBuildings.push({
Adds a new building object to the bgBuildings array
x: nextBgX,
Places the building at nextBgX, which increments after each building is added
w: w,
Stores the building's width
h: random(250, 500)
Assigns a random height between 250 and 500 pixels, making buildings vary in size
nextBgX += w;
Increments nextBgX by this building's width so the next building is placed immediately after this one

addFgBuilding()

addFgBuilding() generates foreground buildings with pre-calculated window positions. The key insight here is performance: windows are computed once when the building is created, not every frame during rendering. This is why fgBuildings store a windows array—it's a performance optimization. Notice the gap calculation at the end: random(80, 200) ensures every building has a different-sized gap, creating varied difficulty and encouraging exploration.

🔬 This nested loop creates a grid of windows with 25-pixel horizontal spacing and 35-pixel vertical spacing, then randomly keeps only 40% of them (random() > 0.6). What happens if you change the spacing to 15 and 20, or increase the threshold to 0.8 so more windows appear?

  for (let wx = 15; wx < w - 20; wx += 25) {
    for (let wy = 20; wy < h; wy += 35) {
      if (random() > 0.6) windows.push({ x: wx, y: wy });
    }
  }
function addFgBuilding() {
  let w = 100 + random(50);
  let h = random(100, 350);
  
  // Pre-calculate windows for performance
  let windows = [];
  for (let wx = 15; wx < w - 20; wx += 25) {
    for (let wy = 20; wy < h; wy += 35) {
      if (random() > 0.6) windows.push({ x: wx, y: wy });
    }
  }
  
  fgBuildings.push({ x: nextFgX, w: w, h: h, windows: windows });
  
  // Add a gap between buildings so the player has somewhere to swing
  nextFgX += w + random(80, 200); 
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Window Pre-generation for (let wx = 15; wx < w - 20; wx += 25) { for (let wy = 20; wy < h; wy += 35) { if (random() > 0.6) windows.push({ x: wx, y: wy }); } }

Pre-generates window positions in a grid pattern with 40% random density, avoiding expensive per-frame window calculations

let w = 100 + random(50);
Calculates a random width between 100 and 150 pixels—wider than background buildings for more prominent foreground presence
let h = random(100, 350);
Assigns a random height between 100 and 350 pixels
for (let wx = 15; wx < w - 20; wx += 25) {
Outer loop: iterates horizontally across the building width in 25-pixel steps, starting at x=15 and ending before w-20 (padding from edges)
for (let wy = 20; wy < h; wy += 35) {
Inner loop: iterates vertically down the building height in 35-pixel steps
if (random() > 0.6) windows.push({ x: wx, y: wy });
With 40% probability (when random() exceeds 0.6), adds a window at this grid position. This creates a scattered pattern instead of a solid grid
fgBuildings.push({ x: nextFgX, w: w, h: h, windows: windows });
Adds the building object to fgBuildings array, storing position, dimensions, and all pre-calculated window positions
nextFgX += w + random(80, 200);
Advances nextFgX by the building's width PLUS a random gap of 80–200 pixels. These gaps are where the player swings through

drawCity()

drawCity() renders both background and foreground building layers plus the ground boundary. Notice the layering: background buildings first (darker, slower parallax), then foreground buildings with windows (more visible, full game speed), then the ground line. Also notice the coordinate system: buildings are drawn from height - b.h (bottom-up) rather than 0 (top-down). This is a common pattern in side-scrollers where the ground is at the bottom and buildings rise upward.

function drawCity() {
  // Background layer
  fill(15, 18, 35);
  noStroke();
  for (let b of bgBuildings) {
    rect(b.x, height - b.h, b.w, b.h);
  }

  // Foreground layer
  for (let b of fgBuildings) {
    fill(10, 12, 25);
    rect(b.x, height - b.h, b.w, b.h);
    
    // Draw Windows
    fill(255, 220, 100, 150); // Warm light
    for (let win of b.windows) {
      rect(b.x + win.x, height - b.h + win.y, 10, 15);
    }
  }
  
  // Draw ground line to show bottom boundary
  fill(5, 7, 15);
  rect(0, height - 10, width, 10);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Background Building Rendering for (let b of bgBuildings) { rect(b.x, height - b.h, b.w, b.h); }

Draws all background buildings as dark rectangles

for-loop Foreground Building Rendering for (let b of fgBuildings) { fill(10, 12, 25); rect(b.x, height - b.h, b.w, b.h);

Draws all foreground buildings as darker rectangles

for-loop Window Rendering for (let win of b.windows) { rect(b.x + win.x, height - b.h + win.y, 10, 15); }

Draws pre-calculated windows as small glowing rectangles on each building

fill(15, 18, 35);
Sets the fill color to dark blue for background buildings
noStroke();
Disables outlines so buildings are solid filled shapes
for (let b of bgBuildings) {
Loops through all background buildings
rect(b.x, height - b.h, b.w, b.h);
Draws a rectangle at position (b.x, height - b.h) with width b.w and height b.h. Note: y is 'height - b.h' because rect() draws downward, but we want buildings drawn upward from the bottom
fill(10, 12, 25);
Darkens the fill color slightly for foreground buildings to add visual depth
fill(255, 220, 100, 150);
Switches fill to warm orange (RGB 255, 220, 100) with 150 alpha (semi-transparent), simulating lit windows at night
for (let win of b.windows) {
Loops through pre-calculated window positions for this building
rect(b.x + win.x, height - b.h + win.y, 10, 15);
Draws each window at position relative to the building (b.x + win.x, height - b.h + win.y) with size 10×15 pixels
fill(5, 7, 15);
Fills with nearly black color for the ground line
rect(0, height - 10, width, 10);
Draws a 10-pixel-tall strip at the bottom of the canvas, creating a visible ground boundary that triggers game over if crossed

drawPlayer()

drawPlayer() renders Spider-Man and the web line. The key technique here is push/pop combined with translate/rotate. push() saves state, translate moves the origin, rotate angles everything, drawSpiderman draws at origin, then pop() restores. This is the standard pattern for drawing rotated/transformed sprites. The web line only renders when isSwinging is true, providing visual feedback about swing state.

function drawPlayer() {
  // Draw the Web Line if swinging
  if (isSwinging) {
    stroke(240);
    strokeWeight(3);
    line(playerX, playerY - 20, webAnchorX, webAnchorY);
  }

  push();
  translate(playerX, playerY);
  // Tilt Spider-Man up when rising, down when falling
  rotate(vy * 0.05); 
  
  // Scale down the original drawing size to fit the game (size ~ 70)
  drawSpiderman(0, 0, 70); 
  
  // DEBUG HITBOX (Uncomment to see the collision circle)
  // noFill(); stroke(0, 255, 0); strokeWeight(2); circle(0, 0, playerRadius * 2);
  
  pop();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Web Line Rendering if (isSwinging) { stroke(240); strokeWeight(3); line(playerX, playerY - 20, webAnchorX, webAnchorY); }

Draws a visible web line from Spider-Man to the anchor point when actively swinging

calculation Player Rotation and Transform translate(playerX, playerY); rotate(vy * 0.05);

Moves drawing origin to Spider-Man's position and rotates him based on vertical velocity for dynamic animation

if (isSwinging) {
Checks if Spider-Man is currently swinging
stroke(240);
Sets stroke color to light gray (nearly white) for the web line
strokeWeight(3);
Sets the web line to 3 pixels thick
line(playerX, playerY - 20, webAnchorX, webAnchorY);
Draws a line from (playerX, playerY - 20) to (webAnchorX, webAnchorY). The -20 offsets the line to the top of the Spider-Man sprite
push();
Saves the current drawing state (transformation matrix, fill/stroke settings) so changes don't affect other drawings
translate(playerX, playerY);
Moves the drawing origin to Spider-Man's position. Now (0, 0) refers to playerX, playerY instead of the canvas corner
rotate(vy * 0.05);
Rotates the drawing context by vy * 0.05 radians. When rising (vy negative), rotation is negative (tilts up). When falling (vy positive), tilts down. This makes Spider-Man appear to angle into his motion
drawSpiderman(0, 0, 70);
Calls the Spider-Man drawing function at (0, 0) with size 70. Because we translated/rotated, this draws him at playerX, playerY with applied rotation
// noFill(); stroke(0, 255, 0); strokeWeight(2); circle(0, 0, playerRadius * 2);
This commented line would draw a green circle showing the collision hitbox. Uncomment it for debugging collision issues
pop();
Restores the drawing state to what it was before push(), ensuring rotation and translation don't affect later drawings

drawUI()

drawUI() is a state-aware drawing function. It checks gameState and renders completely different screens: START (menu), PLAY (HUD with score), and GAMEOVER (end screen). Notice how each state has semi-transparent overlays—this is a polished UI pattern. Also notice the textAlign() calls: it switches from CENTER to LEFT for the score (so it stays in the corner), then resets to CENTER for menu consistency.

🔬 This draws a semi-transparent black overlay and then white text on top. What happens if you change fill(0, 150) to fill(0, 255)? The overlay becomes completely opaque, blocking the background entirely. What if you remove the rect line? The overlay disappears but text still shows.

  if (gameState === "START") {
    fill(0, 150);
    rect(0, 0, width, height);
    
    fill(255);
    textSize(40);
    text("SPIDER-MAN SWINGER", width / 2, height / 2 - 40);
function drawUI() {
  if (gameState === "START") {
    fill(0, 150);
    rect(0, 0, width, height);
    
    fill(255);
    textSize(40);
    text("SPIDER-MAN SWINGER", width / 2, height / 2 - 40);
    textSize(20);
    text("Hold Left-Click or SPACE to Swing Up", width / 2, height / 2 + 20);
    text("Release to Fall", width / 2, height / 2 + 50);
    fill(255, 200, 100);
    text("Click to Start", width / 2, height / 2 + 100);
  } 
  else if (gameState === "PLAY") {
    fill(255);
    textSize(24);
    textAlign(LEFT, TOP);
    text("Score: " + score, 20, 20);
    textAlign(CENTER, CENTER); // reset
  } 
  else if (gameState === "GAMEOVER") {
    fill(0, 150);
    rect(0, 0, width, height);
    
    fill(255, 50, 50);
    textSize(50);
    text("GAME OVER", width / 2, height / 2 - 40);
    
    fill(255);
    textSize(24);
    text("Score: " + score, width / 2, height / 2 + 20);
    text("High Score: " + highScore, width / 2, height / 2 + 55);
    
    fill(255, 200, 100);
    textSize(20);
    text("Click or press SPACE to Restart", width / 2, height / 2 + 110);
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Start Screen UI if (gameState === "START") { fill(0, 150); rect(0, 0, width, height); // ... text ... }

Displays title, instructions, and start prompt when game is in START state

conditional In-Game HUD else if (gameState === "PLAY") { fill(255); textSize(24); textAlign(LEFT, TOP); text("Score: " + score, 20, 20); textAlign(CENTER, CENTER); }

Shows current score in top-left corner during gameplay

conditional Game Over Screen UI else if (gameState === "GAMEOVER") { fill(0, 150); rect(0, 0, width, height); // ... game over text ... }

Displays game-over message, final score, high score, and restart prompt

if (gameState === "START") {
Checks if the game is in START state
fill(0, 150);
Sets fill to black with 150 alpha (semi-transparent). This creates a dark overlay
rect(0, 0, width, height);
Draws a semi-transparent black rectangle covering the entire canvas, dimming the background
textSize(40);
Sets font size to 40 pixels for the title
text("SPIDER-MAN SWINGER", width / 2, height / 2 - 40);
Draws the title centered horizontally and positioned 40 pixels above the vertical center
textSize(20);
Reduces font size to 20 pixels for instruction text
textAlign(LEFT, TOP);
Changes text alignment from CENTER, CENTER to LEFT, TOP. Now text is positioned from its top-left corner
text("Score: " + score, 20, 20);
Draws score text at position (20, 20) in the top-left corner with LEFT, TOP alignment
textAlign(CENTER, CENTER); // reset
Resets text alignment back to CENTER, CENTER so other UI elements align correctly
fill(255, 50, 50);
Sets fill to red for the game-over message
text("GAME OVER", width / 2, height / 2 - 40);
Draws "GAME OVER" in red, centered on screen
text("High Score: " + highScore, width / 2, height / 2 + 55);
Displays the high score below the current score

drawSpiderman(cx, cy, size)

drawSpiderman() is a parametric sprite function. The key innovation is the size parameter and the u = size / 100 line—this scales every drawing coordinate proportionally, so the entire character grows or shrinks while maintaining proportions. The spider logo uses trigonometry: cos(a) * radius gives x-coordinates around a circle, sin(a) * radius gives y-coordinates. This is how procedural graphics create complex shapes from math. Notice the use of bezierVertex() for the eyes—bezier curves are crucial for smooth organic shapes in p5.js.

🔬 This loop draws the web lines on the head. It starts at 0°, loops to 360° (TWO_PI), incrementing by 30° (PI / 6) each time, drawing 12 lines total. What happens if you change PI / 6 to PI / 4 (45°)? You'd get 8 lines instead. What if you change it to PI / 12 (15°)? You'd get 24 lines for a denser web.

  for (let a = 0; a < TWO_PI; a += PI / 6) {
    line(0, headCenterY, cos(a) * 32 * u, headCenterY + sin(a) * 37 * u);
  }
function drawSpiderman(cx, cy, size) {
  let u = size / 100;
  
  push();
  translate(cx, cy);

  // --- ARMS (Reaching up) ---
  noStroke();
  fill(30, 60, 160); // Classic Blue
  rect(-35 * u, -50 * u, 16 * u, 60 * u, 8 * u);
  rect(19 * u, -50 * u, 16 * u, 60 * u, 8 * u); 

  // Gloves
  fill(210, 30, 40); // Classic Red
  rect(-37 * u, -65 * u, 20 * u, 30 * u, 10 * u);
  rect(17 * u, -65 * u, 20 * u, 30 * u, 10 * u);

  // --- LEGS ---
  fill(30, 60, 160); 
  rect(-22 * u, 30 * u, 20 * u, 40 * u, 10 * u);
  rect(2 * u, 30 * u, 20 * u, 40 * u, 10 * u);

  // Boots
  fill(210, 30, 40); 
  rect(-24 * u, 60 * u, 24 * u, 35 * u, 8 * u);
  rect(0, 60 * u, 24 * u, 35 * u, 8 * u);

  // --- TORSO ---
  fill(30, 60, 160); 
  rect(-25 * u, -10 * u, 50 * u, 55 * u, 15 * u);

  // Red chest piece
  fill(210, 30, 40);
  beginShape();
  vertex(-25 * u, -10 * u);
  vertex(25 * u, -10 * u);
  vertex(15 * u, 45 * u);
  vertex(-15 * u, 45 * u);
  endShape(CLOSE);

  // Red belt
  rect(-18 * u, 40 * u, 36 * u, 10 * u, 5 * u);

  // Spider Logo on chest
  fill(10);
  ellipse(0, 10 * u, 12 * u, 18 * u);
  stroke(10);
  strokeWeight(1.5 * u);
  line(-5 * u, 5 * u, -15 * u, -5 * u);
  line(5 * u, 5 * u, 15 * u, -5 * u);
  line(-5 * u, 10 * u, -18 * u, 5 * u);
  line(5 * u, 10 * u, 18 * u, 5 * u);
  line(-5 * u, 15 * u, -18 * u, 20 * u);
  line(5 * u, 15 * u, 18 * u, 20 * u);
  line(-5 * u, 18 * u, -15 * u, 28 * u);
  line(5 * u, 18 * u, 15 * u, 28 * u);
  noStroke();

  // --- HEAD ---
  let headCenterY = -35 * u;
  fill(210, 30, 40);
  ellipse(0, headCenterY, 65 * u, 75 * u);

  // Head Webbing Pattern
  stroke(0, 80); 
  strokeWeight(1 * u);
  noFill();
  for (let a = 0; a < TWO_PI; a += PI / 6) {
    line(0, headCenterY, cos(a) * 32 * u, headCenterY + sin(a) * 37 * u);
  }
  for (let r = 0.4; r <= 1; r += 0.3) {
    ellipse(0, headCenterY, 65 * u * r, 75 * u * r);
  }

  // --- EYES ---
  fill(10); 
  stroke(10);
  strokeWeight(1.5 * u);
  beginShape();
  vertex(-6 * u, headCenterY + 5 * u);
  bezierVertex(-15 * u, headCenterY - 15 * u, -32 * u, headCenterY - 10 * u, -30 * u, headCenterY + 5 * u);
  bezierVertex(-20 * u, headCenterY + 15 * u, -10 * u, headCenterY + 15 * u, -6 * u, headCenterY + 5 * u);
  endShape(CLOSE);

  fill(255); 
  noStroke();
  beginShape();
  vertex(-8 * u, headCenterY + 5 * u);
  bezierVertex(-15 * u, headCenterY - 10 * u, -27 * u, headCenterY - 6 * u, -26 * u, headCenterY + 4 * u);
  bezierVertex(-18 * u, headCenterY + 11 * u, -11 * u, headCenterY + 11 * u, -8 * u, headCenterY + 5 * u);
  endShape(CLOSE);

  fill(10); 
  stroke(10);
  strokeWeight(1.5 * u);
  beginShape();
  vertex(6 * u, headCenterY + 5 * u);
  bezierVertex(15 * u, headCenterY - 15 * u, 32 * u, headCenterY - 10 * u, 30 * u, headCenterY + 5 * u);
  bezierVertex(20 * u, headCenterY + 15 * u, 10 * u, headCenterY + 15 * u, 6 * u, headCenterY + 5 * u);
  endShape(CLOSE);

  fill(255);
  noStroke();
  beginShape();
  vertex(8 * u, headCenterY + 5 * u);
  bezierVertex(15 * u, headCenterY - 10 * u, 27 * u, headCenterY - 6 * u, 26 * u, headCenterY + 4 * u);
  bezierVertex(18 * u, headCenterY + 11 * u, 11 * u, headCenterY + 11 * u, 8 * u, headCenterY + 5 * u);
  endShape(CLOSE);

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

🔧 Subcomponents:

calculation Size Scaling let u = size / 100;

Converts the size parameter into a unit scale factor—all coordinates multiply by u to scale proportionally

let u = size / 100;
Divides the size parameter by 100 to create a scale multiplier. If size is 100, u = 1 (normal scale). If size is 50, u = 0.5 (half size). Every coordinate in the drawing gets multiplied by u
push();
Saves the drawing state before applying transformations
translate(cx, cy);
Moves the drawing origin to (cx, cy), so all drawing coordinates are relative to this point
fill(30, 60, 160); // Classic Blue
Sets fill color to Spider-Man's classic blue
rect(-35 * u, -50 * u, 16 * u, 60 * u, 8 * u);
Draws the left arm as a rounded rectangle. The 8 * u creates rounded corners. All coordinates scale by u
ellipse(0, headCenterY, 65 * u, 75 * u);
Draws the head as an ellipse centered at headCenterY with width 65 * u and height 75 * u
for (let a = 0; a < TWO_PI; a += PI / 6) {
Loops from 0 to TWO_PI (360°) in steps of PI/6 (30°), creating 12 iterations for a web pattern
line(0, headCenterY, cos(a) * 32 * u, headCenterY + sin(a) * 37 * u);
For each angle a, draws a line from the center (0, headCenterY) to a point on a circle. cos() and sin() calculate the circular coordinates, creating radial web spokes
for (let r = 0.4; r <= 1; r += 0.3) {
Loops through scale factors 0.4, 0.7, and 1.0 to draw concentric ellipses, completing the web pattern
beginShape();
Starts defining a custom polygon shape
bezierVertex(...);
Uses bezier curves to draw smooth eye shapes. bezierVertex() creates curves between vertex points for expressive eyes
endShape(CLOSE);
Closes the shape, connecting the last vertex back to the first
pop();
Restores the drawing state to what it was before push()

📦 Key Variables

gameState string

Tracks the current game mode: 'START' (menu), 'PLAY' (active), or 'GAMEOVER' (end screen). Determines which logic runs and what displays

let gameState = "START";
score number

Current game score. Increments every 10 frames during PLAY state. Resets on game restart

let score = 0;
highScore number

Best score achieved so far. Updates when a game's score exceeds it. Persists across game restarts

let highScore = 0;
gameSpeed number

Pixels per frame that buildings scroll. Starts at 5, increases by 0.2 every 100 points to increase difficulty

let gameSpeed = 5;
playerX number

Spider-Man's horizontal position on the canvas. Stays constant (200) throughout the game—only the world scrolls

let playerX = 200;
playerY number

Spider-Man's vertical position on the canvas. Changes based on velocity (vy) and gravity/lift forces

let playerY = 400;
vy number

Vertical velocity—how many pixels Spider-Man moves per frame. Positive = downward, negative = upward. Modified by gravity, lift, and air friction

let vy = 0;
gravity number

Downward acceleration applied when not swinging. Added to vy each frame to simulate falling

let gravity = 0.5;
lift number

Upward acceleration applied when swinging. Added to vy each frame (negative value means upward)

let lift = -1.2;
isSwinging boolean

True when the player holds down mouse/SPACE (actively swinging). False when released (falling under gravity)

let isSwinging = false;
webAnchorX number

X-coordinate of where the web attaches above. Used to draw the web line visually and position relative to Spider-Man

let webAnchorX = 0;
webAnchorY number

Y-coordinate of where the web attaches. Set to -50 (above screen) when swinging starts

let webAnchorY = 0;
playerRadius number

Radius of Spider-Man's collision circle in pixels. Used in collision detection against buildings

let playerRadius = 25;
bgBuildings array

Array of background building objects. Each has properties: x (position), w (width), h (height). Scrolls slower for parallax depth

let bgBuildings = [];
fgBuildings array

Array of foreground building objects. Each has: x (position), w (width), h (height), windows (array of window objects). Used for collisions and rendering

let fgBuildings = [];
nextFgX number

The x-position where the next foreground building will be created. Increments as buildings are generated

let nextFgX = 0;
nextBgX number

The x-position where the next background building will be created. Starts at -50 (off-screen)

let nextBgX = 0;
stars array

Array of star objects for the night sky. Each star has x, y, and r (radius) properties. Generated once in setup(), never changes

let stars = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateGameLogic() collision detection

Collision detection only checks vertical overlap on the building's top surface (roofline). If Spider-Man moves sideways through a building, no collision triggers unless they're at roof height.

💡 Add proper circle-rectangle collision: check if the circle's center is within building bounds OR if the circle overlaps any edge of the rectangle. This prevents phase-through bugs.

PERFORMANCE drawCity() window rendering

Windows are pre-calculated once during building creation and stored, which is good. However, if high-score games generate thousands of buildings, the fgBuildings array grows indefinitely.

💡 Add a building pool or limit: when buildings scroll off-screen (fgBuildings.shift()), recycle them into a pool instead of discarding. This prevents memory bloat in very long play sessions.

STYLE handleInputStart() and handleInputStop()

These functions are separate handlers called from four different input events (mousePressed, mouseReleased, keyPressed, keyReleased). Code duplication could cause bugs if one handler isn't updated.

💡 Consolidate into a single input handler that branches on key vs. mouse, or use a more abstract input system that calls the same logic regardless of input method.

FEATURE Game mechanics

Spider-Man always appears in the center horizontally (playerX = 200). The game lacks horizontal movement controls, making it feel more limited.

💡 Add left/right arrow key controls to change playerX. This would add skill depth: players must navigate horizontally through gaps while managing vertical swinging.

FEATURE Visual feedback

When you hit a building, the game just ends. No impact animation, screen shake, or visual feedback to signal the crash.

💡 Add a brief red flash, screen shake, or particle effect when hitting a building. This improves the feel and makes failure less abrupt.

BUG resetGame() building generation

Buildings are pre-generated with while loops in resetGame(), but if the canvas size changes (responsive design), these loops might not generate enough buildings.

💡 Use canvas dimensions directly in the loop conditions: while (nextFgX < width + 200) instead of hard-coded values. This makes the game adaptable to different screen sizes.

🔄 Code Flow

Code flow showing setup, draw, updategamelogic, handleinputstart, handleinputstop, resetgame, triggergameover, addbgbuilding, addfgbuilding, drawcity, drawplayer, drawui, drawspiderman

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> star-generation[Star Generation Loop] draw --> background-render[Star Rendering Loop] draw --> game-state-update[Game State Logic] draw --> player-rendering[Player Render Gate] draw --> score-increment[Score Increment] draw --> difficulty-scaling[Difficulty Progression] draw --> gravity-physics[Physics System] draw --> velocity-damping[Air Friction] draw --> velocity-constraint[Terminal Velocity Clamp] draw --> position-update[Position Update] draw --> screen-constraint[Top Screen Boundary] draw --> bottom-gameover[Bottom Boundary Game Over] draw --> bg-building-scroll[Background Building Parallax] draw --> bg-building-recycle[Background Building Recycling] draw --> fg-building-collision[Foreground Building Processing] draw --> web-render[Web Line Rendering] draw --> drawcity[drawCity] draw --> drawplayer[drawPlayer] draw --> drawui[drawUI] click setup href "#fn-setup" click draw href "#fn-draw" click star-generation href "#sub-star-generation" click background-render href "#sub-background-render" click game-state-update href "#sub-game-state-update" click player-rendering href "#sub-player-rendering" click score-increment href "#sub-score-increment" click difficulty-scaling href "#sub-difficulty-scaling" click gravity-physics href "#sub-gravity-physics" click velocity-damping href "#sub-velocity-damping" click velocity-constraint href "#sub-velocity-constraint" click position-update href "#sub-position-update" click screen-constraint href "#sub-screen-constraint" click bottom-gameover href "#sub-bottom-gameover" click bg-building-scroll href "#sub-bg-building-scroll" click bg-building-recycle href "#sub-bg-building-recycle" click fg-building-collision href "#sub-fg-building-collision" click web-render href "#sub-web-render" click drawcity href "#fn-drawcity" click drawplayer href "#fn-drawplayer" click drawui href "#fn-drawui"

❓ Frequently Asked Questions

What visual experience does the 'Spider Man Find Home' p5.js sketch create?

The sketch visually simulates a nighttime cityscape with dynamic elements like stars, a moon, and animated buildings, featuring Spider-Man swinging through the environment.

How can users interact with the 'Spider Man Find Home' sketch?

Users can interact by clicking the mouse or pressing the space bar or up arrow to start the game, initiate swinging, and control Spider-Man's movements.

What creative coding techniques are demonstrated in the p5.js Spider Man sketch?

The sketch showcases techniques such as dynamic game state management, object-oriented programming for game elements, and real-time rendering for interactive graphics.

Preview

spider man find home p5ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of spider man find home p5ai - Code flow showing setup, draw, updategamelogic, handleinputstart, handleinputstop, resetgame, triggergameover, addbgbuilding, addfgbuilding, drawcity, drawplayer, drawui, drawspiderman
Code Flow Diagram