falling lava survival

This sketch creates a physics-based survival game where players control a blue ball to dodge an endless stream of falling red rectangles. Using Matter.js for realistic physics, the player slides across the ground, jumps to avoid collisions, and earns points for each dodged object while racing against a 60-second timer.

🧪 Try This!

Experiment with the code by making these changes:

  1. Increase difficulty by spawning objects faster — Lower OBJECT_SPAWN_INTERVAL makes objects fall more frequently, ramping up the chaos and forcing the player to dodge more intensely.
  2. Paint the falling objects a different color — Change the RGB values in the fallingObject fill to create a new color scheme; try bright yellow (255, 255, 0) or cyan (0, 255, 255).
  3. Give the player a higher jump — Increase jumpForce so the player launches higher into the air, making big dodges possible.
  4. Make objects bounce higher — Increase the restitution value so falling objects bounce with more energy when they hit the player or ground, creating wilder collisions.
  5. Extend the game time — Increase the starting timer to play for longer before Game Over triggers.
  6. Reward more points per dodge — Increase the score awarded when each object falls off-screen, making high scores easier to achieve.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a playable survival game where you control a bouncy blue ball and dodge falling red shapes for 60 seconds. The game combines Matter.js physics simulation with p5.js rendering to create realistic collisions, bouncing, and sliding motion. What makes it compelling is the constant pressure: objects spawn every second, gravity pulls everything down, and a single collision ends the game—yet your ball's slippery physics make dodging chaotic and fun.

The code is organized into five core parts: setup() initializes the Matter.js physics engine and creates the player, ground, and collision detector; draw() runs the main game loop that updates physics, spawns objects, checks win conditions, and renders everything; handlePlayerMovement() translates keyboard and touch input into physics forces; spawnFallingObject() creates random rectangles from the top; and handleCollision() detects when the player touches a falling object and ends the game. By reading this, you'll learn how to pair a physics engine with game state, how to spawn entities on timers, and how to structure a complete playable game.

⚙️ How It Works

  1. When the sketch loads, setup() creates a p5.js canvas filling the window, initializes a Matter.js physics world with gravity, and adds three physics bodies: a circular player at the bottom center, a static rectangular ground, and a collision event listener.
  2. Every frame, draw() calculates deltaTime (milliseconds since last frame) to keep physics and timers frame-rate-independent, decrements the 60-second timer, and updates the Matter.js engine with that deltaTime.
  3. On a timer (every 1000 milliseconds), spawnFallingObject() creates a random-sized rectangle above the canvas with a physics body and adds it to both the Matter.js world and the fallingObjects array.
  4. Each frame, renderBody() draws the player (blue circle), ground (gray rectangle), and all falling objects (red rectangles) at their current physics positions and rotations.
  5. handlePlayerMovement() reads keyboard input (arrow keys or WASD) and applies physics forces to move the player left, right, or jump; it also caps horizontal velocity to prevent runaway speed and keeps the player on-screen.
  6. When a falling object exits the bottom of the screen, it's removed and the player gains 10 points. If the player touches a falling object, handleCollision() ends the game. When the timer reaches zero, gameState becomes 'gameOver' and the screen shows the final score with a restart prompt.
  7. Pressing Spacebar on the Game Over screen calls restartGame() to reset the score, timer, and all physics bodies, returning to gameplay.

🎓 Concepts You'll Learn

Matter.js physics engineCollision detectionGame state managementDelta time for frame-rate independencePhysics forces and velocitySpawn timers and object poolingKeyboard and touch inputCanvas rendering with transforms

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the Matter.js engine and creates the three key bodies: the player (which you control), the ground (which the player slides on), and a collision event listener (which detects when the player loses). Understanding this function teaches you how to initialize a physics world from scratch.

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

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

  // Create the player body
  const playerSize = 40;
  const playerX = width / 2;
  const playerY = height - playerSize; // Start just above the ground
  player = Matter.Bodies.circle(playerX, playerY, playerSize / 2, {
    restitution: 0.8, // Bounciness for chaotic collisions
    friction: 0.1,    // Low friction for slippery movement
    mass: 1,          // Player mass
    label: 'player'   // Custom label for collision detection
  });
  Matter.World.add(world, player);

  // Create the ground body
  ground = Matter.Bodies.rectangle(width / 2, height - 10, width, 20, {
    isStatic: true, // Ground doesn't move
    friction: 0.5,  // Some friction to allow player to move
    label: 'ground' // Custom label for collision detection
  });
  Matter.World.add(world, ground);

  // Set up collision event listener
  Matter.Events.on(engine, 'collisionStart', handleCollision);

  // Initialize lastFrameTime for deltaTime calculation
  lastFrameTime = millis();
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Physics Engine Initialization engine = Matter.Engine.create();

Creates the Matter.js physics engine that will simulate all body interactions

calculation Gravity Configuration world.gravity.y = 1; // Standard gravity

Sets the downward acceleration that pulls all bodies toward the ground

calculation Player Body Creation player = Matter.Bodies.circle(playerX, playerY, playerSize / 2, {

Creates a circular physics body for the player with bounciness and low friction

calculation Ground Body Creation ground = Matter.Bodies.rectangle(width / 2, height - 10, width, 20, {

Creates a static rectangular platform that the player lands on and slides across

calculation Collision Event Listener Matter.Events.on(engine, 'collisionStart', handleCollision);

Registers the handleCollision function to run whenever two bodies touch

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire browser window, making the game responsive
engine = Matter.Engine.create();
Creates a Matter.js physics engine that will simulate gravity, forces, and collisions
world = engine.world;
Stores a reference to the physics world (where all bodies live) for easier access
world.gravity.y = 1;
Sets gravity to 1 unit downward; higher values pull bodies down faster
const playerSize = 40;
Defines the player circle's diameter in pixels
const playerX = width / 2;
Places the player horizontally in the center of the canvas
const playerY = height - playerSize;
Positions the player near the bottom, just above the ground
player = Matter.Bodies.circle(playerX, playerY, playerSize / 2, {
Creates a circular physics body with the specified position and radius (diameter / 2)
restitution: 0.8,
Sets bounciness to 0.8; the player bounces back 80% of its impact energy (makes collisions chaotic)
friction: 0.1,
Low friction value makes the player slippery—it slides easily across surfaces
mass: 1,
Assigns the player a mass of 1 unit; heavier bodies are harder to push
label: 'player'
Adds a string label so collision detection can identify this body as the player
Matter.World.add(world, player);
Adds the player body to the physics world so it experiences gravity and collisions
ground = Matter.Bodies.rectangle(width / 2, height - 10, width, 20, {
Creates a large rectangular body for the ground, positioned near the bottom of the canvas
isStatic: true,
Marks the ground as static (immovable); it won't fall or be pushed by other bodies
friction: 0.5,
Moderate friction on the ground allows the player to slide but provides traction
Matter.World.add(world, ground);
Adds the ground body to the physics world
Matter.Events.on(engine, 'collisionStart', handleCollision);
Registers handleCollision to run each time two bodies first collide, enabling game-over detection
lastFrameTime = millis();
Records the current time in milliseconds to calculate deltaTime (time since last frame) in draw()

draw()

draw() is the heartbeat of the sketch, running 60 times per second (by default). It orchestrates the entire game loop: updating physics, spawning objects, checking win/lose conditions, rendering everything, and displaying the UI. The key insight is using deltaTime to make physics and timers frame-rate-independent—this ensures the game feels the same whether it runs at 30 FPS or 120 FPS.

🔬 This code counts down the timer and ends the game at zero. What happens if you change the second line to `timer -= deltaTime / 2000` instead? How much slower does the countdown become?

    // Update game timer
    timer -= deltaTime / 1000;
    if (timer <= 0) {
      gameState = 'gameOver';
      timer = 0;
    }

🔬 This loop removes objects when they fall 50 pixels below the screen. What happens if you change `height + 50` to `height + 200`? Objects will stay longer—does your score go up faster?

    for (let i = fallingObjects.length - 1; i >= 0; i--) {
      let obj = fallingObjects[i];
      renderBody(obj);
      // Remove objects that go off-screen to prevent performance issues
      if (obj.position.y > height + 50) {
function draw() {
  background(220);

  if (gameState === 'playing') {
    // Calculate deltaTime for consistent physics and timer updates
    let currentTime = millis();
    let deltaTime = currentTime - lastFrameTime;
    lastFrameTime = currentTime;

    // Update game timer
    timer -= deltaTime / 1000;
    if (timer <= 0) {
      gameState = 'gameOver';
      timer = 0;
    }

    // Update Matter.js engine with deltaTime
    Matter.Engine.update(engine, deltaTime);

    // Spawn falling objects at intervals
    timeElapsedSinceLastObject += deltaTime;
    if (timeElapsedSinceLastObject >= OBJECT_SPAWN_INTERVAL) {
      spawnFallingObject();
      timeElapsedSinceLastObject = 0;
    }

    // Render all physics bodies
    renderBody(player);
    renderBody(ground);
    for (let i = fallingObjects.length - 1; i >= 0; i--) {
      let obj = fallingObjects[i];
      renderBody(obj);
      // Remove objects that go off-screen to prevent performance issues
      if (obj.position.y > height + 50) {
        Matter.World.remove(world, obj);
        fallingObjects.splice(i, 1);
        score += 10; // Reward for dodging an object
      }
    }

    // Handle player movement based on keyboard or touch input
    handlePlayerMovement();

    // Display score and timer
    textSize(24);
    fill(0);
    noStroke();
    text(`Score: ${score}`, 20, 40);
    text(`Time: ${ceil(timer)}s`, 20, 80);

  } else if (gameState === 'gameOver') {
    // Display Game Over screen
    textSize(48);
    fill(255, 0, 0);
    textAlign(CENTER, CENTER);
    text('Game Over!', width / 2, height / 2 - 50);
    textSize(32);
    fill(0);
    text(`Final Score: ${score}`, width / 2, height / 2 + 10);
    textSize(24);
    text('Press Space to Restart', width / 2, height / 2 + 80);
  }
}
Line-by-line explanation (35 lines)

🔧 Subcomponents:

calculation Delta Time Calculation let deltaTime = currentTime - lastFrameTime;

Measures milliseconds since the last frame to keep physics and timers consistent regardless of frame rate

calculation Timer Countdown timer -= deltaTime / 1000;

Decrements the timer by seconds (deltaTime / 1000 converts milliseconds to seconds)

conditional Win Condition Check if (timer <= 0) { gameState = 'gameOver'; timer = 0; }

Ends the game when the 60-second timer expires, triggering the Game Over state

conditional Object Spawn Timer if (timeElapsedSinceLastObject >= OBJECT_SPAWN_INTERVAL) { spawnFallingObject(); timeElapsedSinceLastObject = 0; }

Spawns a new falling object every 1000 milliseconds and resets the spawn counter

for-loop Falling Objects Render & Cleanup Loop for (let i = fallingObjects.length - 1; i >= 0; i--) {

Loops backwards through all falling objects to render and remove ones that exit the screen

conditional Game Over Screen Rendering } else if (gameState === 'gameOver') {

Displays the Game Over message and final score when the game ends

background(220);
Clears the canvas with a light gray color (220) each frame, preventing trails and making the animation clean
if (gameState === 'playing') {
Checks if the game is in 'playing' mode; if so, updates and renders; otherwise shows Game Over screen
let currentTime = millis();
Gets the current time in milliseconds since the sketch started
let deltaTime = currentTime - lastFrameTime;
Calculates how many milliseconds passed since the last frame—used to keep physics and timers frame-rate-independent
lastFrameTime = currentTime;
Updates lastFrameTime so the next frame can calculate the new deltaTime
timer -= deltaTime / 1000;
Subtracts seconds from the timer; dividing by 1000 converts milliseconds to seconds
if (timer <= 0) {
If the timer runs out, the game ends
gameState = 'gameOver';
Changes the game state to trigger the Game Over screen on the next frame
Matter.Engine.update(engine, deltaTime);
Updates the Matter.js physics simulation; deltaTime ensures consistent physics regardless of frame rate
timeElapsedSinceLastObject += deltaTime;
Adds the time elapsed this frame to the spawn timer counter
if (timeElapsedSinceLastObject >= OBJECT_SPAWN_INTERVAL) {
When enough time has passed (1000 milliseconds), spawn a new falling object
spawnFallingObject();
Calls spawnFallingObject() to create a new random rectangle above the canvas
timeElapsedSinceLastObject = 0;
Resets the spawn timer to zero so the next object spawns after another 1000 milliseconds
renderBody(player);
Draws the player circle at its current physics position and rotation
renderBody(ground);
Draws the ground rectangle at its position (which doesn't change since it's static)
for (let i = fallingObjects.length - 1; i >= 0; i--) {
Loops backwards through the falling objects array; backward iteration allows safe removal during the loop
let obj = fallingObjects[i];
Gets the current falling object
renderBody(obj);
Draws the falling object at its current position and rotation
if (obj.position.y > height + 50) {
Checks if the object has fallen below the bottom of the canvas (with 50-pixel buffer)
Matter.World.remove(world, obj);
Removes the object from the physics simulation so it no longer affects other bodies
fallingObjects.splice(i, 1);
Removes the object from the fallingObjects array
score += 10;
Awards 10 points to the player for successfully dodging the object
handlePlayerMovement();
Calls handlePlayerMovement() to apply keyboard/touch input as physics forces on the player
textSize(24);
Sets the font size for the score and timer text
fill(0);
Sets text color to black
noStroke();
Removes the stroke (outline) around text
text(`Score: ${score}`, 20, 40);
Displays the current score at position (20, 40) in the top-left corner
text(`Time: ${ceil(timer)}s`, 20, 80);
Displays the remaining time rounded up to the nearest whole second at (20, 80)
} else if (gameState === 'gameOver') {
If the game state is 'gameOver', display the Game Over screen instead
textSize(48);
Sets a large font size for the 'Game Over!' title
fill(255, 0, 0);
Sets text color to red for the Game Over message
textAlign(CENTER, CENTER);
Centers text horizontally and vertically on its position
text('Game Over!', width / 2, height / 2 - 50);
Displays 'Game Over!' centered in the middle of the canvas
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Displays the final score below the Game Over message
text('Press Space to Restart', width / 2, height / 2 + 80);
Instructs the player to press Space to restart the game

handlePlayerMovement()

This function is the input handler that translates player actions (keyboard presses and touch) into physics forces. It demonstrates several key physics patterns: applyForce() for gradual acceleration, setVelocity() for clamping speed, setPosition() for boundary enforcement, and conditional checks for grounding logic (only allowing jumps when near the ground). Understanding this teaches you how to build responsive, constrained player movement.

🔬 This code caps the player's horizontal speed at 5 pixels per frame. What happens if you increase maxHorizontalVelocity to 10? Will the player zoom across the screen faster? Try it!

  // Cap horizontal velocity to prevent infinite acceleration and keep player on screen
  const maxHorizontalVelocity = 5;
  Matter.Body.setVelocity(player, {
    x: constrain(player.velocity.x, -maxHorizontalVelocity, maxHorizontalVelocity),
    y: player.velocity.y
  });
function handlePlayerMovement() {
  const moveForce = 0.005; // Force for horizontal movement
  const jumpForce = 0.02;  // Force for jumping (exaggerated for chaos)

  // Keyboard controls
  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT or 'A'
    Matter.Body.applyForce(player, player.position, { x: -moveForce, y: 0 });
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT or 'D'
    Matter.Body.applyForce(player, player.position, { x: moveForce, y: 0 });
  }

  // Jump (Up arrow or W) - Looser check for "exaggerated jumps"
  // Allows some mid-air boosts if player velocity isn't too high upwards
  if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // UP or 'W'
    if (player.position.y > height - 60 || player.velocity.y > -2) {
      Matter.Body.applyForce(player, player.position, { x: 0, y: -jumpForce });
    }
  }

  // Cap horizontal velocity to prevent infinite acceleration and keep player on screen
  const maxHorizontalVelocity = 5;
  Matter.Body.setVelocity(player, {
    x: constrain(player.velocity.x, -maxHorizontalVelocity, maxHorizontalVelocity),
    y: player.velocity.y
  });

  // Keep player within horizontal bounds
  let playerRadius = player.circleRadius;
  if (player.position.x - playerRadius < 0) {
    Matter.Body.setPosition(player, { x: playerRadius, y: player.position.y });
    Matter.Body.setVelocity(player, { x: 0, y: player.velocity.y });
  } else if (player.position.x + playerRadius > width) {
    Matter.Body.setPosition(player, { x: width - playerRadius, y: player.position.y });
    Matter.Body.setVelocity(player, { x: 0, y: player.velocity.y });
  }

  // Touch controls (basic horizontal movement)
  if (touches.length > 0) {
    let touchX = touches[0].x;
    if (touchX < width / 2) {
      Matter.Body.applyForce(player, player.position, { x: -moveForce * 1.5, y: 0 });
    } else {
      Matter.Body.applyForce(player, player.position, { x: moveForce * 1.5, y: 0 });
    }
  }
}
Line-by-line explanation (26 lines)

🔧 Subcomponents:

conditional Left Movement Input if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT or 'A'

Checks if the player pressed left arrow or 'A' key and applies leftward force to the player

conditional Right Movement Input if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT or 'D'

Checks if the player pressed right arrow or 'D' key and applies rightward force to the player

conditional Jump Input with Ground Check if (player.position.y > height - 60 || player.velocity.y > -2) {

Allows jumping only when the player is near the ground or not moving upward too fast, preventing infinite air jumps

conditional Horizontal Velocity Limiter x: constrain(player.velocity.x, -maxHorizontalVelocity, maxHorizontalVelocity),

Prevents the player from accelerating infinitely; caps horizontal speed at ±5 pixels per frame

conditional Left Edge Boundary Check if (player.position.x - playerRadius < 0) {

Detects when the player's left edge crosses the left edge of the canvas and pushes them back

conditional Right Edge Boundary Check } else if (player.position.x + playerRadius > width) {

Detects when the player's right edge crosses the right edge of the canvas and pushes them back

conditional Touch Input Handler if (touches.length > 0) {

Enables mobile/tablet play by moving the player left or right based on where the user touches the screen

const moveForce = 0.005; // Force for horizontal movement
Defines the strength of the force applied to move the player left or right when you press the arrow keys or WASD
const jumpForce = 0.02; // Force for jumping (exaggerated for chaos)
Defines the upward force applied when you press the jump key; higher values produce bigger jumps
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT or 'A'
Checks if the left arrow key or the 'A' key is currently held down (65 is the ASCII code for 'A')
Matter.Body.applyForce(player, player.position, { x: -moveForce, y: 0 });
Applies a leftward force (negative x) to the player's body; the force is applied at the player's center position
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT or 'D'
Checks if the right arrow key or the 'D' key is currently held down (68 is the ASCII code for 'D')
Matter.Body.applyForce(player, player.position, { x: moveForce, y: 0 });
Applies a rightward force (positive x) to the player's body
if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // UP or 'W'
Checks if the up arrow key or the 'W' key is held down (87 is the ASCII code for 'W')
if (player.position.y > height - 60 || player.velocity.y > -2) {
Only allows jumping if the player is within 60 pixels of the bottom (near ground) OR not moving upward too fast—prevents infinite jumps mid-air
Matter.Body.applyForce(player, player.position, { x: 0, y: -jumpForce });
Applies an upward force (negative y, since y increases downward) to launch the player into the air
const maxHorizontalVelocity = 5;
Sets a speed limit so the player can't accelerate infinitely left or right
Matter.Body.setVelocity(player, {
Sets the player's velocity directly, overriding accumulated forces; allows clamping the horizontal speed
x: constrain(player.velocity.x, -maxHorizontalVelocity, maxHorizontalVelocity),
Clamps the horizontal velocity between -5 and +5, preventing runaway acceleration
y: player.velocity.y
Keeps the vertical velocity unchanged so vertical physics (gravity, jumping) work naturally
let playerRadius = player.circleRadius;
Stores the player's radius (half the diameter) for boundary checking
if (player.position.x - playerRadius < 0) {
Checks if the player's left edge has gone past the left side of the canvas
Matter.Body.setPosition(player, { x: playerRadius, y: player.position.y });
Teleports the player back so their left edge is exactly at x = 0
Matter.Body.setVelocity(player, { x: 0, y: player.velocity.y });
Stops the player's leftward motion so they don't immediately push back out of bounds again
} else if (player.position.x + playerRadius > width) {
Checks if the player's right edge has gone past the right side of the canvas
Matter.Body.setPosition(player, { x: width - playerRadius, y: player.position.y });
Teleports the player back so their right edge is exactly at x = width
Matter.Body.setVelocity(player, { x: 0, y: player.velocity.y });
Stops the player's rightward motion
if (touches.length > 0) {
Checks if there are any active touch points (mouse clicks or finger touches) on the screen
let touchX = touches[0].x;
Gets the x-coordinate of the first touch point
if (touchX < width / 2) {
If the touch is on the left half of the screen, apply leftward force
Matter.Body.applyForce(player, player.position, { x: -moveForce * 1.5, y: 0 });
Applies leftward force 1.5 times stronger than keyboard input for easier mobile control
} else {
If the touch is on the right half of the screen, apply rightward force
Matter.Body.applyForce(player, player.position, { x: moveForce * 1.5, y: 0 });
Applies rightward force 1.5 times stronger than keyboard input

spawnFallingObject()

spawnFallingObject() is called every 1000 milliseconds (from draw()) to create new obstacles. Each call generates a new random-sized rectangle at a random horizontal position and adds it to both the Matter.js world (so physics apply) and the fallingObjects array (so draw() can track and render it). This pattern of object pooling is fundamental to games: spawning, tracking, rendering, and cleaning up entities efficiently.

🔬 These four lines set up the falling object's random size and position. What happens if you change `const objectY = -objectHeight / 2` to `const objectY = 0`? Will objects spawn at the top edge of the canvas instead of above it?

  const objectWidth = random(20, 50);
  const objectHeight = random(20, 50);
  const objectX = random(objectWidth / 2, width - objectWidth / 2);
  const objectY = -objectHeight / 2; // Start above canvas
function spawnFallingObject() {
  const objectWidth = random(20, 50);
  const objectHeight = random(20, 50);
  const objectX = random(objectWidth / 2, width - objectWidth / 2);
  const objectY = -objectHeight / 2; // Start above canvas

  let obj = Matter.Bodies.rectangle(objectX, objectY, objectWidth, objectHeight, {
    restitution: 0.5, // Bouncy
    friction: 0.1,    // Slippery
    mass: 0.5,        // Lighter than player
    label: 'fallingObject' // Custom label
  });
  Matter.World.add(world, obj);
  fallingObjects.push(obj);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Random Object Width const objectWidth = random(20, 50);

Creates variation by randomizing each falling object's width between 20 and 50 pixels

calculation Random Object Height const objectHeight = random(20, 50);

Randomizes the falling object's height to make each one unique

calculation Horizontal Spawn Position const objectX = random(objectWidth / 2, width - objectWidth / 2);

Places the object randomly across the width of the canvas, but keeps it on-screen (doesn't let it spawn half off-screen)

calculation Physics Body Creation let obj = Matter.Bodies.rectangle(objectX, objectY, objectWidth, objectHeight, {

Creates a rectangular physics body with the random dimensions and physical properties

calculation Add to Physics World Matter.World.add(world, obj);

Adds the new body to the physics simulation so it falls due to gravity and can collide

calculation Add to Tracking Array fallingObjects.push(obj);

Adds the body to the fallingObjects array so draw() can track and render it

const objectWidth = random(20, 50);
Generates a random width between 20 and 50 pixels; different widths make objects look varied and unpredictable
const objectHeight = random(20, 50);
Generates a random height between 20 and 50 pixels
const objectX = random(objectWidth / 2, width - objectWidth / 2);
Picks a random horizontal position, but constrains it so the object stays fully on-screen (offset by its half-width)
const objectY = -objectHeight / 2; // Start above canvas
Places the object above the canvas (negative y-coordinate) so it falls into view
let obj = Matter.Bodies.rectangle(objectX, objectY, objectWidth, objectHeight, {
Creates a rectangular physics body at position (objectX, objectY) with the random dimensions
restitution: 0.5, // Bouncy
Sets bounciness to 0.5; the object bounces back 50% of its impact energy when it hits the ground or player
friction: 0.1, // Slippery
Low friction makes objects slide easily when they hit other bodies
mass: 0.5, // Lighter than player
Sets the object's mass to 0.5 (half the player's mass), making them easier to push around on impact
label: 'fallingObject' // Custom label
Adds a label so collision detection can identify this as a falling object (vs. the player or ground)
Matter.World.add(world, obj);
Adds the physics body to the world so it experiences gravity and can collide with other bodies
fallingObjects.push(obj);
Adds the object to the fallingObjects array so the draw() function can render it and check if it's off-screen

handleCollision()

Matter.js fires a 'collisionStart' event every time two bodies first touch. This function runs in response to that event, checking if the collision involves the player and a falling object. If so, the game ends immediately. This is the core lose condition and demonstrates how to detect specific pairs of bodies colliding—essential for any physics-based game with interactive hazards.

🔬 This checks if a player touches a falling object and ends the game. What if you add a second condition that ALSO detects player-ground collisions? Could you use that to respawn the player instead of ending the game?

    // Check if player collided with a falling object
    if ((bodyA.label === 'player' && bodyB.label === 'fallingObject') ||
        (bodyA.label === 'fallingObject' && bodyB.label === 'player')) {
      gameState = 'gameOver';
function handleCollision(event) {
  let pairs = event.pairs;
  for (let i = 0; i < pairs.length; i++) {
    let bodyA = pairs[i].bodyA;
    let bodyB = pairs[i].bodyB;

    // Check if player collided with a falling object
    if ((bodyA.label === 'player' && bodyB.label === 'fallingObject') ||
        (bodyA.label === 'fallingObject' && bodyB.label === 'player')) {
      gameState = 'gameOver';
      // Optionally remove the falling object that hit the player
      let fallingObj = (bodyA.label === 'fallingObject') ? bodyA : bodyB;
      Matter.World.remove(world, fallingObj);
      fallingObjects = fallingObjects.filter(obj => obj !== fallingObj);
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Collision Pairs Loop for (let i = 0; i < pairs.length; i++) {

Iterates through all collision pairs detected this frame

conditional Player-Object Collision Detection if ((bodyA.label === 'player' && bodyB.label === 'fallingObject') ||

Checks if the two colliding bodies are a player and a falling object (in either order)

calculation Game Over Trigger gameState = 'gameOver';

Ends the game when a collision is detected

calculation Collision Object Removal fallingObjects = fallingObjects.filter(obj => obj !== fallingObj);

Removes the colliding object from the tracking array

let pairs = event.pairs;
Extracts the array of collision pairs from the Matter.js collision event; each pair is two bodies that collided
for (let i = 0; i < pairs.length; i++) {
Loops through each collision pair this frame (there might be 0, 1, or more collisions per frame)
let bodyA = pairs[i].bodyA;
Gets the first body in the current collision pair
let bodyB = pairs[i].bodyB;
Gets the second body in the current collision pair
if ((bodyA.label === 'player' && bodyB.label === 'fallingObject') ||
Checks if bodyA is the player and bodyB is a falling object, OR the reverse (checks both possible orderings)
(bodyA.label === 'fallingObject' && bodyB.label === 'player')) {
Completes the OR condition: if either ordering matches, we have a player-object collision
gameState = 'gameOver';
Sets the game state to 'gameOver', which triggers the Game Over screen on the next draw() call
let fallingObj = (bodyA.label === 'fallingObject') ? bodyA : bodyB;
Uses a ternary operator to identify which body is the falling object (doesn't matter which order they collided in)
Matter.World.remove(world, fallingObj);
Removes the falling object from the physics simulation so it stops moving and interacting
fallingObjects = fallingObjects.filter(obj => obj !== fallingObj);
Uses filter() to create a new fallingObjects array without the collided object; this removes it from tracking

renderBody()

renderBody() is the rendering workhorse: it translates the p5.js coordinate system to each physics body's position, rotates to match its angle, and draws the appropriate shape and color based on the body's label. This function is called once per frame for every body (player, ground, and falling objects), synchronizing the p5.js visuals with the Matter.js physics. Understanding push/pop and translate/rotate is core to p5.js graphics and essential for any sketch involving transforms.

🔬 These four lines set up the drawing transformation. What happens if you comment out the `rotate(body.angle);` line (add // before it)? Objects will stop spinning visually—do they still rotate in the physics?

  push();
  translate(body.position.x, body.position.y);
  rotate(body.angle);
  noStroke();
// Function to render a Matter.js body on the p5.js canvas
function renderBody(body) {
  push();
  translate(body.position.x, body.position.y);
  rotate(body.angle);
  noStroke();

  if (body.label === 'player') {
    fill(0, 150, 200); // Blue for player
    ellipse(0, 0, body.circleRadius * 2);
  } else if (body.label === 'ground') {
    fill(100); // Gray for ground
    rectMode(CENTER);
    rect(0, 0, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
  } else if (body.label === 'fallingObject') {
    fill(255, 0, 0); // Red for falling objects
    rectMode(CENTER);
    rect(0, 0, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
  }
  pop();
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

calculation Transform Stack Management push();

Saves the current transformation state so local transforms don't affect subsequent drawings

calculation Position Translation translate(body.position.x, body.position.y);

Moves the drawing origin to the body's physics position

calculation Rotation Application rotate(body.angle);

Rotates the drawing to match the body's angular orientation from the physics engine

conditional Player Rendering if (body.label === 'player') {

Draws a blue circle if the body is the player

conditional Ground Rendering } else if (body.label === 'ground') {

Draws a gray rectangle if the body is the ground

conditional Falling Object Rendering } else if (body.label === 'fallingObject') {

Draws a red rectangle if the body is a falling object

push();
Saves the current p5.js transformation state (position, rotation, fill color, etc.) so local changes don't affect other drawings
translate(body.position.x, body.position.y);
Moves the drawing origin to the body's current physics position—all subsequent draws happen from this point
rotate(body.angle);
Rotates the drawing context by the body's current angle in radians; makes shapes rotate as they tumble in physics
noStroke();
Disables shape outlines so bodies are drawn as solid fills only
if (body.label === 'player') {
Checks if this body is labeled 'player'
fill(0, 150, 200); // Blue for player
Sets the fill color to blue (RGB: 0, 150, 200)
ellipse(0, 0, body.circleRadius * 2);
Draws a circle centered at (0, 0) with diameter = 2 × circleRadius; the local coordinates (0, 0) become the body's position due to translate()
} else if (body.label === 'ground') {
Checks if this body is labeled 'ground'
fill(100); // Gray for ground
Sets the fill color to gray (RGB: 100, 100, 100)
rectMode(CENTER);
Changes rectangle drawing mode so the rectangle is centered at (0, 0) instead of having the corner there
rect(0, 0, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
Draws a rectangle centered at (0, 0) with width and height calculated from the body's bounding box (the space it occupies)
} else if (body.label === 'fallingObject') {
Checks if this body is labeled 'fallingObject'
fill(255, 0, 0); // Red for falling objects
Sets the fill color to red (RGB: 255, 0, 0)
rectMode(CENTER);
Centers rectangles at (0, 0)
rect(0, 0, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
Draws a red rectangle at the falling object's position and rotation
pop();
Restores the saved transformation state, so the next body renders with fresh position/rotation/color

keyPressed()

keyPressed() is a p5.js built-in function that runs once whenever the user presses any key. This sketch uses it to detect spacebar presses after Game Over and restart the game. It's a simple but essential pattern for menu navigation and game state transitions.

function keyPressed() {
  // Restart game on Spacebar press when Game Over
  if (gameState === 'gameOver' && key === ' ') {
    restartGame();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Restart Condition Check if (gameState === 'gameOver' && key === ' ') {

Checks if the game is over AND the user pressed the spacebar

if (gameState === 'gameOver' && key === ' ') {
Checks if the game state is 'gameOver' AND the key pressed is the spacebar (' ' is the space character)
restartGame();
Calls the restartGame() function to reset all game variables and return to playing

restartGame()

restartGame() is called when the player presses spacebar on the Game Over screen. It resets all game variables (state, score, timer), cleans up all falling objects from both the physics world and the tracking array, and resets the player's position and velocity. This function demonstrates the importance of thorough cleanup when restarting state-driven simulations; forgetting to reset can cause subtle bugs.

function restartGame() {
  gameState = 'playing';
  score = 0;
  timer = 60;
  lastFrameTime = millis();
  timeElapsedSinceLastObject = 0;

  // Remove all falling objects from the Matter.js world and the array
  for (let obj of fallingObjects) {
    Matter.World.remove(world, obj);
  }
  fallingObjects = [];

  // Reset player position, velocity, and angular velocity
  Matter.Body.setPosition(player, { x: width / 2, y: height - player.circleRadius });
  Matter.Body.setVelocity(player, { x: 0, y: 0 });
  Matter.Body.setAngularVelocity(player, 0);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Game State Reset gameState = 'playing';

Changes the game state back to 'playing' to resume the game loop

calculation Score Reset score = 0;

Sets the score back to 0 for a fresh start

calculation Timer Reset timer = 60;

Resets the game duration back to 60 seconds

for-loop Falling Objects Cleanup Loop for (let obj of fallingObjects) {

Removes all remaining falling objects from the physics world and the tracking array

calculation Player State Reset Matter.Body.setPosition(player, { x: width / 2, y: height - player.circleRadius });

Moves the player back to the starting position at the bottom center of the canvas

gameState = 'playing';
Changes the game state from 'gameOver' back to 'playing' so the game loop resumes
score = 0;
Resets the score to 0 for a fresh game
timer = 60;
Resets the timer back to 60 seconds
lastFrameTime = millis();
Records the current time so the next draw() call calculates deltaTime correctly
timeElapsedSinceLastObject = 0;
Resets the spawn timer so the first new object spawns after 1000 milliseconds
for (let obj of fallingObjects) {
Loops through all remaining falling objects (uses for...of syntax)
Matter.World.remove(world, obj);
Removes each falling object from the physics world
fallingObjects = [];
Clears the fallingObjects array by setting it to an empty array
Matter.Body.setPosition(player, { x: width / 2, y: height - player.circleRadius });
Teleports the player back to the center-bottom of the canvas
Matter.Body.setVelocity(player, { x: 0, y: 0 });
Stops the player by setting both horizontal and vertical velocity to zero
Matter.Body.setAngularVelocity(player, 0);
Stops the player from spinning by setting angular velocity to zero

windowResized()

windowResized() is a p5.js built-in that fires whenever the browser window is resized. This sketch uses it to make the game responsive: the canvas stretches to fit the window, and the ground scales to match. The code demonstrates the complexity of resizing physics bodies in Matter.js—for static bodies like ground, you must manually update bounding boxes and related properties. This is a practical example of responsive design in interactive physics sketches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Update ground position and size for responsiveness
  Matter.Body.setPosition(ground, { x: width / 2, y: height - 10 });
  // Scaling a static body in Matter.js can be tricky. For a simple rectangle like ground,
  // we can roughly scale and reset properties. For complex shapes, recreating is better.
  Matter.Body.scale(ground, width / (ground.bounds.max.x - ground.bounds.min.x), 1);
  ground.bounds.max.x = width / 2 + ground.width / 2;
  ground.bounds.min.x = width / 2 - ground.width / 2;
  ground.area = ground.width * ground.height;
  ground.inertia = Matter.Body.nextId(); // Reset inertia for static bodies after scaling
  ground.mass = ground.area * 0.001; // Recalculate mass based on new area
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Canvas Resize resizeCanvas(windowWidth, windowHeight);

Adjusts the p5.js canvas to match the new window size

calculation Ground Repositioning Matter.Body.setPosition(ground, { x: width / 2, y: height - 10 });

Moves the ground to stay centered horizontally and near the bottom of the resized canvas

calculation Ground Scaling Matter.Body.scale(ground, width / (ground.bounds.max.x - ground.bounds.min.x), 1);

Stretches the ground horizontally to match the new canvas width

resizeCanvas(windowWidth, windowHeight);
Adjusts the p5.js canvas to the new window dimensions whenever the browser window is resized
Matter.Body.setPosition(ground, { x: width / 2, y: height - 10 });
Moves the ground to the horizontal center and near the bottom of the new canvas
Matter.Body.scale(ground, width / (ground.bounds.max.x - ground.bounds.min.x), 1);
Scales the ground horizontally to span the new canvas width (vertical scale stays at 1, no vertical stretching)
ground.bounds.max.x = width / 2 + ground.width / 2;
Manually updates the right edge of the ground's bounding box after scaling
ground.bounds.min.x = width / 2 - ground.width / 2;
Manually updates the left edge of the ground's bounding box after scaling
ground.area = ground.width * ground.height;
Recalculates the ground's area based on its new dimensions
ground.inertia = Matter.Body.nextId();
Resets the ground's rotational inertia after scaling (a workaround for Matter.js static body updates)
ground.mass = ground.area * 0.001;
Recalculates the ground's mass based on its new area

📦 Key Variables

engine object

Stores the Matter.js physics engine; used to update all physics calculations and handle collisions

engine = Matter.Engine.create();
world object

Stores the Matter.js physics world (where all bodies live); used to add/remove bodies and set gravity

world = engine.world;
player object

Stores the player's physics body; a circular body the user controls with keyboard/touch

player = Matter.Bodies.circle(...);
ground object

Stores the ground's physics body; a static rectangular platform that the player slides on

ground = Matter.Bodies.rectangle(...);
fallingObjects array

An array storing all active falling object physics bodies; used for rendering and cleanup

let fallingObjects = [];
gameState string

Tracks the current game state: 'playing' runs the main loop, 'gameOver' shows the end screen

let gameState = 'playing';
score number

Tracks the player's score; increases by 10 for each dodged object, displays on screen

let score = 0;
timer number

Counts down from 60 seconds; when it reaches 0, the game ends

let timer = 60;
lastFrameTime number

Stores the timestamp (in milliseconds) of the last frame; used to calculate deltaTime

let lastFrameTime = 0;
timeElapsedSinceLastObject number

Counts milliseconds since the last falling object spawned; resets after each spawn

let timeElapsedSinceLastObject = 0;
OBJECT_SPAWN_INTERVAL number

The millisecond interval between falling object spawns; constant value of 1000

const OBJECT_SPAWN_INTERVAL = 1000;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG windowResized()

Scaling the ground body in Matter.js is fragile and error-prone. The current code manually updates properties after scaling, which can cause subtle physics bugs or inconsistencies.

💡 Instead of scaling, recreate the ground body from scratch: remove the old one from the world, create a new rectangle with the new dimensions, and add it back. This is cleaner and avoids state corruption.

PERFORMANCE draw() - falling objects loop

Looping backwards through fallingObjects and splicing is safe but slow for large arrays. With many objects, array splice() becomes O(n) per removal.

💡 Use a different cleanup strategy: mark objects for deletion with a flag, then filter the array once per frame instead of during iteration. Or use an object pool to reuse physics bodies instead of creating and destroying them constantly.

STYLE handlePlayerMovement()

The jump condition `player.position.y > height - 60 || player.velocity.y > -2` is a loose approximation for 'is grounded' and can allow mid-air jumps in edge cases.

💡 Implement proper ground detection using collision events: track whether the player is currently touching the ground, and only allow jumps when that flag is true. This is more robust and commonly used in physics-based platformers.

FEATURE Game design

The game has no difficulty progression: objects spawn at the same rate for the entire 60 seconds, making the challenge static.

💡 Add difficulty scaling: increase OBJECT_SPAWN_INTERVAL or object size as time passes, or increase gravity over time. This creates a ramp that keeps the game engaging throughout.

BUG restartGame()

If the player presses spacebar very quickly on Game Over, race conditions could occur between the restart and the next collision detection, potentially causing unexpected behavior.

💡 Add a 'restarting' state flag that blocks input for a brief moment after restart is triggered, preventing accidental double-restarts or collision artifacts.

STYLE Multiple functions

Magic numbers like 0.005 (moveForce), 0.02 (jumpForce), 60 (timer), and 1000 (spawn interval) are hardcoded throughout the code, making it hard to tune the game experience globally.

💡 Move these to a configuration object at the top of the file (e.g., `const CONFIG = { moveForce: 0.005, jumpForce: 0.02, ... }`), then reference CONFIG.moveForce instead. This makes experimenting with game balance much easier.

🔄 Code Flow

Code flow showing setup, draw, handleplayermovement, spawnfallingobject, handlecollision, renderbody, keypressed, restartgame, 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 --> gravity-setup[Gravity Setup] setup --> player-creation[Player Creation] setup --> ground-creation[Ground Creation] setup --> collision-listener[Collision Listener] setup --> draw[draw loop] click setup href "#fn-setup" click engine-creation href "#sub-engine-creation" click gravity-setup href "#sub-gravity-setup" click player-creation href "#sub-player-creation" click ground-creation href "#sub-ground-creation" click collision-listener href "#sub-collision-listener" draw --> deltatime-calc[Delta Time Calculation] draw --> timer-update[Timer Update] draw --> win-condition[Win Condition Check] draw --> spawn-timer[Spawn Timer] draw --> render-loop[Render Loop] draw --> game-over-display[Game Over Display] click draw href "#fn-draw" click deltatime-calc href "#sub-deltatime-calc" click timer-update href "#sub-timer-update" click win-condition href "#sub-win-condition" click spawn-timer href "#sub-spawn-timer" click render-loop href "#sub-render-loop" click game-over-display href "#sub-game-over-display" spawn-timer --> spawnfallingobject[spawnFallingObject] spawnfallingobject --> width-random[Width Random] spawnfallingobject --> height-random[Height Random] spawnfallingobject --> spawn-x-position[Spawn X Position] spawnfallingobject --> body-creation[Body Creation] spawnfallingobject --> world-add[World Add] spawnfallingobject --> array-push[Array Push] click spawnfallingobject href "#fn-spawnfallingobject" click width-random href "#sub-width-random" click height-random href "#sub-height-random" click spawn-x-position href "#sub-spawn-x-position" click body-creation href "#sub-body-creation" click world-add href "#sub-world-add" click array-push href "#sub-array-push" render-loop --> pairs-loop[Pairs Loop] pairs-loop --> collision-check[Collision Check] collision-check --> game-end[Game End] game-end --> object-cleanup[Object Cleanup] click pairs-loop href "#sub-pairs-loop" click collision-check href "#sub-collision-check" click game-end href "#sub-game-end" click object-cleanup href "#sub-object-cleanup" keypressed[keyPressed] --> restart-check[Restart Check] restart-check --> restartgame[restartGame] restartgame --> state-reset[State Reset] restartgame --> score-reset[Score Reset] restartgame --> timer-reset[Timer Reset] restartgame --> objects-cleanup[Objects Cleanup] restartgame --> player-reset[Player Reset] click keypressed href "#fn-keypressed" click restart-check href "#sub-restart-check" click restartgame href "#fn-restartgame" click state-reset href "#sub-state-reset" click score-reset href "#sub-score-reset" click timer-reset href "#sub-timer-reset" click objects-cleanup href "#sub-objects-cleanup" click player-reset href "#sub-player-reset" windowresized[windowResized] --> canvas-resize[Canvas Resize] windowresized --> ground-reposition[Ground Repositioning] windowresized --> ground-scale[Ground Scaling] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click ground-reposition href "#sub-ground-reposition" click ground-scale href "#sub-ground-scale" handleplayermovement[handlePlayerMovement] --> left-movement[Left Movement] handleplayermovement --> right-movement[Right Movement] handleplayermovement --> jump-condition[Jump Condition] handleplayermovement --> velocity-cap[Velocity Cap] handleplayermovement --> left-boundary[Left Boundary] handleplayermovement --> right-boundary[Right Boundary] handleplayermovement --> touch-control[Touch Control] click handleplayermovement href "#fn-handleplayermovement" click left-movement href "#sub-left-movement" click right-movement href "#sub-right-movement" click jump-condition href "#sub-jump-condition" click velocity-cap href "#sub-velocity-cap" click left-boundary href "#sub-left-boundary" click right-boundary href "#sub-right-boundary" click touch-control href "#sub-touch-control" handlecollision[handleCollision] --> pairs-loop click handlecollision href "#fn-handlecollision"

❓ Frequently Asked Questions

What visual experience does the falling lava survival sketch provide?

The sketch creates a dynamic playground filled with an endless rain of bouncing shapes, where a slippery ball character navigates through obstacles in a vibrant, physics-driven environment.

How can players interact with the falling lava survival game?

Players can control the ball character's movement to dodge the falling objects, aiming to survive as long as possible while tracking their score and time.

What creative coding concepts are showcased in the falling lava survival sketch?

This sketch demonstrates the use of Matter.js for physics simulation, including collision detection and object dynamics, alongside real-time game state management.

Preview

falling lava survival - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of falling lava survival - Code flow showing setup, draw, handleplayermovement, spawnfallingobject, handlecollision, renderbody, keypressed, restartgame, windowresized
Code Flow Diagram