Simple MineCraft Cline

This sketch creates a 2D Minecraft-like sandbox game where players can move around a procedurally generated terrain, jump, and place or break blocks. The world is made of tiles (grass, dirt, stone, wood, leaves) with physics simulation, a camera system that follows the player, and both keyboard/mouse and touch controls for mobile devices.

๐ŸŽ“ Concepts You'll Learn

2D tile-based world generationPerlin noise for terrainPhysics simulation with gravity and collision detectionCamera systemBlock placement and breakingTouch controls and mobile supportAudio synthesis with p5.soundHotbar/inventory systemProcedural tree generation

๐Ÿ”„ Code Flow

Code flow showing setup, draw, initworld, handleinput, keypressed, updateplayer, updatecamera, drawworld, drawplayer, drawcursorhighlight, drawui, getblockcolor, mousepressed, handleworldtap, touchstarted, touchended, handletouches, isintouchbutton, drawtouchcontrols, handlehotbarclick, isinworld, issolid, issolidtile, rectsoverlap, playtone, playjumpsound, playblockplacesound, playselectsound, playblockbreaksound, initaudiocontext, windowresized

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> noise-seed[Random Noise Seed] setup --> touch-detection[Touch Device Detection] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click noise-seed href "#sub-noise-seed" click touch-detection href "#sub-touch-detection" draw --> input-handling[Input Processing] draw --> physics-update[Physics Update] draw --> camera-update[Camera Update] draw --> drawworld[drawWorld] draw --> drawui[drawUI] draw --> drawcursorhighlight[drawCursorHighlight] draw --> drawplayer[drawPlayer] draw --> drawtouchcontrols[drawTouchControls] click draw href "#fn-draw" click input-handling href "#sub-input-handling" click physics-update href "#sub-physics-update" click camera-update href "#sub-camera-update" click drawworld href "#fn-drawworld" click drawui href "#fn-drawui" click drawcursorhighlight href "#fn-drawcursorhighlight" click drawplayer href "#fn-drawplayer" click drawtouchcontrols href "#fn-drawtouchcontrols" input-handling --> reset-velocity[Reset Horizontal Velocity] input-handling --> left-movement[Left Movement Check] input-handling --> right-movement[Right Movement Check] input-handling --> touch-jump[Touch Jump Handler] input-handling --> jump-handler[Keyboard Jump Handler] input-handling --> hotbar-select[Hotbar Selection] click reset-velocity href "#sub-reset-velocity" click left-movement href "#sub-left-movement" click right-movement href "#sub-right-movement" click touch-jump href "#sub-touch-jump" click jump-handler href "#sub-jump-handler" click hotbar-select href "#sub-hotbar-select" physics-update --> gravity-application[Gravity Application] physics-update --> horizontal-collision[Horizontal Collision Detection] physics-update --> vertical-collision[Vertical Collision Detection] click gravity-application href "#sub-gravity-application" click horizontal-collision href "#sub-horizontal-collision" click vertical-collision href "#sub-vertical-collision" camera-update --> horizontal-camera[Horizontal Camera Centering] camera-update --> vertical-camera[Vertical Camera Centering] click horizontal-camera href "#sub-horizontal-camera" click vertical-camera href "#sub-vertical-camera" drawworld --> culling-horizontal[Horizontal Culling] drawworld --> culling-vertical[Vertical Culling] drawworld --> block-rendering[Block Rendering Loop] click culling-horizontal href "#sub-culling-horizontal" click culling-vertical href "#sub-culling-vertical" click block-rendering href "#sub-block-rendering" block-rendering --> color-switch[Block Color Selection] click color-switch href "#sub-color-switch" drawui --> background-bar[UI Bar Background] drawui --> hotbar-loop[Hotbar Slot Rendering] drawui --> instructions-panel[Instructions Panel] click background-bar href "#sub-background-bar" click hotbar-loop href "#sub-hotbar-loop" click instructions-panel href "#sub-instructions-panel" hotbar-loop --> slot-highlight[Selected Slot Highlight] click slot-highlight href "#sub-slot-highlight" drawcursorhighlight --> mouse-to-tile[Mouse to Tile Conversion] drawcursorhighlight --> bounds-check[World Bounds Check] click mouse-to-tile href "#sub-mouse-to-tile" click bounds-check href "#sub-bounds-check" drawplayer --> body-drawing[Body Drawing] drawplayer --> pants-drawing[Pants Drawing] click body-drawing href "#sub-body-drawing" click pants-drawing href "#sub-pants-drawing" drawtouchcontrols --> button-dimensions[Button Dimensions] drawtouchcontrols --> left-button-check[Left Button Detection] drawtouchcontrols --> right-button-check[Right Button Detection] drawtouchcontrols --> jump-button-check[Jump Button Detection] click button-dimensions href "#sub-button-dimensions" click left-button-check href "#sub-left-button-check" click right-button-check href "#sub-right-button-check" click jump-button-check href "#sub-jump-button-check"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas size, world generation, and detects whether the user is on a touch device (mobile/tablet) or desktop.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noiseSeed(floor(random(100000)));
  initWorld();
  isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

๐Ÿ”ง Subcomponents:

function-call Canvas Creation createCanvas(windowWidth, windowHeight)

Creates a canvas that fills the entire window

function-call Random Noise Seed noiseSeed(floor(random(100000)))

Seeds the Perlin noise generator with a random value so terrain is different each time

conditional Touch Device Detection isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0

Detects if the device supports touch input to enable on-screen buttons

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a p5.js canvas that fills the entire browser window, allowing the game to be responsive
noiseSeed(floor(random(100000)))
Seeds the Perlin noise function with a random number so each game generates different terrain
initWorld()
Calls the function that creates the 2D world array and generates all terrain, trees, and player spawn
isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
Checks if the device has touch capabilities by looking for touch event support or touch points

draw()

draw() runs 60 times per second (default frame rate). Each frame, it updates the game state (input, physics, camera) and then renders everything to the screen in order.

function draw() {
  background(135, 206, 235);
  handleInput();
  updatePlayer();
  updateCamera();
  drawWorld();
  drawPlayer();
  drawCursorHighlight();
  if (isTouchDevice) {
    drawTouchControls();
  }
  drawUI();
}

๐Ÿ”ง Subcomponents:

function-call Input Processing handleInput()

Processes keyboard and touch button input to update player velocity

function-call Physics Update updatePlayer()

Applies gravity, updates position, and handles collision detection

function-call Camera Update updateCamera()

Centers the camera on the player and constrains it to world bounds

conditional Touch Controls Display if (isTouchDevice) { drawTouchControls(); }

Only draws on-screen buttons if the device supports touch

Line by Line:

background(135, 206, 235)
Fills the entire canvas with sky blue color, clearing the previous frame
handleInput()
Reads keyboard keys and touch button states to determine player movement direction
updatePlayer()
Applies gravity to the player, updates position based on velocity, and checks for collisions with blocks
updateCamera()
Moves the camera to follow the player, keeping them centered on screen
drawWorld()
Renders all visible tiles in the world based on camera position
drawPlayer()
Draws the player character at their current position
drawCursorHighlight()
Draws a yellow outline around the tile the mouse is pointing at
if (isTouchDevice) { drawTouchControls(); }
On touch devices, draws the directional and jump buttons on screen
drawUI()
Renders the hotbar at the bottom and instruction text at the top

initWorld()

initWorld() creates the entire game world using procedural generation. It uses Perlin noise to create natural-looking terrain, adds surface layers (grass and dirt), randomly places trees, and spawns the player on top of solid ground.

function initWorld() {
  world = new Array(WORLD_WIDTH);
  for (let x = 0; x < WORLD_WIDTH; x++) {
    world[x] = new Array(WORLD_HEIGHT).fill(AIR);
  }
  const baseHeight = 30;
  for (let x = 0; x < WORLD_WIDTH; x++) {
    let h = floor(noise(x * 0.12) * 10) + baseHeight;
    h = constrain(h, 12, WORLD_HEIGHT - 6);
    for (let y = h; y < WORLD_HEIGHT; y++) {
      world[x][y] = STONE;
    }
    if (h - 1 >= 0) world[x][h - 1] = GRASS;
    if (h - 2 >= 0) world[x][h - 2] = DIRT;
    if (h - 3 >= 0) world[x][h - 3] = DIRT;
    if (random() < 0.08 && h - 4 > 0) {
      const trunkHeight = 4;
      for (let t = 0; t < trunkHeight; t++) {
        const ty = h - 2 - t;
        if (ty >= 0) world[x][ty] = WOOD;
      }
      const leafY = h - 2 - trunkHeight;
      for (let lx = -2; lx <= 2; lx++) {
        for (let ly = -2; ly <= 1; ly++) {
          const tx = x + lx;
          const ty = leafY + ly;
          if (tx >= 0 && tx < WORLD_WIDTH && ty >= 0 && ty < WORLD_HEIGHT) {
            if (dist(lx, ly, 0, 0) < 2.6 && world[tx][ty] === AIR) {
              world[tx][ty] = LEAVES;
            }
          }
        }
      }
    }
  }
  const spawnX = floor(WORLD_WIDTH / 2);
  let spawnY = 0;
  for (let y = 0; y < WORLD_HEIGHT; y++) {
    if (world[spawnX][y] !== AIR) {
      spawnY = y - 1;
      break;
    }
  }
  player.x = spawnX * TILE_SIZE;
  player.y = spawnY * TILE_SIZE - player.h;
}

๐Ÿ”ง Subcomponents:

for-loop World Array Initialization for (let x = 0; x < WORLD_WIDTH; x++) { world[x] = new Array(WORLD_HEIGHT).fill(AIR); }

Creates a 2D array where each column is filled with AIR blocks

for-loop Terrain Generation Loop for (let x = 0; x < WORLD_WIDTH; x++) { ... }

Generates terrain for each column using Perlin noise and creates surface layers

calculation Perlin Noise Height let h = floor(noise(x * 0.12) * 10) + baseHeight

Uses Perlin noise to create smooth, natural-looking terrain height variations

for-loop Stone Underground Layer for (let y = h; y < WORLD_HEIGHT; y++) { world[x][y] = STONE; }

Fills everything below the terrain height with stone

conditional Random Tree Generation if (random() < 0.08 && h - 4 > 0) { ... }

Randomly places trees (8% chance) with trunks and leaf canopies

for-loop Player Spawn Detection for (let y = 0; y < WORLD_HEIGHT; y++) { if (world[spawnX][y] !== AIR) { ... } }

Finds the first solid block at the spawn location and places player on top

Line by Line:

world = new Array(WORLD_WIDTH)
Creates an array with 200 slots, one for each column of the world
for (let x = 0; x < WORLD_WIDTH; x++) { world[x] = new Array(WORLD_HEIGHT).fill(AIR); }
For each column, creates a new array of 60 rows and fills it all with AIR (empty blocks)
const baseHeight = 30
Sets the base terrain height to 30 tiles from the bottom
let h = floor(noise(x * 0.12) * 10) + baseHeight
Uses Perlin noise scaled by 0.12 to create smooth height variations, then adds the base height
h = constrain(h, 12, WORLD_HEIGHT - 6)
Ensures terrain height stays between 12 and 54, preventing terrain from going off-screen
for (let y = h; y < WORLD_HEIGHT; y++) { world[x][y] = STONE; }
Fills all blocks from the terrain surface down to the bottom with stone
if (h - 1 >= 0) world[x][h - 1] = GRASS
Places a grass block at the very top of the terrain
if (h - 2 >= 0) world[x][h - 2] = DIRT; if (h - 3 >= 0) world[x][h - 3] = DIRT
Places two layers of dirt below the grass for a natural appearance
if (random() < 0.08 && h - 4 > 0) { ... }
8% chance to generate a tree at this location if there's enough space
const trunkHeight = 4; for (let t = 0; t < trunkHeight; t++) { ... }
Creates a tree trunk 4 blocks tall by placing WOOD blocks vertically
if (dist(lx, ly, 0, 0) < 2.6 && world[tx][ty] === AIR) { world[tx][ty] = LEAVES; }
Places leaves in a circular pattern around the tree top (distance < 2.6 from center) only in empty spaces
const spawnX = floor(WORLD_WIDTH / 2)
Sets player spawn to the middle of the world horizontally
for (let y = 0; y < WORLD_HEIGHT; y++) { if (world[spawnX][y] !== AIR) { spawnY = y - 1; break; } }
Finds the first solid block from top to bottom and places player one block above it
player.x = spawnX * TILE_SIZE; player.y = spawnY * TILE_SIZE - player.h
Converts tile coordinates to pixel coordinates and sets the player's starting position

handleInput()

handleInput() runs every frame and checks which keys/buttons are pressed. It updates the player's horizontal velocity for movement and handles jumping from touch controls. Keyboard jumping is handled separately in keyPressed().

function handleInput() {
  player.vx = 0;
  if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchControls.left) {
    player.vx = -MOVE_SPEED;
  }
  if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchControls.right) {
    player.vx = MOVE_SPEED;
  }
  if (touchControls.jump && player.onGround) {
    player.vy = -JUMP_SPEED;
    player.onGround = false;
    playJumpSound();
    touchControls.jump = false;
  }
}

๐Ÿ”ง Subcomponents:

calculation Reset Horizontal Velocity player.vx = 0

Resets velocity to 0 each frame so player only moves when keys are held

conditional Left Movement Check if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchControls.left) { player.vx = -MOVE_SPEED; }

Moves player left if A key, left arrow, or left touch button is pressed

conditional Right Movement Check if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchControls.right) { player.vx = MOVE_SPEED; }

Moves player right if D key, right arrow, or right touch button is pressed

conditional Touch Jump Handler if (touchControls.jump && player.onGround) { ... }

Makes player jump when touch jump button is pressed and player is on ground

Line by Line:

player.vx = 0
Resets horizontal velocity to 0 at the start of each frame
if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchControls.left)
Checks if A key (65), left arrow, or left touch button is currently pressed
player.vx = -MOVE_SPEED
Sets velocity to -3 pixels per frame (negative = leftward)
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchControls.right)
Checks if D key (68), right arrow, or right touch button is currently pressed
player.vx = MOVE_SPEED
Sets velocity to 3 pixels per frame (positive = rightward)
if (touchControls.jump && player.onGround)
Only allows jump if touch jump button is set AND player is standing on ground
player.vy = -JUMP_SPEED
Sets vertical velocity to -12 (negative = upward)
player.onGround = false
Marks player as airborne so they can't jump again until landing
touchControls.jump = false
Resets the jump flag so one tap only causes one jump

keyPressed()

keyPressed() is called once when any key is pressed. It handles keyboard jumping (space/W/up arrow) and hotbar selection (number keys 1-5). It also unlocks audio on first interaction.

function keyPressed() {
  initAudioContext();
  if ((key === ' ' || key === 'w' || key === 'W' || keyCode === UP_ARROW) && player.onGround) {
    player.vy = -JUMP_SPEED;
    player.onGround = false;
    playJumpSound();
  }
  if (key >= '1' && key <= '5') {
    let idx = int(key) - 1;
    if (idx >= 0 && idx < inventory.length) {
      if (selectedIndex !== idx) {
        selectedIndex = idx;
        playSelectSound();
      } else {
        selectedIndex = idx;
      }
    }
  }
}

๐Ÿ”ง Subcomponents:

function-call Audio Context Initialization initAudioContext()

Unlocks audio on first user interaction (required by browsers)

conditional Keyboard Jump Handler if ((key === ' ' || key === 'w' || key === 'W' || keyCode === UP_ARROW) && player.onGround)

Allows jumping with space, W, or up arrow keys

conditional Hotbar Selection if (key >= '1' && key <= '5')

Selects inventory slots 1-5 when number keys are pressed

Line by Line:

initAudioContext()
Initializes audio on first keyboard press (browsers require user interaction to play sound)
if ((key === ' ' || key === 'w' || key === 'W' || keyCode === UP_ARROW) && player.onGround)
Checks if space, W, or up arrow was pressed AND player is on the ground
player.vy = -JUMP_SPEED; player.onGround = false; playJumpSound()
Makes player jump by setting upward velocity, marking as airborne, and playing jump sound
if (key >= '1' && key <= '5')
Checks if a number key 1 through 5 was pressed
let idx = int(key) - 1
Converts the character '1'-'5' to array index 0-4
if (selectedIndex !== idx) { selectedIndex = idx; playSelectSound(); }
If selecting a different slot, update selection and play sound

updatePlayer()

updatePlayer() is the physics engine. Each frame it applies gravity, calculates new position, and checks for collisions with blocks in all four directions. It uses AABB (axis-aligned bounding box) collision detection by checking which tiles the player overlaps with.

function updatePlayer() {
  player.vy += GRAVITY;
  player.vy = min(player.vy, 18);
  player.onGround = false;
  let newX = player.x + player.vx;
  newX = constrain(newX, 0, WORLD_WIDTH * TILE_SIZE - player.w);
  if (player.vx > 0) {
    const right = newX + player.w;
    const top = player.y;
    const bottom = player.y + player.h - 1;
    const tileRight = floor(right / TILE_SIZE);
    for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
      if (isSolidTile(tileRight, ty)) {
        newX = tileRight * TILE_SIZE - player.w;
        player.vx = 0;
        break;
      }
    }
  } else if (player.vx < 0) {
    const left = newX;
    const top = player.y;
    const bottom = player.y + player.h - 1;
    const tileLeft = floor(left / TILE_SIZE);
    for (let ty = floor(top / TILE_SIZE); ty <= floor(bottom / TILE_SIZE); ty++) {
      if (isSolidTile(tileLeft, ty)) {
        newX = (tileLeft + 1) * TILE_SIZE;
        player.vx = 0;
        break;
      }
    }
  }
  player.x = newX;
  let newY = player.y + player.vy;
  if (player.vy > 0) {
    let bottom = newY + player.h;
    if (bottom >= WORLD_HEIGHT * TILE_SIZE) {
      newY = WORLD_HEIGHT * TILE_SIZE - player.h;
      player.vy = 0;
      player.onGround = true;
    } else {
      const left = player.x;
      const right = player.x + player.w - 1;
      const tileBottom = floor(bottom / TILE_SIZE);
      for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
        if (isSolidTile(tx, tileBottom)) {
          newY = tileBottom * TILE_SIZE - player.h;
          player.vy = 0;
          player.onGround = true;
          break;
        }
      }
    }
  } else if (player.vy < 0) {
    let top = newY;
    if (top <= 0) {
      newY = 0;
      player.vy = 0;
    } else {
      const left = player.x;
      const right = player.x + player.w - 1;
      const tileTop = floor(top / TILE_SIZE);
      for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) {
        if (isSolidTile(tx, tileTop)) {
          newY = (tileTop + 1) * TILE_SIZE;
          player.vy = 0;
          break;
        }
      }
    }
  }
  player.y = newY;
}

๐Ÿ”ง Subcomponents:

calculation Gravity Application player.vy += GRAVITY; player.vy = min(player.vy, 18)

Applies gravity each frame and caps falling speed at 18 pixels/frame

conditional Horizontal Collision Detection if (player.vx > 0) { ... } else if (player.vx < 0) { ... }

Checks for collisions when moving left or right and stops player at block edges

conditional Vertical Collision Detection if (player.vy > 0) { ... } else if (player.vy < 0) { ... }

Checks for collisions when moving down or up and sets onGround flag

Line by Line:

player.vy += GRAVITY
Adds gravity (0.8) to vertical velocity each frame, making the player fall faster
player.vy = min(player.vy, 18)
Caps falling speed at 18 pixels/frame to prevent instant death from high falls
player.onGround = false
Resets ground flag each frame; it will be set to true only if player collides with ground
let newX = player.x + player.vx; newX = constrain(newX, 0, WORLD_WIDTH * TILE_SIZE - player.w)
Calculates new X position and keeps it within world bounds
if (player.vx > 0) { ... const tileRight = floor(right / TILE_SIZE); ... if (isSolidTile(tileRight, ty))
When moving right, checks the tile to the right of the player for collisions
newX = tileRight * TILE_SIZE - player.w; player.vx = 0
Stops player at the block edge and sets velocity to 0 to prevent passing through
else if (player.vx < 0) { ... const tileLeft = floor(left / TILE_SIZE); ... if (isSolidTile(tileLeft, ty))
When moving left, checks the tile to the left of the player for collisions
newX = (tileLeft + 1) * TILE_SIZE
Stops player at the right edge of the left block
player.x = newX
Updates player's actual X position after collision checks
let newY = player.y + player.vy
Calculates new Y position based on vertical velocity (gravity + jump)
if (player.vy > 0) { ... if (bottom >= WORLD_HEIGHT * TILE_SIZE)
When falling, checks if player hit the invisible floor at world bottom
const tileBottom = floor(bottom / TILE_SIZE); for (let tx = floor(left / TILE_SIZE); tx <= floor(right / TILE_SIZE); tx++) { if (isSolidTile(tx, tileBottom))
Checks all tiles below the player's width for solid blocks
newY = tileBottom * TILE_SIZE - player.h; player.vy = 0; player.onGround = true
Stops player on top of the block, stops falling, and marks as on ground
else if (player.vy < 0) { ... const tileTop = floor(top / TILE_SIZE); ... if (isSolidTile(tx, tileTop))
When jumping/moving up, checks tiles above the player for collisions
newY = (tileTop + 1) * TILE_SIZE; player.vy = 0
Stops player below the block and stops upward velocity (bonks head)
player.y = newY
Updates player's actual Y position after all collision checks

updateCamera()

updateCamera() keeps the camera centered on the player while preventing it from scrolling beyond the world edges. The camera position is used in drawWorld() and other functions to convert world coordinates to screen coordinates.

function updateCamera() {
  const targetX = player.x + player.w / 2 - width / 2;
  const maxCamX = max(0, WORLD_WIDTH * TILE_SIZE - width);
  cameraX = constrain(targetX, 0, maxCamX);
  const targetY = player.y + player.h / 2 - height / 2;
  const maxCamY = max(0, WORLD_HEIGHT * TILE_SIZE - height);
  cameraY = constrain(targetY, 0, maxCamY);
}

๐Ÿ”ง Subcomponents:

calculation Horizontal Camera Centering const targetX = player.x + player.w / 2 - width / 2; cameraX = constrain(targetX, 0, maxCamX)

Centers camera on player horizontally while keeping it within world bounds

calculation Vertical Camera Centering const targetY = player.y + player.h / 2 - height / 2; cameraY = constrain(targetY, 0, maxCamY)

Centers camera on player vertically while keeping it within world bounds

Line by Line:

const targetX = player.x + player.w / 2 - width / 2
Calculates where camera should be to center player on screen (player center minus half screen width)
const maxCamX = max(0, WORLD_WIDTH * TILE_SIZE - width)
Calculates the rightmost camera position so the world edge doesn't go past the right side of screen
cameraX = constrain(targetX, 0, maxCamX)
Clamps camera X between 0 and maxCamX so it never shows beyond world boundaries
const targetY = player.y + player.h / 2 - height / 2
Calculates where camera should be to center player vertically on screen
const maxCamY = max(0, WORLD_HEIGHT * TILE_SIZE - height)
Calculates the bottommost camera position so the world edge doesn't go past the bottom
cameraY = constrain(targetY, 0, maxCamY)
Clamps camera Y between 0 and maxCamY so it never shows beyond world boundaries

drawWorld()

drawWorld() renders all visible blocks. It uses culling (only drawing tiles on screen) for performance. It converts world coordinates to screen coordinates by subtracting the camera position. Each block type has a different color defined in the switch statement.

function drawWorld() {
  noStroke();
  let startCol = floor(cameraX / TILE_SIZE);
  let endCol = startCol + floor(width / TILE_SIZE) + 2;
  startCol = max(0, startCol);
  endCol = min(WORLD_WIDTH - 1, endCol);
  let startRow = floor(cameraY / TILE_SIZE);
  let endRow = startRow + floor(height / TILE_SIZE) + 2;
  startRow = max(0, startRow);
  endRow = min(WORLD_HEIGHT - 1, endRow);
  for (let x = startCol; x <= endCol; x++) {
    for (let y = startRow; y <= endRow; y++) {
      const block = world[x][y];
      if (block === AIR) continue;
      const sx = x * TILE_SIZE - cameraX;
      const sy = y * TILE_SIZE - cameraY;
      switch (block) {
        case GRASS:  fill(95, 159, 53);      break;
        case DIRT:   fill(121, 85, 58);      break;
        case STONE:  fill(140);              break;
        case WOOD:   fill(102, 51, 0);       break;
        case LEAVES: fill(46, 139, 87, 230); break;
      }
      rect(sx, sy, TILE_SIZE, TILE_SIZE);
    }
  }
}

๐Ÿ”ง Subcomponents:

calculation Horizontal Culling let startCol = floor(cameraX / TILE_SIZE); let endCol = startCol + floor(width / TILE_SIZE) + 2

Calculates which columns are visible on screen to avoid drawing off-screen tiles

calculation Vertical Culling let startRow = floor(cameraY / TILE_SIZE); let endRow = startRow + floor(height / TILE_SIZE) + 2

Calculates which rows are visible on screen to avoid drawing off-screen tiles

for-loop Block Rendering Loop for (let x = startCol; x <= endCol; x++) { for (let y = startRow; y <= endRow; y++) { ... } }

Loops through visible tiles and draws each non-air block with appropriate color

switch-case Block Color Selection switch (block) { case GRASS: fill(...) ... }

Sets the fill color based on block type before drawing

Line by Line:

noStroke()
Disables outlines on rectangles for cleaner block appearance
let startCol = floor(cameraX / TILE_SIZE); let endCol = startCol + floor(width / TILE_SIZE) + 2
Calculates which tile columns are visible on screen (adds +2 buffer for safety)
startCol = max(0, startCol); endCol = min(WORLD_WIDTH - 1, endCol)
Clamps column range to valid world bounds
let startRow = floor(cameraY / TILE_SIZE); let endRow = startRow + floor(height / TILE_SIZE) + 2
Calculates which tile rows are visible on screen
startRow = max(0, startRow); endRow = min(WORLD_HEIGHT - 1, endRow)
Clamps row range to valid world bounds
for (let x = startCol; x <= endCol; x++) { for (let y = startRow; y <= endRow; y++) {
Nested loops that iterate through only the visible tiles
const block = world[x][y]; if (block === AIR) continue
Gets the block type and skips drawing if it's empty (AIR)
const sx = x * TILE_SIZE - cameraX; const sy = y * TILE_SIZE - cameraY
Converts world tile coordinates to screen pixel coordinates using camera position
switch (block) { case GRASS: fill(95, 159, 53); ... }
Sets the fill color based on block type (green for grass, brown for dirt, etc.)
rect(sx, sy, TILE_SIZE, TILE_SIZE)
Draws a 32x32 pixel rectangle at the screen position with the selected color

drawPlayer()

drawPlayer() renders the player character as two rectangles: a skin-colored body and blue pants. It uses push/pop to isolate drawing state changes. The player position is converted to screen coordinates using the camera offset.

function drawPlayer() {
  push();
  const sx = player.x - cameraX;
  const sy = player.y - cameraY;
  noStroke();
  fill(255, 220, 180);
  rect(sx, sy, player.w, player.h);
  fill(0, 0, 255);
  rect(sx, sy + player.h / 2, player.w, player.h / 2);
  pop();
}

๐Ÿ”ง Subcomponents:

calculation Screen Coordinate Conversion const sx = player.x - cameraX; const sy = player.y - cameraY

Converts player world position to screen position using camera offset

function-call Body Drawing fill(255, 220, 180); rect(sx, sy, player.w, player.h)

Draws the player's body (skin color rectangle)

function-call Pants Drawing fill(0, 0, 255); rect(sx, sy + player.h / 2, player.w, player.h / 2)

Draws blue pants on the bottom half of the player

Line by Line:

push()
Saves the current drawing state (fill, stroke, etc.) so we can restore it later
const sx = player.x - cameraX; const sy = player.y - cameraY
Converts player's world coordinates to screen coordinates by subtracting camera position
noStroke()
Disables outlines on the player rectangles
fill(255, 220, 180)
Sets fill color to skin tone (light peachy color)
rect(sx, sy, player.w, player.h)
Draws the player's body as a 22x32 pixel rectangle
fill(0, 0, 255)
Sets fill color to blue for pants
rect(sx, sy + player.h / 2, player.w, player.h / 2)
Draws blue pants covering the bottom half of the player (starting at halfway down)
pop()
Restores the previous drawing state

drawCursorHighlight()

drawCursorHighlight() shows which block the player is pointing at with a yellow outline. It converts mouse coordinates to world tile coordinates, checks bounds, and draws an outline. This gives visual feedback for block breaking/placing.

function drawCursorHighlight() {
  const tileX = floor((mouseX + cameraX) / TILE_SIZE);
  const tileY = floor((mouseY + cameraY) / TILE_SIZE);
  if (!isInWorld(tileX, tileY)) return;
  if (mouseY > height - UI_BAR_H) return;
  const sx = tileX * TILE_SIZE - cameraX;
  const sy = tileY * TILE_SIZE - cameraY;
  noFill();
  stroke(255, 255, 0);
  strokeWeight(2);
  rect(sx, sy, TILE_SIZE, TILE_SIZE);
}

๐Ÿ”ง Subcomponents:

calculation Mouse to Tile Conversion const tileX = floor((mouseX + cameraX) / TILE_SIZE); const tileY = floor((mouseY + cameraY) / TILE_SIZE)

Converts mouse screen position to world tile coordinates

conditional World Bounds Check if (!isInWorld(tileX, tileY)) return; if (mouseY > height - UI_BAR_H) return

Prevents highlighting if mouse is outside world or over UI

Line by Line:

const tileX = floor((mouseX + cameraX) / TILE_SIZE)
Converts mouse X screen position to world tile X by adding camera offset and dividing by tile size
const tileY = floor((mouseY + cameraY) / TILE_SIZE)
Converts mouse Y screen position to world tile Y by adding camera offset and dividing by tile size
if (!isInWorld(tileX, tileY)) return
Exits early if the tile is outside the world bounds
if (mouseY > height - UI_BAR_H) return
Exits early if mouse is over the UI bar at the bottom (don't highlight over hotbar)
const sx = tileX * TILE_SIZE - cameraX; const sy = tileY * TILE_SIZE - cameraY
Converts the tile coordinates back to screen coordinates for drawing
noFill(); stroke(255, 255, 0); strokeWeight(2)
Sets up yellow outline with 2 pixel thickness
rect(sx, sy, TILE_SIZE, TILE_SIZE)
Draws the yellow outline rectangle around the tile the mouse is pointing at

drawUI()

drawUI() renders the hotbar at the bottom and the instructions panel at the top. The hotbar shows 5 inventory slots with block previews and highlights the selected slot with a thicker border. The instructions panel explains all controls.

function drawUI() {
  push();
  const barH = UI_BAR_H;
  noStroke();
  fill(0, 0, 0, 130);
  rect(0, height - barH, width, barH);
  const slots = inventory.length;
  const slotSize = 40;
  const gap = 12;
  const totalW = slots * slotSize + (slots - 1) * gap;
  const startX = (width - totalW) / 2;
  const startY = height - barH + (barH - slotSize) / 2;
  textAlign(CENTER, CENTER);
  textSize(12);
  for (let i = 0; i < slots; i++) {
    const x = startX + i * (slotSize + gap);
    const y = startY;
    stroke(255);
    strokeWeight(i === selectedIndex ? 3 : 1);
    fill(50, 50, 50, 220);
    rect(x, y, slotSize, slotSize, 5);
    const blockId = inventory[i];
    const c = getBlockColor(blockId);
    if (blockId !== AIR) {
      noStroke();
      if (c.length === 1) fill(c[0]);
      else fill(c[0], c[1], c[2]);
      rect(x + 6, y + 6, slotSize - 12, slotSize - 12, 3);
    }
    fill(255);
    noStroke();
    text(i + 1, x + slotSize / 2, y + slotSize / 2);
  }
  const panelW = 330;
  const panelH = 130;
  fill(0, 0, 0, 140);
  rect(10, 10, panelW, panelH, 8);
  fill(255);
  textAlign(LEFT, TOP);
  text(
    "Move: A/D or โ†/โ†’\n" +
    "Jump: W / Space / โ†‘\n" +
    "1โ€“5 or tap hotbar: choose block\n" +
    "Mouse: Left=break, Right=place\n" +
    "Touch: tap blocks to break/place\n" +
    "Touch buttons: โ—€ / โ–ฒ / โ–ถ",
    18,
    18
  );
  pop();
}

๐Ÿ”ง Subcomponents:

function-call UI Bar Background fill(0, 0, 0, 130); rect(0, height - barH, width, barH)

Draws a semi-transparent black bar at the bottom for the hotbar

for-loop Hotbar Slot Rendering for (let i = 0; i < slots; i++) { ... }

Loops through inventory slots and draws each one with its block preview

conditional Selected Slot Highlight strokeWeight(i === selectedIndex ? 3 : 1)

Makes the selected slot have a thicker border

function-call Instructions Panel fill(0, 0, 0, 140); rect(10, 10, panelW, panelH, 8); ... text(...)

Draws the help text panel in the top-left corner

Line by Line:

push()
Saves drawing state before making UI changes
const barH = UI_BAR_H
Stores the UI bar height (60) in a local variable for easier use
noStroke(); fill(0, 0, 0, 130); rect(0, height - barH, width, barH)
Draws a semi-transparent black background bar at the bottom of the screen
const slots = inventory.length; const slotSize = 40; const gap = 12
Sets up hotbar dimensions: 5 slots, 40 pixels each, 12 pixel gaps between
const totalW = slots * slotSize + (slots - 1) * gap; const startX = (width - totalW) / 2
Calculates total hotbar width and centers it horizontally on screen
const startY = height - barH + (barH - slotSize) / 2
Calculates vertical position to center slots vertically within the UI bar
for (let i = 0; i < slots; i++) { const x = startX + i * (slotSize + gap); const y = startY
Loops through each inventory slot and calculates its screen position
strokeWeight(i === selectedIndex ? 3 : 1)
Makes the selected slot have a thicker border (3 pixels) vs unselected (1 pixel)
fill(50, 50, 50, 220); rect(x, y, slotSize, slotSize, 5)
Draws the slot background as a dark gray rounded rectangle
const blockId = inventory[i]; const c = getBlockColor(blockId)
Gets the block type in this slot and its color
if (blockId !== AIR) { ... rect(x + 6, y + 6, slotSize - 12, slotSize - 12, 3) }
If the slot has a block, draws a smaller colored rectangle inside to preview the block
fill(255); noStroke(); text(i + 1, x + slotSize / 2, y + slotSize / 2)
Draws the slot number (1-5) centered in white text
fill(0, 0, 0, 140); rect(10, 10, panelW, panelH, 8)
Draws a semi-transparent black rounded rectangle for the instructions panel
fill(255); textAlign(LEFT, TOP); text(...)
Draws white instruction text aligned to top-left of the panel
pop()
Restores the previous drawing state

getBlockColor(id)

getBlockColor() is a helper function that returns the RGB color for each block type. It's used in drawUI() to show block previews in the hotbar and in drawWorld() to color the blocks.

function getBlockColor(id) {
  switch (id) {
    case GRASS:  return [95, 159, 53];
    case DIRT:   return [121, 85, 58];
    case STONE:  return [140];
    case WOOD:   return [102, 51, 0];
    case LEAVES: return [46, 139, 87];
    default:     return [0];
  }
}

๐Ÿ”ง Subcomponents:

switch-case Block Color Lookup switch (id) { case GRASS: return [95, 159, 53]; ... }

Returns the RGB color array for each block type

Line by Line:

switch (id) {
Starts a switch statement that checks the block ID
case GRASS: return [95, 159, 53];
Returns green color for grass blocks
case DIRT: return [121, 85, 58];
Returns brown color for dirt blocks
case STONE: return [140];
Returns gray color for stone blocks (single value = grayscale)
case WOOD: return [102, 51, 0];
Returns dark brown color for wood blocks
case LEAVES: return [46, 139, 87];
Returns dark green color for leaf blocks
default: return [0];
Returns black as a fallback for unknown block types

mousePressed()

mousePressed() is called when the mouse button is clicked. It unlocks audio, prevents conflicts with touch events, and routes the click to either hotbar selection or world editing.

function mousePressed() {
  initAudioContext();
  if (touches && touches.length > 0) return;
  if (mouseY > height - UI_BAR_H) {
    handleHotbarClick(mouseX, mouseY);
    return;
  }
  handleWorldTap(mouseX, mouseY);
}

๐Ÿ”ง Subcomponents:

function-call Audio Context Unlock initAudioContext()

Unlocks audio on first mouse click

conditional Touch/Mouse Conflict Prevention if (touches && touches.length > 0) return

Prevents double-handling on touch devices (touch events already handled)

conditional Hotbar Click Detection if (mouseY > height - UI_BAR_H) { handleHotbarClick(mouseX, mouseY); return; }

Checks if click is on hotbar and handles slot selection

Line by Line:

initAudioContext()
Initializes audio on first mouse click (browsers require user interaction)
if (touches && touches.length > 0) return
Exits early if this is a touch device with active touches (prevent double-handling)
if (mouseY > height - UI_BAR_H) { handleHotbarClick(mouseX, mouseY); return; }
If click is in the UI bar area, handle hotbar slot selection and exit
handleWorldTap(mouseX, mouseY)
Otherwise, handle the click as a world tap (break/place block)

handleWorldTap(px, py)

handleWorldTap() is called when the player taps/clicks on the world. It converts screen coordinates to tile coordinates, then either breaks the tapped block or places the selected block (if the space is empty and doesn't overlap the player).

function handleWorldTap(px, py) {
  const tileX = floor((px + cameraX) / TILE_SIZE);
  const tileY = floor((py + cameraY) / TILE_SIZE);
  if (!isInWorld(tileX, tileY)) return;
  if (world[tileX][tileY] !== AIR) {
    world[tileX][tileY] = AIR;
    playBlockBreakSound();
  } else {
    const blockId = inventory[selectedIndex];
    if (blockId !== AIR) {
      const bx = tileX * TILE_SIZE;
      const by = tileY * TILE_SIZE;
      if (!rectsOverlap(bx, by, TILE_SIZE, TILE_SIZE, player.x, player.y, player.w, player.h)) {
        world[tileX][tileY] = blockId;
        playBlockPlaceSound();
      }
    }
  }
}

๐Ÿ”ง Subcomponents:

calculation Tap Position to Tile Conversion const tileX = floor((px + cameraX) / TILE_SIZE); const tileY = floor((py + cameraY) / TILE_SIZE)

Converts screen tap coordinates to world tile coordinates

conditional Block Breaking if (world[tileX][tileY] !== AIR) { world[tileX][tileY] = AIR; playBlockBreakSound(); }

If tapping a solid block, removes it and plays break sound

conditional Block Placement else { ... if (!rectsOverlap(...)) { world[tileX][tileY] = blockId; playBlockPlaceSound(); } }

If tapping empty space, places selected block if it doesn't overlap player

Line by Line:

const tileX = floor((px + cameraX) / TILE_SIZE); const tileY = floor((py + cameraY) / TILE_SIZE)
Converts screen pixel coordinates to world tile coordinates using camera offset
if (!isInWorld(tileX, tileY)) return
Exits early if the tap is outside the world bounds
if (world[tileX][tileY] !== AIR) { world[tileX][tileY] = AIR; playBlockBreakSound(); }
If the tile has a block, removes it (sets to AIR) and plays break sound
const blockId = inventory[selectedIndex]
Gets the block type from the currently selected hotbar slot
if (blockId !== AIR) {
Only tries to place if the selected slot has a block (not empty)
const bx = tileX * TILE_SIZE; const by = tileY * TILE_SIZE
Converts tile coordinates to pixel coordinates for collision checking
if (!rectsOverlap(bx, by, TILE_SIZE, TILE_SIZE, player.x, player.y, player.w, player.h))
Checks if the new block would overlap the player (prevents placing blocks inside player)
world[tileX][tileY] = blockId; playBlockPlaceSound()
Places the block and plays placement sound

touchStarted()

touchStarted() is called when the user touches the screen. It supports multiple simultaneous touches, routing each to the appropriate handler (hotbar, control buttons, or world editing).

function touchStarted() {
  initAudioContext();
  if (!touches || touches.length === 0) return false;
  handleTouches(touches, true);
  for (let t of touches) {
    const tx = t.x;
    const ty = t.y;
    if (ty > height - UI_BAR_H) {
      handleHotbarClick(tx, ty);
      continue;
    }
    if (isInTouchButton(tx, ty)) {
      continue;
    }
    handleWorldTap(tx, ty);
  }
  return false;
}

๐Ÿ”ง Subcomponents:

function-call Audio Unlock on Touch initAudioContext()

Unlocks audio on first touch (required by browsers)

for-loop Multi-touch Loop for (let t of touches) { ... }

Processes each simultaneous touch point

conditional Touch Hotbar Detection if (ty > height - UI_BAR_H) { handleHotbarClick(tx, ty); continue; }

Handles taps on the hotbar area

conditional Touch Button Detection if (isInTouchButton(tx, ty)) { continue; }

Skips world tap if touch is on a control button

Line by Line:

initAudioContext()
Unlocks audio on first touch interaction
if (!touches || touches.length === 0) return false
Exits early if there are no active touches
handleTouches(touches, true)
Updates touch control button states (left/right/jump) based on active touches
for (let t of touches) { const tx = t.x; const ty = t.y
Loops through each active touch point and gets its screen coordinates
if (ty > height - UI_BAR_H) { handleHotbarClick(tx, ty); continue; }
If touch is in the hotbar area, handle slot selection and skip to next touch
if (isInTouchButton(tx, ty)) { continue; }
If touch is on a control button, skip world tap (button already handled by handleTouches)
handleWorldTap(tx, ty)
Otherwise, treat the touch as a world tap (break/place block)
return false
Returns false to prevent default browser touch behavior (scrolling, zooming)

touchEnded()

touchEnded() is called when a touch point is released. It resets movement controls and rechecks remaining active touches in case the user is holding multiple buttons.

function touchEnded() {
  touchControls.left = false;
  touchControls.right = false;
  handleTouches(touches, false);
  return false;
}

๐Ÿ”ง Subcomponents:

calculation Reset Movement Controls touchControls.left = false; touchControls.right = false

Clears left/right movement flags when touch ends

function-call Recheck Active Touches handleTouches(touches, false)

Updates control states based on remaining active touches

Line by Line:

touchControls.left = false; touchControls.right = false
Resets movement flags when a touch ends
handleTouches(touches, false)
Re-evaluates which control buttons are still being touched (in case multiple touches)
return false
Prevents default browser touch behavior

handleTouches(touchList, isStart)

handleTouches() updates the touchControls object based on which control buttons are currently being touched. It's called from touchStarted() and touchEnded() to maintain the state of movement and jump controls.

function handleTouches(touchList, isStart) {
  const btnSize = 70;
  const margin = 20;
  const bottomY = height - 80 - margin;
  touchControls.left = false;
  touchControls.right = false;
  for (let t of touchList) {
    const tx = t.x;
    const ty = t.y;
    if (
      tx >= margin &&
      tx <= margin + btnSize &&
      ty >= bottomY - btnSize &&
      ty <= bottomY
    ) {
      touchControls.left = true;
    }
    if (
      tx >= margin + btnSize + 10 &&
      tx <= margin + btnSize * 2 + 10 &&
      ty >= bottomY - btnSize &&
      ty <= bottomY
    ) {
      touchControls.right = true;
    }
    const jumpX = width - margin - btnSize;
    if (
      tx >= jumpX &&
      tx <= jumpX + btnSize &&
      ty >= bottomY - btnSize &&
      ty <= bottomY &&
      isStart
    ) {
      touchControls.jump = true;
    }
  }
}

๐Ÿ”ง Subcomponents:

calculation Button Dimensions const btnSize = 70; const margin = 20; const bottomY = height - 80 - margin

Calculates positions and sizes of touch control buttons

conditional Left Button Detection if (tx >= margin && tx <= margin + btnSize && ty >= bottomY - btnSize && ty <= bottomY)

Checks if touch is within left directional button bounds

conditional Right Button Detection if (tx >= margin + btnSize + 10 && tx <= margin + btnSize * 2 + 10 && ...)

Checks if touch is within right directional button bounds

conditional Jump Button Detection if (tx >= jumpX && tx <= jumpX + btnSize && ... && isStart)

Checks if touch is within jump button bounds (only on touch start)

Line by Line:

const btnSize = 70; const margin = 20; const bottomY = height - 80 - margin
Sets button size (70x70), margin from edges (20), and vertical position (80 pixels above hotbar)
touchControls.left = false; touchControls.right = false
Resets movement flags before checking active touches
for (let t of touchList) { const tx = t.x; const ty = t.y
Loops through each active touch and gets its coordinates
if (tx >= margin && tx <= margin + btnSize && ty >= bottomY - btnSize && ty <= bottomY)
Checks if touch is in the left button rectangle (left side of screen)
touchControls.left = true
Sets left movement flag if touch is in left button
if (tx >= margin + btnSize + 10 && tx <= margin + btnSize * 2 + 10 && ...)
Checks if touch is in the right button rectangle (10 pixels right of left button)
touchControls.right = true
Sets right movement flag if touch is in right button
const jumpX = width - margin - btnSize
Calculates X position of jump button (right side of screen)
if (tx >= jumpX && tx <= jumpX + btnSize && ty >= bottomY - btnSize && ty <= bottomY && isStart)
Checks if touch is in jump button AND this is a new touch (isStart = true)
touchControls.jump = true
Sets jump flag if touch is in jump button

isInTouchButton(tx, ty)

isInTouchButton() is a helper function that checks if a touch point is inside any of the three control buttons. It's used in touchStarted() to avoid treating button touches as world taps.

function isInTouchButton(tx, ty) {
  const btnSize = 70;
  const margin = 20;
  const bottomY = height - 80 - margin;
  const leftX = margin;
  const rightX = margin + btnSize + 10;
  const jumpX = width - margin - btnSize;
  const inRect = (x, y, w, h) =>
    tx >= x && tx <= x + w && ty >= y && ty <= y + h;
  if (inRect(leftX, bottomY - btnSize, btnSize, btnSize)) return true;
  if (inRect(rightX, bottomY - btnSize, btnSize, btnSize)) return true;
  if (inRect(jumpX, bottomY - btnSize, btnSize, btnSize)) return true;
  return false;
}

๐Ÿ”ง Subcomponents:

calculation Button Position Calculations const btnSize = 70; const margin = 20; const bottomY = height - 80 - margin; const leftX = margin; const rightX = margin + btnSize + 10; const jumpX = width - margin - btnSize

Calculates positions of all three touch buttons

calculation Rectangle Overlap Helper const inRect = (x, y, w, h) => tx >= x && tx <= x + w && ty >= y && ty <= y + h

Line by Line:

const btnSize = 70; const margin = 20; const bottomY = height - 80 - margin
Sets button dimensions and vertical position
const leftX = margin; const rightX = margin + btnSize + 10; const jumpX = width - margin - btnSize
Calculates X positions of left, right, and jump buttons
const inRect = (x, y, w, h) => tx >= x && tx <= x + w && ty >= y && ty <= y + h
Defines a helper function that checks if the touch point is inside a rectangle
if (inRect(leftX, bottomY - btnSize, btnSize, btnSize)) return true
Returns true if touch is in the left button area
if (inRect(rightX, bottomY - btnSize, btnSize, btnSize)) return true
Returns true if touch is in the right button area
if (inRect(jumpX, bottomY - btnSize, btnSize, btnSize)) return true
Returns true if touch is in the jump button area
return false
Returns false if touch is not in any button

drawTouchControls()

drawTouchControls() renders the on-screen directional and jump buttons for touch devices. Buttons become brighter when pressed (touchControls flag is true) to give visual feedback. Only runs on touch devices.

function drawTouchControls() {
  if (!isTouchDevice) return;
  push();
  const btnSize = 70;
  const margin = 20;
  const bottomY = height - 80 - margin;
  textAlign(CENTER, CENTER);
  textSize(32);
  const leftX = margin;
  fill(touchControls.left ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.3)');
  stroke(255);
  strokeWeight(2);
  rect(leftX, bottomY - btnSize, btnSize, btnSize, 10);
  fill(255);
  noStroke();
  text('โ—€', leftX + btnSize / 2, bottomY - btnSize / 2);
  const rightX = margin + btnSize + 10;
  fill(touchControls.right ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.3)');
  stroke(255);
  strokeWeight(2);
  rect(rightX, bottomY - btnSize, btnSize, btnSize, 10);
  fill(255);
  noStroke();
  text('โ–ถ', rightX + btnSize / 2, bottomY - btnSize / 2);
  const jumpX = width - margin - btnSize;
  fill(touchControls.jump ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.3)');
  stroke(255);
  strokeWeight(2);
  rect(jumpX, bottomY - btnSize, btnSize, btnSize, 10);
  fill(255);
  noStroke();
  text('โ–ฒ', jumpX + btnSize / 2, bottomY - btnSize / 2);
  pop();
}

๐Ÿ”ง Subcomponents:

function-call Left Button Drawing fill(...); rect(leftX, bottomY - btnSize, btnSize, btnSize, 10); text('โ—€', ...)

Draws the left directional button with dynamic opacity

function-call Right Button Drawing fill(...); rect(rightX, bottomY - btnSize, btnSize, btnSize, 10); text('โ–ถ', ...)

Draws the right directional button with dynamic opacity

function-call Jump Button Drawing fill(...); rect(jumpX, bottomY - btnSize, btnSize, btnSize, 10); text('โ–ฒ', ...)

Draws the jump button with dynamic opacity

Line by Line:

if (!isTouchDevice) return
Exits early if device doesn't support touch (desktop)
push()
Saves drawing state
const btnSize = 70; const margin = 20; const bottomY = height - 80 - margin
Sets button dimensions and positions
textAlign(CENTER, CENTER); textSize(32)
Sets up text alignment and size for button symbols
fill(touchControls.left ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.3)')
Makes button brighter (0.6 opacity) when pressed, dimmer (0.3) when not
stroke(255); strokeWeight(2); rect(leftX, bottomY - btnSize, btnSize, btnSize, 10)
Draws left button with white border and rounded corners
fill(255); noStroke(); text('โ—€', leftX + btnSize / 2, bottomY - btnSize / 2)
Draws left arrow symbol centered in the button
const rightX = margin + btnSize + 10; ... rect(rightX, ...); text('โ–ถ', ...)
Draws right button with right arrow symbol
const jumpX = width - margin - btnSize; ... rect(jumpX, ...); text('โ–ฒ', ...)
Draws jump button on right side with up arrow symbol
pop()
Restores drawing state

handleHotbarClick(px, py)

handleHotbarClick() detects which inventory slot was clicked and updates the selection. It plays a sound when switching to a different slot. This function is called from both mousePressed() and touchStarted().

function handleHotbarClick(px, py) {
  const barH = UI_BAR_H;
  const slots = inventory.length;
  const slotSize = 40;
  const gap = 12;
  const totalW = slots * slotSize + (slots - 1) * gap;
  const startX = (width - totalW) / 2;
  const startY = height - barH + (barH - slotSize) / 2;
  for (let i = 0; i < slots; i++) {
    const x = startX + i * (slotSize + gap);
    const y = startY;
    if (px >= x && px <= x + slotSize && py >= y && py <= y + slotSize) {
      if (selectedIndex !== i) {
        selectedIndex = i;
        playSelectSound();
      } else {
        selectedIndex = i;
      }
      return true;
    }
  }
  return false;
}

๐Ÿ”ง Subcomponents:

calculation Slot Layout Calculation const barH = UI_BAR_H; const slots = inventory.length; const slotSize = 40; const gap = 12; const totalW = slots * slotSize + (slots - 1) * gap; const startX = (width - totalW) / 2; const startY = height - barH + (barH - slotSize) / 2

Calculates positions of all hotbar slots

for-loop Slot Click Detection for (let i = 0; i < slots; i++) { ... if (px >= x && px <= x + slotSize && py >= y && py <= y + slotSize)

Loops through slots to find which one was clicked

Line by Line:

const barH = UI_BAR_H; const slots = inventory.length; const slotSize = 40; const gap = 12
Gets hotbar dimensions: bar height (60), number of slots (5), slot size (40), gap (12)
const totalW = slots * slotSize + (slots - 1) * gap; const startX = (width - totalW) / 2
Calculates total hotbar width and centers it horizontally
const startY = height - barH + (barH - slotSize) / 2
Calculates Y position to center slots vertically within the bar
for (let i = 0; i < slots; i++) { const x = startX + i * (slotSize + gap); const y = startY
Loops through each slot and calculates its position
if (px >= x && px <= x + slotSize && py >= y && py <= y + slotSize)
Checks if click is within this slot's rectangle
if (selectedIndex !== i) { selectedIndex = i; playSelectSound(); } else { selectedIndex = i; }
Selects the slot and plays sound if it's a different slot than currently selected
return true
Returns true to indicate a slot was clicked
return false
Returns false if no slot was clicked

isInWorld(tx, ty)

isInWorld() is a simple helper function that checks if tile coordinates are within the valid world range. Used throughout the code for bounds checking.

function isInWorld(tx, ty) {
  return tx >= 0 && tx < WORLD_WIDTH && ty >= 0 && ty < WORLD_HEIGHT;
}

Line by Line:

return tx >= 0 && tx < WORLD_WIDTH && ty >= 0 && ty < WORLD_HEIGHT
Returns true if tile coordinates are within world bounds (0-199 x, 0-59 y)

isSolid(blockId)

isSolid() checks if a block type is solid. In this game, everything except AIR is solid, meaning the player can stand on leaves and all blocks are collidable.

function isSolid(blockId) {
  return blockId !== AIR;
}

Line by Line:

return blockId !== AIR
Returns true if the block is not air (all non-air blocks are solid, including leaves)

isSolidTile(tx, ty)

isSolidTile() combines bounds checking and solid checking. It's used in collision detection to determine if a tile the player is colliding with is solid.

function isSolidTile(tx, ty) {
  if (!isInWorld(tx, ty)) return false;
  return isSolid(world[tx][ty]);
}

Line by Line:

if (!isInWorld(tx, ty)) return false
Returns false if the tile is outside the world (out of bounds = not solid)
return isSolid(world[tx][ty])
Returns whether the block at this tile is solid

rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2)

rectsOverlap() checks if two axis-aligned rectangles overlap. It's used in handleWorldTap() to prevent placing blocks inside the player. The logic checks if rectangles are separated on any axis; if none are separated, they must overlap.

function rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
  return !(
    x1 + w1 <= x2 ||
    x1 >= x2 + w2 ||
    y1 + h1 <= y2 ||
    y1 >= y2 + h2
  );
}

Line by Line:

return !( x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2 )
Uses the AABB (axis-aligned bounding box) algorithm: returns true if rectangles overlap, false if they don't. The negation (!) inverts the 'no overlap' conditions to 'overlap'.

playTone(freq, type, vol, attack, decay)

playTone() creates a simple synthesized sound using an oscillator with an attack-decay envelope. It's used for all UI and game sounds. The envelope makes the sound feel more natural by fading in and out rather than starting/stopping abruptly.

function playTone(freq, type = 'sine', vol = 0.3, attack = 0.01, decay = 0.15) {
  if (!audioInitialized) return;
  const osc = new p5.Oscillator(type);
  osc.freq(freq);
  osc.amp(0);
  osc.start();
  osc.amp(vol, attack);
  osc.amp(0, decay, attack);
  osc.stop(attack + decay + 0.05);
}

๐Ÿ”ง Subcomponents:

calculation Oscillator Creation const osc = new p5.Oscillator(type); osc.freq(freq); osc.amp(0); osc.start()

Creates an oscillator with specified waveform, sets frequency, and starts it

calculation ADSR Envelope osc.amp(vol, attack); osc.amp(0, decay, attack); osc.stop(attack + decay + 0.05)

Creates attack-decay envelope: fades in then out

Line by Line:

if (!audioInitialized) return
Exits early if audio hasn't been unlocked yet (browsers require user interaction)
const osc = new p5.Oscillator(type)
Creates a new oscillator with the specified waveform type (sine, square, triangle, etc.)
osc.freq(freq)
Sets the oscillator frequency in Hz
osc.amp(0); osc.start()
Sets initial amplitude to 0 and starts the oscillator
osc.amp(vol, attack)
Fades in to the target volume over the attack time (default 0.01 seconds)
osc.amp(0, decay, attack)
Fades out to 0 over the decay time (default 0.15 seconds), starting after attack completes
osc.stop(attack + decay + 0.05)
Stops the oscillator after the envelope is complete (plus small buffer)

playJumpSound()

playJumpSound() is a simple wrapper that calls playTone() with specific parameters for the jump sound effect. The square wave and longer decay give it a chunky, satisfying feel.

function playJumpSound() {
  playTone(330, 'square', 0.35, 0.01, 0.2);
}

Line by Line:

playTone(330, 'square', 0.35, 0.01, 0.2)
Plays a 330 Hz square wave (E4 note) with 35% volume, 10ms attack, 200ms decay for a chunky jump sound

playBlockPlaceSound()

playBlockPlaceSound() creates the sound for placing blocks. The triangle wave and shorter decay make it a quick, light blip.

function playBlockPlaceSound() {
  playTone(550, 'triangle', 0.25, 0.005, 0.12);
}

Line by Line:

playTone(550, 'triangle', 0.25, 0.005, 0.12)
Plays a 550 Hz triangle wave (C#5 note) with 25% volume, 5ms attack, 120ms decay for a soft placement sound

playSelectSound()

playSelectSound() creates a quick beep for UI interactions like selecting a hotbar slot. The high frequency and short duration make it feel snappy.

function playSelectSound() {
  playTone(880, 'sine', 0.2, 0.005, 0.08);
}

Line by Line:

playTone(880, 'sine', 0.2, 0.005, 0.08)
Plays a 880 Hz sine wave (A5 note) with 20% volume, 5ms attack, 80ms decay for a short UI bleep

playBlockBreakSound()

playBlockBreakSound() creates a crunchy breaking sound using brown noise instead of a tone. Brown noise has lower frequencies and sounds more like a physical impact, making block breaking feel satisfying.

function playBlockBreakSound() {
  if (!audioInitialized) return;
  const n = new p5.Noise('brown');
  n.amp(0);
  n.start();
  n.amp(0.4, 0.01);
  n.amp(0, 0.25, 0.02);
  n.stop(0.4);
}

๐Ÿ”ง Subcomponents:

calculation Brown Noise Creation const n = new p5.Noise('brown'); n.amp(0); n.start()

Creates brown noise (low-frequency noise) and starts it

calculation Noise Envelope n.amp(0.4, 0.01); n.amp(0, 0.25, 0.02); n.stop(0.4)

Fades noise in and out with quick attack and longer decay

Line by Line:

if (!audioInitialized) return
Exits early if audio hasn't been unlocked
const n = new p5.Noise('brown')
Creates brown noise (lower frequencies than white noise, sounds more natural)
n.amp(0); n.start()
Sets initial amplitude to 0 and starts the noise
n.amp(0.4, 0.01)
Fades in to 40% volume over 10ms
n.amp(0, 0.25, 0.02)
Fades out to 0 over 250ms, starting 20ms after fade-in completes
n.stop(0.4)
Stops the noise generator after 400ms total

initAudioContext()

initAudioContext() unlocks audio on the first user interaction (click, key press, or touch). Modern browsers require user interaction before playing sound for accessibility reasons. This function is called from keyPressed(), mousePressed(), and touchStarted().

function initAudioContext() {
  if (!audioInitialized) {
    userStartAudio();
    audioInitialized = true;
  }
}

Line by Line:

if (!audioInitialized) {
Only initializes once (checks flag)
userStartAudio()
p5.sound helper function that resumes the AudioContext (required by modern browsers)
audioInitialized = true
Sets flag to prevent re-initialization

windowResized()

windowResized() is a p5.js built-in function that's called automatically whenever the window is resized. It keeps the canvas responsive to window size changes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes the canvas to match the new window dimensions when the browser window is resized

๐Ÿ“ฆ Key Variables

touchControls object

Tracks which touch buttons are currently pressed (left, right, jump). Used to update player movement each frame.

let touchControls = { left: false, right: false, jump: false };
isTouchDevice boolean

Stores whether the device supports touch input. Used to determine whether to draw on-screen buttons.

let isTouchDevice = false;
TILE_SIZE number

The pixel size of each block tile (32x32). Used for all coordinate conversions between tiles and pixels.

const TILE_SIZE = 32;
WORLD_WIDTH number

The number of tile columns in the world (200). Defines the horizontal extent of the game world.

const WORLD_WIDTH = 200;
WORLD_HEIGHT number

The number of tile rows in the world (60). Defines the vertical extent of the game world.

const WORLD_HEIGHT = 60;
UI_BAR_H number

Height of the UI bar at the bottom in pixels (60). Includes hotbar and touch controls.

const UI_BAR_H = 60;
AIR number

Block ID constant for empty space (0). Used to represent empty tiles.

const AIR = 0;
GRASS number

Block ID constant for grass blocks (1). Top layer of terrain.

const GRASS = 1;
DIRT number

Block ID constant for dirt blocks (2). Below grass layer.

const DIRT = 2;
STONE number

Block ID constant for stone blocks (3). Underground layer.

const STONE = 3;
WOOD number

Block ID constant for wood blocks (4). Tree trunks.

const WOOD = 4;
LEAVES number

Block ID constant for leaf blocks (5). Tree canopy.

const LEAVES = 5;
world array

2D array storing the entire game world. world[x][y] contains the block ID at tile position (x, y).

let world; // initialized in initWorld() as world[WORLD_WIDTH][WORLD_HEIGHT]
player object

Stores all player state: position (x, y), size (w, h), velocity (vx, vy), and ground status (onGround).

let player = { x: 0, y: 0, w: 22, h: 32, vx: 0, vy: 0, onGround: false };
cameraX number

Horizontal camera position in pixels. Used to convert world coordinates to screen coordinates.

let cameraX = 0;
cameraY number

Vertical camera position in pixels. Used to convert world coordinates to screen coordinates.

let cameraY = 0;
inventory array

Array of block IDs in the hotbar. Stores which blocks the player can place (5 slots).

let inventory = [DIRT, STONE, WOOD, LEAVES, GRASS];
selectedIndex number

Index of the currently selected hotbar slot (0-4). Determines which block type is placed.

let selectedIndex = 0;
GRAVITY number

Acceleration due to gravity in pixels per frame squared (0.8). Applied each frame to player vertical velocity.

const GRAVITY = 0.8;
MOVE_SPEED number

Horizontal movement speed in pixels per frame (3). Applied when moving left/right.

const MOVE_SPEED = 3;
JUMP_SPEED number

Initial upward velocity when jumping in pixels per frame (12). Negative because up is negative Y.

const JUMP_SPEED = 12;
audioInitialized boolean

Flag tracking whether audio context has been unlocked. Prevents errors when playing sounds before user interaction.

let audioInitialized = false;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change MOVE_SPEED from 3 to 5 in the constants section and see how much faster the player moves left and right.
  2. Modify JUMP_SPEED from 12 to 15 to make the player jump higher. Try values between 10 and 20 to find your preferred jump height.
  3. In initWorld(), change the tree generation chance from 0.08 to 0.15 to create a denser forest with more trees.
  4. In drawPlayer(), change the pants color from fill(0, 0, 255) to fill(255, 0, 0) to give the player red pants instead of blue.
  5. In getBlockColor(), modify the GRASS color from [95, 159, 53] to [0, 255, 0] to make grass bright green.
  6. In playJumpSound(), change the frequency from 330 to 440 to make the jump sound higher pitched.
  7. In initWorld(), change baseHeight from 30 to 40 to raise the terrain higher on the screen.
  8. Modify the hotbar in drawUI() to show 6 blocks instead of 5 by changing the loop condition and adding a 6th block type to inventory.
  9. In handleInput(), change the MOVE_SPEED multiplier from 3 to -3 for left movement to reverse the controls (left moves right).
  10. In drawWorld(), change the STONE color from fill(140) to fill(100, 100, 255) to make stone blue instead of gray.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

BUG updatePlayer() collision detection

Player can get stuck on block edges or fall through blocks if moving too fast. The collision detection checks discrete tile positions but doesn't account for sub-tile precision.

๐Ÿ’ก Use continuous collision detection or reduce maximum velocity further. Alternatively, add a small buffer zone around the player collision box.

PERFORMANCE drawWorld()

Recalculating culling bounds every frame is slightly inefficient, and the switch statement for colors is called for every visible block.

๐Ÿ’ก Pre-calculate color arrays as constants outside draw() and use a lookup object instead of switch statement. Cache culling calculations when camera doesn't move significantly.

BUG handleWorldTap()

Player can place blocks inside themselves by tapping at their feet, then the block prevents jumping.

๐Ÿ’ก Use a larger collision buffer when checking rectsOverlap, or expand the player collision box slightly for placement checks.

STYLE Multiple functions

Button position calculations (btnSize, margin, bottomY) are duplicated in drawTouchControls(), handleTouches(), and isInTouchButton().

๐Ÿ’ก Create a helper function that returns button positions, or define them as global constants to avoid duplication.

FEATURE Game mechanics

No way to undo block placement mistakes or clear the world. Players can only build, not reset.

๐Ÿ’ก Add a clear button or press C to reset the world. Or implement an undo system that tracks recent block changes.

PERFORMANCE Audio functions

Creating new oscillator/noise objects every time a sound plays could accumulate memory over long sessions.

๐Ÿ’ก Implement an object pool for oscillators and noise generators, or use a single oscillator that's reused with parameter changes.

BUG touchStarted() and touchEnded()

Jump flag in touchControls is set to true but never reset to false in handleInput(), causing potential double-jumps.

๐Ÿ’ก In handleInput(), add touchControls.jump = false after checking it, or reset it in touchEnded().

FEATURE World generation

Trees are generated randomly but can overlap or generate in invalid positions. No validation of tree placement.

๐Ÿ’ก Add checks to ensure trees don't overlap with each other, and validate that the trunk and leaves don't extend beyond world bounds before placing.

Preview

Simple MineCraft Cline - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Simple MineCraft Cline - Code flow showing setup, draw, initworld, handleinput, keypressed, updateplayer, updatecamera, drawworld, drawplayer, drawcursorhighlight, drawui, getblockcolor, mousepressed, handleworldtap, touchstarted, touchended, handletouches, isintouchbutton, drawtouchcontrols, handlehotbarclick, isinworld, issolid, issolidtile, rectsoverlap, playtone, playjumpsound, playblockplacesound, playselectsound, playblockbreaksound, initaudiocontext, windowresized
Code Flow Diagram