Sketch 2026-04-23 22:09

This sketch creates a vibrant 3D side-view dino runner game where players control a blocky dinosaur, jumping over obstacles and collecting coins while the landscape scrolls by at increasing speed. The game features shop upgrades, progressive difficulty, and a parallax background to enhance the sense of depth and motion.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make gravity stronger — The dino falls faster and jumps feel snappier, making the game more challenging and arcade-like.
  2. Increase starting game speed — Obstacles and coins move faster from the moment the game starts, immediately raising difficulty.
  3. Make coins worth more — Lower the coin cost of upgrades so the player can buy them faster, unlocking red skin and double jump sooner.
  4. Spawn obstacles less frequently — Obstacles appear with more time between them, making the game easier and less hectic, especially at the start.
  5. Make the dino a different color — The dino starts as a new color instead of blue, and you can still buy the red skin as a second color option.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an endless runner game in 3D space using p5.js's WEBGL renderer and a clever side-view camera. The player controls a blocky dinosaur that must leap over incoming obstacles (cacti) and collect coins floating in the air. The game is visually interesting because it uses a 2.5D perspective—a 3D world viewed from the side—which makes the Z-axis (depth) appear as horizontal left-right motion across the screen. The core techniques powering this are WEBGL 3D rendering, collision detection using axis-aligned bounding boxes (AABB), game state management, and progressive difficulty scaling.

The code is organized into three layers: game logic (physics, spawning, collision), rendering (drawing the dino, obstacles, coins, and scenery), and state management (menu, shop, play, game over). By reading it you will learn how to structure a full game in p5.js, handle real-time physics and collision, manage multiple game states, create a shop system with persistent upgrades, and use the camera() function to achieve creative 3D perspectives that enhance gameplay.

⚙️ How It Works

  1. When the sketch loads, setup() creates a WEBGL canvas, wires up button event listeners, and populates the scenery array with 30 random mountain particles that will scroll in the background.
  2. Every frame, draw() renders the sky, positions the camera to a 2.5D side view, and lights the 3D scene. The draw loop then branches based on gameState: if PLAY, it calls updateGameLogic(); otherwise, it keeps scenery scrolling for visual feedback.
  3. In updateGameLogic(), the dino's vertical velocity increases due to gravity and its position updates each frame. When the dino's Y coordinate reaches the ground, it stops falling and resets jumpsTaken to 0, allowing a new jump.
  4. Every frame, the spawn timer increments. When it exceeds the spawn interval, a new obstacle or coin is created far to the right (at Z = -3000). As gameSpeed increases with the player's score, new entities spawn more frequently and move faster.
  5. Obstacles and coins move toward the camera (their Z coordinate increases by gameSpeed each frame). For each obstacle and coin, the code performs AABB collision detection: if the dino's 3D bounding box overlaps with an entity's bounding box, either the game ends (obstacle) or the coin counter increments and the coin is removed.
  6. The parallax scenery scrolls slower than the foreground (multiplied by 0.4), wrapping around when it passes the camera to create an infinite backdrop. When the game ends, setUIState('OVER') displays the final score and lets the player restart or return to the menu.

🎓 Concepts You'll Learn

WEBGL 3D renderingCamera positioning and perspectiveAxis-aligned bounding box (AABB) collision detectionPhysics simulation (gravity and velocity)Game state managementSpawning and object poolingParallax scrollingShop and upgrade systemProgressive difficultyEvent handling (keyboard and touch)

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It initializes the canvas, wires up all UI interactions, and pre-populates data structures so the game is ready to play. The use of WEBGL is critical here—it unlocks 3D rendering, lighting, and the camera() function that makes the 2.5D effect possible.

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  
  // Setup Buttons
  select('#start-btn').mousePressed(() => setUIState('PLAY'));
  select('#restart-btn').mousePressed(() => setUIState('PLAY'));
  select('#menu-btn').mousePressed(() => setUIState('MENU'));
  select('#shop-btn-menu').mousePressed(() => setUIState('SHOP'));
  select('#close-shop-btn').mousePressed(() => setUIState('MENU'));
  
  select('#buy-red').mousePressed(buyRed);
  select('#buy-jump').mousePressed(buyDoubleJump);
  
  // Generate infinitely looping background mountains/scenery
  for (let i = 0; i < 30; i++) {
    sceneryParticles.push({
      y: groundY - random(20, 150),
      z: random(-3000, 1000),
      size: random(40, 120),
      rot: random(TWO_PI)
    });
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Canvas creation createCanvas(windowWidth, windowHeight, WEBGL);

Creates a full-screen WEBGL canvas, enabling 3D rendering with lighting and transforms

for-loop Button event listeners select('#start-btn').mousePressed(() => setUIState('PLAY'));

Connects all UI buttons to state-change functions so the game responds to player clicks

for-loop Scenery initialization loop for (let i = 0; i < 30; i++) {

Pre-populates the sceneryParticles array with mountains at random heights and depths so parallax scrolling works immediately

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a full-window WEBGL canvas (3D renderer) instead of the default 2D—necessary for translate(), rotateX(), lighting, and 3D transforms
select('#start-btn').mousePressed(() => setUIState('PLAY'));
Finds the HTML button with id 'start-btn' and links it so clicking it calls setUIState('PLAY') to begin the game
for (let i = 0; i < 30; i++) {
Loops 30 times to create 30 mountain particles at the start, pre-filling the background scenery array
sceneryParticles.push({
Adds a new mountain object to the sceneryParticles array with random position, size, and rotation

draw()

draw() runs 60 times per second (by default) and is the heartbeat of the animation. Every frame, it clears the screen, positions the camera, updates physics (if playing), and re-renders all objects at their new positions. The order of calls matters: scenery first (back), then obstacles and coins (middle), then dino (front) ensures correct visual layering.

🔬 Currently the menu keeps scrolling the scenery. What happens if you comment out scrollScenery(baseSpeed)? Will the mountains freeze or disappear?

  if (gameState === "PLAY") {
    updateGameLogic();
  } else {
    // Keep scenery scrolling even in menus for a dynamic background
    scrollScenery(baseSpeed);
  }
function draw() {
  background(135, 206, 235); // Sky blue background
  
  // Setup 2.5D Side Camera
  // Look from the side (+X axis) towards the middle (X=0)
  // This causes the Z-axis (depth) to become horizontal left/right movement
  camera(600, -80, 0, 0, -30, 0, 0, 1, 0);
  
  // Lighting
  ambientLight(150);
  directionalLight(255, 255, 255, -1, 1, -0.5);

  drawEnvironment();

  if (gameState === "PLAY") {
    updateGameLogic();
  } else {
    // Keep scenery scrolling even in menus for a dynamic background
    scrollScenery(baseSpeed);
  }

  drawObstacles();
  drawCoins();
  
  // Only draw Dino if not in main menu
  if (gameState !== "MENU" && gameState !== "SHOP") {
    drawDino();
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Background clear background(135, 206, 235);

Fills the entire canvas with sky blue each frame, erasing the previous frame so animation appears smooth

function-call 2.5D side camera camera(600, -80, 0, 0, -30, 0, 0, 1, 0);

Positions the camera to look from the right side of the world toward the center, making the Z-axis appear as left-right motion—this is the trick that creates the side-view effect

conditional Game state branching if (gameState === "PLAY") {

Decides whether to run full game physics and spawning, or just keep the menu scenery scrolling

calculation Render order drawEnvironment(); drawObstacles(); drawCoins(); drawDino();

Renders objects in back-to-front order: scenery first (furthest), then obstacles and coins, then dino (closest), so layering looks correct

background(135, 206, 235);
Clears the entire canvas with sky blue (RGB 135, 206, 235) so each frame starts fresh—without this, all shapes would overlap and create trails
camera(600, -80, 0, 0, -30, 0, 0, 1, 0);
Positions the camera at (600, -80, 0) looking toward the point (0, -30, 0). The camera is far to the right (positive X), making you look back at the world from a side angle where depth (Z) appears as horizontal motion
ambientLight(150);
Adds overall ambient lighting at brightness 150, so all 3D shapes are visible from all angles even without direct light
directionalLight(255, 255, 255, -1, 1, -0.5);
Adds a white directional light coming from direction (-1, 1, -0.5), creating shadows and depth cues on 3D shapes
if (gameState === "PLAY") {
Checks if the game is currently running—if so, update physics and spawning; otherwise, just keep the background scenery moving for visual appeal
if (gameState !== "MENU" && gameState !== "SHOP") {
Only draws the dino if the game is actively playing or game over—it stays hidden during menu and shop screens

updateGameLogic()

updateGameLogic() is where all the game's core systems run: physics (gravity and jumping), difficulty scaling (speed and spawn rate increase), spawning new obstacles and coins, collision detection (both obstacle hits and coin collection), and cleanup of off-screen objects. This function runs every frame only when gameState === 'PLAY', making it the engine of gameplay.

🔬 These lines move coins and spin them. What happens if you change c.rot += 0.1 to c.rot += 0.5? Will coins spin faster or slower?

  // Coin Management
  for (let i = coinObjects.length - 1; i >= 0; i--) {
    let c = coinObjects[i];
    c.z += gameSpeed;
    c.rot += 0.1; // Spin coin
function updateGameLogic() {
  // Score and Speed progression
  score += gameSpeed * 0.01;
  select('#score-display').html(`Score: ${floor(score)}`);
  
  // Gradually increase difficulty
  gameSpeed = baseSpeed + (score * 0.015);
  spawnInterval = max(25, 60 - (score * 0.05));

  // Physics
  dino.vy += dino.gravity;
  dino.y += dino.vy;
  
  // Floor collision
  let floorLevel = groundY - dino.h / 2;
  if (dino.y >= floorLevel) {
    dino.y = floorLevel;
    dino.vy = 0;
    dino.isGrounded = true;
    jumpsTaken = 0; // Reset double jumps on ground hit
  }

  // Spawning system
  spawnTimer++;
  if (spawnTimer >= spawnInterval) {
    spawnEntity();
    spawnTimer = 0;
  }

  // Obstacle Management
  for (let i = obstacles.length - 1; i >= 0; i--) {
    let obs = obstacles[i];
    obs.z += gameSpeed; // Move from right to left in our side view
    
    // Check Collision (AABB 3D)
    let zOverlap = abs(dino.z - obs.z) < (dino.d / 2 + obs.d / 2) - 6;
    let yOverlap = (dino.y + dino.h / 2) > (obs.y - obs.h / 2);
    
    if (zOverlap && yOverlap) {
      triggerGameOver();
    }

    if (obs.z > 1000) obstacles.splice(i, 1); // Remove when off screen left
  }
  
  // Coin Management
  for (let i = coinObjects.length - 1; i >= 0; i--) {
    let c = coinObjects[i];
    c.z += gameSpeed;
    c.rot += 0.1; // Spin coin
    
    // Coin Collection Collision
    let zOverlap = abs(dino.z - c.z) < (dino.d / 2 + c.size);
    let yOverlap = abs(dino.y - c.y) < (dino.h / 2 + c.size);
    
    if (zOverlap && yOverlap) {
      coins++;
      updateCoinDisplays();
      coinObjects.splice(i, 1);
      continue;
    }

    if (c.z > 1000) coinObjects.splice(i, 1);
  }
  
  scrollScenery(gameSpeed);
}
Line-by-line explanation (28 lines)

🔧 Subcomponents:

calculation Score calculation score += gameSpeed * 0.01;

Increases score every frame based on current game speed, making faster play reward more points

calculation Difficulty scaling gameSpeed = baseSpeed + (score * 0.015);

Makes the game progressively harder by increasing obstacle speed as score grows

calculation Gravity and velocity dino.vy += dino.gravity; dino.y += dino.vy;

Simulates gravity by accelerating downward and updating vertical position, creating realistic jumping and falling

conditional Floor collision if (dino.y >= floorLevel) {

Detects when dino reaches the ground and stops downward motion, allowing new jumps

conditional Spawn timer check if (spawnTimer >= spawnInterval) {

Triggers obstacle/coin spawning at intervals that shrink as difficulty increases

for-loop Obstacle update loop for (let i = obstacles.length - 1; i >= 0; i--) {

Moves each obstacle toward the camera and checks collision with the dino; removes off-screen obstacles

conditional AABB collision test if (zOverlap && yOverlap) {

Ends the game if dino's bounding box overlaps an obstacle's bounding box in both Z and Y axes

for-loop Coin update loop for (let i = coinObjects.length - 1; i >= 0; i--) {

Moves each coin, spins it, checks collection collision, and removes it or old coins

score += gameSpeed * 0.01;
Increments the score by a tiny fraction of gameSpeed each frame—faster game speed = faster score growth, rewarding skilled play
select('#score-display').html(`Score: ${floor(score)}`);
Updates the on-screen score display by finding the HTML element with id 'score-display' and setting its text to the current score (rounded down)
gameSpeed = baseSpeed + (score * 0.015);
Increases gameSpeed gradually as score grows, making obstacles move faster and creating progressive difficulty
spawnInterval = max(25, 60 - (score * 0.05));
Shrinks the spawn interval (time between spawns) as score increases, but never below 25 frames, so new obstacles appear more frequently and the game gets busier
dino.vy += dino.gravity;
Accelerates the dino downward by adding gravity (1.2) to its vertical velocity each frame—this creates the feeling of falling
dino.y += dino.vy;
Updates the dino's vertical position by adding its velocity, moving it down if falling or up if recently jumped
let floorLevel = groundY - dino.h / 2;
Calculates where the ground should be relative to the dino's size—the dino's bottom edge touches the ground at this Y position
if (dino.y >= floorLevel) {
Checks if the dino has reached or passed the floor level
dino.y = floorLevel;
Snaps the dino exactly to the floor level so it doesn't pass through
dino.vy = 0;
Zeros out vertical velocity so the dino stops falling when it lands
dino.isGrounded = true;
Sets a flag that the dino is on the ground, allowing a new jump
jumpsTaken = 0;
Resets the jump counter to 0 so the dino can jump again (or double-jump if the upgrade is owned)
spawnTimer++;
Increments the spawn timer by 1 each frame
if (spawnTimer >= spawnInterval) {
When the spawn timer reaches the spawn interval, it's time to spawn a new obstacle or coin
for (let i = obstacles.length - 1; i >= 0; i--) {
Loops backward through the obstacles array (from last to first) so we can safely delete items during iteration without skipping any
obs.z += gameSpeed;
Moves the obstacle toward the camera (increases Z) by gameSpeed pixels, making it approach the dino
let zOverlap = abs(dino.z - obs.z) < (dino.d / 2 + obs.d / 2) - 6;
Checks if the dino and obstacle are close enough in the Z-axis (depth)—the - 6 gives a small tolerance zone for hitbox size
let yOverlap = (dino.y + dino.h / 2) > (obs.y - obs.h / 2);
Checks if the dino's bottom edge is below the obstacle's top edge, meaning vertical collision is possible
if (zOverlap && yOverlap) {
If both the Z and Y axes overlap, the dino has hit the obstacle—game over
if (obs.z > 1000) obstacles.splice(i, 1);
Removes the obstacle from the array once it has passed the camera (Z > 1000), freeing memory
c.rot += 0.1;
Increments the coin's rotation by 0.1 radians each frame, making it spin continuously
let zOverlap = abs(dino.z - c.z) < (dino.d / 2 + c.size);
Checks if the dino and coin overlap in depth—coins have a circular collision zone (size) not a full box
let yOverlap = abs(dino.y - c.y) < (dino.h / 2 + c.size);
Checks if the dino and coin are within collection range vertically
if (zOverlap && yOverlap) {
If both axes overlap, the dino has collected the coin
coins++;
Increments the coin counter by 1
updateCoinDisplays();
Updates the on-screen coin display so the player sees their new total
coinObjects.splice(i, 1);
Removes the collected coin from the array
continue;
Skips the rest of the loop iteration so the code doesn't try to splice the coin twice

scrollScenery(speed)

scrollScenery() creates an infinite looping background using a parallax trick: mountains move slower than foreground obstacles, which makes them appear farther away. When a mountain leaves the screen to the left, it wraps back to the right with a fresh random height, so the scenery never ends. This function is called both during gameplay and in menus to keep the world feeling alive.

🔬 This loop moves mountains and wraps them around. What happens if you remove the line p.y = groundY - random(20, 150); so mountains don't get new heights when wrapping? Will scenery look less varied?

  for (let p of sceneryParticles) {
    p.z += speed * 0.4; // Parallax effect (moves slower than foreground)
    if (p.z > 1000) {
      p.z = random(-3500, -3000); // Wrap around to the right
      p.y = groundY - random(20, 150);
    }
  }
function scrollScenery(speed) {
  for (let p of sceneryParticles) {
    p.z += speed * 0.4; // Parallax effect (moves slower than foreground)
    if (p.z > 1000) {
      p.z = random(-3500, -3000); // Wrap around to the right
      p.y = groundY - random(20, 150);
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Parallax scrolling loop for (let p of sceneryParticles) {

Updates each mountain particle by moving it slower (0.4x) than obstacles, creating depth illusion

conditional Wrap-around check if (p.z > 1000) {

When a mountain passes off-screen left, resets it far to the right with a new random height to create infinite looping scenery

for (let p of sceneryParticles) {
Loops through each mountain particle in the sceneryParticles array
p.z += speed * 0.4;
Moves the mountain in the Z-axis (depth) by speed × 0.4—moving it slower than the game speed (which moves obstacles by the full speed). This creates a parallax effect where background scenery lags behind foreground motion, tricking the eye into perceiving depth
if (p.z > 1000) {
Checks if the mountain has scrolled past the left edge of the screen (off-camera)
p.z = random(-3500, -3000);
Resets the mountain far to the right (negative Z means far away in our setup) at a random depth, so it will scroll in again from the right
p.y = groundY - random(20, 150);
Gives the mountain a new random vertical position so it doesn't always appear at the same height

spawnEntity()

spawnEntity() is a factory function that creates new obstacles or coins at random intervals. It gives each entity a randomized size or height so gameplay stays varied—some coins float higher (requiring jumps), some obstacles are wide (harder to dodge), and some are tall (requiring well-timed jumps). It always spawns new entities far to the right (Z = -3000) so players have time to react.

🔬 These lines randomize obstacle shapes. What happens if you change obsHeight = random(40, 80) to obsHeight = random(20, 100)? Will obstacles be more varied in size?

    let isWide = random() > 0.7;
    let obsWidth = isWide ? 60 : 30;
    let obsHeight = random(40, 80);
function spawnEntity() {
  // 30% chance to spawn a coin instead of an obstacle
  if (random() > 0.7) {
    coinObjects.push({
      x: 0,
      y: groundY - random(50, 130), // Varies height to encourage jumping
      z: -3000,
      size: 15,
      rot: 0
    });
  } else {
    // Spawn Obstacle (Cactus)
    let isWide = random() > 0.7;
    let obsWidth = isWide ? 60 : 30;
    let obsHeight = random(40, 80);
    
    obstacles.push({
      x: 0,
      y: groundY - obsHeight / 2,
      z: -3000, // Spawns far right
      w: obsWidth,
      h: obsHeight,
      d: 30
    });
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Coin spawn probability if (random() > 0.7) {

Uses random chance (30%) to decide whether to spawn a coin or obstacle, creating variety

calculation Coin object creation coinObjects.push({

Adds a new coin object with randomized height to the coins array

conditional Obstacle size variation let isWide = random() > 0.7;

Randomizes whether the obstacle is narrow (30px) or wide (60px) for gameplay variety

calculation Obstacle object creation obstacles.push({

Adds a new obstacle with randomized height and width to the obstacles array

if (random() > 0.7) {
Generates a random number between 0 and 1. If it's greater than 0.7 (30% chance), spawn a coin; otherwise, spawn an obstacle
y: groundY - random(50, 130),
Places the coin at a random height between 50 and 130 pixels above the ground, encouraging the player to jump for some coins
z: -3000,
Spawns the coin far to the right (negative Z), so it takes a moment to reach the player and they can see it coming
let isWide = random() > 0.7;
Uses random chance (30% probability) to decide if this obstacle is wide (60px) or narrow (30px), making gameplay unpredictable
let obsWidth = isWide ? 60 : 30;
Sets the width to 60 if isWide is true, otherwise 30—a ternary operator for conditional assignment
let obsHeight = random(40, 80);
Randomizes the obstacle's height between 40 and 80 pixels, so some obstacles are tall and some are short, requiring different jump timings
y: groundY - obsHeight / 2,
Positions the obstacle on the ground by subtracting half its height from the ground Y position

handleJump()

handleJump() is the core of player control. It applies the jump velocity to the dino, but only when the game is active (not in menus) and only when the dino has jumps available (grounded or upgraded with double jump). The double-jump system is elegant: before owning the upgrade, maxJumps = 1, so only one jump per landing is possible; after purchase, maxJumps = 2, and the same code allows two consecutive jumps.

function handleJump() {
  if (gameState !== "PLAY") return;
  
  if (dino.isGrounded || jumpsTaken < maxJumps) {
    dino.vy = dino.jumpForce;
    dino.isGrounded = false;
    jumpsTaken++;
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional State guard if (gameState !== "PLAY") return;

Prevents jumping during menus, shop, or game over screens—ignores jump input if not actively playing

conditional Jump availability check if (dino.isGrounded || jumpsTaken < maxJumps) {

Allows jumping if either the dino is on the ground OR it still has double jumps left (depending on upgrades)

if (gameState !== "PLAY") return;
Early exit: if the game is not currently running, ignore the jump input and don't execute the rest of this function
if (dino.isGrounded || jumpsTaken < maxJumps) {
Checks if the dino can jump: either it's on the ground (normal jump) OR it has used fewer jumps than maxJumps allows (double jump if the upgrade is owned). Without the upgrade, maxJumps = 1 and jumpsTaken starts at 0 on the ground, so only one jump is possible until landing
dino.vy = dino.jumpForce;
Sets the dino's upward velocity to jumpForce (-16), which is negative because Y increases downward, so negative velocity pushes the dino up
dino.isGrounded = false;
Marks the dino as no longer on the ground, preventing infinite jumps while in the air
jumpsTaken++;
Increments the jump counter so the next jump (if available) will be the dino's second jump or second double jump

keyPressed()

keyPressed() is a built-in p5.js event function that fires whenever any key is pressed. By checking the key and keyCode variables, we detect which specific key and then call handleJump(). This allows desktop players to jump with either spacebar or arrow key.

function keyPressed() {
  if (key === ' ' || keyCode === UP_ARROW) handleJump();
}
Line-by-line explanation (1 lines)

🔧 Subcomponents:

conditional Jump key check if (key === ' ' || keyCode === UP_ARROW) handleJump();

Responds to either spacebar or up arrow key press by calling the jump handler

if (key === ' ' || keyCode === UP_ARROW) handleJump();
Checks if the pressed key was either the spacebar (key === ' ') or the UP_ARROW key (keyCode === UP_ARROW). If either is true, calls handleJump() to make the dino jump

touchStarted(event)

touchStarted() is a built-in p5.js event function for mobile and tablet support. By checking event.target.tagName, we ensure that touching the canvas makes the dino jump, but touching HTML buttons (which are positioned over the canvas) triggers those buttons instead. The return false is crucial to override mobile browser defaults.

function touchStarted(event) {
  // Only handle touch if tapping exactly on the canvas (ignore UI buttons)
  if (event.target.tagName === 'CANVAS') {
    handleJump();
    return false; // Prevent scrolling zoom
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Canvas-only touch handler if (event.target.tagName === 'CANVAS') {

Checks that the touch event occurred on the canvas, not on an HTML button, so tapping buttons doesn't jump

if (event.target.tagName === 'CANVAS') {
Checks the HTML tag name of the element that was touched. If it's 'CANVAS' (the p5.js drawing surface), then the touch happened on the game, not a button
handleJump();
Calls handleJump() to make the dino jump in response to the touch
return false;
Returns false to prevent the browser's default touch behavior (like pinch-to-zoom or scrolling), so touches are dedicated to the game

setUIState(state)

setUIState() manages all the UI transitions in the game. It's a central state machine: set the gameState variable, hide all panels, then show the panel(s) appropriate to the new state. This pattern keeps UI logic organized and makes it easy to add new screens later (e.g., a pause menu).

function setUIState(state) {
  gameState = state;
  
  // Hide all panels initially
  select('#menu-ui').style('display', 'none');
  select('#shop-ui').style('display', 'none');
  select('#game-over').style('display', 'none');
  
  if (state === 'MENU') {
    select('#menu-ui').style('display', 'block');
    select('#top-bar').style('display', 'none');
  } else if (state === 'SHOP') {
    updateCoinDisplays();
    select('#shop-ui').style('display', 'block');
    select('#top-bar').style('display', 'none');
  } else if (state === 'PLAY') {
    resetGame();
    select('#top-bar').style('display', 'flex');
  } else if (state === 'OVER') {
    select('#game-over').style('display', 'block');
    select('#top-bar').style('display', 'flex');
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Hide all UI panels select('#menu-ui').style('display', 'none');

Hides all UI panels as a baseline so only the relevant one for the new state becomes visible

switch-case State-specific UI setup if (state === 'MENU') { ... } else if (state === 'SHOP') { ... }

Shows the correct UI panel and top bar for the current game state (menu, shop, play, or game over)

gameState = state;
Updates the global gameState variable so draw() and other functions know which screen the game is on
select('#menu-ui').style('display', 'none');
Hides the menu panel by setting its CSS display property to 'none'
select('#shop-ui').style('display', 'none');
Hides the shop panel
select('#game-over').style('display', 'none');
Hides the game over panel
if (state === 'MENU') {
If the new state is 'MENU', run the menu setup code
select('#menu-ui').style('display', 'block');
Shows the menu panel by setting display to 'block'
select('#top-bar').style('display', 'none');
Hides the score/coins top bar when on the main menu since the game isn't running
} else if (state === 'SHOP') {
If the new state is 'SHOP', run the shop setup code
updateCoinDisplays();
Updates the coin display in the shop to show the player their current balance before they shop
select('#shop-ui').style('display', 'block');
Shows the shop panel
} else if (state === 'PLAY') {
If the new state is 'PLAY', the game is starting or restarting
resetGame();
Resets all game variables (score, speed, obstacles, coins collected) to their starting values
select('#top-bar').style('display', 'flex');
Shows the top bar with score and coin counters during gameplay
} else if (state === 'OVER') {
If the new state is 'OVER', the game has ended
select('#game-over').style('display', 'block');
Shows the game over panel with restart and menu buttons

triggerGameOver()

triggerGameOver() is a simple callback that fires when the dino collides with an obstacle. It transitions to the game over screen and displays the final score so the player can see how well they did.

function triggerGameOver() {
  setUIState("OVER");
  select('#final-score').html(`Score: ${floor(score)}`);
}
Line-by-line explanation (2 lines)
setUIState("OVER");
Transitions the game to the OVER state, which hides gameplay UI and shows the game over panel
select('#final-score').html(`Score: ${floor(score)}`);
Finds the HTML element with id 'final-score' and updates its text to show the player's final score (rounded down with floor())

resetGame()

resetGame() is called whenever the player presses PLAY (either starting a fresh game or restarting after game over). It clears all game state: score, speed, obstacles, coins, and resets the dino to the ground. This ensures each game starts fresh without any carryover from the previous run.

function resetGame() {
  score = 0;
  gameSpeed = baseSpeed;
  obstacles = [];
  coinObjects = [];
  spawnTimer = 0;
  dino.vy = 0;
  dino.y = groundY - dino.h / 2;
  updateCoinDisplays();
}
Line-by-line explanation (8 lines)
score = 0;
Resets the score to zero for a new game
gameSpeed = baseSpeed;
Resets the game speed to baseSpeed (12), removing any difficulty scaling from the previous game
obstacles = [];
Clears the obstacles array, removing all on-screen cacti so the new game starts empty
coinObjects = [];
Clears the coins array, removing all on-screen coins
spawnTimer = 0;
Resets the spawn timer, so new obstacles and coins start spawning immediately
dino.vy = 0;
Resets the dino's vertical velocity to zero (stops any falling motion from the previous game)
dino.y = groundY - dino.h / 2;
Positions the dino on the ground at the start of a new game
updateCoinDisplays();
Updates the on-screen score display to show 0

buyRed()

buyRed() implements the shop purchase logic for the red skin. It checks affordability, deducts the cost, enables the upgrade, changes the visual appearance (skin color), and disables the button to prevent re-purchase. This pattern is reusable for any shop item.

function buyRed() {
  if (coins >= 50 && !hasRedSkin) {
    coins -= 50;
    hasRedSkin = true;
    skinColor = [220, 50, 50]; // Change Dino to red
    let btn = select('#buy-red');
    btn.html("EQUIPPED");
    btn.attribute('disabled', '');
    updateCoinDisplays();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Purchase validation if (coins >= 50 && !hasRedSkin) {

Checks that the player has enough coins (50+) and hasn't already bought this skin to prevent duplicate purchases

if (coins >= 50 && !hasRedSkin) {
Checks two conditions: the player has at least 50 coins (coins >= 50) AND they don't already own the red skin (!hasRedSkin means 'not hasRedSkin'). Only if both are true does the purchase go through
coins -= 50;
Deducts 50 coins from the player's balance
hasRedSkin = true;
Sets the hasRedSkin flag to true, marking that the player now owns this upgrade
skinColor = [220, 50, 50];
Changes the dino's color to red [R=220, G=50, B=50] so subsequent draws will paint it red instead of blue
let btn = select('#buy-red');
Finds the 'Buy Red Skin' button element in the HTML
btn.html("EQUIPPED");
Changes the button text to 'EQUIPPED' to show the player they own this skin
btn.attribute('disabled', '');
Adds the 'disabled' attribute to the button, graying it out and preventing further clicks
updateCoinDisplays();
Updates the coin display to reflect the new lower balance after the purchase

buyDoubleJump()

buyDoubleJump() is the shop purchase logic for the double jump upgrade. It works identically to buyRed() but changes a gameplay mechanic (maxJumps) instead of a visual property (skinColor). This shows how upgrades can affect both aesthetics and mechanics.

function buyDoubleJump() {
  if (coins >= 100 && !hasDoubleJump) {
    coins -= 100;
    hasDoubleJump = true;
    maxJumps = 2; // Enable double jumps!
    let btn = select('#buy-jump');
    btn.html("BOUGHT");
    btn.attribute('disabled', '');
    updateCoinDisplays();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Purchase validation if (coins >= 100 && !hasDoubleJump) {

Checks that the player has enough coins (100+) and hasn't already bought double jump to prevent duplicate purchases

if (coins >= 100 && !hasDoubleJump) {
Checks two conditions: the player has at least 100 coins AND they don't already own double jump (!hasDoubleJump). Only if both are true does the purchase go through
coins -= 100;
Deducts 100 coins from the player's balance
hasDoubleJump = true;
Sets the hasDoubleJump flag to true, marking that the player now owns this upgrade
maxJumps = 2;
Changes maxJumps from 1 to 2, unlocking the double jump ability—now handleJump() will allow two consecutive air jumps before landing
let btn = select('#buy-jump');
Finds the 'Buy Double Jump' button element in the HTML
btn.html("BOUGHT");
Changes the button text to 'BOUGHT' to show the player they own this upgrade
btn.attribute('disabled', '');
Disables the button to prevent further clicks
updateCoinDisplays();
Updates the coin display to reflect the new lower balance

updateCoinDisplays()

updateCoinDisplays() is a utility function that syncs the coin display across multiple UI panels. Whenever coins change (collected or spent), calling this function ensures both the gameplay HUD and shop screen show the correct balance.

function updateCoinDisplays() {
  select('#coin-display').html(`🪙 ${coins}`);
  select('#shop-coins').html(coins);
}
Line-by-line explanation (2 lines)
select('#coin-display').html(`🪙 ${coins}`);
Finds the HTML element with id 'coin-display' (the in-game coin counter on the top bar) and updates its text to show a coin emoji and the current coin count
select('#shop-coins').html(coins);
Finds the HTML element with id 'shop-coins' (the balance display in the shop screen) and updates it to show the current coin count

drawDino()

drawDino() assembles the dino from 3D shapes using nested translate() calls—each translate() moves the origin, and subsequent shapes draw relative to that origin. The dino is built bottom-up: body first, then head offset upward, then eye offset to the side. The drop shadow uses the map() function to shrink as the dino jumps, providing visual feedback that reinforces the feeling of height.

🔬 These lines draw the head and eye. What happens if you change the translate values for the head from (0, -15, -15) to (0, -25, -20)? Will the head move up/down or forward/backward?

  // Head (Facing right towards the negative Z axis where obstacles come from)
  translate(0, -15, -15);
  box(20, 20, 25);
  
  // Eyes (Offset on the X axis so they show up on the side)
  fill(255);
  translate(10, -3, -8); // Right eye (closest to camera)
  box(4, 4, 4);
function drawDino() {
  push();
  translate(0, dino.y, dino.z);
  
  fill(skinColor);
  noStroke();
  box(dino.w, dino.h, dino.d); // Body
  
  // Head (Facing right towards the negative Z axis where obstacles come from)
  translate(0, -15, -15);
  box(20, 20, 25);
  
  // Eyes (Offset on the X axis so they show up on the side)
  fill(255);
  translate(10, -3, -8); // Right eye (closest to camera)
  box(4, 4, 4);
  
  pop();
  
  // Drop Shadow
  push();
  translate(0, groundY + 1, dino.z);
  rotateX(HALF_PI);
  fill(0, 0, 0, 50);
  noStroke();
  let shadowScale = map(dino.y, groundY - dino.h/2, -150, 1, 0.3, true);
  ellipse(0, 0, 40 * shadowScale, 40 * shadowScale);
  pop();
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Dino body box(dino.w, dino.h, dino.d);

Draws the main body as a 3D box using the dino's width, height, and depth

calculation Dino head translate(0, -15, -15); box(20, 20, 25);

Draws a smaller box offset from the body to create the head, positioned toward the front-top of the body

calculation Dino eye translate(10, -3, -8); box(4, 4, 4);

Draws a tiny white box to represent the eye, positioned to show on the side of the head

calculation Drop shadow ellipse(0, 0, 40 * shadowScale, 40 * shadowScale);

Draws a dark ellipse under the dino that shrinks as the dino jumps higher, creating a visual ground anchor

push();
Saves the current transformation state (position, rotation, scale) so changes inside this function don't affect other objects
translate(0, dino.y, dino.z);
Moves the origin to the dino's current position (0 on X-axis, dino.y vertically, dino.z in depth), so all subsequent shapes draw at this location
fill(skinColor);
Sets the fill color to skinColor, which is [50, 150, 200] (blue) by default or [220, 50, 50] (red) if the player bought the red skin
noStroke();
Removes the outline stroke so shapes appear solid
box(dino.w, dino.h, dino.d);
Draws a 3D box (rectangular prism) with width dino.w (30), height dino.h (40), and depth dino.d (30)—this is the dino's body
translate(0, -15, -15);
Moves the origin 15 pixels up and 15 pixels forward (toward negative Z), so the next shape draws at this new position relative to the body
box(20, 20, 25);
Draws the head as a smaller box (20×20×25) at the new position, creating the appearance of a head on top of the body
fill(255);
Changes the fill color to white [255, 255, 255] for the eye
translate(10, -3, -8);
Moves the origin 10 pixels to the right (positive X, which is away from the camera in our 2.5D setup), 3 pixels down, and 8 pixels forward, positioning the eye on the side of the head
box(4, 4, 4);
Draws a tiny white cube (4×4×4) to represent the eye
pop();
Restores the transformation state saved by the earlier push(), so the shadow drawing won't be affected by the dino's position/rotation
translate(0, groundY + 1, dino.z);
Moves the origin to the ground level directly below the dino (groundY + 1 for a slight offset), keeping the same Z position
rotateX(HALF_PI);
Rotates the coordinate system 90 degrees around the X-axis, flattening the ellipse so it lays flat on the ground
let shadowScale = map(dino.y, groundY - dino.h/2, -150, 1, 0.3, true);
Maps the dino's Y position to a shadow size: when on the ground (dino.y = groundY - dino.h/2), shadowScale = 1 (full size); when high in the air (dino.y = -150), shadowScale = 0.3 (tiny). This creates a shrinking shadow effect as the dino jumps higher
ellipse(0, 0, 40 * shadowScale, 40 * shadowScale);
Draws a dark semi-transparent ellipse (40 pixels × shadowScale in diameter) at the origin, creating the drop shadow

drawObstacles()

drawObstacles() loops through the obstacles array and renders each one as a 3D box at its current position. Wide obstacles get additional boxes (arms) to visually distinguish them from narrow cacti, making it clear to the player that they're harder to dodge. The loop is straightforward—the real complexity is in updateGameLogic(), which moves these obstacles each frame.

function drawObstacles() {
  for (let obs of obstacles) {
    push();
    translate(obs.x, obs.y, obs.z);
    fill(40, 180, 60); // Cactus Green
    stroke(20, 100, 30);
    strokeWeight(1);
    box(obs.w, obs.h, obs.d);
    
    // Cactus arms if it's wide
    if (obs.w > 40) {
      translate(0, -10, obs.w/2 - 10);
      box(15, 30, 15);
      translate(0, 15, -obs.w + 20);
      box(15, 20, 15);
    }
    pop();
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Obstacle render loop for (let obs of obstacles) {

Iterates through each obstacle in the obstacles array and draws it at its current position

conditional Conditional arms if (obs.w > 40) {

Adds cactus arms (extra boxes) only to wide obstacles (width > 40), making them visually distinct from narrow cacti

for (let obs of obstacles) {
Loops through each obstacle object in the obstacles array
push();
Saves the transformation state so each obstacle's position/rotation doesn't affect other obstacles
translate(obs.x, obs.y, obs.z);
Moves the origin to the obstacle's 3D position (obs.x, obs.y, obs.z)
fill(40, 180, 60);
Sets the fill color to green [40, 180, 60], making the cactus green
stroke(20, 100, 30);
Sets the outline color to a darker green [20, 100, 30]
strokeWeight(1);
Sets the outline thickness to 1 pixel for subtle edges
box(obs.w, obs.h, obs.d);
Draws the main cactus body as a box with width obs.w, height obs.h, and depth obs.d
if (obs.w > 40) {
Checks if the obstacle is wide (width > 40 pixels)—only wide cacti get arms
translate(0, -10, obs.w/2 - 10);
Moves the origin 10 pixels up and forward (toward negative Z) to position the first arm
box(15, 30, 15);
Draws the first cactus arm as a tall, thin box (15×30×15)
translate(0, 15, -obs.w + 20);
Moves the origin 15 pixels down and further back to position the second arm
box(15, 20, 15);
Draws the second cactus arm, slightly shorter than the first (15×20×15)

drawCoins()

drawCoins() renders each coin as a spinning gold cube. The rotateX() and rotateZ() calls rotate the coin around two axes based on its c.rot value, which is incremented in updateGameLogic(). The double rotation (X and Z) creates a tumbling effect that makes the coins look dynamic and eye-catching.

function drawCoins() {
  for (let c of coinObjects) {
    push();
    translate(c.x, c.y, c.z);
    rotateX(c.rot);
    rotateZ(c.rot);
    fill(255, 215, 0); // Gold
    stroke(200, 150, 0);
    strokeWeight(2);
    box(c.size); // Rotating gold box
    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Coin render loop for (let c of coinObjects) {

Iterates through each coin in the coinObjects array and draws it at its current position with its current rotation

for (let c of coinObjects) {
Loops through each coin object in the coinObjects array
push();
Saves the transformation state so each coin's rotation doesn't affect other coins
translate(c.x, c.y, c.z);
Moves the origin to the coin's 3D position (c.x, c.y, c.z)
rotateX(c.rot);
Rotates around the X-axis by c.rot radians (in updateGameLogic(), c.rot increases by 0.1 each frame, making the coin spin)
rotateZ(c.rot);
Rotates around the Z-axis by the same amount, adding a second spin axis so the coin tumbles in 3D space
fill(255, 215, 0);
Sets the fill color to gold [255, 215, 0]
stroke(200, 150, 0);
Sets the outline color to a darker gold [200, 150, 0]
strokeWeight(2);
Sets the outline thickness to 2 pixels for a visible edge
box(c.size);
Draws a cube with all sides equal to c.size (15 pixels by default), creating a shiny gold box that represents the coin
pop();
Restores the transformation state saved by push()

drawEnvironment()

drawEnvironment() draws the static world backdrop: a long sand-colored ground platform and parallax mountains. The ground is a thin, extremely long box that extends far into the distance, providing a continuous surface. The mountains are 4-sided cones (pyramids) positioned far to the left (off-camera) and scrolling via the sceneryParticles array, creating a parallax background effect that scrollScenery() continuously updates.

function drawEnvironment() {
  // Endless Ground Base
  push();
  translate(0, groundY + 5, -1000);
  fill(240, 200, 140); // Sand
  noStroke();
  box(200, 10, 5000); // Narrow width, extremely long length
  pop();
  
  // Parallax Scenery (Mountains/Pyramids)
  push();
  fill(180, 120, 80); // Mountain color
  noStroke();
  for (let p of sceneryParticles) {
    push();
    // Push mountains deeply into the background (negative X axis away from camera)
    translate(-250, p.y + p.size/2, p.z);
    rotateY(p.rot);
    cone(p.size, p.size * 2, 4); // 4-sided cone makes a nice pyramid/mountain
    pop();
  }
  pop();
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Ground platform box(200, 10, 5000);

Draws a long sand-colored platform that extends deep into the distance, defining the ground level

for-loop Mountain render loop for (let p of sceneryParticles) {

Draws each mountain particle from the sceneryParticles array, rotated and at its scrolling position

// Endless Ground Base
Comment explaining the next section
push();
Saves the transformation state for the ground
translate(0, groundY + 5, -1000);
Positions the ground at the groundY level, slightly below (Y + 5), and deep in the background (Z = -1000)
fill(240, 200, 140);
Sets the fill color to sand [240, 200, 140]
noStroke();
Removes the outline
box(200, 10, 5000);
Draws a very long box: 200 pixels wide (narrow), 10 pixels tall (thin), and 5000 pixels deep (extremely long from near to far). This creates a continuous ground that extends far into the distance
pop();
Restores the transformation state
// Parallax Scenery (Mountains/Pyramids)
Comment explaining the next section
for (let p of sceneryParticles) {
Loops through each mountain particle in the sceneryParticles array
push();
Saves the transformation state for each mountain so rotations don't affect others
translate(-250, p.y + p.size/2, p.z);
Positions the mountain far to the left (X = -250, away from the camera), at height p.y + p.size/2 (accounting for the cone's height), and at depth p.z (which scrolls via scrollScenery())
rotateY(p.rot);
Rotates the mountain around the Y-axis (up/down) by p.rot radians, making each mountain point in a different direction for visual variety
cone(p.size, p.size * 2, 4);
Draws a cone with base radius p.size and height p.size * 2. The 4-sided parameter makes it a 4-sided pyramid instead of a smooth cone, which looks more like a mountain

windowResized()

windowResized() is a built-in p5.js callback that fires whenever the browser window is resized. By calling resizeCanvas() inside it, the game canvas always stays full-screen. Without this function, the canvas would stay at its original size even if the window grows or shrinks.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the current window width and height, allowing the game to fill the full screen even if the user resizes their browser or rotates their device

📦 Key Variables

gameState string

Tracks which screen the player is on: 'MENU' (main menu), 'PLAY' (active game), 'OVER' (game over screen), or 'SHOP' (shop screen). Controls which UI elements are visible and what logic runs each frame.

let gameState = "MENU";
score number

The player's current score, which increases continuously during gameplay based on gameSpeed. Displayed on-screen and reset to 0 when restarting.

let score = 0;
coins number

The total number of coins the player has collected across all games. Used as currency for shop purchases and persists between game sessions.

let coins = 0;
gameSpeed number

How fast obstacles and coins move toward the player each frame. Starts at baseSpeed (12) and increases gradually with score as difficulty progresses.

let gameSpeed = 12;
baseSpeed number

The starting speed used when a new game begins. Set to 12 and doesn't change during a game.

let baseSpeed = 12;
spawnTimer number

A counter that increments each frame. When it reaches spawnInterval, a new obstacle or coin is spawned and the timer resets. Controls spawn frequency.

let spawnTimer = 0;
spawnInterval number

The number of frames between obstacle/coin spawns. Decreases as score increases, making the game busier. Has a minimum of 25 to prevent spawning too frequently.

let spawnInterval = 60;
hasRedSkin boolean

Whether the player has purchased the red skin from the shop. If true, the dino is rendered red instead of blue.

let hasRedSkin = false;
hasDoubleJump boolean

Whether the player has purchased the double jump upgrade from the shop. If true, maxJumps is set to 2, allowing two consecutive air jumps.

let hasDoubleJump = false;
maxJumps number

The maximum number of consecutive jumps allowed. Default is 1 (single jump only). Changes to 2 if the player buys the double jump upgrade.

let maxJumps = 1;
jumpsTaken number

A counter for how many jumps the dino has used while in the air. Resets to 0 when the dino lands. Used to enforce the maxJumps limit.

let jumpsTaken = 0;
skinColor array

An RGB array [R, G, B] that defines the dino's color. Default is [50, 150, 200] (blue). Changes to [220, 50, 50] (red) if the player buys the red skin.

let skinColor = [50, 150, 200];
groundY number

The Y-coordinate of the ground level. Set to 50 and defines where the dino lands and where obstacles sit.

const groundY = 50;
sceneryParticles array

An array of mountain/pyramid objects. Each has properties: y (height), z (depth for scrolling), size, and rot (rotation). Scrolls continuously to create the parallax background.

let sceneryParticles = [];
obstacles array

An array of obstacle objects (cacti). Each has x, y, z position, w (width), h (height), d (depth). Obstacles move toward the player and are removed when off-screen.

let obstacles = [];
coinObjects array

An array of coin objects the player can collect. Each has x, y, z position, size, and rot (rotation for spinning animation).

let coinObjects = [];
dino object

An object storing the player's dinosaur state: y (vertical position), z (depth position), w/h/d (dimensions), vy (vertical velocity), gravity, jumpForce (upward velocity from jump), isGrounded (whether on the ground).

let dino = { y: 0, z: 200, w: 30, h: 40, d: 30, vy: 0, gravity: 1.2, jumpForce: -16, isGrounded: true };

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Collision detection in updateGameLogic()

The AABB collision for obstacles checks if dino's y+h/2 > obs.y-h/2, but this only checks if the dino's bottom is above the obstacle's top, not if they actually overlap. A dino falling from above could pass through an obstacle without collision.

💡 Improve the yOverlap check to account for both top and bottom edges: (dino.y + dino.h/2) > (obs.y - obs.h/2) && (dino.y - dino.h/2) < (obs.y + obs.h/2)

PERFORMANCE updateCoinDisplays()

This function is called every time coins change (collection, purchase, reset), but selecting DOM elements with select() is slow. Multiple selections of the same element waste CPU.

💡 Cache the DOM elements at startup (e.g., let coinDisplay = select('#coin-display'); let shopCoins = select('#shop-coins');) and reuse the cached references in updateCoinDisplays().

STYLE Variable naming

The variable name 'coinObjects' is less clear than it could be—it's not obvious that these are collectible coins, not decorative elements.

💡 Rename to 'collectibleCoins' or 'coinPickups' to improve code readability and intent.

FEATURE Game mechanics

There's no way to pause the game mid-play. If the player needs to step away, they either crash or lose their run.

💡 Add a 'PAUSE' game state and a pause button that users can toggle during gameplay to pause/resume.

FEATURE Score and persistence

The coin count persists across games (good), but the high score is lost when the page refreshes. Players who die can't see their best score.

💡 Use localStorage to save the high score and coin count so they persist across browser sessions: localStorage.setItem('highScore', score); and let highScore = parseInt(localStorage.getItem('highScore')) || 0;

🔄 Code Flow

Code flow showing setup, draw, updategamelogic, scrollscenery, spawnentity, handlejump, keypressed, touchstarted, setuistate, triggergameover, resetgame, buyred, buydoublejump, updatecoindisplays, drawdino, drawobstacles, drawcoins, drawenvironment, 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 --> button-wiring[Button Wiring] setup --> scenery-loop[Scenery Initialization Loop] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click button-wiring href "#sub-button-wiring" click scenery-loop href "#sub-scenery-loop" draw --> background-clear[Background Clear] draw --> camera-setup[Camera Setup] draw --> game-state-branch[Game State Branch] draw --> render-order[Render Order] draw --> updategamelogic[updateGameLogic] draw --> scrollscenery[scrollScenery] draw --> drawdino[drawDino] draw --> drawobstacles[drawObstacles] draw --> drawcoins[drawCoins] draw --> drawenvironment[drawEnvironment] click draw href "#fn-draw" click background-clear href "#sub-background-clear" click camera-setup href "#sub-camera-setup" click game-state-branch href "#sub-game-state-branch" click render-order href "#sub-render-order" click updategamelogic href "#fn-updategamelogic" click scrollscenery href "#fn-scrollscenery" click drawdino href "#fn-drawdino" click drawobstacles href "#fn-drawobstacles" click drawcoins href "#fn-drawcoins" click drawenvironment href "#fn-drawenvironment" updategamelogic --> score-update[Score Update] updategamelogic --> difficulty-scaling[Difficulty Scaling] updategamelogic --> physics-gravity[Physics Gravity] updategamelogic --> floor-collision[Floor Collision] updategamelogic --> spawn-loop[Spawn Loop] updategamelogic --> obstacle-loop[Obstacle Loop] updategamelogic --> coin-loop[Coin Loop] click score-update href "#sub-score-update" click difficulty-scaling href "#sub-difficulty-scaling" click physics-gravity href "#sub-physics-gravity" click floor-collision href "#sub-floor-collision" click spawn-loop href "#sub-spawn-loop" click obstacle-loop href "#sub-obstacle-loop" click coin-loop href "#sub-coin-loop" spawn-loop --> coin-spawn-check[Coin Spawn Check] spawn-loop --> obstacle-width-vary[Obstacle Width Vary] spawn-loop --> coin-object-push[Coin Object Push] spawn-loop --> obstacle-push[Obstacle Push] click coin-spawn-check href "#sub-coin-spawn-check" click obstacle-width-vary href "#sub-obstacle-width-vary" click coin-object-push href "#sub-coin-object-push" click obstacle-push href "#sub-obstacle-push" obstacle-loop --> aabb-collision[AABB Collision] obstacle-loop --> cactus-arms[Cactus Arms] click aabb-collision href "#sub-aabb-collision" click cactus-arms href "#sub-cactus-arms" coin-loop --> coin-loop[Coin Render Loop] click coin-loop href "#sub-coin-loop" scrollscenery --> parallax-scroll[Parallax Scroll] scrollscenery --> wrap-around[Wrap-Around Check] click parallax-scroll href "#sub-parallax-scroll" click wrap-around href "#sub-wrap-around"

Preview

Sketch 2026-04-23 22:09 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-04-23 22:09 - Code flow showing setup, draw, updategamelogic, scrollscenery, spawnentity, handlejump, keypressed, touchstarted, setuistate, triggergameover, resetgame, buyred, buydoublejump, updatecoindisplays, drawdino, drawobstacles, drawcoins, drawenvironment, windowresized
Code Flow Diagram