diary of a wimpy kid123

This is a side-scrolling parkour game themed around Diary of a Wimpy Kid where players jump over obstacles while being chased by Rodrick and Fregley. The game features double-jumping, collectible cheese items, increasing difficulty, and a notebook-style visual presentation.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the game start easier — Reducing baseSpeed slows down obstacles at game start, giving you more time to react to each jump.
  2. Strengthen gravity for snappier landing — Higher gravity makes the player fall faster and feel heavier, creating punchier collision feedback.
  3. Enable triple jumping — Increasing MAX_JUMPS allows three consecutive jumps before landing, making difficult sections passable.
  4. Make Rodrick faster and more threatening — Increasing Rodrick's speed makes the chaser threat more immediate and intense.
  5. Make cheese spawn twice as often — Doubling the spawn probability from 0.8% to 1.6% means more cheese to collect and higher bonus scores.
  6. Remove margin line for cleaner aesthetic — Deleting the red notebook margin line creates a wider, less constrained game space.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full side-scrolling parkour game with a Wimpy Kid theme. The player controls Greg navigating a notebook-paper canvas, jumping over obstacles like lockers and trash cans while two chasers close in from behind. The game combines essential p5.js techniques: the draw loop for smooth animation, velocity-based physics for jumping, collision detection for both obstacles and collectibles, and game state management to switch between title, playing, and game-over screens.

The code is organized into distinct sections: global variables that track game state and physics, a setup() function that initializes the canvas, an initGame() helper that resets the game, a main draw() function that dispatches to different screen renderers, and specialized drawing functions for each visual element (background, player, obstacles, chasers). By reading it, you will learn how to structure a complete game loop, manage multiple game objects in arrays, implement double-jumping mechanics, spawn obstacles dynamically, and create screen-shake effects on collision.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, initializes game variables, and calls initGame() to populate the player, obstacles, and background doodles. The canvas is stored in localStorage to remember the high score between sessions.
  2. Every frame, draw() translates the canvas for screen shake if needed, then calls one of three screen renderers—drawTitle(), drawGame(), or drawDeath()—based on the current state variable.
  3. During gameplay, drawGame() scrolls the background lines and doodles rightward, spawns new obstacles at the right edge when gaps form, and moves them leftward at an increasing gameSpeed that accelerates with distance. Obstacles are checked for collision against the player using rectangular hitbox math.
  4. The player's vertical velocity is increased by gravity each frame, then applied to the player's y position. When the player reaches the ground, their velocity resets and they regain double-jump capability. Pressing space or tapping calls doJump(), which applies JUMP_FORCE or JUMP_FORCE_2 (weaker second jump) to the player's velocity.
  5. Two chasers, Rodrick and Fregley, continuously move rightward at different speeds that also accelerate with distance. If Rodrick's x position reaches the player's x position, the player dies, triggering a screen shake and saving the high score if beaten.
  6. Milestone messages flash at specific score thresholds (500, 1500, 3000, etc.), and the HUD displays current score, cheese count, speed multiplier, and warnings when Rodrick gets dangerously close.

🎓 Concepts You'll Learn

Game state managementCollision detectionVelocity and gravity physicsDynamic obstacle spawningMultiple animated objectsParticle effectsScreen shakelocalStorage persistence

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares the canvas, loads persistent data from localStorage, and initializes game objects. This is the perfect place to set up anything that needs to happen before the game begins.

function setup() {
  createCanvas(windowWidth, windowHeight);
  groundY = height - 80;
  highScore = int(localStorage.getItem('wimpyHighScore') || '0');
  textFont('monospace');
  initGame();
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window—windowWidth and windowHeight are p5.js variables that automatically update if the window resizes.
groundY = height - 80;
Sets the ground level 80 pixels from the bottom of the canvas—all obstacles and the player sit on this line.
highScore = int(localStorage.getItem('wimpyHighScore') || '0');
Retrieves the saved high score from the browser's localStorage, or defaults to 0 if nothing is saved—this survives page refreshes.
textFont('monospace');
Sets all text rendering to use monospace font, giving the sketch a code/notebook aesthetic.
initGame();
Calls the initGame() helper function to populate the player, obstacles, and other game objects—keeps setup() clean.

initGame()

initGame() is called once at startup and every time the player restarts after dying. It resets every variable and array to fresh values, then seeds the initial obstacles and background elements. Understanding this function teaches you how to structure a proper game reset.

function initGame() {
  player = {
    x: width * 0.18,
    y: groundY,
    w: 30,
    h: 55,
    vy: 0,
    grounded: true,
    runFrame: 0
  };

  rodrick = { x: -120, y: 0, size: 65, speed: 0.9 };
  fregley = { x: -250, y: 0, size: 45, speed: 0.6 };

  obstacles = [];
  cheeses = [];
  dustParticles = [];
  bgElements = [];
  score = 0;
  distance = 0;
  cheeseCount = 0;
  gameSpeed = baseSpeed;
  jumpsLeft = MAX_JUMPS;
  shakeAmount = 0;
  lastMilestone = -1;
  milestoneMsg = '';
  milestoneMsgTimer = 0;

  for (let i = 0; i < 15; i++) {
    bgElements.push({
      x: random(width * 2),
      y: random(60, groundY - 100),
      type: floor(random(4)),
      size: random(15, 35)
    });
  }

  for (let i = 0; i < 3; i++) {
    spawnObstacle(width + 200 + i * 350);
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Player object initialization player = { x: width * 0.18, y: groundY, w: 30, h: 55, vy: 0, grounded: true, runFrame: 0 };

Creates the player object with position, dimensions, velocity, and animation state—x is 18% across the screen so obstacles appear ahead

calculation Chasers initialization rodrick = { x: -120, y: 0, size: 65, speed: 0.9 }; fregley = { x: -250, y: 0, size: 45, speed: 0.6 };

Positions Rodrick and Fregley off-screen to the left with negative x values—they move at different speeds

for-loop Background doodles loop for (let i = 0; i < 15; i++) { bgElements.push({ ... }); }

Generates 15 random background doodles (stars, spirals, smileys, arrows) scattered across the width

for-loop Initial obstacles loop for (let i = 0; i < 3; i++) { spawnObstacle(width + 200 + i * 350); }

Spawns the first 3 obstacles spaced 350 pixels apart off-screen to the right, so the player has time to react

player = { x: width * 0.18, y: groundY, w: 30, h: 55, vy: 0, grounded: true, runFrame: 0 };
Creates a player object with all properties needed for movement and drawing: x and y for position, w and h for dimensions, vy for vertical velocity, grounded to track if standing on ground, and runFrame for animation timing.
rodrick = { x: -120, y: 0, size: 65, speed: 0.9 };
Rodrick starts off-screen at x = -120 (to the left) with size 65 pixels and speed 0.9 pixels per frame—he is the main threat chasing the player.
fregley = { x: -250, y: 0, size: 45, speed: 0.6 };
Fregley starts further off-screen at x = -250 with smaller size and slower speed—a secondary chaser that accelerates later.
obstacles = [];
Empties the obstacles array, erasing any obstacles from the previous game.
cheeses = [];
Empties the cheeses array so collectible cheese items are removed.
dustParticles = [];
Empties the dust particle array, clearing any leftover landing dust or jump effects.
bgElements = [];
Empties the background doodles array, which will be repopulated with fresh random doodles.
score = 0;
Resets the score to zero for the new game.
distance = 0;
Resets distance to zero—distance increments every frame and is used to accelerate the game speed.
cheeseCount = 0;
Resets the cheese counter so you start with no collected cheese.
gameSpeed = baseSpeed;
Resets gameSpeed to its base value (5)—it will gradually increase as distance grows.
jumpsLeft = MAX_JUMPS;
Gives the player a fresh double-jump by setting jumpsLeft to MAX_JUMPS (2).
for (let i = 0; i < 15; i++) { bgElements.push({ x: random(width * 2), y: random(60, groundY - 100), type: floor(random(4)), size: random(15, 35) }); }
Creates 15 background doodles at random positions across the width and at heights between 60 and groundY-100 pixels—each has a random type (0-3) and size (15-35).
for (let i = 0; i < 3; i++) { spawnObstacle(width + 200 + i * 350); }
Calls spawnObstacle() three times to place the first obstacles at x = width+200, width+550, and width+900—spacing them 350 pixels apart so the player has room to react.

draw()

draw() is the heartbeat of p5.js—it runs 60 times per second. Here it acts as a dispatcher that calls the correct rendering function based on game state. The screen shake effect uses push()/pop() to isolate transformations so they don't affect future frames—a critical p5.js pattern.

🔬 This block controls screen shake. What happens if you change shakeAmount *= 0.9 to shakeAmount *= 0.8? What about *= 0.95? How does the decay speed change the feel of the collision?

  push();
  if (shakeAmount > 0) {
    translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount));
    shakeAmount *= 0.9;
    if (shakeAmount < 0.5) shakeAmount = 0;
  }
function draw() {
  push();
  if (shakeAmount > 0) {
    translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount));
    shakeAmount *= 0.9;
    if (shakeAmount < 0.5) shakeAmount = 0;
  }

  if (state === STATE_TITLE) drawTitle();
  else if (state === STATE_PLAYING) drawGame();
  else if (state === STATE_DEAD) drawDeath();

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

🔧 Subcomponents:

conditional Screen shake effect if (shakeAmount > 0) { translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount)); shakeAmount *= 0.9; if (shakeAmount < 0.5) shakeAmount = 0; }

When the player collides with an obstacle, shakeAmount is set to 12, then gradually decays each frame—translate() shifts the entire canvas randomly to create a shake effect

switch-case Game state renderer dispatch if (state === STATE_TITLE) drawTitle(); else if (state === STATE_PLAYING) drawGame(); else if (state === STATE_DEAD) drawDeath();

Calls different drawing functions based on the current game state variable (title screen, active gameplay, or death screen)

push();
Saves the current transformation matrix (rotation, scale, translation) so that screen shake doesn't affect future frames.
if (shakeAmount > 0) {
Checks if screen shake is active—shakeAmount is set to 12 when the player dies, then decays each frame.
translate(random(-shakeAmount, shakeAmount), random(-shakeAmount, shakeAmount));
Shifts the entire canvas by a random amount on both axes—this creates the jittery feeling of an impact or collision.
shakeAmount *= 0.9;
Multiplies shakeAmount by 0.9 each frame, making the shake smaller and smaller until it's imperceptible.
if (shakeAmount < 0.5) shakeAmount = 0;
When the shake gets too small to see, sets it to exactly 0 to stop wasting processing power on tiny calculations.
if (state === STATE_TITLE) drawTitle();
If state is STATE_TITLE (0), calls drawTitle() to render the title screen with instructions and high score.
else if (state === STATE_PLAYING) drawGame();
If state is STATE_PLAYING (1), calls drawGame() to render the active game loop with obstacles, player, and HUD.
else if (state === STATE_DEAD) drawDeath();
If state is STATE_DEAD (2), calls drawDeath() to render the game-over screen with final score and restart prompt.
pop();
Restores the saved transformation matrix, clearing any screen shake effect applied this frame.

drawTitle()

drawTitle() is called every frame when state === STATE_TITLE. It renders a static welcome screen that responds to window resizing using min() to cap text sizes. The blinking prompt uses frameCount modulo arithmetic—a common technique in creative coding for rhythmic timing effects.

function drawTitle() {
  drawNotebookBg();
  drawGround();

  fill(30);
  noStroke();
  textAlign(CENTER, CENTER);
  textSize(min(width * 0.07, 42));
  text("Diary of a Wimpy Kid", width / 2, height * 0.22);
  textSize(min(width * 0.1, 56));
  fill(180, 30, 30);
  text("PARKOUR", width / 2, height * 0.33);

  drawGreg(width / 2, groundY, 70, 0, false);

  fill(80);
  textSize(min(width * 0.035, 18));
  text("SPACE / UP / TAP to Jump", width / 2, height * 0.55);
  text("Double jump allowed!", width / 2, height * 0.60);

  fill(200, 180, 40);
  textSize(14);
  text("🧀 Collect cheese for bonus points!", width / 2, height * 0.68);

  if (highScore > 0) {
    fill(100);
    textSize(14);
    text("High Score: " + highScore, width / 2, height * 0.75);
  }

  if (frameCount % 60 < 40) {
    fill(30);
    textSize(min(width * 0.04, 22));
    text("[ TAP or PRESS SPACE ]", width / 2, height * 0.85);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

drawNotebookBg();
Renders the off-white notebook paper background with red margin line and blue horizontal lines.
drawGround();
Draws the ground line and grass hatching at the bottom of the screen.
fill(30);
Sets the fill color to dark gray (RGB: 30, 30, 30) for the title text.
textSize(min(width * 0.07, 42));
Sets text size to 7% of window width, but caps it at 42 pixels so it doesn't blow up on huge screens—responsive sizing.
text("Diary of a Wimpy Kid", width / 2, height * 0.22);
Draws the title text centered horizontally at 22% down the screen.
textSize(min(width * 0.1, 56));
Increases text size to 10% of width (max 56) for the subtitle.
fill(180, 30, 30);
Changes fill to red for 'PARKOUR' text to make it stand out.
text("PARKOUR", width / 2, height * 0.33);
Draws the red 'PARKOUR' subtitle at 33% down the screen.
drawGreg(width / 2, groundY, 70, 0, false);
Draws a still (runFrame = 0) standing Greg in the center of the screen at the ground level—a static welcome image.
if (frameCount % 60 < 40) {
frameCount is incremented every frame; frameCount % 60 cycles 0-59, then resets. This condition is true for 40 out of every 60 frames, creating a blink effect.
text("[ TAP or PRESS SPACE ]", width / 2, height * 0.85);
Draws the blinking prompt near the bottom of the screen—only visible when frameCount % 60 < 40.

drawGame()

drawGame() is the core gameplay loop running every frame. It manages obstacles, cheese, the player's physics, chasers, and collision detection. Understanding this function teaches you how to structure a complete game update: scroll the world, move objects, detect collisions, update state, and render the HUD.

function drawGame() {
  drawNotebookBg();

  gameSpeed = baseSpeed + distance / 3000;
  lineOffset = (lineOffset + gameSpeed) % 30;
  distance++;

  for (let i = 0; i < milestones.length; i++) {
    if (score >= milestones[i].score && i > lastMilestone) {
      milestoneMsg = milestones[i].msg;
      milestoneMsgTimer = 120;
      lastMilestone = i;
    }
  }

  for (let bg of bgElements) {
    bg.x -= gameSpeed * 0.3;
    if (bg.x < -50) {
      bg.x = width + random(50, 200);
      bg.y = random(60, groundY - 100);
      bg.type = floor(random(4));
      bg.size = random(15, 35);
    }
    drawDoodle(bg);
  }

  drawGround();

  for (let i = obstacles.length - 1; i >= 0; i--) {
    let o = obstacles[i];
    o.x -= gameSpeed;
    drawObstacle(o);

    let px = player.x, py = player.y - player.h / 2;
    let pw = player.w * 0.5, ph = player.h * 0.8;
    if (px + pw > o.x && px - pw < o.x + o.w && py + ph > o.y && py - ph / 2 < o.y + o.h) {
      die();
    }

    if (o.x + o.w < -20) obstacles.splice(i, 1);
  }

  let lastObs = obstacles[obstacles.length - 1];
  let gap = map(gameSpeed, baseSpeed, baseSpeed + 4, 280, 200);
  gap = max(gap, 160);
  if (!lastObs || width - lastObs.x > gap + random(-40, 60)) {
    spawnObstacle(width + random(20, 80));
  }

  for (let i = cheeses.length - 1; i >= 0; i--) {
    let ch = cheeses[i];
    ch.x -= gameSpeed;
    ch.bobY = sin(frameCount * 0.08 + ch.phase) * 6;

    push();
    fill(220, 190, 40);
    noStroke();
    let cy = ch.y + ch.bobY;
    triangle(ch.x, cy - 10, ch.x - 10, cy + 6, ch.x + 10, cy + 6);
    fill(240, 210, 60);
    ellipse(ch.x - 2, cy - 1, 4, 4);
    ellipse(ch.x + 3, cy + 2, 3, 3);
    pop();

    let d = dist(player.x, player.y - player.h / 2, ch.x, cy);
    if (d < 30) {
      cheeseCount++;
      score += 50;
      cheeses.splice(i, 1);
      continue;
    }
    if (ch.x < -20) cheeses.splice(i, 1);
  }

  if (random() < 0.008) {
    cheeses.push({
      x: width + 20,
      y: groundY - random(60, 150),
      phase: random(TWO_PI),
      bobY: 0
    });
  }

  player.vy += gravity;
  player.y += player.vy;

  if (player.y >= groundY) {
    if (!player.grounded && player.vy > 2) {
      for (let j = 0; j < 5; j++) {
        dustParticles.push({
          x: player.x + random(-10, 10),
          y: groundY,
          vx: random(-2, 2),
          vy: random(-3, -0.5),
          life: 20
        });
      }
    }
    player.y = groundY;
    player.vy = 0;
    player.grounded = true;
    jumpsLeft = MAX_JUMPS;
  } else {
    player.grounded = false;
  }

  player.runFrame += gameSpeed * 0.15;
  drawGreg(player.x, player.y, player.h, player.runFrame, !player.grounded);

  for (let i = dustParticles.length - 1; i >= 0; i--) {
    let p = dustParticles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.15;
    p.life--;
    let a = map(p.life, 0, 20, 0, 150);
    fill(160, 140, 120, a);
    noStroke();
    ellipse(p.x, p.y, 5);
    if (p.life <= 0) dustParticles.splice(i, 1);
  }

  let speedBoost = distance / 4000;
  rodrick.x += rodrick.speed + speedBoost;
  fregley.x += fregley.speed + speedBoost * 0.6;
  rodrick.x = min(rodrick.x, player.x - 25);
  fregley.x = min(fregley.x, rodrick.x - 25);
  rodrick.y = groundY;
  fregley.y = groundY;

  if (rodrick.x > -10) drawGreg(rodrick.x, rodrick.y, rodrick.size, frameCount * 0.2, false, color(160, 40, 40));
  if (fregley.x > -10) drawGreg(fregley.x, fregley.y, fregley.size, frameCount * 0.18, false, color(40, 120, 40));

  if (rodrick.x > 5) {
    fill(160, 40, 40);
    noStroke();
    textSize(10);
    textAlign(CENTER);
    text("Rodrick", rodrick.x, rodrick.y - rodrick.size - 8);
  }
  if (fregley.x > 5) {
    fill(40, 120, 40);
    textSize(10);
    textAlign(CENTER);
    text("Fregley", fregley.x, fregley.y - fregley.size - 8);
  }

  if (rodrick.x >= player.x - player.w * 0.7) die();

  score++;
  drawHUD();
}
Line-by-line explanation (29 lines)

🔧 Subcomponents:

calculation Speed acceleration over distance gameSpeed = baseSpeed + distance / 3000;

As distance increases, gameSpeed increases—game gets harder the longer you survive

for-loop Milestone message check for (let i = 0; i < milestones.length; i++) { if (score >= milestones[i].score && i > lastMilestone) { milestoneMsg = milestones[i].msg; milestoneMsgTimer = 120; lastMilestone = i; } }

Checks if score has reached any milestone thresholds; if so, displays a celebratory message for 120 frames

for-loop Background doodle scrolling for (let bg of bgElements) { bg.x -= gameSpeed * 0.3; if (bg.x < -50) { ... } drawDoodle(bg); }

Scrolls background doodles rightward at 30% of game speed, and recycles them off-screen by regenerating with new properties

for-loop Obstacle update and collision for (let i = obstacles.length - 1; i >= 0; i--) { let o = obstacles[i]; o.x -= gameSpeed; drawObstacle(o); ... if (o.x + o.w < -20) obstacles.splice(i, 1); }

Moves each obstacle leftward, draws it, checks collision with player, and removes it when off-screen (loop goes backwards to safely splice)

conditional Obstacle hit detection if (px + pw > o.x && px - pw < o.x + o.w && py + ph > o.y && py - ph / 2 < o.y + o.h) { die(); }

Uses AABB (axis-aligned bounding box) collision: checks if player's x/y range overlaps obstacle's x/y range

conditional Obstacle spawn check if (!lastObs || width - lastObs.x > gap + random(-40, 60)) { spawnObstacle(width + random(20, 80)); }

Spawns a new obstacle when the last one is far enough away—gap shrinks as game speed increases for harder difficulty

for-loop Cheese collection and bobbing for (let i = cheeses.length - 1; i >= 0; i--) { let ch = cheeses[i]; ch.x -= gameSpeed; ch.bobY = sin(...) * 6; ... }

Scrolls cheese left, makes it bob up/down with sine wave, checks if player touches it, and removes it when off-screen

conditional Random cheese spawn if (random() < 0.008) { cheeses.push({ ... }); }

0.8% chance per frame to spawn a cheese item—averages to about one cheese every 125 frames

conditional Landing dust particles if (!player.grounded && player.vy > 2) { for (let j = 0; j < 5; j++) { dustParticles.push({ ... }); } }

When the player lands hard (vy > 2), spawns 5 dust particles at the ground—particle effect adds visual feedback

calculation Chaser acceleration let speedBoost = distance / 4000; rodrick.x += rodrick.speed + speedBoost; fregley.x += fregley.speed + speedBoost * 0.6;

Both chasers get faster as distance increases, Fregley slower than Rodrick—they progressively catch up to threaten the player

calculation Chaser position cap rodrick.x = min(rodrick.x, player.x - 25); fregley.x = min(fregley.x, rodrick.x - 25);

Prevents chasers from passing the player—they maintain distance but can't overtake, creating tension

conditional Chaser collision if (rodrick.x >= player.x - player.w * 0.7) die();

If Rodrick gets within player.w * 0.7 pixels horizontally of the player, the game ends

gameSpeed = baseSpeed + distance / 3000;
Increases gameSpeed by dividing distance by 3000—for every 3000 pixels traveled, speed increases by 1 unit, making the game harder.
lineOffset = (lineOffset + gameSpeed) % 30;
Increments lineOffset by gameSpeed each frame, cycling 0-29—this offsets the blue lines in the background to create the illusion of scrolling.
distance++;
Increments distance every frame—used for speed acceleration and chaser speedup calculations.
for (let bg of bgElements) { bg.x -= gameSpeed * 0.3; ... }
Updates each background doodle: moves it left at 30% of game speed (slower than the main action), and recycles it to the right if it goes off-screen.
for (let i = obstacles.length - 1; i >= 0; i--) { let o = obstacles[i]; o.x -= gameSpeed; ... }
Loops backwards through obstacles (so splicing doesn't skip items), moves each left by gameSpeed, draws it, checks collision, and removes it if off-screen.
let px = player.x, py = player.y - player.h / 2;
Calculates the player's center position (py is adjusted up by half height) for collision checking.
let pw = player.w * 0.5, ph = player.h * 0.8;
Creates a slightly smaller collision hitbox (50% width, 80% height) to be forgiving—player can clip slightly and not die.
if (px + pw > o.x && px - pw < o.x + o.w && py + ph > o.y && py - ph / 2 < o.y + o.h) { die(); }
AABB collision test: checks if player's x range overlaps obstacle x range AND player's y range overlaps obstacle y range—if both true, collision detected.
if (o.x + o.w < -20) obstacles.splice(i, 1);
Removes obstacles that have scrolled completely off the left side of the screen—cleans up memory by removing objects you can't see.
let gap = map(gameSpeed, baseSpeed, baseSpeed + 4, 280, 200);
As gameSpeed increases from baseSpeed to baseSpeed+4, gap decreases from 280 to 200—obstacles spawn closer together as difficulty increases.
gap = max(gap, 160);
Caps the gap at 160 pixels minimum—ensures obstacles never spawn closer than that even at high speeds.
if (!lastObs || width - lastObs.x > gap + random(-40, 60)) { spawnObstacle(width + random(20, 80)); }
Spawns a new obstacle when there are no obstacles OR when the last obstacle is more than gap pixels away—random variance keeps spacing unpredictable.
ch.bobY = sin(frameCount * 0.08 + ch.phase) * 6;
Uses sine wave with frameCount to animate bobbing—each cheese has its own phase so they bob at different times, creating organic movement.
let d = dist(player.x, player.y - player.h / 2, ch.x, cy);
Calculates distance between player center and cheese center—the dist() function returns Euclidean distance.
if (d < 30) { cheeseCount++; score += 50; cheeses.splice(i, 1); continue; }
If distance is less than 30 pixels, the player has collected the cheese—increment cheese count, add 50 points, remove cheese from array, and skip to next iteration.
if (random() < 0.008) { cheeses.push({ ... }); }
random() returns 0.0-1.0; 0.8% chance per frame to spawn cheese—averages to one every ~125 frames (60 fps / 0.008 ≈ 7500 frames).
player.vy += gravity;
Applies gravity to the player's vertical velocity—accelerates downward each frame simulating falling.
player.y += player.vy;
Updates player's y position by adding velocity—moves the player up or down based on accumulated vy.
if (player.y >= groundY) {
Checks if player has reached or passed the ground—triggers landing logic.
if (!player.grounded && player.vy > 2) { for (let j = 0; j < 5; j++) { dustParticles.push({ ... }); } }
If the player was in the air and lands hard (vy > 2), spawn 5 dust particles—creates a landing effect.
player.y = groundY; player.vy = 0; player.grounded = true; jumpsLeft = MAX_JUMPS;
Resets player y to exactly groundY, zeroes velocity, marks grounded as true, and restores double-jump capability.
player.runFrame += gameSpeed * 0.15;
Increments runFrame for animation—multiplied by gameSpeed so running animation speeds up when the game speed increases.
for (let i = dustParticles.length - 1; i >= 0; i--) { ... p.life--; ... if (p.life <= 0) dustParticles.splice(i, 1); }
Updates dust particles: moves them, applies gravity, decreases alpha with life countdown, and removes them when life reaches 0—backwards loop for safe splicing.
let speedBoost = distance / 4000;
Calculates how much extra speed the chasers gain from distance—every 4000 pixels, speedBoost increases by 1.
rodrick.x += rodrick.speed + speedBoost; fregley.x += fregley.speed + speedBoost * 0.6;
Moves both chasers right at their base speed plus the distance-based speedBoost—Fregley gets 60% of the boost so Rodrick is the main threat.
rodrick.x = min(rodrick.x, player.x - 25); fregley.x = min(fregley.x, rodrick.x - 25);
Caps each chaser at a maximum x position—Rodrick can't go past player - 25 pixels, Fregley can't go past Rodrick - 25 pixels.
if (rodrick.x >= player.x - player.w * 0.7) die();
If Rodrick's x reaches within player.w * 0.7 pixels of the player, the game ends—Rodrick is the winning condition.
score++;
Increments score by 1 every frame—at 60 fps, this is 3600 points per minute of survival (in addition to cheese bonuses).
drawHUD();
Calls the HUD rendering function to display score, cheese count, speed, and warnings on top of the game.

drawDeath()

drawDeath() renders a static death screen with statistics. It uses push()/pop() and rotate() to show Greg fallen on his side, creating a memorable death animation. The high score comparison teaches you how to present game stats in a way that motivates replay.

function drawDeath() {
  drawNotebookBg();
  drawGround();

  push();
  translate(player.x, groundY);
  rotate(HALF_PI);
  drawGregBody(0, 0, player.h, 0, false, color(0));
  pop();

  fill(30);
  noStroke();
  textAlign(CENTER, CENTER);
  textSize(min(width * 0.09, 48));
  text("GAME OVER", width / 2, height * 0.25);

  textSize(min(width * 0.05, 28));
  text("Score: " + score, width / 2, height * 0.38);

  if (cheeseCount > 0) {
    fill(200, 180, 40);
    textSize(18);
    text("🧀 x " + cheeseCount, width / 2, height * 0.45);
  }

  fill(100);
  textSize(16);
  text("High Score: " + highScore, width / 2, height * 0.53);

  if (score >= highScore && score > 0) {
    fill(180, 30, 30);
    textSize(20);
    text("★ NEW HIGH SCORE! ★", width / 2, height * 0.60);
  }

  if (frameCount % 60 < 40) {
    fill(30);
    textSize(min(width * 0.035, 20));
    text("[ TAP or PRESS SPACE ]", width / 2, height * 0.75);
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Fallen Greg rotation push(); translate(player.x, groundY); rotate(HALF_PI); drawGregBody(0, 0, player.h, 0, false, color(0)); pop();

Rotates Greg 90 degrees (HALF_PI radians) to show him lying on his side—visual feedback for losing

conditional New high score message if (score >= highScore && score > 0) { fill(180, 30, 30); textSize(20); text("★ NEW HIGH SCORE! ★", width / 2, height * 0.60); }

Displays a celebratory message in red if the current score beats the saved high score

drawNotebookBg();
Renders the notebook background so the death screen matches the visual style.
drawGround();
Draws the ground line and grass.
push();
Saves the current transformation matrix before rotating.
translate(player.x, groundY);
Moves the origin to the player's position so rotation happens at that point.
rotate(HALF_PI);
Rotates 90 degrees (π/2 radians)—HALF_PI is a p5.js constant equal to 1.5708 radians.
drawGregBody(0, 0, player.h, 0, false, color(0));
Draws Greg's body at the origin (rotated to lie on his side)—runFrame = 0 keeps him still, color(0) makes him black.
pop();
Restores the transformation matrix, clearing the rotation.
text("GAME OVER", width / 2, height * 0.25);
Displays the game over message in large text near the top of the screen.
text("Score: " + score, width / 2, height * 0.38);
Shows the final score below the game over message.
if (cheeseCount > 0) { fill(200, 180, 40); textSize(18); text("🧀 x " + cheeseCount, width / 2, height * 0.45); }
If any cheese was collected, displays the count in yellow below the score.
text("High Score: " + highScore, width / 2, height * 0.53);
Displays the saved high score in gray, allowing comparison with the current game's score.
if (score >= highScore && score > 0) { fill(180, 30, 30); textSize(20); text("★ NEW HIGH SCORE! ★", width / 2, height * 0.60); }
If the current score matches or exceeds the high score (and is greater than 0), shows a celebratory message in red with stars.

drawNotebookBg()

drawNotebookBg() creates the visual foundation of the entire sketch. It demonstrates how static lines (margin) and dynamic lines (scrolling blue rules) combine to create a scrolling background effect. The -lineOffset trick is an elegant way to achieve infinite scrolling with a small offset variable.

function drawNotebookBg() {
  background(250, 245, 235);

  stroke(200, 150, 150);
  strokeWeight(2);
  line(70, 0, 70, height);

  stroke(190, 210, 230);
  strokeWeight(1);
  let startY = state === STATE_PLAYING ? -lineOffset : 0;
  for (let y = startY; y < height; y += 30) {
    if (y > 20) line(0, y, width, y);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Scrolling blue lines let startY = state === STATE_PLAYING ? -lineOffset : 0; for (let y = startY; y < height; y += 30) { if (y > 20) line(0, y, width, y); }

When playing, offset the blue lines by -lineOffset to create the visual effect of scrolling; on other screens, lines are static

background(250, 245, 235);
Fills the canvas with an off-white color (RGB: 250, 245, 235) to simulate old notebook paper.
stroke(200, 150, 150);
Sets stroke color to a soft red for the margin line.
strokeWeight(2);
Makes the margin line 2 pixels thick.
line(70, 0, 70, height);
Draws a vertical red line 70 pixels from the left edge, running from top to bottom—the iconic notebook margin.
stroke(190, 210, 230);
Changes stroke color to soft blue for the horizontal lines.
strokeWeight(1);
Makes the horizontal lines 1 pixel thick (thinner than the margin).
let startY = state === STATE_PLAYING ? -lineOffset : 0;
If the game is playing, startY is negative (offset by lineOffset), creating the illusion that lines are scrolling down—on other screens, lines are static.
for (let y = startY; y < height; y += 30) {
Loops from startY to the canvas height, incrementing by 30 each time—draws horizontal lines 30 pixels apart.
if (y > 20) line(0, y, width, y);
Only draws the line if y > 20 to avoid clipping at the top of the screen—keeps the top margin clean.

drawGround()

drawGround() creates both the structural ground line and visual detail with grass hatching. The random variation in height and x position makes the effect look hand-drawn rather than mechanical—a key technique in sketch-like visual styles.

function drawGround() {
  stroke(60);
  strokeWeight(3);
  line(0, groundY, width, groundY);

  strokeWeight(1);
  stroke(100, 140, 80);
  for (let x = 0; x < width; x += 8) {
    let h = random(4, 12);
    line(x, groundY, x + random(-3, 3), groundY + h);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Grass hatching effect for (let x = 0; x < width; x += 8) { let h = random(4, 12); line(x, groundY, x + random(-3, 3), groundY + h); }

Draws random short lines below the ground line to create a sketchy grass/terrain effect

stroke(60);
Sets stroke color to dark gray (RGB: 60, 60, 60) for the ground line.
strokeWeight(3);
Makes the ground line 3 pixels thick so it stands out.
line(0, groundY, width, groundY);
Draws a horizontal line at the groundY height across the entire width—this is the main ground line where the player stands.
strokeWeight(1);
Makes subsequent lines 1 pixel thick for the grass detail.
stroke(100, 140, 80);
Changes stroke color to a dull green (RGB: 100, 140, 80) to suggest grass.
for (let x = 0; x < width; x += 8) {
Loops across the width, stepping 8 pixels at a time—places grass marks every 8 pixels.
let h = random(4, 12);
Picks a random height between 4 and 12 pixels for each grass stroke—adds organic variation.
line(x, groundY, x + random(-3, 3), groundY + h);
Draws a line from the ground downward, with x slightly jittered (±3 pixels) to look natural and sketchy.

drawGreg()

drawGreg() is a wrapper function that isolates the transformation. Using push()/translate/pop() around drawGregBody() allows you to easily position Greg anywhere on the canvas without affecting other drawings—a core pattern in p5.js for managing coordinate systems.

function drawGreg(x, y, h, frame, inAir, col) {
  push();
  translate(x, y);
  drawGregBody(0, 0, h, frame, inAir, col || color(0));
  pop();
}
Line-by-line explanation (4 lines)
push();
Saves the current transformation matrix before translating.
translate(x, y);
Moves the origin to (x, y) so drawGregBody() can draw at local coordinates starting at (0, 0).
drawGregBody(0, 0, h, frame, inAir, col || color(0));
Calls the helper function to actually draw Greg's body at (0, 0)—col || color(0) uses the provided color or defaults to black.
pop();
Restores the transformation matrix, undoing the translation so subsequent drawings aren't affected.

drawGregBody()

drawGregBody() builds a stick-figure character from scratch using lines, ellipses, and sine-wave animation. It teaches scaling (using the s factor), coordinate math (positioning body parts relative to each other), and how to animate joints using oscillating functions. This is a foundational technique for drawing any procedural character.

function drawGregBody(x, y, h, frame, inAir, col) {
  let s = h / 55; // scale factor
  stroke(col);
  strokeWeight(2.5 * s);
  noFill();

  let headY = y - h + 12 * s;
  ellipse(x, headY, 22 * s, 24 * s);

  fill(col);
  noStroke();
  ellipse(x - 4 * s, headY - 2 * s, 3 * s, 3 * s);
  ellipse(x + 4 * s, headY - 2 * s, 3 * s, 3 * s);

  stroke(col);
  noFill();
  strokeWeight(2.5 * s);
  line(x, headY + 12 * s, x, y - 22 * s);

  let armSwing = sin(frame * 0.8) * 15 * s;
  if (inAir) armSwing = -10 * s;
  line(x, y - h * 0.55, x - 14 * s, y - h * 0.35 + armSwing);
  line(x, y - h * 0.55, x + 14 * s, y - h * 0.35 - armSwing);

  let legSwing = sin(frame * 0.8) * 12 * s;
  if (inAir) legSwing = 8 * s;
  line(x, y - 22 * s, x - 10 * s + legSwing, y - 2 * s);
  line(x, y - 22 * s, x + 10 * s - legSwing, y - 2 * s);

  strokeWeight(3 * s);
  point(x - 10 * s + legSwing, y - 2 * s);
  point(x + 10 * s - legSwing, y - 2 * s);
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Head drawing let headY = y - h + 12 * s; ellipse(x, headY, 22 * s, 24 * s);

Positions and draws the head as an ellipse, sized proportionally to h

calculation Eyes drawing ellipse(x - 4 * s, headY - 2 * s, 3 * s, 3 * s); ellipse(x + 4 * s, headY - 2 * s, 3 * s, 3 * s);

Draws two filled eyes as small ellipses

calculation Animated arm swing let armSwing = sin(frame * 0.8) * 15 * s; if (inAir) armSwing = -10 * s;

Uses sine wave on frame to swing arms while running, or holds them up when jumping

calculation Animated leg swing let legSwing = sin(frame * 0.8) * 12 * s; if (inAir) legSwing = 8 * s;

Uses sine wave on frame to swing legs while running, or holds them in running position when jumping

let s = h / 55;
Calculates a scale factor based on height h (default 55)—ensures all body parts scale proportionally when Greg is drawn at different sizes.
stroke(col); strokeWeight(2.5 * s); noFill();
Sets up stroking: use color col, thickness scaled to s, and don't fill shapes—prepares for outline drawing.
let headY = y - h + 12 * s;
Calculates head position: starts from y (feet level), subtracts h (height), then adds 12*s (head offset from top)—places head near the top of the figure.
ellipse(x, headY, 22 * s, 24 * s);
Draws the head as an ellipse centered at (x, headY) with width 22*s and height 24*s—slightly taller than wide to look like a head.
fill(col); noStroke();
Switches to filled mode using col and no stroke—prepares to draw filled eyes.
ellipse(x - 4 * s, headY - 2 * s, 3 * s, 3 * s);
Draws the left eye as a small filled circle offset left and up from the head center.
ellipse(x + 4 * s, headY - 2 * s, 3 * s, 3 * s);
Draws the right eye as a small filled circle offset right and up from the head center.
stroke(col); noFill(); strokeWeight(2.5 * s);
Switches back to stroke mode for drawing the body with outlines.
line(x, headY + 12 * s, x, y - 22 * s);
Draws the body (torso) as a vertical line from below the head to above the feet.
let armSwing = sin(frame * 0.8) * 15 * s;
Uses sine wave of the frame number to create oscillating arm swing—sin(frame * 0.8) cycles from -1 to 1, multiplied by 15*s for swing distance.
if (inAir) armSwing = -10 * s;
When jumping (inAir = true), ignores the swing calculation and sets armSwing to -10*s (upward) to show arms raised.
line(x, y - h * 0.55, x - 14 * s, y - h * 0.35 + armSwing);
Draws left arm from shoulder to left hand, with hand y-position affected by armSwing for animation.
line(x, y - h * 0.55, x + 14 * s, y - h * 0.35 - armSwing);
Draws right arm from shoulder to right hand, with opposite swing (-armSwing) so arms swing in opposition.
let legSwing = sin(frame * 0.8) * 12 * s;
Uses sine wave to create leg swing during running—oscillates by ±12*s.
if (inAir) legSwing = 8 * s;
When jumping, sets legSwing to 8*s—a fixed forward position to show legs extended while airborne.
line(x, y - 22 * s, x - 10 * s + legSwing, y - 2 * s);
Draws left leg from hip to left foot, with x-position affected by legSwing—swinging leg moves forward/back.
line(x, y - 22 * s, x + 10 * s - legSwing, y - 2 * s);
Draws right leg from hip to right foot, with opposite swing (-legSwing) for alternating motion.
strokeWeight(3 * s); point(x - 10 * s + legSwing, y - 2 * s); point(x + 10 * s - legSwing, y - 2 * s);
Draws feet as thicker points at the end of each leg—visual emphasis on leg endpoints.

drawObstacle()

drawObstacle() demonstrates how to use object properties to drive visual rendering. The obstacle object o carries color, dimensions, and type information; drawObstacle() reads these and draws accordingly. The detail lines (handle, rim, shelves) make the sketch feel polished—without them, obstacles would be indistinguishable colored rectangles.

function drawObstacle(o) {
  push();
  let c = o.col;
  fill(c[0], c[1], c[2]);
  stroke(c[0] * 0.6, c[1] * 0.6, c[2] * 0.6);
  strokeWeight(2);
  rectMode(CORNER);
  rect(o.x, o.y, o.w, o.h, 3);

  stroke(c[0] * 0.5, c[1] * 0.5, c[2] * 0.5);
  strokeWeight(1);
  if (o.type === 'locker') {
    line(o.x + o.w / 2, o.y + 5, o.x + o.w / 2, o.y + o.h - 5);
    rect(o.x + o.w * 0.6, o.y + o.h * 0.4, 4, 4);
  } else if (o.type === 'trashcan') {
    line(o.x + 4, o.y + 5, o.x + o.w - 4, o.y + 5);
    line(o.x + 3, o.y, o.x + o.w - 3, o.y);
  } else if (o.type === 'bookstack') {
    for (let ly = o.y + 8; ly < o.y + o.h - 4; ly += 8) {
      line(o.x + 2, ly, o.x + o.w - 2, ly);
    }
  } else if (o.type === 'desk') {
    line(o.x + 3, o.y + o.h * 0.3, o.x + o.w - 3, o.y + o.h * 0.3);
  }
  pop();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

switch-case Obstacle type detail rendering if (o.type === 'locker') { ... } else if (o.type === 'trashcan') { ... } else if (o.type === 'bookstack') { ... } else if (o.type === 'desk') { ... }

Draws different detail lines for each obstacle type to make them visually distinct—locker has a vertical line and handle, trashcan has rim lines, bookstack has horizontal shelf lines, desk has a flat top line

push();
Saves the current drawing state so fill/stroke changes don't affect other drawings.
let c = o.col;
Stores the obstacle's color array (e.g., [70, 90, 140] for a locker) in variable c for convenience.
fill(c[0], c[1], c[2]);
Sets fill color to the obstacle's assigned color using its RGB channels.
stroke(c[0] * 0.6, c[1] * 0.6, c[2] * 0.6);
Sets stroke color to a darker shade (60% brightness) by multiplying each channel by 0.6—creates outline contrast.
strokeWeight(2);
Makes the obstacle outline 2 pixels thick.
rectMode(CORNER);
Sets rect() to use (x, y) as the top-left corner—standard drawing mode.
rect(o.x, o.y, o.w, o.h, 3);
Draws the obstacle as a filled rectangle with rounded corners (3-pixel radius)—looks softer and less mechanical.
if (o.type === 'locker') { line(o.x + o.w / 2, o.y + 5, o.x + o.w / 2, o.y + o.h - 5); rect(o.x + o.w * 0.6, o.y + o.h * 0.4, 4, 4); }
For lockers: draws a vertical line down the middle (door seam) and a small square (handle)—clearly identifies it as a school locker.
else if (o.type === 'trashcan') { line(o.x + 4, o.y + 5, o.x + o.w - 4, o.y + 5); line(o.x + 3, o.y, o.x + o.w - 3, o.y); }
For trashcans: draws two horizontal lines to represent a rim and lid—looks like a cylindrical trash bin.
else if (o.type === 'bookstack') { for (let ly = o.y + 8; ly < o.y + o.h - 4; ly += 8) { line(o.x + 2, ly, o.x + o.w - 2, ly); } }
For bookstacks: draws multiple horizontal lines spaced 8 pixels apart to represent stacked books.
else if (o.type === 'desk') { line(o.x + 3, o.y + o.h * 0.3, o.x + o.w - 3, o.y + o.h * 0.3); }
For desks: draws a single horizontal line at 30% down the height to represent the desk top.
pop();
Restores the previous drawing state, clearing all style changes.

drawDoodle()

drawDoodle() generates procedural decorations using polar coordinates (cos/sin and angles). Each doodle type teaches a different technique: stars show alternating radii, spirals show expanding radius with angle, smileys show basic shape composition, and arrows show constructive geometry. These are the building blocks of procedurally generated art.

function drawDoodle(bg) {
  push();
  stroke(180, 190, 200);
  strokeWeight(1.5);
  noFill();
  let x = bg.x, y = bg.y, s = bg.size;
  if (bg.type === 0) {
    for (let a = -HALF_PI; a < TWO_PI - HALF_PI; a += TWO_PI / 5) {
      let x1 = x + cos(a) * s * 0.5;
      let y1 = y + sin(a) * s * 0.5;
      let x2 = x + cos(a + TWO_PI / 10) * s * 0.2;
      let y2 = y + sin(a + TWO_PI / 10) * s * 0.2;
      line(x1, y1, x2, y2);
    }
  } else if (bg.type === 1) {
    beginShape();
    for (let a = 0; a < TWO_PI * 2; a += 0.3) {
      let r = a * s * 0.04;
      vertex(x + cos(a) * r, y + sin(a) * r);
    }
    endShape();
  } else if (bg.type === 2) {
    ellipse(x, y, s, s);
    point(x - s * 0.15, y - s * 0.1);
    point(x + s * 0.15, y - s * 0.1);
    arc(x, y + s * 0.05, s * 0.4, s * 0.3, 0, PI);
  } else {
    line(x - s * 0.4, y, x + s * 0.4, y);
    line(x + s * 0.2, y - s * 0.2, x + s * 0.4, y);
    line(x + s * 0.2, y + s * 0.2, x + s * 0.4, y);
  }
  pop();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Star doodle type 0 if (bg.type === 0) { for (let a = -HALF_PI; a < TWO_PI - HALF_PI; a += TWO_PI / 5) { ... } }

Draws a 5-pointed star by calculating alternating outer and inner points using polar coordinates

for-loop Spiral doodle type 1 else if (bg.type === 1) { beginShape(); for (let a = 0; a < TWO_PI * 2; a += 0.3) { let r = a * s * 0.04; vertex(...); } endShape(); }

Draws a spiral by increasing radius r as the angle a increases—creates a logarithmic spiral effect

calculation Smiley doodle type 2 else if (bg.type === 2) { ellipse(x, y, s, s); point(...); point(...); arc(...); }

Draws a smiley face with circle head, two point eyes, and arc mouth

calculation Arrow doodle type 3 else { line(x - s * 0.4, y, x + s * 0.4, y); line(...); line(...); }

Draws an arrow pointing right with a shaft and arrowhead

push();
Saves drawing state so doodle styling doesn't affect other drawings.
stroke(180, 190, 200);
Sets stroke to a pale blue-gray color that fits the notebook aesthetic.
strokeWeight(1.5);
Makes doodle lines 1.5 pixels thick—thin enough to look sketchy.
noFill();
Draws only outlines, no filled shapes—keeps doodles looking hand-drawn.
if (bg.type === 0) { for (let a = -HALF_PI; a < TWO_PI - HALF_PI; a += TWO_PI / 5) { ... } }
Type 0 draws a star: loops 5 times (TWO_PI / 5 ≈ 72° increments), calculating alternating outer points (radius 0.5s) and inner points (radius 0.2s) using polar coordinates.
let x1 = x + cos(a) * s * 0.5; let y1 = y + sin(a) * s * 0.5;
Calculates outer star point at angle a, distance 0.5s from center using cosine and sine.
let x2 = x + cos(a + TWO_PI / 10) * s * 0.2; let y2 = y + sin(a + TWO_PI / 10) * s * 0.2;
Calculates inner star point offset by TWO_PI/10 (half a star segment) at distance 0.2s—creates alternating peak-valley pattern.
else if (bg.type === 1) { beginShape(); for (let a = 0; a < TWO_PI * 2; a += 0.3) { ... } endShape(); }
Type 1 draws a spiral: loops twice around the circle (TWO_PI * 2), incrementing radius r as angle increases—r = a * s * 0.04 makes the spiral expand.
else if (bg.type === 2) { ellipse(x, y, s, s); point(...); point(...); arc(...); }
Type 2 draws a smiley: circle for head, two points for eyes, and arc for mouth—uses the size s to scale everything.
else { line(x - s * 0.4, y, x + s * 0.4, y); line(...); line(...); }
Type 3 draws an arrow: horizontal shaft from left to right, then two diagonal lines forming the arrowhead.
pop();
Restores previous drawing state.

drawHUD()

drawHUD() demonstrates UI design and information hierarchy. It uses conditional rendering to show only relevant information (cheese count only if > 0, jump indicator only when airborne), dynamic sizing and positioning (warnings over the player), and alpha transparency (fade effects and urgency opacity). Good HUD design doesn't clutter the screen but makes critical information always available.

function drawHUD() {
  fill(30);
  noStroke();
  textAlign(LEFT, TOP);
  textSize(22);
  text("Score: " + score, 80, 15);

  if (cheeseCount > 0) {
    textSize(16);
    fill(200, 180, 40);
    text("🧀 x " + cheeseCount, 80, 42);
  }

  fill(150);
  textSize(11);
  textAlign(RIGHT, TOP);
  text("Speed: " + nf(gameSpeed, 1, 1) + "x", width - 15, 15);

  if (!player.grounded && jumpsLeft > 0) {
    fill(80, 130, 200, 180);
    textAlign(CENTER);
    textSize(12);
    text("▲ " + jumpsLeft + " jump" + (jumpsLeft > 1 ? "s" : "") + " left", player.x, player.y - player.h - 20);
  }

  let gap = player.x - rodrick.x;
  if (gap < 90 && rodrick.x > 0) {
    let urgency = map(gap, 90, 25, 0, 255);
    fill(200, 30, 30, urgency);
    textAlign(CENTER, TOP);
    textSize(15);
    text("⚠ RODRICK IS CLOSE!", width / 2, 12);
  }

  if (milestoneMsgTimer > 0) {
    milestoneMsgTimer--;
    let a = milestoneMsgTimer > 20 ? 255 : map(milestoneMsgTimer, 0, 20, 0, 255);
    fill(30, a);
    textAlign(CENTER, CENTER);
    textSize(24);
    text(milestoneMsg, width / 2, height * 0.15);
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Cheese counter display if (cheeseCount > 0) { textSize(16); fill(200, 180, 40); text("🧀 x " + cheeseCount, 80, 42); }

Shows cheese collected count in yellow only if cheeseCount > 0—doesn't clutter the HUD when no cheese is collected

conditional Jump indicator above player if (!player.grounded && jumpsLeft > 0) { fill(80, 130, 200, 180); textAlign(CENTER); textSize(12); text(...); }

Shows remaining jumps in a semi-transparent blue box above the player when airborne—helps player understand their double-jump capability

conditional Rodrick proximity warning let gap = player.x - rodrick.x; if (gap < 90 && rodrick.x > 0) { let urgency = map(gap, 90, 25, 0, 255); fill(200, 30, 30, urgency); ... }

Displays a warning when Rodrick gets within 90 pixels of the player; opacity increases as gap shrinks—escalating visual urgency

conditional Milestone celebration message if (milestoneMsgTimer > 0) { milestoneMsgTimer--; let a = milestoneMsgTimer > 20 ? 255 : map(...); fill(30, a); text(milestoneMsg, ...); }

Fades out milestone messages over 120 frames—creates a celebratory pop-up effect

fill(30); noStroke(); textAlign(LEFT, TOP); textSize(22);
Sets up dark text styling, left-aligned from top-left corner, large size—standard HUD setup.
text("Score: " + score, 80, 15);
Displays the current score at (80, 15)—offset 80 pixels right to clear the notebook margin line.
if (cheeseCount > 0) { textSize(16); fill(200, 180, 40); text("🧀 x " + cheeseCount, 80, 42); }
If cheese has been collected, displays the count in yellow below the score—only shows when relevant.
fill(150); textSize(11); textAlign(RIGHT, TOP);
Switches to smaller gray text, right-aligned—prepares for speed indicator in top-right.
text("Speed: " + nf(gameSpeed, 1, 1) + "x", width - 15, 15);
Displays gameSpeed formatted to 1 decimal place (nf = number format) in the top-right corner (width - 15)—shows current difficulty level.
if (!player.grounded && jumpsLeft > 0) { fill(80, 130, 200, 180); textAlign(CENTER); textSize(12); text("▲ " + jumpsLeft + " jump" + (jumpsLeft > 1 ? "s" : "") + " left", player.x, player.y - player.h - 20); }
When the player is airborne and has jumps remaining, displays a semi-transparent blue indicator above the player showing how many jumps are left—grammatically correct with singular/plural 'jump' vs 'jumps'.
let gap = player.x - rodrick.x;
Calculates the horizontal distance between player and Rodrick.
if (gap < 90 && rodrick.x > 0) {
Checks if Rodrick is within 90 pixels AND is visible on screen (x > 0)—prevents warning at game start.
let urgency = map(gap, 90, 25, 0, 255);
Maps gap range [90, 25] to opacity [0, 255]—as Rodrick gets closer, the warning becomes more opaque and urgent.
fill(200, 30, 30, urgency);
Sets fill to red with opacity from urgency variable—creates a dynamic danger indicator.
text("⚠ RODRICK IS CLOSE!", width / 2, 12);
Displays the warning message in the top center of the screen.
if (milestoneMsgTimer > 0) { milestoneMsgTimer--; ... }
Decrements milestone timer each frame and displays the message with fading alpha—lasts 120 frames total, fades out in the last 20 frames.
let a = milestoneMsgTimer > 20 ? 255 : map(milestoneMsgTimer, 0, 20, 0, 255);
Keeps message fully opaque for 100 frames (milestoneMsgTimer > 20), then fades from 255 to 0 opacity over the last 20 frames—smooth exit animation.

spawnObstacle()

spawnObstacle() demonstrates data-driven design: the OBS_TYPES array defines all obstacle properties, and spawnObstacle() simply reads from it to generate random instances. This makes it easy to add new obstacle types without changing the spawn logic—just add an entry to OBS_TYPES.

function spawnObstacle(xPos) {
  let type = OBS_TYPES[floor(random(OBS_TYPES.length))];
  let w = random(type.wRange[0], type.wRange[1]);
  let h = random(type.hRange[0], type.hRange[1]);
  obstacles.push({
    x: xPos,
    y: groundY - h,
    w: w,
    h: h,
    col: type.color,
    type: type.name
  });
}
Line-by-line explanation (4 lines)
let type = OBS_TYPES[floor(random(OBS_TYPES.length))];
Picks a random obstacle type from the OBS_TYPES array—floor(random(...)) selects a random index.
let w = random(type.wRange[0], type.wRange[1]);
Picks a random width within the selected type's range—e.g., lockers are 30-45 pixels wide.
let h = random(type.hRange[0], type.hRange[1]);
Picks a random height within the selected type's range—adds variation so obstacles aren't all identical.
obstacles.push({ x: xPos, y: groundY - h, w: w, h: h, col: type.color, type: type.name });
Creates an obstacle object with position (x at edge of screen, y above ground by h), dimensions (w, h), color, and type name—adds it to the obstacles array.

doJump()

doJump() handles both game state transitions and jump mechanics. It's called by keyboard and touch input, and uses different logic based on game state—a good example of state-driven control flow. The ternary operator for jump force shows how to implement variable difficulty based on game context.

function doJump() {
  if (state === STATE_TITLE) {
    state = STATE_PLAYING;
    initGame();
    return;
  }
  if (state === STATE_DEAD) {
    state = STATE_TITLE;
    return;
  }
  if (state === STATE_PLAYING && jumpsLeft > 0) {
    let force = jumpsLeft === MAX_JUMPS ? JUMP_FORCE : JUMP_FORCE_2;
    player.vy = force;
    player.grounded = false;
    jumpsLeft--;

    for (let j = 0; j < 3; j++) {
      dustParticles.push({
        x: player.x + random(-8, 8),
        y: groundY,
        vx: random(-1.5, 1.5),
        vy: random(-2, -0.3),
        life: 15
      });
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Title to playing state transition if (state === STATE_TITLE) { state = STATE_PLAYING; initGame(); return; }

Pressing jump on title screen starts the game by switching state and initializing

conditional Dead to title state transition if (state === STATE_DEAD) { state = STATE_TITLE; return; }

Pressing jump on death screen returns to title for restart

conditional Double jump force selection let force = jumpsLeft === MAX_JUMPS ? JUMP_FORCE : JUMP_FORCE_2;

Uses full strength jump force if this is the first jump, weaker force if it's the second jump

for-loop Jump dust particles for (let j = 0; j < 3; j++) { dustParticles.push({ ... }); }

Spawns 3 dust particles at the ground when jumping—visual feedback

if (state === STATE_TITLE) { state = STATE_PLAYING; initGame(); return; }
If on title screen, pressing jump starts the game: change state to PLAYING, initialize the game, and return early to prevent jump logic from executing.
if (state === STATE_DEAD) { state = STATE_TITLE; return; }
If on death screen, pressing jump returns to title screen—allows restart without reloading the page.
if (state === STATE_PLAYING && jumpsLeft > 0) {
Only executes jump logic if actively playing AND jumps remain—prevents jumping after using both jumps.
let force = jumpsLeft === MAX_JUMPS ? JUMP_FORCE : JUMP_FORCE_2;
Ternary operator: if jumpsLeft equals MAX_JUMPS (2), use JUMP_FORCE (-13); otherwise use JUMP_FORCE_2 (-11)—weakens the second jump for game balance.
player.vy = force;
Sets the player's vertical velocity to the jump force—negative values make them move up (against gravity).
player.grounded = false;
Marks the player as airborne—prevents double-jumping until landing.
jumpsLeft--;
Decrements jumps remaining by 1—after two jumps, jumpsLeft = 0 and no more jumps are allowed.
for (let j = 0; j < 3; j++) { dustParticles.push({ ... }); }
Spawns 3 dust particles at the player's feet to show the jump impact—adds visual feedback and polish.

die()

die() is the game-over trigger. It demonstrates persistent data storage using localStorage and state management. The early return pattern prevents re-entry bugs, and the screen shake provides immediate sensory feedback that something catastrophic happened.

function die() {
  if (state !== STATE_PLAYING) return;
  state = STATE_DEAD;
  shakeAmount = 12;
  if (score > highScore) {
    highScore = score;
    localStorage.setItem('wimpyHighScore', str(highScore));
  }
}
Line-by-line explanation (6 lines)
if (state !== STATE_PLAYING) return;
Early return if not actively playing—prevents die() from being called multiple times if somehow invoked while already dead.
state = STATE_DEAD;
Changes game state to dead, which will cause draw() to call drawDeath() next frame.
shakeAmount = 12;
Triggers screen shake by setting shakeAmount to 12—creates an impact effect.
if (score > highScore) {
Checks if the current score beats the saved high score.
highScore = score;
Updates highScore to the new best score.
localStorage.setItem('wimpyHighScore', str(highScore));
Saves the new high score to the browser's localStorage so it persists across page refreshes—str() converts the number to a string.

keyPressed()

keyPressed() is a p5.js event function called whenever a key is pressed. Returning false prevents default behavior, crucial for keeping spacebar from scrolling the page. The OR operator (||) allows multiple keys to trigger the same action.

function keyPressed() {
  if (key === ' ' || keyCode === UP_ARROW) {
    doJump();
    return false;
  }
}
Line-by-line explanation (3 lines)
if (key === ' ' || keyCode === UP_ARROW) {
Checks if the pressed key is spacebar or up arrow—handles both common jump inputs.
doJump();
Calls the doJump() function to execute jump or state transition logic.
return false;
Returns false to prevent the browser's default behavior for spacebar (scrolling page)—keeps the game responsive.

touchStarted()

touchStarted() is a p5.js event function called when the user touches the screen. This makes the game playable on mobile and tablet devices alongside keyboard controls. Returning false is essential to prevent unwanted scrolling during gameplay.

function touchStarted() {
  doJump();
  return false;
}
Line-by-line explanation (2 lines)
doJump();
Calls doJump() when the screen is touched—enables mobile gameplay.
return false;
Returns false to prevent default touch behavior (scrolling, zooming)—keeps the game responsive.

windowResized()

windowResized() is a p5.js event function that fires when the browser window is resized. Without this, the canvas wouldn't adapt to new dimensions. Recalculating groundY ensures game logic stays consistent across different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  groundY = height - 80;
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
p5.js function that resizes the canvas to match the new window dimensions—called automatically whenever the browser window is resized.
groundY = height - 80;
Recalculates groundY based on the new canvas height—ensures the ground stays 80 pixels from the bottom on any screen size.

📦 Key Variables

state number

Tracks current game screen: STATE_TITLE (0), STATE_PLAYING (1), or STATE_DEAD (2)—controls which draw function executes

let state = STATE_TITLE;
score number

Current game score—increments by 1 each frame during gameplay, +50 for each cheese collected

let score = 0;
highScore number

Best score ever achieved—loaded from localStorage at startup, updated when beaten

let highScore = 0;
distance number

Total distance traveled—increments every frame, used to accelerate game speed and chasers

let distance = 0;
groundY number

Y-coordinate of the ground line—height minus 80 pixels, where player and obstacles collide

let groundY;
gravity number

Acceleration due to gravity applied each frame—adds to player's vertical velocity

let gravity = 0.65;
baseSpeed number

Starting speed of obstacles and world scrolling—increases difficulty as game speed accelerates from this base

let baseSpeed = 5;
gameSpeed number

Current speed multiplier for obstacles and world—starts at baseSpeed, increases with distance for progressive difficulty

let gameSpeed = 5;
player object

Player character object containing x, y position, width, height, vertical velocity, grounded state, and animation frame

let player = { x: 0, y: 0, w: 30, h: 55, vy: 0, grounded: true, runFrame: 0 };
dustParticles array

Array of particle objects for landing dust and jump effects—each has x, y, vx, vy, and life countdown

let dustParticles = [];
MAX_JUMPS number

Maximum number of jumps available per ground touch—set to 2 for double jumping

const MAX_JUMPS = 2;
JUMP_FORCE number

Velocity applied to player on first jump—negative value moves player upward

const JUMP_FORCE = -13;
JUMP_FORCE_2 number

Velocity applied to player on second jump—weaker than first jump for game balance

const JUMP_FORCE_2 = -11;
jumpsLeft number

Remaining jumps before landing—decrements on each jump, resets to MAX_JUMPS when touching ground

let jumpsLeft = 0;
obstacles array

Array of all active obstacles on screen—each has x, y, width, height, color, and type

let obstacles = [];
cheeses array

Array of collectible cheese items—each has x, y, phase for bob animation, and bobY offset

let cheeses = [];
bgElements array

Array of background doodles (stars, spirals, smileys, arrows)—each has x, y, type, and size

let bgElements = [];
lineOffset number

Offset for blue notebook lines—cycles 0-29 to create scrolling effect

let lineOffset = 0;
shakeAmount number

Current screen shake intensity—set to 12 on collision, decays each frame

let shakeAmount = 0;
cheeseCount number

Number of cheese items collected in current game

let cheeseCount = 0;
rodrick object

Main chaser character—has x, y, size, and speed; catches player to end game

let rodrick = { x: -120, y: 0, size: 65, speed: 0.9 };
fregley object

Secondary chaser character—slower than Rodrick, starts further behind

let fregley = { x: -250, y: 0, size: 45, speed: 0.6 };
milestoneMsg string

Current milestone message to display—set when score reaches thresholds like 500, 1500, 3000

let milestoneMsg = '';
milestoneMsgTimer number

Countdown timer for milestone message display—decrements each frame, message disappears at 0

let milestoneMsgTimer = 0;
lastMilestone number

Index of the last milestone message shown—prevents duplicate messages at same score

let lastMilestone = -1;
OBS_TYPES array

Array defining obstacle types: locker, trashcan, bookstack, desk, cone—each with color and size ranges

const OBS_TYPES = [{ name: 'locker', color: [70, 90, 140], wRange: [30, 45], hRange: [50, 80] }, ...];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG drawGame() collision detection

Collision hitbox uses player.y - player.h / 2 as center, but this doesn't match where Greg is actually drawn (which uses player.y as feet level). This can cause inconsistent collisions.

💡 Standardize the hitbox calculation to match visual representation. Either use player.y as the base and adjust the collision rect accordingly, or document why the offset is intentional.

PERFORMANCE drawGame() for loops

The backwards loop pattern `for (let i = array.length - 1; i >= 0; i--)` is good for splicing, but many loops don't actually splice (bgElements, cheeses initially), wasting loop overhead.

💡 Use forward loops `for (let i = 0; i < array.length; i++)` for arrays that don't need splicing within the loop. Reserve backwards loops only for arrays with splice() calls.

BUG initGame() chaser initialization

Chasers (Rodrick and Fregley) are never drawn until their x > -10 (see drawGame), but they start with negative x values. If the game ends before they scroll into view, they never appear visually, making it unclear what the threat is.

💡 Document the off-screen behavior clearly, or initialize chasers slightly on-screen (x = -50 instead of -120) so players see them immediately.

STYLE OBS_TYPES array

The obstacle type 'cone' is defined but drawObstacle() has no else-if for 'cone', so cones fall through to the desk drawing (horizontal line), making them visually identical to desks.

💡 Either add a cone-specific drawing branch in drawObstacle() (e.g., a triangle), or remove 'cone' from OBS_TYPES and replace it with another type that has full visual implementation.

FEATURE draw() screen shake

Screen shake uses random translate each frame, which can feel jittery. A perlin-noise-based or sinusoidal shake would feel smoother and more cinematic.

💡 Replace `translate(random(-shakeAmount, shakeAmount), ...)` with a sine-wave-based shake: `let shakeX = sin(frameCount * 0.3) * shakeAmount; translate(shakeX, ...)` for smoother motion.

FEATURE drawDeath() high score

High score is displayed but there's no visual distinction or animation when a new high score is achieved—just static text.

💡 Add pulse animation or color change when displaying '★ NEW HIGH SCORE! ★'. For example, scale the text using sin(frameCount * 0.1) or animate the fill color with lerp().

🔄 Code Flow

Code flow showing setup, initgame, draw, drawtitle, drawgame, drawdeath, drawnotebookbg, drawground, drawgreg, drawgregbody, drawobstacle, drawdoodle, drawhud, spawnobstacle, dojump, die, keypressed, touchstarted, windowresized

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

graph TD start[Start] --> setup[setup] setup --> initgame[initGame] initgame --> draw[draw loop] draw --> state-dispatch[state-dispatch] state-dispatch -->|STATE_TITLE| drawtitle[drawTitle] state-dispatch -->|STATE_GAME| drawgame[drawGame] state-dispatch -->|STATE_DEATH| drawdeath[drawDeath] draw --> screen-shake[screen-shake] draw --> scrolling-lines[scrolling-lines] draw --> bg-loop[bg-loop] draw --> cheese-loop[cheese-loop] draw --> obstacle-loop[obstacle-loop] draw --> drawhud[drawHUD] click setup href "#fn-setup" click initgame href "#fn-initgame" click draw href "#fn-draw" click drawtitle href "#fn-drawtitle" click drawgame href "#fn-drawgame" click drawdeath href "#fn-drawdeath" click screen-shake href "#sub-screen-shake" click scrolling-lines href "#sub-scrolling-lines" click bg-loop href "#sub-bg-loop" click cheese-loop href "#sub-cheese-loop" click obstacle-loop href "#sub-obstacle-loop" click drawhud href "#fn-drawhud" initgame --> player-init[player-init] initgame --> chasers-init[chasers-init] initgame --> obstacle-spawn-loop[obstacle-spawn-loop] click player-init href "#sub-player-init" click chasers-init href "#sub-chasers-init" click obstacle-spawn-loop href "#sub-obstacle-spawn-loop" drawgame --> speed-acceleration[speed-acceleration] drawgame --> milestone-loop[milestone-loop] drawgame --> bg-doodle-loop[bg-doodle-loop] click speed-acceleration href "#sub-speed-acceleration" click milestone-loop href "#sub-milestone-loop" click bg-doodle-loop href "#sub-bg-doodle-loop" obstacle-loop --> obstacle-collision[obstacle-collision] obstacle-loop --> obstacle-spawn[obstacle-spawn] click obstacle-collision href "#sub-obstacle-collision" click obstacle-spawn href "#sub-obstacle-spawn" cheese-loop --> cheese-spawn[cheese-spawn] click cheese-spawn href "#sub-cheese-spawn" screen-shake -->|if collision| shakeAmount[shakeAmount] drawtitle --> title-blink[title-blink] click title-blink href "#sub-title-blink" drawdeath --> fallen-greg[fallen-greg] drawdeath --> high-score-check[high-score-check] click fallen-greg href "#sub-fallen-greg" click high-score-check href "#sub-high-score-check" drawhud --> cheese-display[cheese-display] drawhud --> jump-indicator[jump-indicator] drawhud --> danger-warning[danger-warning] drawhud --> milestone-message[milestone-message] click cheese-display href "#sub-cheese-display" click jump-indicator href "#sub-jump-indicator" click danger-warning href "#sub-danger-warning" click milestone-message href "#sub-milestone-message"

❓ Frequently Asked Questions

What visual elements are present in the diary of a wimpy kid123 sketch?

The sketch features a parallax background with moving elements, a character named Greg Heffley running, and additional characters Rodrick and Fregley, along with various obstacles.

How can players interact with the diary of a wimpy kid123 game?

Players can interact by jumping over obstacles using keyboard inputs, with Greg's jump height controlled by the jumpForce variable.

What creative coding techniques are showcased in this p5.js sketch?

The sketch demonstrates concepts such as object-oriented programming with character objects, gravity physics for movement, and parallax scrolling for enhanced visual depth.

Preview

diary of a wimpy kid123 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of diary of a wimpy kid123 - Code flow showing setup, initgame, draw, drawtitle, drawgame, drawdeath, drawnotebookbg, drawground, drawgreg, drawgregbody, drawobstacle, drawdoodle, drawhud, spawnobstacle, dojump, die, keypressed, touchstarted, windowresized
Code Flow Diagram