dino game but with a twist:)

This sketch creates a fast-paced dinosaur runner game where players tap to jump over randomly-sized cacti racing toward them. When you hit a cactus, an animated GIF briefly celebrates your crash before the game resets, adding a humorous twist to the classic endless runner genre.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the game twice as fast — Double the gameSpeed variable to make cacti race across the screen faster, dramatically increasing difficulty
  2. Make cacti huge — Increase the random size ranges in the Cactus constructor so obstacles are massive and much easier to see coming
  3. Change the dino to a circle — Replace the rect() that draws the dino with a circle() call to visualize how the collision detection still works on non-rectangular shapes
  4. Award more points per cactus — Increase the score increment so progression feels faster and players feel more rewarded for dodging
  5. Make GIFs appear much more often — Lower the 0.1 threshold to 0.5 so celebration GIFs trigger 50% of the time instead of just 10%
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch brings the classic Chrome dinosaur game to life in p5.js with a fun animated GIF twist. When your dino collides with a cactus, instead of instant game-over, a celebratory GIF pops up before you restart. The game combines several foundational p5.js techniques: a draw loop that runs 60 times per second, two ES6 classes (Dino and Cactus) that encapsulate game objects and their behavior, gravity physics to make jumping feel natural, and axis-aligned bounding box (AABB) collision detection to determine when obstacles hit the player.

The code is split into three main sections: a Dino class that handles player movement and collision, a Cactus class that spawns obstacles with random sizes, and a draw() function that orchestrates the game state. By studying it you will learn how to build a complete playable game in under 300 lines, how classes organize game logic, how to use game state flags (gameOver and showGif) to pause and resume action, and how to load and display animated GIFs dynamically.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, defines the ground line 50 pixels from the bottom, instantiates a single Dino object at the left side, and initializes an empty cacti array. The preload() function quietly loads an animated GIF from a URL and hides it until needed.
  2. Every frame, draw() clears the background and draws a ground line. If the game is not over and the GIF is not showing, the main game loop runs: the dino's position updates based on gravity and jump velocity, cacti move toward the left at a speed stored in gameSpeed, and collision detection checks if the dino's rectangular bounding box overlaps with any cactus.
  3. When the dino collides with a cactus, gameOver is set to true, which pauses all movement and draws the game-over screen with the final score. Clicking or tapping then calls resetGame() to start fresh.
  4. Each time a cactus scrolls off the left edge of the screen without hitting the dino, the score increases by 10 and a 1-in-10 chance triggers showGif to become true, which pauses game logic and displays the animated GIF centered on the canvas for 1 second.
  5. New cacti are spawned at the right edge of the canvas with random widths (20–50 pixels) and heights (40–80 pixels), spaced by random gaps (300–800 pixels), creating unpredictable difficulty that keeps gameplay engaging.
  6. If the player resizes the window, the canvas and ground level recalculate, and the dino snaps to the new ground height so gameplay remains consistent.

🎓 Concepts You'll Learn

Game loop and frame-based animationES6 classes and object-oriented designGravity and velocity physicsAxis-aligned bounding box collision detectionGame state management with boolean flagsSpawning and object poolingInput handling (mouse and touch)Dynamic GIF loading and display

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. Use it to initialize your canvas, define constants, and create the objects your game needs. After setup(), draw() takes over and runs repeatedly at 60 frames per second.

function setup() {
  createCanvas(windowWidth, windowHeight);
  groundY = height - 50; // Define the ground level
  gameSpeed = 6; // Initial speed of the game
  dino = new Dino();
  resetGame(); // Initialize game state
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window so the game responds to any screen size
groundY = height - 50;
Sets the ground line 50 pixels from the bottom of the canvas—this is where the dino stands and cacti spawn
gameSpeed = 6;
Stores the initial speed at which cacti move left across the screen; you can increase this to make the game harder over time
dino = new Dino();
Creates a new Dino object at position (50, groundY) with default jump and gravity properties
resetGame();
Calls the resetGame() function to initialize score, cacti array, and game state flags

draw()

draw() runs 60 times per second and is the heartbeat of your game. Notice how the function uses early return statements to stop execution when the game is over or the GIF is showing—this is a clean way to manage game state without deeply nested if-else chains. The backwards loop (i >= 0; i--) is a pro tip: it's safe to modify an array while iterating backwards, whereas iterating forwards can skip elements when you remove them.

🔬 This loop moves and draws every cactus. What happens if you change `i--` to `i -= 2`? This would skip every other cactus in the array during each loop—what would you expect to see?

  // Update and show Cacti
  for (let i = cacti.length - 1; i >= 0; i--) {
    cacti[i].move();
    cacti[i].show();

🔬 This line controls how often the GIF appears: 0.1 means 10% chance. What happens if you change 0.1 to 0.5 so the GIF appears 50% of the time?

      if (random(1) < 0.1) {
        console.log("TRIGGERING GIF! 1 in 10 chance hit.");
        showGif = true;
function draw() {
  background(220); // Light gray background

  // Draw ground line
  stroke(0);
  strokeWeight(2);
  line(0, groundY, width, groundY);

  // --- Game Over Logic ---
  if (gameOver) {
    textAlign(CENTER);
    textSize(48);
    fill(0);
    text("Game Over!", width / 2, height / 2 - 24);
    textSize(24);
    text("Score: " + score, width / 2, height / 2 + 24);
    text("Click or touch to restart", width / 2, height / 2 + 60);
    return; // Stop draw loop if game over
  }

  // --- GIF Active Logic (pauses game) ---
  if (showGif) {
    // Draw the GIF on the canvas
    if (gif.elt.naturalWidth > 0 && gif.elt.naturalHeight > 0) {
      let gifWidth = min(gif.elt.naturalWidth, width * 0.8);
      let gifHeight = min(gif.elt.naturalHeight, height * 0.8);
      let gifX = (width - gifWidth) / 2;
      let gifY = (height - gifHeight) / 2;
      image(gif.elt, gifX, gifY, gifWidth, gifHeight);
    }

    // Check if GIF duration is over
    if (millis() > gifEndTime) {
      console.log("GIF DURATION OVER! Resuming game.");
      showGif = false;
      gif.elt.style.display = 'none'; // Hide the HTML GIF element
      console.log("GIF hidden.");
    }
    return; // Stop game logic if GIF is showing
  }

  // --- Main Game Logic (only runs if not gameOver and not showGif) ---
  // Update and show Dino
  dino.move();
  dino.show();

  // Update and show Cacti
  for (let i = cacti.length - 1; i >= 0; i--) {
    cacti[i].move();
    cacti[i].show();

    // Check collision
    if (dino.collides(cacti[i])) {
      gameOver = true;
      gif.elt.style.display = 'none'; // Ensure GIF is hidden on game over
      console.log("Game Over! Dino collided.");
      break; // End game on collision
    }

    // Remove off-screen cacti and update score
    if (cacti[i].isOffScreen()) {
      cacti.splice(i, 1);
      score += 10;

      // 1 in 10 chance to show GIF when a cactus is successfully passed
      if (random(1) < 0.1) {
        console.log("TRIGGERING GIF! 1 in 10 chance hit.");
        showGif = true;
        gifEndTime = millis() + GIF_DURATION;
        gif.elt.style.display = 'block'; // Show the HTML GIF element
        console.log("GIF activated. gifEndTime:", gifEndTime);
      }
    }
  }

  // Add new cacti
  // Ensure there's a gap before adding the next cactus
  if (cacti.length === 0 || width - cacti[cacti.length - 1].x > random(300, 800)) {
    cacti.push(new Cactus(width));
  }

  // Display score
  textAlign(RIGHT);
  textSize(24);
  fill(0);
  text("Score: " + score, width - 20, 30);
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

conditional Game Over State Check if (gameOver) {

If the dino has collided with a cactus, displays the game-over screen and halts all game logic

conditional GIF Active State Check if (showGif) {

If a celebration GIF should be displayed, pauses the game and centers the animated image on the canvas

for-loop Cactus Update and Collision Loop for (let i = cacti.length - 1; i >= 0; i--) {

Iterates through all cacti from last to first, updating positions, drawing them, and checking collisions with the dino

conditional Collision Detection if (dino.collides(cacti[i])) {

Tests if the dino's bounding box overlaps a cactus's bounding box; if yes, ends the game

conditional Off-Screen Cactus Removal if (cacti[i].isOffScreen()) {

Removes cacti that have scrolled past the left edge, awards points, and randomly triggers the GIF animation

conditional Cactus Spawning if (cacti.length === 0 || width - cacti[cacti.length - 1].x > random(300, 800)) {

Ensures new cacti are spawned at randomized intervals to maintain gameplay pacing

background(220);
Clears the entire canvas with light gray every frame, erasing the previous frame's drawing so objects appear to move smoothly
stroke(0);
Sets the stroke color to black for the next shape drawn (the ground line)
strokeWeight(2);
Sets the thickness of stroked lines to 2 pixels
line(0, groundY, width, groundY);
Draws a horizontal black line across the full width of the canvas at the ground level, creating a visual ground
if (gameOver) {
Checks if the collision happened—if true, all game logic below is skipped and only the game-over screen displays
return;
Exits the draw() function immediately, preventing the game-logic code below from running on this frame
if (showGif) {
Checks if a celebration GIF should be displayed; if true, game movement pauses and only the GIF and timer logic runs
dino.move();
Updates the dino's vertical position by adding gravity and velocity, then checks if it hits the ground
dino.show();
Draws the dino as a filled black rectangle at its current position
for (let i = cacti.length - 1; i >= 0; i--) {
Loops through the cacti array backwards (from last element to first) so you can safely remove elements while iterating
if (dino.collides(cacti[i])) {
Calls the dino's collides() method to test if its bounding box overlaps this cactus's bounding box
gameOver = true;
Sets the gameOver flag to true, which causes the next frame's draw() to skip game logic and show the game-over screen instead
if (cacti[i].isOffScreen()) {
Checks if this cactus has moved far enough left that it's completely off the canvas
cacti.splice(i, 1);
Removes this cactus from the array using splice, freeing memory since the player successfully avoided it
score += 10;
Increases the player's score by 10 points for each cactus successfully dodged
if (random(1) < 0.1) {
Generates a random number between 0 and 1; if it's less than 0.1 (a 10% chance), the GIF celebration triggers
showGif = true;
Sets the showGif flag to true, which pauses game logic next frame and displays the animated GIF
gifEndTime = millis() + GIF_DURATION;
Records the exact millisecond timestamp when the GIF should stop displaying (current time plus 1000 milliseconds)
if (cacti.length === 0 || width - cacti[cacti.length - 1].x > random(300, 800)) {
Spawns a new cactus if the array is empty OR if the rightmost cactus is far enough from the right edge of the canvas
cacti.push(new Cactus(width));
Creates a new Cactus object at the right edge of the canvas and adds it to the cacti array
text("Score: " + score, width - 20, 30);
Draws the current score in the top-right corner of the canvas, aligned right and positioned 20 pixels from the right edge

preload()

preload() runs before setup() and is designed to load assets (images, sounds, fonts) that your sketch needs before it can draw. It's essential here because we need the GIF to be ready to display instantly when triggered, rather than waiting for it to download mid-game. Note that we use createImg() instead of loadImage()—this is because p5's loadImage() treats GIFs as static images and doesn't animate them. createImg() wraps a real HTML img tag, which browsers animate automatically.

function preload() {
  // Use createImg() to load the GIF as an HTML element.
  // p5.js's loadImage() function does not animate GIFs on the canvas directly.
  gif = createImg(gifURL, 'dino gif');
  // Directly hide the underlying HTML element using its style
  gif.elt.style.display = 'none';
  console.log("GIF element created and initially hidden.");
}
Line-by-line explanation (3 lines)
gif = createImg(gifURL, 'dino gif');
Uses p5.js's createImg() to load the animated GIF from a URL as an HTML img element, not a p5 Image, because p5's loadImage() doesn't animate GIFs
gif.elt.style.display = 'none';
Accesses the underlying HTML element (via .elt) and hides it using CSS so the GIF isn't visible until we intentionally show it
console.log("GIF element created and initially hidden.");
Prints a debug message to the browser console so you can verify the GIF loaded successfully when you open developer tools

Dino (class)

The Dino class is a perfect example of object-oriented design in games: it bundles properties (x, y, size, velocity) and methods (jump, move, show, collides) together so a dino is a self-contained unit. The gravity physics here is a classic pattern: every frame, velocity increases by a fixed gravity amount, creating a parabolic arc. The AABB (Axis-Aligned Bounding Box) collision detection is simple and efficient—it works by checking if two axis-parallel rectangles overlap on both the x and y axes. This is the foundation for collision detection in games.

🔬 This is the core of the gravity physics: velocity increases each frame. What happens if you change the += to = so gravity overwrites velocity instead of adding to it? (Try: this.velY = this.gravity;)

  move() {
    this.y += this.velY;
    this.velY += this.gravity;

🔬 This block lands the dino. What happens if you remove the line `this.isJumping = false;`? The dino would think it's still jumping even after landing—could you jump while standing still?

    if (this.y >= groundY) {
      this.y = groundY;
      this.velY = 0;
      this.isJumping = false;
    }
class Dino {
  constructor() {
    this.x = 50;
    this.y = groundY;
    this.size = 60;
    this.velY = 0;
    this.gravity = 0.8;
    this.jumpPower = -15;
    this.isJumping = false;
  }

  jump() {
    if (!this.isJumping) {
      this.velY = this.jumpPower;
      this.isJumping = true;
    }
  }

  move() {
    this.y += this.velY;
    this.velY += this.gravity;

    // Prevent falling through ground
    if (this.y >= groundY) {
      this.y = groundY;
      this.velY = 0;
      this.isJumping = false;
    }
  }

  show() {
    fill(0);
    // Draw the dino as a simple rectangle
    rect(this.x, this.y - this.size, this.size, this.size);
  }

  // Check collision with a cactus using Axis-Aligned Bounding Box (AABB)
  collides(cactus) {
    let dinoRect = {
      x: this.x,
      y: this.y - this.size,
      width: this.size,
      height: this.size
    };
    let cactusRect = {
      x: cactus.x,
      y: cactus.y - cactus.height,
      width: cactus.width,
      height: cactus.height
    };

    return dinoRect.x < cactusRect.x + cactusRect.width &&
           dinoRect.x + dinoRect.width > cactusRect.x &&
           dinoRect.y < cactusRect.y + cactusRect.height &&
           dinoRect.y + dinoRect.height > cactusRect.y;
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

conditional Jump Method if (!this.isJumping) {

Checks that the dino is not already mid-jump before allowing a new jump to start

calculation Gravity and Velocity Update this.velY += this.gravity;

Continuously increases downward velocity to simulate gravity pulling the dino down

conditional Ground Collision if (this.y >= groundY) {

Stops the dino from falling through the ground by resetting its position and velocity

calculation AABB Collision Detection return dinoRect.x < cactusRect.x + cactusRect.width &&

Tests all four sides of the dino's bounding box against the cactus's bounding box to detect overlap

this.x = 50;
Sets the dino's starting x-position 50 pixels from the left edge of the canvas
this.y = groundY;
Sets the dino's starting y-position to the ground level (defined in setup as height - 50)
this.size = 60;
Stores the width and height of the dino (drawn as a 60×60 pixel square)
this.velY = 0;
Initializes vertical velocity to 0—the dino starts still and only moves when gravity or jumps apply
this.gravity = 0.8;
Sets how much gravity pulls the dino down each frame; higher values make falling feel heavier
this.jumpPower = -15;
Negative number that gets assigned to velY when jumping; negative because positive y points downward in p5, so negative pushes the dino up
this.isJumping = false;
Boolean flag that tracks whether the dino is currently in the air; prevents double-jumping
if (!this.isJumping) {
Only allows a jump if isJumping is false—once jumping starts, this blocks further jumps until the dino lands
this.velY = this.jumpPower;
Sets velocity to the jump power (–15), which instantly pushes the dino upward
this.isJumping = true;
Sets the flag to true so subsequent clicks won't trigger another jump mid-air
this.y += this.velY;
Updates the dino's y-position by adding its velocity—if velocity is negative (jumping), y decreases and the dino moves up
this.velY += this.gravity;
Every frame, adds gravity (0.8) to velocity, making it less negative (slower upward) then more positive (faster downward), simulating realistic arc motion
if (this.y >= groundY) {
Checks if the dino has fallen to or below the ground level
this.y = groundY;
Clamps the dino's y-position to the ground so it never sinks into the earth
this.velY = 0;
Resets velocity to 0 so the dino stops moving vertically once it lands
this.isJumping = false;
Sets the flag back to false, allowing the player to jump again on the next click
rect(this.x, this.y - this.size, this.size, this.size);
Draws the dino as a black square; the y-coordinate is offset by –this.size so the square's bottom sits at this.y (the ground)
let dinoRect = {
Creates a plain JavaScript object that stores the four sides of the dino's bounding box for collision testing
x: this.x,
The left edge of the dino's bounding box
width: this.size,
The dino's width (60 pixels)
return dinoRect.x < cactusRect.x + cactusRect.width &&
First condition: dino's left edge is to the left of the cactus's right edge
dinoRect.x + dinoRect.width > cactusRect.x &&
Second condition: dino's right edge is to the right of the cactus's left edge (boxes overlap horizontally)
dinoRect.y < cactusRect.y + cactusRect.height &&
Third condition: dino's top edge is above the cactus's bottom edge
dinoRect.y + dinoRect.height > cactusRect.y;
Fourth condition: dino's bottom edge is below the cactus's top edge (boxes overlap vertically). All four must be true for a collision.

Cactus (class)

The Cactus class is a lightweight obstacle that the Dino must dodge. Notice that width and height are randomized in the constructor—this adds unpredictability to gameplay. The move() method is dead simple: subtract gameSpeed from x each frame. The isOffScreen() check is crucial for memory management; without removing off-screen cacti, your array would grow infinitely and the game would slow down.

🔬 These two lines make every cactus a different size. What happens if you change `random(20, 50)` to just `40` so all cacti are the same width? Would the game feel easier or harder?

    this.width = random(20, 50); // Random cactus width
    this.height = random(40, 80); // Random cactus height
class Cactus {
  constructor(x) {
    this.x = x;
    this.y = groundY;
    this.width = random(20, 50); // Random cactus width
    this.height = random(40, 80); // Random cactus height
  }

  move() {
    this.x -= gameSpeed;
  }

  show() {
    fill(0, 150, 0); // Green color for cacti
    rect(this.x, this.y - this.height, this.width, this.height);
  }

  isOffScreen() {
    return this.x < -this.width;
  }
}
Line-by-line explanation (8 lines)
this.x = x;
Stores the cactus's starting x-position, which is always the width of the canvas (the right edge) when spawned
this.y = groundY;
Sets the cactus's y-position to the ground level so it stands upright from the ground
this.width = random(20, 50);
Randomly generates a width between 20 and 50 pixels, making each cactus a different thickness
this.height = random(40, 80);
Randomly generates a height between 40 and 80 pixels, making each cactus a different tallness
this.x -= gameSpeed;
Subtracts gameSpeed from the cactus's x-position each frame, moving it leftward toward the dino
fill(0, 150, 0);
Sets the fill color to green (RGB: red=0, green=150, blue=0) for all cacti
rect(this.x, this.y - this.height, this.width, this.height);
Draws a green rectangle; the y-coordinate is offset by –height so the rectangle's bottom sits on the ground at this.y
return this.x < -this.width;
Returns true if the cactus's left edge has scrolled past the left edge of the canvas, meaning it's fully off-screen

mousePressed()

mousePressed() is a built-in p5.js function that fires every time the player clicks the mouse. This single function handles two different game states: restarting after a crash and jumping during gameplay. The double condition (!showGif) prevents the dino from jumping while a celebration GIF is on screen, keeping the pause effect clean.

function mousePressed() {
  if (gameOver) {
    resetGame();
  } else if (!showGif) { // Only jump if game is playing and GIF is not active
    dino.jump();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Restart on Game Over if (gameOver) {

Detects if the game ended and restarts it when the player clicks

conditional Jump Gate } else if (!showGif) {

Prevents jumping while the GIF is playing, pausing the game without restarting

if (gameOver) {
Checks if the game has ended from a collision
resetGame();
Resets all variables (score, dino position, cacti array) so the player can play again
} else if (!showGif) {
If the game is NOT over but the GIF is also NOT showing, the game is actively playing, so allow a jump
dino.jump();
Calls the dino's jump method to initiate an upward arc

touchStarted()

touchStarted() is the mobile equivalent of mousePressed()—it fires whenever the player taps the screen. The `return false;` statements prevent the browser from scrolling or zooming when the player taps, keeping the focus on your game. This function ensures the game works identically on phones, tablets, and desktops.

function touchStarted() {
  if (gameOver) {
    resetGame();
    return false; // Prevent default touch behavior
  } else if (!showGif) { // Only jump if game is playing and GIF is not active
    dino.jump();
    return false; // Prevent default touch behavior
  }
}
Line-by-line explanation (5 lines)
if (gameOver) {
Checks if the game has ended
resetGame();
Resets the game so a touch can restart gameplay on mobile
return false;
Tells the browser not to perform its default touch action (like scrolling), so the touch only triggers our game logic
} else if (!showGif) {
If the game is playing (not over and not showing GIF), allow a jump on touch
dino.jump();
Calls the dino's jump method in response to a tap

resetGame()

resetGame() is a cleanup function called both at startup (from setup()) and whenever the player restarts after a crash. It systematically resets every mutable variable to its starting state. This pattern—consolidating all reset logic in one function—makes your game maintainable: if you add a new variable, you only need to reset it in one place.

function resetGame() {
  dino = new Dino(); // Re-initialize dino
  cacti = [];
  score = 0;
  gameSpeed = 6;
  gameOver = false; // Reset game over flag
  showGif = false; // Reset GIF flag
  gif.elt.style.display = 'none'; // Ensure GIF is hidden on reset
  console.log("Game reset. GIF hidden.");
}
Line-by-line explanation (8 lines)
dino = new Dino();
Creates a fresh Dino object, placing it at (50, groundY) with all properties reset to defaults
cacti = [];
Clears the cacti array by setting it to an empty array, removing all obstacles from the previous game
score = 0;
Resets the score back to 0 so a new game starts fresh
gameSpeed = 6;
Resets the game speed to its starting value (6 pixels per frame)
gameOver = false;
Sets the gameOver flag back to false, telling draw() to resume normal gameplay logic
showGif = false;
Sets the showGif flag back to false so no celebration GIF displays on the next frame
gif.elt.style.display = 'none';
Hides the GIF element by setting its CSS display property, ensuring it doesn't linger from the previous game
console.log("Game reset. GIF hidden.");
Prints a debug message confirming the reset happened; useful for testing in the browser console

windowResized()

windowResized() is a built-in p5.js callback that fires whenever the browser window is resized. By resizing the canvas and updating groundY, your game stays playable at any screen size. Notice that cacti don't need manual repositioning—they spawn at the correct height automatically because the Cactus constructor reads groundY, which was just updated.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  groundY = height - 50; // Recalculate ground position
  dino.y = groundY; // Reset dino position to new ground height
  // Cacti will naturally respawn at the correct height
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions when the browser window is resized
groundY = height - 50;
Recalculates the ground position based on the new canvas height so it stays 50 pixels from the bottom
dino.y = groundY;
Repositions the dino to the new ground level so it doesn't float or sink when the window resizes

📦 Key Variables

dino object (Dino instance)

Stores the player-controlled dinosaur with properties for position, velocity, and methods for jumping and collision detection

let dino = new Dino();
cacti array

Holds all active Cactus objects currently on screen; new ones are pushed as they spawn, old ones are removed when off-screen

let cacti = [];
groundY number

Stores the y-coordinate of the ground level, defining where the dino stands and where cacti grow from

let groundY = height - 50;
gameSpeed number

Controls how fast cacti move left each frame in pixels; higher values make the game harder

let gameSpeed = 6;
score number

Tracks the player's current score; increases by 10 each time a cactus is dodged

let score = 0;
gameOver boolean

Flag that is true when the dino collides with a cactus, pausing game logic and showing the game-over screen

let gameOver = false;
showGif boolean

Flag that is true when a celebration GIF should be displayed; triggers on a random chance when a cactus is dodged

let showGif = false;
gif object (p5.Renderer or HTML element)

Stores the animated GIF loaded from a URL; can be shown or hidden via its style property

let gif;
gifURL string

Holds the URL of the animated GIF to display on collision celebration

let gifURL = "https://c.tenor.com/6oKfSmPfTW0AAAAC/tenor.gif";
gifEndTime number

Stores the millisecond timestamp when the GIF should stop displaying; compared against millis() each frame

let gifEndTime = 0;
GIF_DURATION number (constant)

Defines how many milliseconds the celebration GIF remains visible (1000ms = 1 second)

const GIF_DURATION = 1000;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() – cactus spawning

The condition `width - cacti[cacti.length - 1].x > random(300, 800)` generates a new random range every frame, creating inconsistent spacing. The random() call is rechecked every frame, not just when deciding to spawn.

💡 Pre-calculate the random gap once per spawn: `let nextGap = random(300, 800); if (cacti.length === 0 || width - cacti[cacti.length - 1].x > nextGap)` to ensure consistent, predictable obstacle timing.

BUG Cactus.collides() – AABB collision in Dino class

The bounding box collision uses <= and >= instead of < and >, which can cause collisions to register when the dino barely touches an edge pixel. This feels unfair to players.

💡 Use < and > instead of <= and >= to create a tiny grace zone: `dinoRect.x + dinoRect.width > cactusRect.x && dinoRect.x < cactusRect.x + cactusRect.width` (without the equals) to feel more forgiving.

PERFORMANCE preload()

The GIF is loaded from an external URL every time the sketch loads. If the network is slow, the game can start without the GIF ready, causing a crash when trying to display it.

💡 Add error handling to preload() and provide a fallback: wrap createImg() in a try-catch, or check gif.elt.naturalWidth before accessing it in draw()—which you already do, so the risk is mitigated but the suggestion improves robustness.

STYLE Global scope

The GIF URL is a hardcoded string pointing to a Tenor placeholder. This should be documented or made easier to swap.

💡 Add a comment explaining how to replace the URL, e.g., `// Change this URL to any animated GIF; visit giphy.com or tenor.com, find a GIF, and copy its direct link` to help future maintainers.

FEATURE draw() – game over logic

There is no difficulty progression; gameSpeed stays at 6 forever. Games like this usually accelerate over time to increase challenge.

💡 Add `gameSpeed += 0.001;` inside the main game loop or `gameSpeed = 6 + (score / 100);` so speed increases as the player's score climbs, creating a natural difficulty curve.

FEATURE draw() – score display

The score is the only feedback for progress; high scores are not persisted, so players can't track their best attempts.

💡 Use localStorage to save and display the high score: `localStorage.setItem('highScore', max(score, parseInt(localStorage.getItem('highScore') || 0)));` and display it on the game-over screen.

🔄 Code Flow

Code flow showing setup, draw, preload, dino, cactus, keypressed, touchstarted, resetgame, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gameovercheck[gameover-check] draw --> gifactivecheck[gif-active-check] draw --> cactusloop[cactus-loop] cactusloop --> collisioncheck[collision-check] cactusloop --> offscreenremoval[offscreen-removal] cactusloop --> cactusspawn[cactus-spawn] draw --> gravityapplication[gravity-application] draw --> groundcollision[ground-collision] draw --> jumpgate[jump-gate] draw --> restartcheck[restart-check] click setup href "#fn-setup" click draw href "#fn-draw" click gameovercheck href "#sub-gameover-check" click gifactivecheck href "#sub-gif-active-check" click cactusloop href "#sub-cactus-loop" click collisioncheck href "#sub-collision-check" click offscreenremoval href "#sub-offscreen-removal" click cactusspawn href "#sub-cactus-spawn" click gravityapplication href "#sub-gravity-application" click groundcollision href "#sub-ground-collision" click jumpgate href "#sub-jump-gate" click restartcheck href "#sub-restart-check" draw --> jumpmethod[jump-method] click jumpmethod href "#sub-jump-method" cactusloop --> aabbcollision[aabb-collision] click aabbcollision href "#sub-aabb-collision"

❓ Frequently Asked Questions

What visual experience can I expect from this p5.js dino game sketch?

The sketch features a minimalist design where a small dino character runs across the screen, dodging randomly-sized cacti that approach from the right.

How can I interact with the dino in this creative coding sketch?

Users can tap the screen to make the dino jump over incoming cacti, adding an engaging element to the gameplay.

What creative coding concepts does this dino game illustrate?

This sketch demonstrates collision detection using Axis-Aligned Bounding Box (AABB) principles, along with basic physics for jumping and gravity.

Preview

dino game but with a twist:) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of dino game but with a twist:) - Code flow showing setup, draw, preload, dino, cactus, keypressed, touchstarted, resetgame, windowresized
Code Flow Diagram