block stacker-2.0

This sketch creates an isometric 3D block-stacking game powered by Matter.js physics. Colorful blocks slide horizontally across the screen, and you click to drop them onto a growing tower, trying to stack as high as possible before the structure becomes unstable and tumbles.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make blocks fall faster — Increase gravity strength to make dropped blocks tumble and settle more quickly, making the game feel snappier.
  2. Make the base block a different color — Change the base block's color from cyan to any other p5.js color, making your starting platform stand out.
  3. Slow down the moving block — Reduce the starting speed so blocks slide more slowly and are easier to catch, making the game more forgiving.
  4. Widen the accuracy window — Increase the allowed distance from 45 to 60 pixels, letting blocks be placed further from the block below.
  5. Make blocks more slippery — Reduce friction so blocks slide more easily and are harder to balance on top of each other.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a fully functional stacking game where blocks continuously slide left and right, and you time clicks to drop them onto a tower. The visual magic comes from isometric projection—a 2D drawing technique that makes flat shapes look like 3D cubes—combined with Matter.js physics that simulates realistic gravity, friction, and collisions. As your tower grows, the camera smoothly follows upward, creating the illusion of building higher and higher.

The code is organized into three layers: p5.js for drawing and interaction, Matter.js for physics simulation, and custom game logic that tracks blocks, scoring, and game state. By studying this sketch you will learn how to integrate a mature physics engine into p5.js, how isometric projection works, and how camera movement can enhance the sense of scale and progression in a game.

⚙️ How It Works

  1. When the sketch loads, setup() initializes a p5.js canvas, creates a Matter.js physics engine with gravity, and places a static ground body and base block to build on top of.
  2. Every frame, draw() clears the background, updates the Matter.js physics engine, then loops through every stacked block and syncs its drawing position with its physics body's position and rotation.
  3. A moving block continuously slides left and right at the top of the screen; when you click or press a key, mousePressed() checks if the falling block lands close enough to the block below it (hit detection).
  4. If the click is accurate, a new dynamic (non-static) physics body is created high above and falls down realistically, subject to gravity and friction, then settles onto the stack.
  5. The camera gradually scrolls upward as your score increases, keeping your growing tower centered on screen and creating a satisfying sense of building higher.
  6. If any block falls below the ground or the tower becomes too wobbly to support the next block, gameOver is set to true and the game halts with a restart button.

🎓 Concepts You'll Learn

Matter.js physics engineIsometric projection and 3D drawingCollision detection and hit testingCamera movement and viewport controlGame state managementDynamic and static physics bodiesp5.sound audio feedback

📝 Code Breakdown

drawCube(x, y, col, angle)

drawCube demonstrates isometric projection—a technique for drawing 3D shapes on a 2D canvas by using specific coordinate relationships. Each face is a quad() with hardcoded isometric offsets (e.g., y - 15 for the top-right corner). The function also applies rotation from Matter.js and camera translation, showing how to compose multiple transforms without interfering with other objects.

🔬 This function draws three faces. What happens if you comment out the left face quad() call? How does removing that face change what you see?

    // Top Face
    fill(col);
    quad(x, y, x + 30, y - 15, x + 60, y, x + 30, y + 15);
    // Left Face
    fill(lerpColor(col, color(0), 0.2));
    quad(x, y, x + 30, y + 15, x + 30, y + 45, x, y + 30);

🔬 These nested translates and rotate create the rotation magic. What if you changed the second translate to translate(x + 50, y + 30) instead? Where would the pivot point move?

    translate(0, camY); // Translate everything based on camera

    // Translate to the center of the physics body, then rotate
    // Matter.js bodies are centered, so we adjust p5's drawing origin
    // to the center of our isometric block's base for rotation.
    // The isometric block's base center is at (x + 30, y + 15).
    translate(x + 30, y + 15);
    rotate(angle);
    translate(-(x + 30), -(y + 15));
var drawCube = function(x, y, col, angle) {
    push();
    translate(0, camY); // Translate everything based on camera

    // Translate to the center of the physics body, then rotate
    // Matter.js bodies are centered, so we adjust p5's drawing origin
    // to the center of our isometric block's base for rotation.
    // The isometric block's base center is at (x + 30, y + 15).
    translate(x + 30, y + 15);
    rotate(angle);
    translate(-(x + 30), -(y + 15)); // Translate back for actual cube drawing

    // Top Face
    fill(col);
    quad(x, y, x + 30, y - 15, x + 60, y, x + 30, y + 15);
    // Left Face
    fill(lerpColor(col, color(0), 0.2));
    quad(x, y, x + 30, y + 15, x + 30, y + 45, x, y + 30);
    // Right Face
    fill(lerpColor(col, color(0), 0.4));
    quad(x + 30, y + 15, x + 60, y, x + 60, y + 30, x + 30, y + 45);
    
    pop();
};
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Camera Translation translate(0, camY);

Shifts all drawing upward by camY pixels, creating the illusion that the camera is following the tower

calculation Rotation Transform translate(x + 30, y + 15); rotate(angle); translate(-(x + 30), -(y + 15));

Rotates the block around its center point (x+30, y+15) based on its physics body's angle

calculation Top Face (Isometric) quad(x, y, x + 30, y - 15, x + 60, y, x + 30, y + 15);

Draws the top-facing surface of the cube using isometric coordinates (the brightest face)

calculation Left Face (Darkened) quad(x, y, x + 30, y + 15, x + 30, y + 45, x, y + 30);

Draws the left side of the cube, darkened by 20% to create depth

calculation Right Face (Darkest) quad(x + 30, y + 15, x + 60, y, x + 60, y + 30, x + 30, y + 45);

Draws the right side of the cube, darkened by 40% to create strong shadow

var drawCube = function(x, y, col, angle) {
Defines a function that takes the block's isometric x and y position, its color, and its rotation angle from Matter.js
push();
Saves the current transformation matrix so camera and rotation only affect this cube
translate(0, camY);
Moves everything down by camY pixels, which shifts the entire canvas view upward as the tower grows
translate(x + 30, y + 15);
Moves the origin to the center of the cube (offset from its top-left corner)
rotate(angle);
Rotates the cube around its center by the angle provided by the Matter.js physics body
translate(-(x + 30), -(y + 15));
Moves the origin back so the cube draws at its original position, but now rotation is applied
fill(col);
Sets the fill color to the cube's assigned color for the top face
quad(x, y, x + 30, y - 15, x + 60, y, x + 30, y + 15);
Draws the top face using four isometric points that create a diamond shape
fill(lerpColor(col, color(0), 0.2));
Blends the cube's color 20% toward black, darkening the left face for depth
quad(x, y, x + 30, y + 15, x + 30, y + 45, x, y + 30);
Draws the left face of the isometric cube
fill(lerpColor(col, color(0), 0.4));
Blends the cube's color 40% toward black, creating even darker shadow on the right
quad(x + 30, y + 15, x + 60, y, x + 60, y + 30, x + 30, y + 45);
Draws the right face of the isometric cube
pop();
Restores the transformation matrix so the next cube is not affected by this one's rotation and translation

setup()

setup() runs exactly once when the sketch starts. It configures the canvas, initializes the Matter.js physics engine with a world and ground, creates the first block, and prepares sound and UI elements. Understanding setup is crucial because all global variables and physics bodies are created here, laying the foundation for the entire game.

function setup() {
    createCanvas(400, 600);

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

    // Create a static ground body for Matter.js
    // The isometric ground is at y=350. The bottom of our Matter.js block
    // (which is 30px high) will align with 350 + 30 = 380.
    // So the ground should be centered slightly below that.
    let groundThickness = 20;
    groundBody = Matter.Bodies.rectangle(width / 2, 380 + groundThickness / 2, width, groundThickness, { isStatic: true });
    Matter.World.add(world, groundBody);
    physicsBodies.push(groundBody); // Keep track for restart

    // Start base block with its Matter.js body (static)
    // The Matter.js body's center should be at (isometricX + 30, isometricY + 15)
    // Increased density for base block to make it very stable
    // Increased friction for less slipperiness
    let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { 
        isStatic: true, 
        density: 1.0, // FURTHER Increased density for base block
        friction: 0.8, // Increased friction
        frictionStatic: 1.0, // MAX Increased static friction
        restitution: 0 // No bounciness
    }); 
    Matter.World.add(world, baseBlockBody);
    physicsBodies.push(baseBlockBody);
    blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody }); // Store body reference

    // Initialize p5.sound Oscillator
    blockDropSound = new p5.Oscillator('sine');
    blockDropSound.amp(0);
    blockDropSound.start();

    restartButton = createButton('Restart Game');
    restartButton.mousePressed(restartSketch);
    restartButton.style('font-size', '24px');
    restartButton.style('padding', '10px 20px');
    restartButton.style('background-color', '#4CAF50'); // Green
    restartButton.style('color', 'white');
    restartButton.style('border', 'none');
    restartButton.style('cursor', 'pointer');
    restartButton.style('border-radius', '5px');
    restartButton.hide();

    userStartAudio(); // Required for p5.sound to work
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

calculation Canvas Creation createCanvas(400, 600);

Creates a 400x600 pixel canvas for the game

calculation Matter.js Engine Setup engine = Matter.Engine.create(); world = engine.world; world.gravity.y = 1;

Initializes the physics engine and sets gravity downward

calculation Ground Body let groundThickness = 20; groundBody = Matter.Bodies.rectangle(width / 2, 380 + groundThickness / 2, width, groundThickness, { isStatic: true }); Matter.World.add(world, groundBody);

Creates a static (immovable) ground platform at the bottom for blocks to land on

calculation Base Block Creation let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { isStatic: true, density: 1.0, friction: 0.8, frictionStatic: 1.0, restitution: 0 }); Matter.World.add(world, baseBlockBody); physicsBodies.push(baseBlockBody); blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody });

Creates the starting block that the player builds on top of, with high friction to prevent slipping

calculation Audio Setup blockDropSound = new p5.Oscillator('sine'); blockDropSound.amp(0); blockDropSound.start();

Creates an oscillator for sound effects (plays when blocks drop)

calculation Restart Button Styling restartButton = createButton('Restart Game'); restartButton.mousePressed(restartSketch); restartButton.style('font-size', '24px'); restartButton.style('padding', '10px 20px'); restartButton.style('background-color', '#4CAF50'); restartButton.style('color', 'white'); restartButton.style('border', 'none'); restartButton.style('cursor', 'pointer'); restartButton.style('border-radius', '5px'); restartButton.hide();

Creates a styled restart button that is hidden until the game ends

createCanvas(400, 600);
Creates a 400 pixels wide by 600 pixels tall drawing canvas
engine = Matter.Engine.create();
Initializes a new Matter.js physics engine that will simulate all block collisions and gravity
world = engine.world;
Gets a reference to the physics world inside the engine where all bodies will exist
world.gravity.y = 1;
Sets gravity to pull objects downward at a strength of 1 (standard gravity in Matter.js)
let groundThickness = 20;
Defines how thick the ground platform is (20 pixels tall)
groundBody = Matter.Bodies.rectangle(width / 2, 380 + groundThickness / 2, width, groundThickness, { isStatic: true });
Creates a rectangle physics body centered at the bottom of the canvas with isStatic: true so it never moves
Matter.World.add(world, groundBody);
Adds the ground body to the physics world so blocks can collide with it
physicsBodies.push(groundBody);
Stores the ground in a list for cleanup when the game restarts
let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { isStatic: true, density: 1.0, friction: 0.8, frictionStatic: 1.0, restitution: 0 });
Creates the first block the player will stack on; isStatic: true means it never falls, and high friction (0.8) prevents new blocks from sliding off
blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody });
Adds the base block to the game's blocks array with its drawing position, color, and physics body reference
blockDropSound = new p5.Oscillator('sine');
Creates a sine wave oscillator that will make a beep sound when blocks are placed
blockDropSound.amp(0);
Sets the initial volume to 0 (silent) so the sound only plays when we explicitly trigger it
blockDropSound.start();
Starts the oscillator running (at zero volume) so it is ready to make noise instantly when needed
restartButton = createButton('Restart Game');
Creates a button labeled 'Restart Game' that appears when the game ends
restartButton.mousePressed(restartSketch);
Tells the button to call the restartSketch() function when clicked
userStartAudio();
Required by p5.sound—allows the browser to play audio (needed for sound effects to work)

draw()

draw() is the animation heartbeat—it runs 60 times per second and is responsible for updating physics, moving the camera, rendering all blocks, and managing game state. Understanding the order of operations is crucial: background clears first (preventing trails), physics updates second (moving bodies), then we draw based on updated positions. The game over check exits early to prevent drawing the game while in failure state.

🔬 This code waits for a block to stop moving before letting the next one slide in. What happens if you change the threshold from 0.1 to 0.5—will blocks settle faster or slower before the next one appears?

    // Logic to allow the next moving block to appear after the previous one settles
    if (isDropping && blocks.length > 1) { // Only check if we're waiting and there's more than the base block
        let lastPlacedBlock = blocks[blocks.length - 1];
        if (lastPlacedBlock.body && abs(lastPlacedBlock.body.velocity.y) < 0.1) { // If it's almost stopped moving vertically
            isDropping = false; // Allow the next block to appear
        }
    }
function draw() {
    background(25, 25, 35);
    
    if (gameOver) {
        fill(255); textSize(40); textAlign(CENTER, CENTER); text("TOWER FELL!", width / 2, height / 2 - 50);
        textSize(20); text("Final Score: " + score, width / 2, height / 2 - 10);
        
        // Position the restart button
        restartButton.position(width / 2 - restartButton.width / 2, height / 2 + 50);
        restartButton.show();
        return;
    }

    // Update Matter.js physics engine
    Matter.Engine.update(engine);

    // Camera Logic
    if (score >= 11) {
        var targetCam = (score - 10) * 30;
        camY = lerp(camY, targetCam, 0.1); 
    }

    // Draw Stack & Check Game Over
    for (var i = 0; i < blocks.length; i++) {
        let block = blocks[i];
        
        if (block.body) {
            // Update p5.js drawing coordinates from Matter.js body position
            // Convert Matter.js center (body.position) back to isometric top-left (block.x, block.y)
            block.x = block.body.position.x - 30;
            block.y = block.body.position.y - 15;
            
            // Check if block has fallen below the ground
            if (block.body.bounds.max.y > groundBody.bounds.max.y + 5) { // Add a small buffer
                gameOver = true;
                // Optional: Play game over sound
                blockDropSound.freq(100);
                blockDropSound.amp(0.8, 0.05);
                blockDropSound.amp(0, 0.5, 0.2);
                if (navigator.vibrate) {
                    navigator.vibrate([100, 50, 100]); // Longer, pulsed vibration
                }
                break; // No need to check further blocks
            }

            drawCube(block.x, block.y, block.color, block.body.angle);
        } else {
            // This case should ideally not happen if all blocks get bodies,
            // but included for robustness.
            drawCube(block.x, block.y, block.color, 0);
        }
    }

    // Logic to allow the next moving block to appear after the previous one settles
    if (isDropping && blocks.length > 1) { // Only check if we're waiting and there's more than the base block
        let lastPlacedBlock = blocks[blocks.length - 1];
        if (lastPlacedBlock.body && abs(lastPlacedBlock.body.velocity.y) < 0.1) { // If it's almost stopped moving vertically
            isDropping = false; // Allow the next block to appear
        }
    }

    // Moving Block (only moves if the previous block has settled)
    if (!isDropping) {
        currentX += speed * dir;
        if (currentX > 340 || currentX < 0) { dir *= -1; }
        
        // The height of the new block is based on how many are in the list
        var currentHeight = 350 - (blocks.length * 30);
        drawCube(currentX, currentHeight, color(255, 200, 50), 0); // Moving block has no angle yet
    }

    // UI
    fill(255); textSize(25); textAlign(LEFT, TOP);
    text("LEVEL: " + score, 20, 40);
}
Line-by-line explanation (35 lines)

🔧 Subcomponents:

calculation Background Clear background(25, 25, 35);

Erases the previous frame with a dark blue-gray color

conditional Game Over State Display if (gameOver) { fill(255); textSize(40); textAlign(CENTER, CENTER); text("TOWER FELL!", width / 2, height / 2 - 50); textSize(20); text("Final Score: " + score, width / 2, height / 2 - 10); // Position the restart button restartButton.position(width / 2 - restartButton.width / 2, height / 2 + 50); restartButton.show(); return;

If game is over, display failure text and restart button, then exit draw() immediately

calculation Physics Simulation Matter.Engine.update(engine);

Advances the Matter.js physics simulation one frame, moving all bodies and resolving collisions

conditional Camera Movement if (score >= 11) { var targetCam = (score - 10) * 30; camY = lerp(camY, targetCam, 0.1);

Once the tower is 11+ blocks tall, smoothly scrolls the camera upward to follow the tower

for-loop Render All Blocks for (var i = 0; i < blocks.length; i++) {

Loops through every block and updates its drawing position from physics, checks for game over, and draws it

conditional Block Settlement Detection if (isDropping && blocks.length > 1) { let lastPlacedBlock = blocks[blocks.length - 1]; if (lastPlacedBlock.body && abs(lastPlacedBlock.body.velocity.y) < 0.1) { isDropping = false;

Checks if the last dropped block has stopped moving vertically, allowing a new block to slide in

conditional Moving Block Animation if (!isDropping) { currentX += speed * dir; if (currentX > 340 || currentX < 0) { dir *= -1; } // The height of the new block is based on how many are in the list var currentHeight = 350 - (blocks.length * 30); drawCube(currentX, currentHeight, color(255, 200, 50), 0);

Slides the moving block left and right at the top of the screen, bouncing off edges

background(25, 25, 35);
Clears the entire canvas with a dark blue-gray color, erasing everything drawn last frame
if (gameOver) {
Checks whether the tower has fallen or a block was missed
fill(255); textSize(40); textAlign(CENTER, CENTER); text("TOWER FELL!", width / 2, height / 2 - 50);
Sets white text at size 40, centered, and displays 'TOWER FELL!' in the middle of the screen
textSize(20); text("Final Score: " + score, width / 2, height / 2 - 10);
Displays the final score at a smaller size just below the main message
restartButton.position(width / 2 - restartButton.width / 2, height / 2 + 50);
Centers the restart button horizontally and places it below the game over text
restartButton.show();
Makes the restart button visible on screen
return;
Exits the draw() function immediately, skipping the rest of the frame (physics, drawing, etc.)
Matter.Engine.update(engine);
Tells the physics engine to calculate one frame of gravity, movement, and collisions for all bodies
if (score >= 11) {
Once the player stacks 11 or more blocks, start moving the camera
var targetCam = (score - 10) * 30;
Calculates how far up the camera should move: each block above 10 scrolls the view up 30 pixels
camY = lerp(camY, targetCam, 0.1);
Smoothly interpolates camY toward its target value at 10% per frame, creating a gentle scrolling effect
for (var i = 0; i < blocks.length; i++) {
Loops through every block in the blocks array, one at a time
let block = blocks[i];
Gets the current block from the array
if (block.body) {
Checks if this block has a physics body (all stacked blocks should)
block.x = block.body.position.x - 30;
Updates the block's drawing x position from its physics body's x coordinate (subtracting 30 because Matter.js uses center, isometric uses top-left)
block.y = block.body.position.y - 15;
Updates the block's drawing y position from its physics body's y coordinate
if (block.body.bounds.max.y > groundBody.bounds.max.y + 5) {
Checks if the block has fallen below the ground—if so, the tower has collapsed
gameOver = true;
Sets the game state to game over
blockDropSound.freq(100);
Sets the sound effect frequency to 100 Hz (a low beep)
blockDropSound.amp(0.8, 0.05);
Fades the sound up to volume 0.8 over 0.05 seconds
blockDropSound.amp(0, 0.5, 0.2);
Fades the sound out to 0 volume starting after 0.5 seconds, taking 0.2 seconds to fade
if (navigator.vibrate) {
Checks if the device supports vibration (most mobile phones do)
navigator.vibrate([100, 50, 100]);
Triggers a vibration pattern: 100ms vibrate, 50ms pause, 100ms vibrate
drawCube(block.x, block.y, block.color, block.body.angle);
Draws the block at its current position with its current rotation angle from the physics body
if (isDropping && blocks.length > 1) {
If a block is currently settling and there are multiple blocks, check whether it has stopped
let lastPlacedBlock = blocks[blocks.length - 1];
Gets the most recently placed block from the end of the array
if (lastPlacedBlock.body && abs(lastPlacedBlock.body.velocity.y) < 0.1) {
If the block's vertical velocity is very small (nearly stationary), it has settled
isDropping = false;
Allows the next block to start sliding into view
if (!isDropping) {
Only draw and move the sliding block if no previous block is currently settling
currentX += speed * dir;
Moves the sliding block left or right by speed pixels per frame, in the direction of dir (1 or -1)
if (currentX > 340 || currentX < 0) { dir *= -1; }
When the block reaches the edge of the canvas, flips dir to reverse direction and bounce it back
var currentHeight = 350 - (blocks.length * 30);
Calculates how high the sliding block should be: it stacks 30 pixels per existing block
drawCube(currentX, currentHeight, color(255, 200, 50), 0);
Draws the sliding block in yellow at the sliding position (angle is 0 because it hasn't been dropped yet)
fill(255); textSize(25); textAlign(LEFT, TOP);
Sets up text drawing: white color, size 25, positioned at top-left
text("LEVEL: " + score, 20, 40);
Displays the current score as 'LEVEL:' and a number at coordinates (20, 40)

mousePressed()

mousePressed() is called automatically by p5.js whenever the user clicks. It performs hit detection by measuring the distance between the sliding block and the block below, creates a new falling physics body if accurate, or ends the game if the click misses. This function is where all the core gameplay logic happens—it bridges player interaction with physics simulation.

🔬 This code measures how accurate your click was. What happens if you change the threshold from 45 to 20—do blocks become easier or harder to place?

    var lastBlock = blocks[blocks.length - 1];
    var accuracy = abs(currentX - lastBlock.x);

    // Hit detection (must be within 45 pixels of the block below)
    if (accuracy < 45) {

🔬 Each successful block places increase the score AND speed up the next block by 0.25 pixels per frame. What happens if you change 0.25 to 0.1—does the game get easier or stay harder?

        score++;
        speed += 0.25; // Speed up
function mousePressed() {
    // Only allow interaction if game is not over and no block is currently settling
    if (gameOver || isDropping) { 
        return; 
    }
    
    var lastBlock = blocks[blocks.length - 1];
    var accuracy = abs(currentX - lastBlock.x);

    // Hit detection (must be within 45 pixels of the block below)
    if (accuracy < 45) {
        // Create a new Matter.js body for the block
        // It's non-static, so it will fall down.
        // Start it slightly above its target isometric Y.
        // Increased density for falling blocks
        // Increased friction for less slipperiness
        let bodyCenterX = currentX + 30;
        let bodyCenterY = (350 - (blocks.length * 30)) + 15 - 80; // Isometric target Y + 15 (center) - 80 (initial drop offset)
        let newBlockBody = Matter.Bodies.rectangle(bodyCenterX, bodyCenterY, 50, 30, { 
            density: 0.8, // FURTHER Increased density
            friction: 0.8, // Increased friction
            frictionStatic: 1.0, // MAX Increased static friction
            restitution: 0 // No bounciness
        }); 
        Matter.World.add(world, newBlockBody);
        physicsBodies.push(newBlockBody); // Add to our list for cleanup

        blocks.push({
            x: currentX, // This will be updated by the physics engine in draw()
            y: (350 - (blocks.length * 30)), // This is the isometric target Y, but physics will take over
            color: color(random(100, 255), 100, random(100, 255)),
            body: newBlockBody // Store reference to Matter.js body
        });
        score++;
        speed += 0.25; // Speed up

        // Trigger sound effect
        blockDropSound.freq(random(300, 600));
        blockDropSound.amp(0.5, 0.05);
        blockDropSound.amp(0, 0.2, 0.1);

        // Trigger haptic feedback (vibration)
        if (navigator.vibrate) {
            navigator.vibrate(50);
        }

        isDropping = true; // Set flag: a block is currently settling
        currentX = 0;      // Reset currentX for the next moving block
        dir = 1;           // Reset direction for the next moving block
    } else {
        gameOver = true;
        // Play game over sound and vibration (already in draw's gameOver check now)
    }
}
Line-by-line explanation (28 lines)

🔧 Subcomponents:

conditional Game State Validation if (gameOver || isDropping) { return; }

Prevents clicks from registering if the game is over or a block is still settling

conditional Accuracy Check var lastBlock = blocks[blocks.length - 1]; var accuracy = abs(currentX - lastBlock.x); // Hit detection (must be within 45 pixels of the block below) if (accuracy < 45) {

Measures the distance between the sliding block and the block below; if close enough, it's a successful placement

calculation New Block Physics let bodyCenterX = currentX + 30; let bodyCenterY = (350 - (blocks.length * 30)) + 15 - 80; let newBlockBody = Matter.Bodies.rectangle(bodyCenterX, bodyCenterY, 50, 30, { density: 0.8, friction: 0.8, frictionStatic: 1.0, restitution: 0 }); Matter.World.add(world, newBlockBody);

Creates a dynamic (falling) physics body for the new block, positioned high above and ready to drop

calculation Block Storage blocks.push({ x: currentX, y: (350 - (blocks.length * 30)), color: color(random(100, 255), 100, random(100, 255)), body: newBlockBody });

Adds the new block to the game's blocks array with a random magenta-ish color

calculation Score and Speed score++; speed += 0.25;

Increases the score by 1 and makes the next block slide 0.25 pixels per frame faster

calculation Audio Feedback blockDropSound.freq(random(300, 600)); blockDropSound.amp(0.5, 0.05); blockDropSound.amp(0, 0.2, 0.1);

Plays a beep sound at a random pitch, ramping up then down in volume

conditional Miss Detection } else { gameOver = true;

If the block is dropped too far from the one below, the game ends

function mousePressed() {
This function is called automatically whenever the mouse is pressed anywhere on the canvas
if (gameOver || isDropping) {
Checks whether the game is already over or a block is currently settling
return;
If either condition is true, exits the function immediately without processing the click
var lastBlock = blocks[blocks.length - 1];
Gets the most recent block in the stack (the one the new block will land on)
var accuracy = abs(currentX - lastBlock.x);
Calculates the distance between the sliding block's position and the block below using absolute value (always positive)
if (accuracy < 45) {
If the distance is less than 45 pixels, the click was accurate enough—place the block
let bodyCenterX = currentX + 30;
Calculates the physics body's center x position (30 pixels to the right of the isometric top-left corner)
let bodyCenterY = (350 - (blocks.length * 30)) + 15 - 80;
Calculates the physics body's center y position: the block's target y, plus 15 for center, minus 80 to start high above where it will fall from
let newBlockBody = Matter.Bodies.rectangle(bodyCenterX, bodyCenterY, 50, 30, { density: 0.8, friction: 0.8, frictionStatic: 1.0, restitution: 0 });
Creates a new physics body rectangle 50 pixels wide and 30 tall with medium density, high friction to prevent slipping, and no bounciness
Matter.World.add(world, newBlockBody);
Adds the new body to the physics world so it experiences gravity and can collide with other blocks
physicsBodies.push(newBlockBody);
Stores the body in a list for cleanup if the game restarts
blocks.push({
Begins adding a new block object to the blocks array
x: currentX,
Sets the block's initial drawing x position to where the sliding block was
y: (350 - (blocks.length * 30)),
Sets the block's initial drawing y position based on the current stack height (will be overridden by physics)
color: color(random(100, 255), 100, random(100, 255)),
Assigns a random color with red and blue channels randomized, green fixed at 100—creating random purple/magenta/pink hues
body: newBlockBody
Stores the reference to the physics body so we can track the block's position and rotation
score++;
Increments the score by 1 (or the level count)
speed += 0.25;
Increases the sliding block's speed by 0.25 pixels per frame, making future blocks harder to catch
blockDropSound.freq(random(300, 600));
Sets the beep sound to a random pitch between 300 and 600 Hz
blockDropSound.amp(0.5, 0.05);
Raises the sound volume to 0.5 over 0.05 seconds (a quick ramp up)
blockDropSound.amp(0, 0.2, 0.1);
Lowers the sound volume back to 0 starting after 0.2 seconds, taking 0.1 seconds to fade out
if (navigator.vibrate) {
Checks if the device supports the vibration API (most modern phones do)
navigator.vibrate(50);
Triggers a 50-millisecond vibration pulse as haptic feedback
isDropping = true;
Sets the flag to prevent a new sliding block from appearing until this one settles
currentX = 0;
Resets the sliding block's position to the left edge for the next block
dir = 1;
Resets the sliding direction to 1 (moving right) for the next block
} else {
If the accuracy check failed (block too far from the target), this else runs
gameOver = true;
Sets the game state to game over, ending the game immediately

restartSketch()

restartSketch() is called by the restart button and resets the entire game to its initial state. It carefully clears physics bodies (except the ground), resets all game variables, and recreates the base block. This function demonstrates careful cleanup—especially the backward loop through physicsBodies, which is essential when removing items from an array.

function restartSketch() {
    score = 0;
    gameOver = false;
    speed = 4;
    currentX = 0;
    dir = 1;
    camY = 0;
    blocks = [];
    
    // Clear all physics bodies from the world except the ground
    for (let i = physicsBodies.length - 1; i >= 0; i--) {
        if (physicsBodies[i] !== groundBody) { // Don't remove the ground
            Matter.World.remove(world, physicsBodies[i]);
            physicsBodies.splice(i, 1);
        }
    }

    // Re-add the base block with its Matter.js body (static)
    // Use the same increased density and friction for the base block
    let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { 
        isStatic: true, 
        density: 1.0, // FURTHER Increased density
        friction: 0.8, // Increased friction
        frictionStatic: 1.0, // MAX Increased static friction
        restitution: 0 // No bounciness
    });
    Matter.World.add(world, baseBlockBody);
    physicsBodies.push(baseBlockBody);
    blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody });

    restartButton.hide();
    isDropping = false; // Reset dropping flag
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Game State Reset score = 0; gameOver = false; speed = 4; currentX = 0; dir = 1; camY = 0; blocks = [];

Resets all game variables to their initial values from setup()

for-loop Physics Body Cleanup for (let i = physicsBodies.length - 1; i >= 0; i--) { if (physicsBodies[i] !== groundBody) { Matter.World.remove(world, physicsBodies[i]); physicsBodies.splice(i, 1); } }

Removes all physics bodies except the ground from the world and the tracking array

calculation Base Block Recreation let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { isStatic: true, density: 1.0, friction: 0.8, frictionStatic: 1.0, restitution: 0 }); Matter.World.add(world, baseBlockBody); physicsBodies.push(baseBlockBody); blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody });

Recreates the starting base block so a fresh tower can be built

function restartSketch() {
Defines the function that runs when the player clicks the restart button
score = 0;
Resets the score back to zero
gameOver = false;
Allows the game to resume by setting gameOver to false
speed = 4;
Resets the sliding block speed to its initial value
currentX = 0;
Resets the sliding block's position to the left edge
dir = 1;
Resets the sliding direction to rightward
camY = 0;
Resets the camera position to the top of the canvas
blocks = [];
Clears the entire blocks array, removing all stacked blocks from game memory
for (let i = physicsBodies.length - 1; i >= 0; i--) {
Loops through the physicsBodies array backward (important for safe splicing)
if (physicsBodies[i] !== groundBody) {
Checks if this body is NOT the ground (we want to keep the ground)
Matter.World.remove(world, physicsBodies[i]);
Removes the body from the physics world so it no longer simulates
physicsBodies.splice(i, 1);
Removes the body from the physicsBodies tracking array at position i
let baseBlockBody = Matter.Bodies.rectangle(170 + 30, 350 + 15, 50, 30, { isStatic: true, density: 1.0, friction: 0.8, frictionStatic: 1.0, restitution: 0 });
Creates a new static base block with the same properties as the original
Matter.World.add(world, baseBlockBody);
Adds the new base block's physics body to the world
physicsBodies.push(baseBlockBody);
Tracks the base block in the physicsBodies array for future cleanup
blocks.push({ x: 170, y: 350, color: color(100, 200, 255), body: baseBlockBody });
Adds the base block to the game's blocks array with a blue color
restartButton.hide();
Hides the restart button so it doesn't stay visible during the new game
isDropping = false;
Resets the settling flag to allow the moving block to slide immediately

📦 Key Variables

engine Matter.Engine

The Matter.js physics engine that simulates gravity, friction, collisions, and movement for all blocks

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

The physics world inside the engine where all bodies exist and interact

world = engine.world;
groundBody Matter.Body

The static (non-moving) physics body representing the ground platform that blocks land on and rest against

groundBody = Matter.Bodies.rectangle(width / 2, 380 + groundThickness / 2, width, groundThickness, { isStatic: true });
physicsBodies array

Stores references to all physics bodies in the world so they can be cleaned up when the game restarts

let physicsBodies = [];
blocks array

Stores all stacked blocks, each with properties: x (horizontal position), y (vertical position), color, and body (physics reference)

var blocks = [];
currentX number

The horizontal position of the sliding block at the top of the screen

var currentX = 0;
speed number

How many pixels per frame the sliding block moves left or right; increases with each successful placement

var speed = 4;
dir number

The direction the sliding block is moving: 1 for right, -1 for left

var dir = 1;
score number

The player's current score (number of successfully placed blocks, displayed as LEVEL)

var score = 0;
gameOver boolean

Flag indicating whether the tower has collapsed or the player missed a block

var gameOver = false;
camY number

The vertical offset of the camera; increases as the tower grows to keep it centered on screen

var camY = 0;
restartButton p5.Renderer

The HTML button element that appears when the game ends and restarts the game when clicked

let restartButton = createButton('Restart Game');
blockDropSound p5.Oscillator

An oscillator that generates a beep sound effect each time a block is successfully placed

blockDropSound = new p5.Oscillator('sine');
isDropping boolean

Flag indicating whether a block is currently falling and settling; prevents new blocks from moving during this time

let isDropping = false;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG drawCube()

Camera translation is applied before rotation, which can cause visual discontinuities when blocks rotate near the screen edge.

💡 Apply camera translation after rotation transforms to ensure blocks rotate consistently relative to the viewport: apply camY translation to the block's final position rather than before rotation.

PERFORMANCE draw()

All blocks are redrawn every frame even if they haven't moved, and physics bodies are never destroyed when blocks fall off-screen.

💡 Track off-screen blocks and remove them from both the blocks array and Matter.js world once they fall below groundBody.bounds.max.y + 200 pixels. This prevents memory leaks in long play sessions.

BUG mousePressed()

The isDropping flag relies on vertical velocity < 0.1, but blocks can oscillate slightly and never reach perfectly zero velocity due to physics friction, potentially causing the next block to appear before visual settlement.

💡 Use a time-based approach instead: set a timestamp when a block is placed, and allow the next block to appear after 500ms has elapsed, regardless of velocity. This provides more consistent timing.

STYLE setup()

Physics body properties (density, friction, restitution) are repeated multiple times with hardcoded values in setup(), mousePressed(), and restartSketch(), making tweaking and maintenance difficult.

💡 Define constants at the top of the sketch for block physics properties: `const BLOCK_DENSITY = 0.8; const BLOCK_FRICTION = 0.8;` and reuse them everywhere, reducing code duplication and making physics easier to tune globally.

FEATURE draw() and mousePressed()

There is no visual feedback (flashing, color change, or animation) when a placement is barely successful vs. barely a miss, making it hard for players to learn timing.

💡 Add a 'lastAccuracy' variable and display a visual indicator (colored ring or text) showing how close the last placement was: bright green for perfect, yellow for close, red for miss. Update it each frame to guide player learning.

🔄 Code Flow

Code flow showing drawcube, setup, draw, mousepressed, restartsketch

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-setup[canvas-setup] setup --> matter-init[matter-init] setup --> ground-creation[ground-creation] setup --> base-block-setup[base-block-setup] setup --> sound-init[sound-init] setup --> button-setup[button-setup] draw --> background-clear[background-clear] draw --> gameover-check[gameover-check] draw --> physics-update[physics-update] draw --> camera-logic[camera-logic] draw --> block-loop[block-loop] draw --> moving-block-render[moving-block-render] click background-clear href "#sub-background-clear" click gameover-check href "#sub-gameover-check" click physics-update href "#sub-physics-update" click camera-logic href "#sub-camera-logic" click block-loop href "#sub-block-loop" click moving-block-render href "#sub-moving-block-render" block-loop --> settle-check[settle-check] block-loop --> state-check[state-check] block-loop --> hit-detection[hit-detection] click settle-check href "#sub-settle-check" click state-check href "#sub-state-check" click hit-detection href "#sub-hit-detection" hit-detection --> miss-condition[miss-condition] hit-detection --> physics-body-creation[physics-body-creation] click miss-condition href "#sub-miss-condition" click physics-body-creation href "#sub-physics-body-creation" physics-body-creation --> block-addition[block-addition] block-addition --> score-increment[score-increment] block-addition --> sound-effect[sound-effect] click block-addition href "#sub-block-addition" click score-increment href "#sub-score-increment" click sound-effect href "#sub-sound-effect" restartsketch --> variable-reset[variable-reset] restartsketch --> physics-cleanup[physics-cleanup] restartsketch --> base-reset[base-reset] click variable-reset href "#sub-variable-reset" click physics-cleanup href "#sub-physics-cleanup" click base-reset href "#sub-base-reset" camera-logic --> camera-translate[camera-translate] click camera-translate href "#sub-camera-translate" draw -->|repeat| draw

❓ Frequently Asked Questions

What visual experience does the block stacker-2.0 sketch provide?

The sketch creates a colorful, isometric 3D scene where users can stack blocks to build a tower that sways slightly, enhancing the illusion of depth and realism.

How can users interact with the block stacker-2.0 sketch?

Users can click or press a key to drop the stacked blocks, allowing them to observe the dynamics of physics as the blocks tumble and settle.

What creative coding concepts are showcased in the block stacker-2.0 sketch?

This sketch demonstrates the integration of physics simulation using Matter.js and the rendering of isometric 3D graphics with p5.js.

Preview

block stacker-2.0 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of block stacker-2.0 - Code flow showing drawcube, setup, draw, mousepressed, restartsketch
Code Flow Diagram