parcore man

This sketch creates an interactive Spider-Man parkour game powered by Matter.js physics engine. Players control Spider-Man to jump across procedurally generated buildings in an endless city, with a camera that follows the character and collision detection that determines when jumping is allowed.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make Spider-Man fly by reducing gravity — Lowering gravity makes the world feel weightless and Spider-Man's jumps become insanely high—experiment with the gravity constant to find the sweet spot for your preferred difficulty
  2. Make the game brutally hard with massive gaps — Increasing the maximum gap between buildings forces Spider-Man to make impossible-looking jumps across the city
  3. Change Spider-Man's suit color to purple — Swap the red suit for purple by changing the dark red RGB value to magenta—a visual change that's immediately satisfying
  4. Make Spider-Man feel heavier with higher gravity — Increasing gravity makes jumps shorter and falling faster, creating a more intense, weighty feeling platformer
  5. Add a debug text overlay showing how many buildings are loaded — Uncomment the debug lines at the bottom of draw() to see real-time stats about player position and game state
  6. Make Spider-Man move twice as fast — Doubling playerSpeed creates a much snappier, more responsive feel as you race across the rooftops
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable Spider-Man parkour game where you control Tobey Maguire's iconic red and blue suit jumping across randomly generated city buildings. The sketch combines five major p5.js and Matter.js techniques: a physics engine that simulates gravity and collisions, procedural building generation that creates an endless level, camera tracking that follows the player, collision event listeners that detect when Spider-Man is grounded, and input handling that reads arrow keys and spacebar. The result feels like a real platformer game with satisfying physics-based movement.

The code is organized into a setup() function that initializes Matter.js and generates the first set of buildings, a draw() function that updates physics every frame and renders everything with a camera offset, helper functions that draw bodies and manage buildings, and event listeners that track collisions. By studying this sketch you will learn how to integrate an external physics engine with p5.js, how to implement procedural level generation, and how to build a game loop that feels responsive and fun.

⚙️ How It Works

  1. When the sketch loads, setup() creates a Matter.js physics engine and world, spawns Spider-Man as a physics body 100 pixels from the left, creates a static ground at the bottom, and generates 10 random buildings ahead of the player. It also registers collision event listeners that set the isGrounded flag when Spider-Man lands on a building or the ground.
  2. Every frame, draw() calls Engine.update() to step the physics simulation forward, then moves the camera to follow Spider-Man horizontally while keeping him slightly left of center for better forward visibility.
  3. The sketch checks if new buildings need to be generated when the camera approaches the rightmost building, and removes buildings that scroll off the left edge of the screen to maintain performance.
  4. Player input is handled continuously: holding left or right arrow applies horizontal velocity, and holding neither gradually decelerates the character. Pressing spacebar applies upward velocity only if isGrounded is true, preventing mid-air jumping.
  5. Collision events update the isGrounded flag: collisionStart sets it to true when Spider-Man lands on a static body from above, and collisionEnd sets it to false only if no other static body is beneath him (allowing smooth ground transitions).
  6. Finally, drawSpiderMan() renders the character at his physics position with rotation, drawBody() renders each building and the ground with their correct physics angles, and the camera translation makes everything scroll smoothly as you move forward through the level.

🎓 Concepts You'll Learn

Physics engine integrationCollision detection and eventsProcedural level generationCamera tracking and translationInput handling with keyIsDownStatic and dynamic bodiesFrame-based animation loop

📝 Code Breakdown

setup()

setup() runs once when the sketch starts and is responsible for all initialization. Notice how it both creates physics objects AND registers event listeners—this pattern is essential for games. The collision detection here is surprisingly subtle: collisionStart tells us when contact begins, but collisionEnd needs to re-check all pairs because Spider-Man might transition from one building to another without ever leaving the air. This is what makes the grounded detection robust.

🔬 This loop creates 10 buildings at startup. What happens if you change initialBuildingCount to 20 in the constants at the top and then change this loop to use 20 instead of initialBuildingCount?

  // 4. Generate initial buildings for the city
  for (let i = 0; i < initialBuildingCount; i++) {
    addBuilding(); // Call our helper function to add a building
  }

🔬 This condition checks 'playerBody.position.y < otherBody.position.y' to verify Spider-Man is above the surface. What happens if you remove that check? Can you jump while touching the side of a building?

      if (bodyA === playerBody || bodyB === playerBody) {
        // The other body in the collision
        const otherBody = (bodyA === playerBody) ? bodyB : bodyA;
        // Check if the other body is static (ground or a building)
        // AND if Spider-Man is positioned above the other body (meaning he's landing on it)
        if (otherBody.isStatic && playerBody.position.y < otherBody.position.y) {
          isGrounded = true; // Set grounded flag to true
        }
      }
function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // 1. Setup Matter.js engine and world
  engine = Engine.create();
  world = engine.world;
  world.gravity.y = gravity; // Set Matter.js gravity

  // 2. Create Spider-Man's physics body
  playerBody = Bodies.rectangle(
    100, // Initial X position
    height - playerSize - 100, // Initial Y position (above ground)
    playerSize, // Width
    playerSize * 1.5, // Height (make him taller than wide)
    {
      frictionAir: 0.05, // Air resistance
      friction: 0.1,     // Surface friction
      restitution: 0.1,  // How much he bounces
      density: 0.001,    // Make him light enough to jump well
      label: 'player'    // Give it a label for collision detection
    }
  );
  World.add(world, playerBody); // Add Spider-Man to the physics world

  // 3. Create the ground physics body
  // It's static, so it doesn't move or fall.
  ground = Bodies.rectangle(width / 2, height, width * 2, 20, {
    isStatic: true, // Make it unmovable
    friction: 0.8,  // High friction to prevent sliding
    label: 'ground' // Label for collision detection
  });
  World.add(world, ground);

  // 4. Generate initial buildings for the city
  for (let i = 0; i < initialBuildingCount; i++) {
    addBuilding(); // Call our helper function to add a building
  }

  // 5. Setup collision event listeners to detect if Spider-Man is grounded
  // 'collisionStart' fires when two bodies begin colliding
  Events.on(engine, 'collisionStart', function(event) {
    const pairs = event.pairs; // Get all collision pairs
    for (let i = 0; i < pairs.length; i++) {
      const pair = pairs[i];
      let bodyA = pair.bodyA;
      let bodyB = pair.bodyB;

      // Check if one of the bodies in the pair is our player
      if (bodyA === playerBody || bodyB === playerBody) {
        // The other body in the collision
        const otherBody = (bodyA === playerBody) ? bodyB : bodyA;
        // Check if the other body is static (ground or a building)
        // AND if Spider-Man is positioned above the other body (meaning he's landing on it)
        if (otherBody.isStatic && playerBody.position.y < otherBody.position.y) {
          isGrounded = true; // Set grounded flag to true
        }
      }
    }
  });

  // 'collisionEnd' fires when two bodies stop colliding
  Events.on(engine, 'collisionEnd', function(event) {
    const pairs = event.pairs;
    let currentlyGrounded = false; // Assume not grounded initially

    // Re-check all current collisions involving the player
    // This is necessary because 'collisionEnd' only tells us what STOPPED colliding.
    // The player might still be colliding with another static body.
    for (let i = 0; i < pairs.length; i++) {
      const pair = pairs[i];
      let bodyA = pair.bodyA;
      let bodyB = pair.bodyB;

      if (bodyA === playerBody || bodyB === playerBody) {
        const otherBody = (bodyA === playerBody) ? bodyB : bodyA;
        if (otherBody.isStatic && playerBody.position.y < otherBody.position.y) {
          currentlyGrounded = true; // Found a collision, player is still grounded
          break; // No need to check further pairs
        }
      }
    }
    // If after checking all current collisions, no static body is found below the player,
    // then the player is truly no longer grounded.
    if (!currentlyGrounded) {
      isGrounded = false;
    }
  });
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Physics Engine Creation engine = Engine.create();

Creates a new Matter.js physics engine that will simulate gravity, velocity, and collisions

calculation Spider-Man Body Creation playerBody = Bodies.rectangle(100, height - playerSize - 100, playerSize, playerSize * 1.5, {...});

Creates a rectangular physics body for Spider-Man with properties that affect how he moves and interacts with the world

calculation Ground Creation ground = Bodies.rectangle(width / 2, height, width * 2, 20, {isStatic: true, ...});

Creates a static (unmovable) rectangular physics body at the bottom of the canvas that Spider-Man can land on

for-loop Initial Building Generation Loop for (let i = 0; i < initialBuildingCount; i++) { addBuilding(); }

Generates 10 random buildings ahead of the starting position to create the initial level

conditional Collision Start Event Handler Events.on(engine, 'collisionStart', function(event) {...});

Registers a listener that fires when Spider-Man touches a building or ground, setting isGrounded to true if he lands from above

conditional Collision End Event Handler Events.on(engine, 'collisionEnd', function(event) {...});

Registers a listener that fires when Spider-Man leaves a surface, carefully checking if he's still touching something else before setting isGrounded to false

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the game responsive to window size
engine = Engine.create();
Creates a Matter.js physics engine that will handle all physics calculations like gravity and collisions
world = engine.world;
Gets a reference to the physics world contained in the engine, where all physics bodies live
world.gravity.y = gravity;
Sets the downward gravitational acceleration in the physics world using our gravity constant
playerBody = Bodies.rectangle(100, height - playerSize - 100, playerSize, playerSize * 1.5, {...});
Creates a rectangular physics body 100 pixels from the left and 100 pixels above the ground, sized to Spider-Man's proportions
World.add(world, playerBody);
Registers the Spider-Man physics body with the world so Matter.js will simulate its movement and collisions
ground = Bodies.rectangle(width / 2, height, width * 2, 20, {isStatic: true, ...});
Creates a wide, static ground rectangle that won't fall or move no matter what hits it
for (let i = 0; i < initialBuildingCount; i++) { addBuilding(); }
Loops 10 times, calling addBuilding() each time to generate the starting set of buildings
Events.on(engine, 'collisionStart', function(event) {...});
Registers a function that runs every time two physics bodies start touching, checking if Spider-Man landed on something static from above
if (otherBody.isStatic && playerBody.position.y < otherBody.position.y) { isGrounded = true; }
Sets isGrounded to true only if the object Spider-Man hit is static (not moving) AND Spider-Man's center is above the object's center (meaning he landed on top)
Events.on(engine, 'collisionEnd', function(event) {...});
Registers a function that runs when two physics bodies stop touching, but only sets isGrounded to false if Spider-Man has no other static surfaces beneath him

draw()

draw() is the core game loop, running 60 times per second. Notice the structure: update physics, clear the screen, check conditions, apply input, draw everything. This order is critical—if you moved the input handling before Engine.update(), the player would feel one frame behind. The camera system here is elegant: by translating the canvas, we can use world coordinates everywhere in our helper functions while keeping the UI in screen space with the pop()/push() pattern.

🔬 This loop deletes buildings that are 100 pixels off the left edge of the camera. What happens if you change the -100 to -500? Buildings will stay around longer—does the game still feel smooth?

  // 4. Remove off-screen buildings to maintain performance
  // Iterate backwards to safely remove elements
  for (let i = buildings.length - 1; i >= 0; i--) {
    // If the building's right edge is far off-screen to the left
    if (buildings[i].position.x + buildings[i].width / 2 < cameraX - 100) {
      World.remove(world, buildings[i]); // Remove from physics world
      buildings.splice(i, 1);             // Remove from our array
    }
  }

🔬 This code sets velocity by directly overwriting it—no gradual acceleration. What if you change 'x: -playerSpeed' to 'x: playerBody.velocity.x - 0.3' to make movement feel more weighty and realistic?

  if (keyIsDown(LEFT_ARROW)) {
    // Apply horizontal velocity to the left
    Body.setVelocity(playerBody, { x: -playerSpeed, y: playerBody.velocity.y });
  } else if (keyIsDown(RIGHT_ARROW)) {
    // Apply horizontal velocity to the right
    Body.setVelocity(playerBody, { x: playerSpeed, y: playerBody.velocity.y });
  } else {
    // If no horizontal key is pressed, gradually reduce horizontal velocity
    Body.setVelocity(playerBody, { x: playerBody.velocity.x * 0.9, y: playerBody.velocity.y });
  }
function draw() {
  // 1. Update the Matter.js physics engine
  Engine.update(engine);

  background(220); // Light gray background for the sky

  // 2. Camera follows Spider-Man horizontally
  // Keep Spider-Man a bit to the left of the center for better forward visibility
  cameraX = playerBody.position.x - width / 4;

  // 3. Generate new buildings if the camera is approaching the end of the existing ones
  if (cameraX + width > lastBuildingX - buildingGenerationThreshold) {
    addBuilding();
  }

  // 4. Remove off-screen buildings to maintain performance
  // Iterate backwards to safely remove elements
  for (let i = buildings.length - 1; i >= 0; i--) {
    // If the building's right edge is far off-screen to the left
    if (buildings[i].position.x + buildings[i].width / 2 < cameraX - 100) {
      World.remove(world, buildings[i]); // Remove from physics world
      buildings.splice(i, 1);             // Remove from our array
    }
  }

  // Apply camera translation to make everything appear to move
  push();
  translate(-cameraX, 0);

  // 5. Draw the ground (only the visible segment)
  drawGroundSegment();

  // 6. Draw all buildings
  for (let body of buildings) {
    drawBody(body, 150); // Gray color for buildings
  }

  // 7. Handle player input for movement
  if (keyIsDown(LEFT_ARROW)) {
    // Apply horizontal velocity to the left
    Body.setVelocity(playerBody, { x: -playerSpeed, y: playerBody.velocity.y });
  } else if (keyIsDown(RIGHT_ARROW)) {
    // Apply horizontal velocity to the right
    Body.setVelocity(playerBody, { x: playerSpeed, y: playerBody.velocity.y });
  } else {
    // If no horizontal key is pressed, gradually reduce horizontal velocity
    Body.setVelocity(playerBody, { x: playerBody.velocity.x * 0.9, y: playerBody.velocity.y });
  }

  // 8. Draw Spider-Man
  drawSpiderMan();

  pop(); // End camera translation

  // 9. Display simple UI (instructions, debug info)
  fill(0);
  noStroke();
  textSize(16);
  text('Use Left/Right arrows to move, Space to jump', cameraX + 10, 30);
  // Optional debug info:
  // text('Player X: ' + nfc(playerBody.position.x, 0), cameraX + 10, 50);
  // text('Is Grounded: ' + isGrounded, cameraX + 10, 70);
  // text('Building Count: ' + buildings.length, cameraX + 10, 90);
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Physics Engine Update Engine.update(engine);

Steps the Matter.js physics simulation forward by one frame, updating all bodies' positions and velocities based on forces and collisions

calculation Camera Position Update cameraX = playerBody.position.x - width / 4;

Calculates where the camera should be so Spider-Man stays roughly one-quarter of the way across the screen, allowing him to look ahead

conditional Building Generation Trigger if (cameraX + width > lastBuildingX - buildingGenerationThreshold) { addBuilding(); }

Spawns a new building when the camera gets close enough to the end of the level to prevent Spider-Man from running out of platforms

for-loop Off-Screen Building Removal for (let i = buildings.length - 1; i >= 0; i--) { if (buildings[i].position.x + buildings[i].width / 2 < cameraX - 100) { World.remove(world, buildings[i]); buildings.splice(i, 1); } }

Removes buildings that have scrolled off-screen to the left, freeing memory and maintaining performance

calculation Camera Translation translate(-cameraX, 0);

Shifts all drawing coordinates by negative cameraX, making the world appear to scroll while the canvas stays still

conditional Keyboard Input Handler if (keyIsDown(LEFT_ARROW)) {...} else if (keyIsDown(RIGHT_ARROW)) {...} else {...}

Checks which arrow keys are held down and applies appropriate horizontal velocities to Spider-Man, or decelerates him if no key is pressed

Engine.update(engine);
This single line runs the entire physics simulation for this frame—Matter.js updates all body positions, applies gravity, resolves collisions, and fires collision events
background(220);
Clears the canvas with a light gray color, erasing everything drawn the previous frame so we get a fresh slate to draw on
cameraX = playerBody.position.x - width / 4;
Positions the camera so Spider-Man is always about one-quarter of the way from the left edge, giving him room to see ahead
if (cameraX + width > lastBuildingX - buildingGenerationThreshold) {
Checks if the right edge of the visible screen is getting close to the last building—if so, spawn a new one before Spider-Man runs out of platform
for (let i = buildings.length - 1; i >= 0; i--) {
Loops backwards through the buildings array (from last to first) so we can safely remove elements without skipping any
if (buildings[i].position.x + buildings[i].width / 2 < cameraX - 100) {
Checks if a building's right edge is more than 100 pixels off-screen to the left—if so, it's far enough away that we can delete it
World.remove(world, buildings[i]);
Removes this building's physics body from the Matter.js world so it no longer affects physics or collisions
buildings.splice(i, 1);
Removes this building from our JavaScript array so we stop trying to draw or manage it
push();
Saves the current drawing state (like transformation matrices) onto a stack so we can restore it later
translate(-cameraX, 0);
Shifts all subsequent drawing by negative cameraX pixels, making the world appear to scroll left as the camera moves right
if (keyIsDown(LEFT_ARROW)) { Body.setVelocity(playerBody, { x: -playerSpeed, y: playerBody.velocity.y }); }
If the left arrow is held down, sets Spider-Man's horizontal velocity to negative (left), keeping his vertical velocity unchanged
else if (keyIsDown(RIGHT_ARROW)) { Body.setVelocity(playerBody, { x: playerSpeed, y: playerBody.velocity.y }); }
If the right arrow is held down, sets Spider-Man's horizontal velocity to positive (right)
else { Body.setVelocity(playerBody, { x: playerBody.velocity.x * 0.9, y: playerBody.velocity.y }); }
If neither arrow is pressed, multiply horizontal velocity by 0.9 to gradually slow down Spider-Man without stopping him instantly
pop();
Restores the drawing state saved by push(), undoing the camera translation so UI elements draw at screen coordinates, not world coordinates
text('Use Left/Right arrows to move, Space to jump', cameraX + 10, 30);
Draws instruction text at a position offset by cameraX so it stays visible in the top-left corner as the camera moves

keyPressed()

keyPressed() is called once per key press (unlike keyIsDown which is called every frame). This is perfect for jump because you want one discrete action per button press. Notice how it reads isGrounded—this flag comes from collision detection, demonstrating how different parts of the game communicate. The preservation of x velocity in the jump means Spider-Man can jump while moving horizontally, realistic for a parkour game.

function keyPressed() {
  if (key === ' ' && isGrounded) { // Check for Space key and if Spider-Man is on the ground
    // Apply an upward velocity for jumping
    Body.setVelocity(playerBody, { x: playerBody.velocity.x, y: -jumpForce });
    isGrounded = false; // Spider-Man is no longer grounded after jumping
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Space Key and Grounded Check if (key === ' ' && isGrounded) {

Verifies that the player pressed spacebar AND is currently touching the ground, preventing mid-air jumps

calculation Jump Velocity Application Body.setVelocity(playerBody, { x: playerBody.velocity.x, y: -jumpForce });

Sets Spider-Man's upward velocity to negative jumpForce (upward in Matter.js where -y is up), preserving horizontal momentum

if (key === ' ' && isGrounded) {
Checks if the spacebar was pressed (key === ' ') AND if Spider-Man is currently touching a surface (isGrounded is true)
Body.setVelocity(playerBody, { x: playerBody.velocity.x, y: -jumpForce });
Applies upward velocity (negative because y increases downward in Matter.js) while preserving Spider-Man's current horizontal velocity so momentum carries forward
isGrounded = false;
Immediately sets isGrounded to false so the next jump can't be triggered until Spider-Man lands on something again

addBuilding()

This function demonstrates procedural generation—creating content algorithmically rather than by hand. Every building is placed after the last one with a gap, creating an endless level without repeating. The key insight is lastBuildingX: by tracking the rightmost edge, each new building knows where to spawn. This pattern (tracking state to generate the next element) is foundational to any endless game. Notice how the function reads and updates global state—Spider-Man's position and the building array—making it tightly integrated with the game's data structures.

🔬 These six constants define the difficulty of the game. What if you change minGap to 150 and maxGap to 250 so Spider-Man always faces huge jumps? Or change minGap to 20 and maxGap to 60 for rapid-fire tiny jumps?

  const minBuildingWidth = 100;
  const maxBuildingWidth = 300;
  const minBuildingHeight = 100; // Allow smaller buildings for vaults/jumps
  const maxBuildingHeight = height - 100;
  const minGap = 50;  // Minimum gap between buildings
  const maxGap = 150; // Maximum gap between buildings

🔬 This code randomly varies the width, height, and gap of each building. What happens if you remove the randomness from one line? Try 'const buildingHeight = 150' instead of the random call—buildings become predictable, but is the game more fun or less?

  const buildingWidth = random(minBuildingWidth, maxBuildingWidth);
  const buildingHeight = random(minBuildingHeight, maxBuildingHeight);
  // Calculate building's X position based on the last building and a random gap
  const buildingX = lastBuildingX + random(minGap, maxGap) + buildingWidth / 2;
function addBuilding() {
  const minBuildingWidth = 100;
  const maxBuildingWidth = 300;
  const minBuildingHeight = 100; // Allow smaller buildings for vaults/jumps
  const maxBuildingHeight = height - 100;
  const minGap = 50;  // Minimum gap between buildings
  const maxGap = 150; // Maximum gap between buildings

  const buildingWidth = random(minBuildingWidth, maxBuildingWidth);
  const buildingHeight = random(minBuildingHeight, maxBuildingHeight);
  // Calculate building's X position based on the last building and a random gap
  const buildingX = lastBuildingX + random(minGap, maxGap) + buildingWidth / 2;
  const buildingY = height - buildingHeight / 2; // Position building from the bottom

  // Create a static rectangular physics body for the building
  const buildingBody = Bodies.rectangle(buildingX, buildingY, buildingWidth, buildingHeight, {
    isStatic: true, // Make it unmovable
    friction: 0.8,  // High friction
    label: 'building' // Label for collision detection
  });
  World.add(world, buildingBody); // Add building to the physics world
  buildings.push(buildingBody);   // Store it in our array

  lastBuildingX = buildingX + buildingWidth / 2; // Update the rightmost edge tracker
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Building Dimension Constraints const minBuildingWidth = 100; const maxBuildingWidth = 300; const minBuildingHeight = 100; const maxBuildingHeight = height - 100; const minGap = 50; const maxGap = 150;

Defines the ranges for random building sizes and gaps, controlling the difficulty and visual variety of the level

calculation Random Building Generation const buildingWidth = random(minBuildingWidth, maxBuildingWidth); const buildingHeight = random(minBuildingHeight, maxBuildingHeight);

Generates random width and height within the constraints, ensuring each building is unique

calculation Building Positioning const buildingX = lastBuildingX + random(minGap, maxGap) + buildingWidth / 2; const buildingY = height - buildingHeight / 2;

Places the building horizontally after the last building with a random gap, and vertically from the bottom upward

calculation Physics Body Creation const buildingBody = Bodies.rectangle(buildingX, buildingY, buildingWidth, buildingHeight, {isStatic: true, friction: 0.8, label: 'building'});

Creates a static physics body with the randomized dimensions that Spider-Man can land on and interact with

const minBuildingWidth = 100;
Sets the smallest building width to 100 pixels, ensuring buildings are always big enough to land on
const maxBuildingWidth = 300;
Sets the largest building width to 300 pixels, preventing buildings from being unreasonably huge
const minBuildingHeight = 100;
Sets the smallest building height to 100 pixels, allowing for short platforms that create interesting jumps
const maxBuildingHeight = height - 100;
Sets the tallest building to almost the full canvas height, creating tall buildings that tower over Spider-Man
const minGap = 50;
Sets the smallest gap between buildings to 50 pixels, the closest Spider-Man will need to jump
const maxGap = 150;
Sets the largest gap between buildings to 150 pixels, the farthest Spider-Man will need to jump
const buildingWidth = random(minBuildingWidth, maxBuildingWidth);
Generates a random width for this specific building between 100 and 300 pixels
const buildingHeight = random(minBuildingHeight, maxBuildingHeight);
Generates a random height for this specific building between 100 and the canvas height
const buildingX = lastBuildingX + random(minGap, maxGap) + buildingWidth / 2;
Places the building's center at: the last building's right edge, plus a random gap, plus half this building's width
const buildingY = height - buildingHeight / 2;
Positions the building vertically so its bottom edge touches the ground (height of canvas minus half the building's height)
const buildingBody = Bodies.rectangle(buildingX, buildingY, buildingWidth, buildingHeight, {...});
Creates a static physics rectangle with the randomized dimensions so Matter.js can handle collisions
World.add(world, buildingBody);
Registers this building's physics body with the world so it participates in collisions and physics
buildings.push(buildingBody);
Stores a reference to this building in our array so we can draw it and remove it later
lastBuildingX = buildingX + buildingWidth / 2;
Updates the tracker of the rightmost building edge so the next building will be placed after this one

drawBody(body, colorVal)

drawBody() is a helper function that bridges Matter.js and p5.js. Matter.js tracks position and angle, but p5.js draws—this function connects them. The push/pop pattern is essential: it lets you transform the canvas without affecting later drawing. By translating to the body's position and rotating by its angle, you ensure the visual matches the physics perfectly, which is critical for a game.

function drawBody(body, colorVal) {
  fill(colorVal);
  noStroke(); // No outline for the body
  push();
  translate(body.position.x, body.position.y); // Move to the body's center
  rotate(body.angle);                         // Rotate to the body's angle
  rectMode(CENTER);                           // Draw rectangles from their center
  rect(0, 0, body.width, body.height);        // Draw the rectangle
  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Fill Color Setup fill(colorVal); noStroke();

Sets the fill color and removes the outline so the body appears solid

calculation Transform and Rotate push(); translate(body.position.x, body.position.y); rotate(body.angle); rectMode(CENTER);

Saves the drawing state, moves the origin to the body's center, rotates to match the physics body's angle, and switches to center-based rectangle drawing

fill(colorVal);
Sets the fill color to the value passed in (like 150 for buildings or 100 for ground)
noStroke();
Removes the outline stroke so shapes appear solid without a border
push();
Saves the current transformation matrix so we can safely translate and rotate without affecting other drawing
translate(body.position.x, body.position.y);
Moves the drawing origin to the physics body's center position in the world
rotate(body.angle);
Rotates the canvas to match the physics body's rotation angle, so the rectangle visually matches its physical rotation
rectMode(CENTER);
Sets rect() to draw from the center point rather than from the top-left corner, matching Matter.js's coordinate system
rect(0, 0, body.width, body.height);
Draws a rectangle at the origin (which is now the body's center) with the same width and height as the physics body
pop();
Restores the previous transformation matrix, undoing the translate and rotate so they don't affect subsequent drawing

drawGroundSegment()

drawGroundSegment() is a simple helper that optimizes drawing. Instead of drawing the entire Matter.js ground (which is huge), we only draw the visible segment. Notice it uses cameraX directly, not the translated coordinate system—this is because we're outside the push/pop camera block in draw(). This demonstrates the importance of understanding where you are in the coordinate system.

function drawGroundSegment() {
  fill(100); // Darker gray for the ground
  noStroke();
  rectMode(CORNER); // Draw rectangles from their top-left corner
  // Draw the ground from the current camera X position across the width of the canvas
  rect(cameraX, height - ground.height / 2, width, ground.height);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Ground Styling fill(100); noStroke();

Sets the ground to a darker gray color with no outline

calculation Ground Rectangle Drawing rect(cameraX, height - ground.height / 2, width, ground.height);

Draws a visible ground segment that scrolls with the camera from its current position across the full canvas width

fill(100);
Sets the fill color to a darker gray (100) to contrast visually with the sky background (220)
noStroke();
Removes the outline so the ground appears as a solid rectangle
rectMode(CORNER);
Switches to CORNER mode so the rect() arguments mean: top-left x, top-left y, width, height
rect(cameraX, height - ground.height / 2, width, ground.height);
Draws a rectangle starting at the camera's current X position (so it scrolls with the camera), positioned at the bottom of the canvas, with the same height as the physics ground body

drawSpiderMan()

drawSpiderMan() demonstrates how to compose complex visuals from simple shapes. Each rectangle and line is positioned relative to the character's center, then the entire group is translated and rotated together. This modular approach (each shape defined locally, then transformed as a whole) is how game artists build characters. The colors are hardcoded constants (like 180 for dark red), making them easy to tweak—you could extract these to named variables at the top for even better maintainability.

🔬 These lines draw the spider logo. What if you change the fill color from black (0) to red (255, 0, 0) or yellow (255, 255, 0)? Try it and see how it changes Spider-Man's appearance.

  // Spider logo (simplified black spider)
  fill(0); // Black color for the logo
  ellipse(0, -playerSize * 0.4, playerSize * 0.6, playerSize * 0.4); // Head/body of spider
  // Draw simplified spider legs
  line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.6);
  line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.6);
  line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.2);
  line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.2);
function drawSpiderMan() {
  push();
  translate(playerBody.position.x, playerBody.position.y); // Move to Spider-Man's center
  rotate(playerBody.angle);                                // Rotate based on physics body angle
  rectMode(CENTER);

  // Red body (Tobey Maguire suit color)
  fill(180, 0, 0); // Dark red
  rect(0, 0, playerSize, playerSize * 1.5); // Main torso

  // Blue arms/legs (simplified)
  fill(0, 0, 180); // Dark blue
  rect(0, playerSize * 0.5, playerSize, playerSize * 0.5); // Simplified legs
  rect(0, -playerSize * 0.5, playerSize, playerSize * 0.5); // Simplified arms (part of torso)

  // Spider logo (simplified black spider)
  fill(0); // Black color for the logo
  ellipse(0, -playerSize * 0.4, playerSize * 0.6, playerSize * 0.4); // Head/body of spider
  // Draw simplified spider legs
  line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.6);
  line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.6);
  line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.2);
  line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.2);

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

🔧 Subcomponents:

calculation Red Torso fill(180, 0, 0); rect(0, 0, playerSize, playerSize * 1.5);

Draws the main red body of Spider-Man's suit

calculation Blue Limbs fill(0, 0, 180); rect(0, playerSize * 0.5, playerSize, playerSize * 0.5); rect(0, -playerSize * 0.5, playerSize, playerSize * 0.5);

Draws simplified blue arms and legs above and below the torso

push();
Saves the drawing state before transforming
translate(playerBody.position.x, playerBody.position.y);
Moves the origin to Spider-Man's physics body center
rotate(playerBody.angle);
Rotates to match Spider-Man's physics angle, so he leans when jumping or falling
rectMode(CENTER);
Switches to center-based rectangle drawing to match the physics center
fill(180, 0, 0);
Sets the fill color to dark red (Tobey Maguire's suit color) for the main body
rect(0, 0, playerSize, playerSize * 1.5);
Draws the main torso rectangle at the center with height 1.5 times the width for a taller proportion
fill(0, 0, 180);
Switches fill color to dark blue for the arms and legs
rect(0, playerSize * 0.5, playerSize, playerSize * 0.5);
Draws simplified legs below the torso (at y = playerSize * 0.5)
rect(0, -playerSize * 0.5, playerSize, playerSize * 0.5);
Draws simplified arms above the torso (at y = -playerSize * 0.5)
fill(0);
Switches to black for the spider logo
ellipse(0, -playerSize * 0.4, playerSize * 0.6, playerSize * 0.4);
Draws a black ellipse for the spider's body on the chest area
line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.6);
Draws one of the spider's front legs (upper left)
line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.6);
Draws another front leg (upper right)
line(-playerSize * 0.2, -playerSize * 0.4, -playerSize * 0.4, -playerSize * 0.2);
Draws a back leg (lower left)
line(playerSize * 0.2, -playerSize * 0.4, playerSize * 0.4, -playerSize * 0.2);
Draws the final back leg (lower right)
pop();
Restores the drawing state, undoing the translate and rotate

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. This sketch uses it to keep the game playable at any window size. Notice the strategy: completely rebuild the ground and buildings rather than trying to rescale them. This is simpler and safer, though it does reset your position in the level. A more sophisticated game might preserve more state and smoothly rescale instead.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize the canvas to fit the new window size

  // When the window is resized, we need to adjust the ground and buildings.
  // For simplicity, we'll re-create the ground and reset/regenerate buildings.
  World.remove(world, ground); // Remove old ground
  ground = Bodies.rectangle(width / 2, height, width * 2, 20, { isStatic: true, friction: 0.8, label: 'ground' });
  World.add(world, ground); // Add new ground

  // Clear and regenerate buildings from the current camera position
  for (let body of buildings) {
    World.remove(world, body); // Remove old buildings from physics world
  }
  buildings = []; // Clear buildings array
  lastBuildingX = cameraX - 100; // Start generating slightly before the camera
  for (let i = 0; i < initialBuildingCount; i++) {
    addBuilding(); // Add new buildings
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Canvas Size Update resizeCanvas(windowWidth, windowHeight);

Resizes the p5.js canvas to match the new browser window dimensions

calculation Ground Reconstruction World.remove(world, ground); ground = Bodies.rectangle(...); World.add(world, ground);

Removes the old ground and creates a new one with dimensions matching the new canvas size

for-loop Buildings Clearing and Regeneration for (let body of buildings) { World.remove(world, body); } buildings = []; lastBuildingX = cameraX - 100; for (let i = 0; i < initialBuildingCount; i++) { addBuilding(); }

Removes all existing buildings from both the physics world and the array, then generates a fresh set of buildings

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window's new size when the user resizes their window
World.remove(world, ground);
Removes the old ground physics body from the Matter.js world so we can replace it
ground = Bodies.rectangle(width / 2, height, width * 2, 20, { isStatic: true, friction: 0.8, label: 'ground' });
Creates a new ground physics body using the updated width and height values
World.add(world, ground);
Adds the new ground body to the physics world
for (let body of buildings) { World.remove(world, body); }
Loops through all buildings and removes each from the physics world
buildings = [];
Clears the buildings array completely
lastBuildingX = cameraX - 100;
Resets the building generation tracker to just before the camera, so the next addBuilding() call generates buildings starting near Spider-Man
for (let i = 0; i < initialBuildingCount; i++) { addBuilding(); }
Generates a fresh set of 10 buildings using the new canvas dimensions

📦 Key Variables

engine Matter.Engine

The Matter.js physics engine instance that handles all physics calculations each frame

let engine; // Initialized in setup()
world Matter.World

The physics world contained in the engine where all physics bodies exist and interact

let world; // Set to engine.world in setup()
playerBody Matter.Body

The physics body representing Spider-Man, tracking his position, velocity, and angle

let playerBody; // Created as Bodies.rectangle(...) in setup()
buildings array of Matter.Body

Array storing all the building physics bodies currently in the level

let buildings = [];
ground Matter.Body

The static physics body representing the ground at the bottom of the canvas

let ground; // Created in setup()
isGrounded boolean

Flag that tracks whether Spider-Man is currently touching a static surface, allowing jumping when true

let isGrounded = false;
cameraX number

The horizontal position of the camera, used to translate the world so Spider-Man stays on screen

let cameraX = 0;
playerSize number

Base size constant (40 pixels) for Spider-Man's body, used to scale all his visual components

const playerSize = 40;
playerSpeed number

Horizontal movement speed (pixels per frame) applied when arrow keys are pressed

const playerSpeed = 5;
jumpForce number

Upward velocity (in negative pixels per second) applied when spacebar is pressed

const jumpForce = 10;
gravity number

Gravitational acceleration constant affecting how fast everything falls

const gravity = 1;
lastBuildingX number

Tracks the rightmost X position of the last generated building, used to position the next building

let lastBuildingX = 0;
initialBuildingCount number

Number of buildings to generate at startup and when the window resizes

const initialBuildingCount = 10;
buildingGenerationThreshold number

Distance in pixels before the end of level buildings that triggers generating new ones

const buildingGenerationThreshold = 500;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG collisionStart event handler

The condition 'playerBody.position.y < otherBody.position.y' prevents jumping off ceilings (e.g., overhanging buildings), but in a parkour game this might be intentional. However, it also prevents jumping immediately after landing on a sloped surface if the slope is steep.

💡 Consider using the collision normal vector from Matter.js instead of just comparing Y positions, which would be more robust for angled surfaces: check if the collision normal points roughly upward (like in Box2D examples).

PERFORMANCE windowResized()

The function clears and regenerates all buildings every time the window resizes, which could cause a noticeable stutter in fullscreen transitions or on slow devices.

💡 Instead of clearing buildings, reposition existing ones or only regenerate buildings in the forward direction. Alternatively, use 'throttle' to debounce resize events so they don't fire multiple times per second.

STYLE drawSpiderMan()

The Spider-Man character is drawn with hardcoded color values like 'fill(180, 0, 0)' scattered throughout the function, making it hard to change the suit colors globally.

💡 Extract color values to constants at the top of the sketch (like 'const SUIT_RED = color(180, 0, 0)') and reference them in drawSpiderMan(), making recoloring easy and the code more maintainable.

FEATURE keyPressed()

The game only allows jumping when isGrounded is true, but a real parkour game often allows mid-air double jumps or wall jumps for more skill expression.

💡 Add a counter for remaining jumps (starting at 1, reset when landing) and allow players to spend those jumps mid-air: 'if (jumpsRemaining > 0)' instead of just 'if (isGrounded)'. Award the player with visual/audio feedback on extra jumps.

BUG addBuilding()

Buildings are always anchored to the ground (buildingY = height - buildingHeight / 2), meaning tall buildings stretch upward rather than floating at random heights, reducing level variety.

💡 Change buildingY to 'const buildingY = random(height - maxBuildingHeight, height - minBuildingHeight)' to allow buildings to spawn at different vertical positions, creating more interesting parkour puzzles.

PERFORMANCE draw() buildings rendering loop

Every frame, the code iterates through ALL buildings to draw them, even if a building is far off-screen—with an endless game, this list could grow very large.

💡 The off-screen removal loop already handles this, but it currently removes buildings 100 pixels off-screen. Increase this threshold or add a distance check before drawing: 'if (body.position.x > cameraX - 200 && body.position.x < cameraX + width + 200)' to skip distant buildings entirely.

🔄 Code Flow

Code flow showing setup, draw, keyPressed, addBuilding, drawBody, drawGroundSegment, drawSpiderMan, windowResized

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

graph TD start[Start] --> setup[setup] setup --> engine-creation[Engine Creation] setup --> player-body-creation[Player Body Creation] setup --> ground-creation[Ground Creation] setup --> initial-buildings[Initial Building Generation Loop] setup --> collision-start-listener[Collision Start Event Handler] setup --> collision-end-listener[Collision End Event Handler] setup --> draw[draw loop] click setup href "#fn-setup" click engine-creation href "#sub-engine-creation" click player-body-creation href "#sub-player-body-creation" click ground-creation href "#sub-ground-creation" click initial-buildings href "#sub-initial-buildings" click collision-start-listener href "#sub-collision-start-listener" click collision-end-listener href "#sub-collision-end-listener" click draw href "#fn-draw" draw --> physics-update[Physics Engine Update] draw --> camera-tracking[Camera Position Update] draw --> building-generation-check[Building Generation Trigger] draw --> building-removal-loop[Off-Screen Building Removal] draw --> camera-translation[Camera Translation] draw --> input-handling[Keyboard Input Handler] draw --> drawGroundSegment[drawGroundSegment] draw --> drawBody[drawBody] draw --> drawSpiderMan[drawSpiderMan] click physics-update href "#sub-physics-update" click camera-tracking href "#sub-camera-tracking" click building-generation-check href "#sub-building-generation-check" click building-removal-loop href "#sub-building-removal-loop" click camera-translation href "#sub-camera-translation" click input-handling href "#sub-input-handling" click drawGroundSegment href "#fn-drawGroundSegment" click drawBody href "#fn-drawBody" click drawSpiderMan href "#fn-drawSpiderMan" input-handling --> space-check[Space Key and Grounded Check] input-handling --> jump-velocity[Jump Velocity Application] click space-check href "#sub-space-check" click jump-velocity href "#sub-jump-velocity" building-generation-check --> addBuilding[addBuilding] click addBuilding href "#fn-addBuilding" building-removal-loop --> buildings-clear-rebuild[Buildings Clearing and Regeneration] click buildings-clear-rebuild href "#sub-buildings-clear-rebuild" drawGroundSegment --> ground-fill[Ground Styling] drawGroundSegment --> ground-rect[Ground Rectangle Drawing] click ground-fill href "#sub-ground-fill" click ground-rect href "#sub-ground-rect" drawBody --> transform-stack[Transform and Rotate] drawBody --> fill-style[Fill Color Setup] click transform-stack href "#sub-transform-stack" click fill-style href "#sub-fill-style" drawSpiderMan --> torso[Red Torso] drawSpiderMan --> arms-legs[Blue Limbs] drawSpiderMan --> spider-logo[Spider Logo] click torso href "#sub-torso" click arms-legs href "#sub-arms-legs" click spider-logo href "#sub-spider-logo" windowResized[windowResized] --> canvas-resize[Canvas Size Update] windowResized --> ground-rebuild[Ground Reconstruction] click windowResized href "#fn-windowResized" click canvas-resize href "#sub-canvas-resize" click ground-rebuild href "#sub-ground-rebuild"

❓ Frequently Asked Questions

What visual elements does the 'parcore man' sketch create?

The 'parcore man' sketch visually represents a dynamic environment where a character resembling Spider-Man navigates through a cityscape filled with buildings, utilizing physics for realistic movement.

How can users interact with the 'parcore man' sketch?

Users can control the character's movement and jumping using keyboard inputs, allowing for an engaging experience as they explore the generated city.

What creative coding concepts are demonstrated in the 'parcore man' sketch?

This sketch showcases the use of the Matter.js physics engine for realistic body dynamics and procedural generation of buildings, illustrating concepts of physics simulation and game development.

Preview

parcore man - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of parcore man - Code flow showing setup, draw, keyPressed, addBuilding, drawBody, drawGroundSegment, drawSpiderMan, windowResized
Code Flow Diagram