Offline game that works offline chicken run

This sketch is a complete offline endless-runner game where a cartoon chicken jumps and ducks to dodge bombs and flying bullets scrolling toward it. Difficulty buttons let the player pick a starting speed, and the game speeds up automatically as the score climbs until a collision ends the run.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the chicken jump higher — Increasing the magnitude of the jump velocity sends the chicken further into the air on every jump.
  2. Recolor the explosion — The bomb's explosion is drawn as two fading circles - swap the colors for a completely different blast effect.
  3. Spawn mostly bullets instead of bombs — Flipping the probability threshold makes flying bullets the dominant obstacle instead of ground bombs.
  4. Give the chicken floatier jumps — Lowering gravity slows how quickly the chicken falls, making jumps feel slow-motion and giving more time to react.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a Chrome-dino-style endless runner starring a cartoon chicken that must jump over bombs and duck under flying bullets as the ground scrolls beneath it. It's a great sketch to study because it combines several core p5.js and JavaScript game-dev patterns in one place: constructor functions acting as classes for the Chicken and obstacles, a finite game-state machine (start/playing/gameOver), scrolling backgrounds made from two swapped rectangles, and axis-aligned bounding box (AABB) collision detection.

The code is organized around three constructor functions (Chicken, Bomb, Bullet) that each bundle their own position, appearance, and update logic, plus a set of screen-management functions (startScreen, playGame, gameOverScreen) that draw() dispatches between based on gameState. Studying this sketch teaches you how to structure a real game loop, how to spawn and recycle objects in an array, how DOM buttons (createButton) can coexist with canvas drawing, and how to handle both keyboard and touch input for the same actions.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, calculates the ground height, builds the chicken object, and creates two DOM buttons ('Easy' and 'Hard') that stay hidden until needed.
  2. draw() runs 60 times a second and simply checks the gameState variable to decide whether to show the start screen, run the live game, or show the game-over screen.
  3. Once playing begins, playGame() scrolls two ground rectangles leftward and swaps them back off-screen to fake infinite scrolling, while gameSpeed slowly increases as the score grows.
  4. Every so often, based on the current speed, a new Bomb or Bullet is pushed into the obstacles array; each obstacle moves itself left every frame inside its own update() method.
  5. checkCollision() compares the chicken's bounding box (which shrinks and shifts when ducking) against each obstacle's box using simple rectangle-overlap math; a hit switches gameState to 'gameOver' and stops the loop with noLoop().
  6. Keyboard (space/up/down) and touch (tap) handlers call the chicken's jump(), duck(), and stand() methods, letting the player react to whichever obstacle is coming.

🎓 Concepts You'll Learn

Game state machine (start/playing/gameOver)Constructor functions as classesAABB collision detectionScrolling background trick with two rectanglesDOM elements (createButton) mixed with canvasArray iteration with splice for object recyclingKeyboard and touch input handlingGravity and velocity-based motion

📝 Code Breakdown

Chicken()

Chicken() is a constructor function used like a class (JavaScript's older 'this' pattern instead of ES6 class syntax). It bundles the chicken's position, physics state, and its own show/update/jump/duck methods into a single reusable object created with `new Chicken()`.

🔬 This function only lets the chicken jump if isJumping is false. What happens if you remove the `if (!this.isJumping)` check entirely - can you now double-jump mid-air?

  this.jump = function() {
    if (!this.isJumping) {
      this.vy = -15; // Higher jump
      this.isJumping = true;
      this.isDucking = false; // Can't duck while jumping
    }
  };
function Chicken() {
  this.x = 50;
  this.y = groundY - 40; // Top-left of the bounding box
  this.w = 30; // Effective width for standing bounding box
  this.h = 40; // Effective height for standing bounding box
  this.vy = 0; // Vertical velocity
  this.gravity = 1;
  this.isJumping = false;
  this.isDucking = false;

  this.show = function() {
    fill(255, 200, 0); // Yellow for chicken body
    noStroke();

    if (this.isDucking) {
      // Ducking chicken shape
      // Body (squashed oval)
      ellipse(this.x + this.w * 0.75, this.y + this.h * 0.75, this.w * 1.5, this.h * 0.5);
      // Head (slightly lower)
      ellipse(this.x + this.w * 1.2, this.y + this.h * 0.7, this.w / 2, this.h / 2);
      // Beak
      fill(255, 100, 0);
      triangle(this.x + this.w * 1.2 + this.w / 4, this.y + this.h * 0.7,
               this.x + this.w * 1.2 + this.w / 4, this.y + this.h * 0.7 + this.h / 8,
               this.x + this.w * 1.2 + this.w / 2, this.y + this.h * 0.7 + this.h / 16);
      // Legs (shorter)
      stroke(150, 75, 0); // Brown for legs
      strokeWeight(2);
      line(this.x + this.w * 0.75 - 5, groundY, this.x + this.w * 0.75 - 5, groundY + 5);
      line(this.x + this.w * 0.75 + 5, groundY, this.x + this.w * 0.75 + 5, groundY + 5);
    } else {
      // Standing chicken shape
      // Body (oval)
      ellipse(this.x + this.w / 2, this.y + this.h / 2, this.w, this.h);
      // Head (smaller circle)
      ellipse(this.x + this.w, this.y + this.h / 4, this.w / 2, this.h / 2);
      // Beak (triangle)
      fill(255, 100, 0); // Orange for beak
      triangle(this.x + this.w + this.w / 4, this.y + this.h / 4,
               this.x + this.w + this.w / 4, this.y + this.h / 4 + this.h / 8,
               this.x + this.w + this.w / 2, this.y + this.h / 4 + this.h / 16);
      // Comb (small triangles)
      fill(200, 0, 0); // Red for comb
      triangle(this.x + this.w, this.y, this.x + this.w + 5, this.y - 5, this.x + this.w + 10, this.y);
      // Legs (lines)
      stroke(150, 75, 0); // Brown for legs
      strokeWeight(2);
      line(this.x + this.w / 2 - 5, groundY, this.x + this.w / 2 - 5, groundY + 10);
      line(this.x + this.w / 2 + 5, groundY, this.x + this.w / 2 + 5, groundY + 10);
    }
  };

  this.jump = function() {
    if (!this.isJumping) {
      this.vy = -15; // Higher jump
      this.isJumping = true;
      this.isDucking = false; // Can't duck while jumping
    }
  };

  this.duck = function() {
    if (!this.isJumping) {
      this.isDucking = true;
    }
  };

  this.stand = function() {
    this.isDucking = false;
  };

  this.update = function() {
    // Apply gravity
    this.y += this.vy;
    this.vy += this.gravity;

    // Prevent going below ground
    this.y = constrain(this.y, 0, groundY - this.h);

    // If on ground and not ducking, reset jumping state
    if (this.y >= groundY - this.h && !this.isDucking) {
      this.vy = 0;
      this.isJumping = false;
    }
    // If on ground and ducking, reset jumping state
    else if (this.y >= groundY - this.h && this.isDucking) {
      this.vy = 0;
      this.isJumping = false;
      this.y = groundY - this.h / 2; // Chicken is half height when ducking
    }
  };
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Ducking vs standing shape if (this.isDucking) { ... } else { ... }

Draws a squashed, wider shape when ducking and a taller upright shape when standing

conditional Prevent double jump if (!this.isJumping) {

Only launches the chicken upward if it isn't already mid-air

conditional Landing state reset if (this.y >= groundY - this.h && !this.isDucking) {

Resets velocity and jumping flag once the chicken touches the ground, handling ducking and standing separately

this.x = 50;
The chicken's horizontal position never changes - only obstacles move, creating the illusion the chicken is running.
this.y = groundY - 40;
Places the chicken standing on the ground, since y is measured from the top of its bounding box.
this.gravity = 1;
How strongly the chicken accelerates downward each frame after jumping.
this.isJumping = false;
Tracks whether the chicken is currently airborne so it can't jump again mid-air.
this.vy = -15; // Higher jump
A negative vertical velocity launches the chicken upward; gravity will pull it back down over subsequent frames.
this.y += this.vy;
Moves the chicken by its current vertical velocity - this is the actual motion each frame.
this.vy += this.gravity;
Gravity constantly increases vy, so upward motion slows, stops, then turns into falling - classic projectile motion.
this.y = constrain(this.y, 0, groundY - this.h);
Clamps the chicken's position so it never flies above the canvas top or sinks through the ground.
this.y = groundY - this.h / 2; // Chicken is half height when ducking
When ducking on the ground, the chicken's y is adjusted upward slightly since its drawn height shrinks.

Bomb()

Bomb() shows how a single obstacle object can have its own tiny state machine ('active' → 'exploding' → 'exploded'), letting you play a short animation before removing it, instead of instantly deleting it on collision.

🔬 This draws two overlapping circles (red and yellow) to fake a fireball. What happens if you swap the color values, or add a third smaller white circle on top?

      fill(255, 0, 0, alpha); // Red fading out
      ellipse(this.x + this.w / 2, this.y + this.h / 2, explosionSize);
      fill(255, 200, 0, alpha); // Yellow fading out
      ellipse(this.x + this.w / 2, this.y + this.h / 2, explosionSize * 0.7);
function Bomb() {
  this.w = random(30, 50); // Bomb width
  this.h = this.w; // Bomb height (make it a circle for collision)
  this.x = width;
  this.y = groundY - this.h; // Place on the ground
  this.type = 'bomb'; // Added type for collision logic
  this.state = 'active'; // 'active', 'exploding', 'exploded'
  this.explosionFrameCount = 0;
  this.explosionDuration = 30; // Frames for explosion animation

  this.show = function() {
    if (this.state === 'active') {
      fill(0); // Black for bomb body
      noStroke();
      ellipse(this.x + this.w / 2, this.y + this.h / 2, this.w, this.h); // Main bomb body

      // Fuse
      fill(100); // Gray for fuse
      rect(this.x + this.w / 2 - this.w / 10, this.y - this.h / 4, this.w / 5, this.h / 4);
    } else if (this.state === 'exploding') {
      // Simple explosion effect: growing red/yellow circle
      let explosionSize = map(this.explosionFrameCount, 0, this.explosionDuration, this.w, this.w * 2.5); // Make it slightly larger
      let alpha = map(this.explosionFrameCount, 0, this.explosionDuration, 255, 0); // Fades out
      noStroke(); // No stroke for explosion
      fill(255, 0, 0, alpha); // Red fading out
      ellipse(this.x + this.w / 2, this.y + this.h / 2, explosionSize);
      fill(255, 200, 0, alpha); // Yellow fading out
      ellipse(this.x + this.w / 2, this.y + this.h / 2, explosionSize * 0.7);
    }
  };

  this.update = function() {
    if (this.state === 'active') {
      this.x -= gameSpeed;
    } else if (this.state === 'exploding') {
      this.explosionFrameCount++;
      // The game over will be triggered by `gameOverTriggered` in `playGame`
      // This just continues the animation for a bit if the loop hasn't stopped yet.
      if (this.explosionFrameCount >= this.explosionDuration) {
        this.state = 'exploded';
      }
    }
  };
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Draw by state if (this.state === 'active') { ... } else if (this.state === 'exploding') { ... }

Draws either the intact bomb or a fading red/yellow explosion depending on its current state

conditional Update by state if (this.state === 'active') { ... } else if (this.state === 'exploding') { ... }

Only moves the bomb while active, and advances the explosion timer while exploding

this.w = random(30, 50); // Bomb width
Gives every bomb a random size so obstacles look varied rather than identical.
this.x = width;
Spawns the bomb just off the right edge of the canvas so it scrolls into view.
this.state = 'active'; // 'active', 'exploding', 'exploded'
A simple state machine tracks whether the bomb is still a threat, currently exploding, or finished and ready to be removed.
let explosionSize = map(this.explosionFrameCount, 0, this.explosionDuration, this.w, this.w * 2.5); // Make it slightly larger
map() converts the explosion's frame counter into a growing size, so the blast visually expands over time.
let alpha = map(this.explosionFrameCount, 0, this.explosionDuration, 255, 0); // Fades out
Maps the same counter to a shrinking transparency value so the explosion fades away as it grows.
this.x -= gameSpeed;
Moves the bomb leftward every frame at the current game speed, same as the scrolling ground.
if (this.explosionFrameCount >= this.explosionDuration) {
Once the explosion animation has played long enough, the bomb is marked 'exploded' so playGame() can remove it from the array.

Bullet()

Bullet() demonstrates using sin() with frameCount to create smooth oscillating motion - a very common p5.js technique for anything that should float, bob, or wave without needing complex physics.

🔬 The bullet's height follows a sine wave. What happens if you replace sin() with cos(), or multiply frameCount by a much larger number like 0.5 instead of 0.05?

  this.update = function() {
    this.x -= gameSpeed * 1.2; // Bullet flies slightly faster
    this.y = this.moveOffsetY + sin(frameCount * 0.05 * this.moveSpeedY) * this.moveAmplitudeY;
  };
function Bullet() {
  this.w = 50;
  this.h = 20; // Bullet height
  this.x = width;
  this.y = random(groundY - 120, groundY - 60); // Bullet flies at different heights
  this.type = 'bullet'; // Added type for collision logic
  this.vy = 0; // For simple up/down movement
  this.moveSpeedY = random(0.5, 1.5);
  this.moveAmplitudeY = random(10, 30);
  this.moveOffsetY = this.y;

  this.show = function() {
    fill(100); // Gray for bullet
    noStroke();
    // Bullet shape (rounded rectangle)
    rect(this.x, this.y, this.w, this.h, 0, 0, this.h / 2, this.h / 2);
  };

  this.update = function() {
    this.x -= gameSpeed * 1.2; // Bullet flies slightly faster
    this.y = this.moveOffsetY + sin(frameCount * 0.05 * this.moveSpeedY) * this.moveAmplitudeY;
  };
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Sine wave bobbing this.y = this.moveOffsetY + sin(frameCount * 0.05 * this.moveSpeedY) * this.moveAmplitudeY;

Makes the bullet drift up and down in a smooth wave instead of a straight line

this.y = random(groundY - 120, groundY - 60); // Bullet flies at different heights
Picks a random flying height so bullets sometimes require jumping and sometimes require ducking.
this.moveOffsetY = this.y;
Remembers the bullet's starting height so the sine wave motion oscillates around it rather than drifting away.
rect(this.x, this.y, this.w, this.h, 0, 0, this.h / 2, this.h / 2);
Draws a rectangle with custom per-corner radii - the last two arguments round only the right side, giving a bullet-like capsule shape.
this.x -= gameSpeed * 1.2; // Bullet flies slightly faster
Bullets move 20% faster than the ground and bombs, making them a distinct, harder-to-predict threat.
this.y = this.moveOffsetY + sin(frameCount * 0.05 * this.moveSpeedY) * this.moveAmplitudeY;
sin() combined with frameCount produces smooth up-and-down bobbing motion; moveAmplitudeY controls how far it swings.

checkCollision()

checkCollision() implements Axis-Aligned Bounding Box (AABB) detection, the simplest and fastest way to check if two rectangles overlap - it's used constantly in 2D games for hit detection between players, enemies, and pickups.

🔬 This is classic AABB collision math - all four comparisons must be true for a hit. What happens gameplay-wise if you shrink the hitbox slightly by multiplying chickenW and chickenH by 0.7 before this check, giving the player some forgiveness?

  return (
    chickenX < obstacle.x + obstacle.w &&
    chickenX + chickenW > obstacle.x &&
    chickenY < obstacle.y + obstacle.h &&
    chickenY + chickenH > obstacle.y
  );
function checkCollision(chicken, obstacle) {
  let chickenX, chickenY, chickenW, chickenH;

  // Define chicken's bounding box based on its state
  if (chicken.isDucking) {
    chickenX = chicken.x;
    chickenY = chicken.y + chicken.h / 2; // Ducking chicken's y starts lower
    chickenW = chicken.w * 1.5; // Wider when ducking
    chickenH = chicken.h / 2; // Shorter when ducking
  } else {
    chickenX = chicken.x;
    chickenY = chicken.y;
    chickenW = chicken.w;
    chickenH = chicken.h;
  }

  // Simple AABB (Axis-Aligned Bounding Box) collision detection
  return (
    chickenX < obstacle.x + obstacle.w &&
    chickenX + chickenW > obstacle.x &&
    chickenY < obstacle.y + obstacle.h &&
    chickenY + chickenH > obstacle.y
  );
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Ducking hitbox adjustment if (chicken.isDucking) { ... } else { ... }

Uses a wider, shorter bounding box when the chicken ducks so it can pass under bullets

calculation AABB overlap test chickenX < obstacle.x + obstacle.w && chickenX + chickenW > obstacle.x && ...

Returns true only if the two rectangles overlap on both the x-axis and y-axis simultaneously

if (chicken.isDucking) {
Checks whether the chicken is currently ducking so its hitbox can be swapped for a shorter, wider one.
chickenW = chicken.w * 1.5; // Wider when ducking
The ducking pose visually stretches sideways, so the hitbox is widened to match what's drawn.
chickenX < obstacle.x + obstacle.w &&
Checks the chicken's left edge is to the left of the obstacle's right edge - the first of four conditions needed for overlap.
chickenY + chickenH > obstacle.y
Checks the chicken's bottom edge is below the obstacle's top edge; all four conditions together mean the two boxes overlap.

setup()

setup() runs exactly once when the page loads. It's the right place to size the canvas, initialize starting objects, and create any DOM elements like buttons that will be reused throughout the game.

function setup() {
  createCanvas(windowWidth, windowHeight);
  groundY = height - 50;
  groundX1 = 0;
  groundX2 = width; // Second ground segment for seamless scrolling

  chicken = new Chicken();

  // Create Easy button
  easyButton = createButton('Easy');
  easyButton.position(width / 2 - 120, height / 2 + 80); // Position below instructions
  easyButton.mousePressed(startGameEasy);
  easyButton.hide(); // Hide initially

  // Create Hard button
  hardButton = createButton('Hard');
  hardButton.position(width / 2 + 20, height / 2 + 80); // Position below instructions
  hardButton.mousePressed(startGameHard);
  hardButton.hide(); // Hide initially
}
Line-by-line explanation (7 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window, so the game works full-screen on any device.
groundY = height - 50;
Defines the vertical position of the ground, leaving a 50-pixel-tall strip at the bottom for it.
groundX2 = width; // Second ground segment for seamless scrolling
Places the second ground copy right after the first, off-screen to the right, ready to scroll into view.
chicken = new Chicken();
Creates the chicken object using the Chicken constructor, giving it starting position and physics values.
easyButton = createButton('Easy');
createButton() adds a real HTML button element on top of the canvas - not something drawn with p5 shapes.
easyButton.mousePressed(startGameEasy);
Registers a callback so clicking the button calls startGameEasy() to begin the game at easy speed.
easyButton.hide(); // Hide initially
Buttons start hidden and are only shown by startScreen()/gameOverScreen() when appropriate.

draw()

draw() is p5.js's main animation loop, running continuously at the frame rate. Here it acts purely as a router, keeping each screen's logic cleanly separated into its own function - a pattern worth reusing in your own multi-screen sketches.

function draw() {
  background(220); // Light gray background

  if (gameState === 'start') {
    startScreen();
  } else if (gameState === 'playing') {
    playGame();
  } else if (gameState === 'gameOver') {
    gameOverScreen();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Game state dispatch if (gameState === 'start') { ... } else if (gameState === 'playing') { ... } else if (gameState === 'gameOver') { ... }

Routes each frame to the correct screen-drawing function based on the current game state

background(220); // Light gray background
Clears the previous frame every time draw() runs, which is necessary since shapes aren't automatically erased.
if (gameState === 'start') {
gameState is the single source of truth for which 'screen' of the game is currently showing.
} else if (gameState === 'playing') {
During active gameplay, all the scrolling, spawning, and collision logic lives inside playGame().
} else if (gameState === 'gameOver') {
After a collision, the loop keeps calling gameOverScreen() to display the final score (though noLoop() has actually stopped new frames by this point).

startScreen()

startScreen() is a simple UI function that just draws instructional text and reveals the DOM buttons - it runs every frame while gameState is 'start', which is why the buttons must be shown here rather than just once.

function startScreen() {
  fill(0);
  textSize(32);
  textAlign(CENTER, CENTER);
  text('Chicken Run!', width / 2, height / 2 - 50);
  textSize(24);
  text('Press SPACE / UP arrow or tap to start (Default speed)', width / 2, height / 2);
  // Removed "tap and hold to duck" as touch is now tap-to-jump
  text('Press DOWN arrow to duck (desktop only)', width / 2, height / 2 + 30);

  // Show difficulty buttons on start screen
  easyButton.show();
  hardButton.show();
  // Removed: restartButton.hide(); // Ensure restart button is hidden
}
Line-by-line explanation (3 lines)
textAlign(CENTER, CENTER);
Makes all following text() calls center themselves horizontally and vertically around the given coordinate.
text('Chicken Run!', width / 2, height / 2 - 50);
Draws the game title centered above the middle of the screen.
easyButton.show();
Reveals the previously hidden Easy button so the player can choose it while on the start screen.

gameOverScreen()

gameOverScreen() shows how the final score and buttons are displayed after a collision, letting the player restart directly by choosing a new difficulty rather than needing a separate restart button.

function gameOverScreen() {
  fill(0);
  textSize(48);
  textAlign(CENTER, CENTER);
  text('GAME OVER!', width / 2, height / 2 - 30);
  textSize(32);
  text('Final Score: ' + score, width / 2, height / 2 + 10);

  // Show only difficulty buttons on game over screen
  // Removed: restartButton.show();
  easyButton.show();
  hardButton.show();
}
Line-by-line explanation (3 lines)
text('GAME OVER!', width / 2, height / 2 - 30);
Shows the game-over headline centered on screen.
text('Final Score: ' + score, width / 2, height / 2 + 10);
String concatenation with '+' turns the numeric score into readable text to display.
easyButton.show();
Re-shows the difficulty buttons so the player can immediately choose a difficulty and try again.

startGameEasy()

This function is wired up as the click handler for the Easy button in setup(); it's a thin wrapper that just picks a speed and hands off to the shared resetGame() logic.

function startGameEasy() {
  gameSpeed = easyGameSpeed; // Set game speed to Easy
  resetGame(gameSpeed); // Pass the desired speed to resetGame
  hideAllButtons();
  gameState = 'playing';
  loop();
}
Line-by-line explanation (3 lines)
gameSpeed = easyGameSpeed; // Set game speed to Easy
Sets the active speed variable to the Easy preset before resetting the game state.
resetGame(gameSpeed); // Pass the desired speed to resetGame
Clears score and obstacles and re-creates the chicken, using the chosen speed as the new baseline.
loop();
Resumes p5's draw loop in case it had been stopped by a previous noLoop() call after game over.

startGameHard()

Identical in structure to startGameEasy(), just using the hardGameSpeed constant - a good example of how small variations of the same pattern can drive different game modes.

function startGameHard() {
  gameSpeed = hardGameSpeed; // Set game speed to Hard
  resetGame(gameSpeed); // Pass the desired speed to resetGame
  hideAllButtons();
  gameState = 'playing';
  loop();
}
Line-by-line explanation (1 lines)
gameSpeed = hardGameSpeed; // Set game speed to Hard
Sets the active speed to the Hard preset, making obstacles arrive much faster from the start.

startGameDefault()

This is called from keyPressed() and touchStarted() when the player starts the game without clicking a difficulty button, giving a sensible default experience.

function startGameDefault() { // For starting with Spacebar/Tap when no difficulty is chosen
  gameSpeed = defaultGameSpeed; // Ensure default speed
  resetGame(gameSpeed); // Pass the default speed to resetGame
  hideAllButtons();
  gameState = 'playing';
  loop();
}
Line-by-line explanation (1 lines)
gameSpeed = defaultGameSpeed; // Ensure default speed
Used when the player skips the difficulty buttons and just presses space, tap, or the up arrow.

hideAllButtons()

A tiny helper that centralizes button-hiding logic so all three 'start game' functions can call one line instead of repeating both hide() calls.

function hideAllButtons() {
  // Removed: restartButton.hide();
  easyButton.hide();
  hardButton.hide();
}
Line-by-line explanation (2 lines)
easyButton.hide();
Hides the Easy button once gameplay starts, since it's only needed on the start and game-over screens.
hardButton.hide();
Hides the Hard button for the same reason, keeping the canvas clear during play.

resetGame()

resetGame() is shared by all three 'start' functions so the reset logic only needs to be written once, following the DRY (Don't Repeat Yourself) principle.

function resetGame(initialSpeed) { // Added initialSpeed parameter
  score = 0;
  obstacles = [];
  gameSpeed = initialSpeed; // Use the provided initial speed
  chicken = new Chicken(); // Re-initialize chicken
  groundX1 = 0;
  groundX2 = width;
}
Line-by-line explanation (3 lines)
score = 0;
Resets the score counter back to zero for a fresh run.
obstacles = [];
Empties the obstacles array completely, removing any bombs or bullets left from a previous game.
chicken = new Chicken(); // Re-initialize chicken
Creates a brand new Chicken object, which resets its position and physics state cleanly.

playGame()

playGame() is the heart of the game loop - it handles scrolling, spawning, physics updates, collision detection, cleanup, and scoring all in one function. Studying how these pieces interleave each frame is key to understanding real-time game programming.

🔬 This controls how often obstacles spawn. What happens if you change the map() output range from (90, 40) to (150, 100), making spawns much rarer overall?

  if (frameCount % int(map(gameSpeed, easyGameSpeed, hardGameSpeed * 1.5, 90, 40)) === 0) {
    if (random() < 0.6) { // 60% chance of Bomb
      obstacles.push(new Bomb());
    } else { // 40% chance of Bullet
      obstacles.push(new Bullet());
    }
  }
function playGame() {
  // Update game speed based on score (capped)
  // This will scale the chosen difficulty (easy, default, or hard) upwards
  gameSpeed = constrain(gameSpeed + score / 100, easyGameSpeed, hardGameSpeed * 1.5); // Cap hard mode scaling higher

  // --- Ground ---
  fill(100); // Darker gray for ground
  noStroke();
  rect(groundX1, groundY, width, 50);
  rect(groundX2, groundY, width, 50);

  groundX1 -= gameSpeed;
  groundX2 -= gameSpeed;

  if (groundX1 <= -width) groundX1 = width;
  if (groundX2 <= -width) groundX2 = width;

  // --- Chicken ---
  chicken.update();
  chicken.show();

  // --- Obstacles ---
  // Spawn obstacles
  // Spawns more frequently as speed increases (based on current gameSpeed)
  if (frameCount % int(map(gameSpeed, easyGameSpeed, hardGameSpeed * 1.5, 90, 40)) === 0) {
    if (random() < 0.6) { // 60% chance of Bomb
      obstacles.push(new Bomb());
    } else { // 40% chance of Bullet
      obstacles.push(new Bullet());
    }
  }

  let gameOverTriggered = false; // Flag to trigger game over after obstacle loop

  for (let i = obstacles.length - 1; i >= 0; i--) {
    let obstacle = obstacles[i];

    // Only update active bombs, and all bullets
    if ((obstacle.type === 'bomb' && obstacle.state === 'active') || obstacle.type === 'bullet') {
      obstacle.update();
    }
    obstacle.show();

    // Check for collision *only if the obstacle is active*
    if (obstacle.state === 'active' && checkCollision(chicken, obstacle)) {
      if (obstacle.type === 'bomb') {
        obstacle.state = 'exploding'; // Start explosion animation for visual feedback
        obstacle.explosionFrameCount = 0;
        gameOverTriggered = true; // IMMEDIATE GAME OVER
      } else if (obstacle.type === 'bullet') {
        gameOverTriggered = true; // IMMEDIATE GAME OVER
      }
      break; // Exit obstacle loop immediately if collision found
    }

    // Remove obstacles that are off-screen OR have finished exploding (if they somehow didn't cause game over)
    if (obstacle.x + obstacle.w < 0 || obstacle.state === 'exploded') {
      obstacles.splice(i, 1);
      score++; // Increase score for dodging/exploding an obstacle
    }
  }

  // Trigger game over *after* the obstacle loop
  if (gameOverTriggered) {
    gameState = 'gameOver';
    noLoop(); // Stop the draw loop
    return; // Exit playGame()
  }

  // --- Score ---
  fill(0);
  textSize(24);
  textAlign(RIGHT, TOP);
  text('Score: ' + score, width - 10, 10);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Ground scroll wrap-around if (groundX1 <= -width) groundX1 = width;

Snaps a ground segment back to the right edge once it's fully scrolled off-screen, creating an endless loop

conditional Obstacle spawn timing if (frameCount % int(map(gameSpeed, easyGameSpeed, hardGameSpeed * 1.5, 90, 40)) === 0) {

Spawns a new obstacle at an interval that shrinks as gameSpeed increases, so faster games feel busier

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

Iterates backwards through all obstacles so items can be safely removed with splice() without skipping elements

conditional Collision handling if (obstacle.state === 'active' && checkCollision(chicken, obstacle)) {

Detects a hit, starts an explosion animation for bombs, and flags that the game should end

conditional Offscreen/finished cleanup if (obstacle.x + obstacle.w < 0 || obstacle.state === 'exploded') {

Removes obstacles that have scrolled past the screen or finished exploding, awarding a point for each

gameSpeed = constrain(gameSpeed + score / 100, easyGameSpeed, hardGameSpeed * 1.5); // Cap hard mode scaling higher
The game gradually speeds up as score rises, but constrain() prevents it from ever going below the easy speed or above 1.5x hard speed.
groundX1 -= gameSpeed;
Scrolls the first ground rectangle leftward at the current game speed, making the ground appear to move.
if (groundX1 <= -width) groundX1 = width;
Once a ground segment has fully scrolled off the left edge, it's teleported back to the right edge, so two segments alternate endlessly.
if (frameCount % int(map(gameSpeed, easyGameSpeed, hardGameSpeed * 1.5, 90, 40)) === 0) {
map() converts the current speed into a spawn interval between 90 and 40 frames; the modulo check fires exactly when frameCount reaches a multiple of that interval.
for (let i = obstacles.length - 1; i >= 0; i--) {
Looping backwards is essential here because obstacles.splice() removes items from the array mid-loop - looping forward would skip the next item after a removal.
if (obstacle.state === 'active' && checkCollision(chicken, obstacle)) {
Only active (not already exploding) obstacles can trigger a new collision, preventing double game-overs from the same bomb.
break; // Exit obstacle loop immediately if collision found
Stops checking further obstacles the instant a collision is found, since the game is about to end anyway.
obstacles.splice(i, 1);
Removes exactly one element at index i from the obstacles array, deleting obstacles that have scrolled off-screen or finished exploding.
noLoop(); // Stop the draw loop
Completely halts p5's draw() calls, freezing the game-over screen in place until loop() is called again by a restart.

keyPressed()

keyPressed() is a p5.js built-in callback that fires once per key press. Checking gameState first ensures the same key does different things depending on which screen is showing.

function keyPressed() {
  if (gameState === 'start' && (key === ' ' || keyCode === UP_ARROW)) {
    startGameDefault();
  } else if (gameState === 'playing' && (key === ' ' || keyCode === UP_ARROW)) {
    chicken.jump();
  } else if (gameState === 'playing' && keyCode === DOWN_ARROW) {
    chicken.duck();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Start game from keyboard if (gameState === 'start' && (key === ' ' || keyCode === UP_ARROW)) {

Lets the player begin the game with default speed by pressing space or the up arrow

conditional Jump input } else if (gameState === 'playing' && (key === ' ' || keyCode === UP_ARROW)) {

Triggers the chicken's jump while the game is actively being played

if (gameState === 'start' && (key === ' ' || keyCode === UP_ARROW)) {
Checks both the game state and which key was pressed before allowing the game to start.
chicken.jump();
Calls the chicken's own jump() method, which only actually jumps if it isn't already mid-air.
chicken.duck();
Calls duck(), which sets isDucking to true as long as the chicken isn't jumping.

keyReleased()

keyReleased() pairs with keyPressed() to implement 'hold down arrow to duck' - the duck lasts exactly as long as the key is held, which requires listening for both press and release events.

function keyReleased() {
  if (gameState === 'playing' && keyCode === DOWN_ARROW) {
    chicken.stand();
  }
}
Line-by-line explanation (2 lines)
if (gameState === 'playing' && keyCode === DOWN_ARROW) {
Only reacts to releasing the down arrow, and only while the game is actively playing.
chicken.stand();
Sets isDucking back to false so the chicken pops back up to standing height.

touchStarted()

touchStarted() shows a common mobile-web challenge: canvas taps and DOM button taps can conflict, so the code manually checks tap coordinates against each button's screen position before deciding to start the game.

function touchStarted() {
  if (gameState === 'start') {
    // Check if the touch is within the bounds of the Easy button
    if (easyButton.elt.offsetParent) { // Check if button is visible
      let easyBtnX = easyButton.position().x;
      let easyBtnY = easyButton.position().y;
      let easyBtnW = easyButton.width;
      let easyBtnH = easyButton.height;
      if (mouseX >= easyBtnX && mouseX <= easyBtnX + easyBtnW &&
          mouseY >= easyBtnY && mouseY <= easyBtnY + easyBtnH) {
        // Touch was on easy button, let its handler take over
        return false;
      }
    }

    // Check if the touch is within the bounds of the Hard button
    if (hardButton.elt.offsetParent) { // Check if button is visible
      let hardBtnX = hardButton.position().x;
      let hardBtnY = hardButton.position().y;
      let hardBtnW = hardButton.width;
      let hardBtnH = hardButton.height;
      if (mouseX >= hardBtnX && mouseX <= hardBtnX + hardBtnW &&
          mouseY >= easyBtnY && mouseY <= easyBtnY + easyBtnH) {
        // Touch was on hard button, let its handler take over
        return false;
      }
    }

    // If touch was not on a button, start with default speed
    startGameDefault();
  } else if (gameState === 'playing') {
    // On touch devices, a tap is a jump (like the Google Dino game)
    if (!chicken.isJumping) {
      chicken.jump();
    }
  }
  return false; // Prevent default browser behavior (scrolling)
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Manual button hit-testing if (mouseX >= easyBtnX && mouseX <= easyBtnX + easyBtnW && mouseY >= easyBtnY && mouseY <= easyBtnY + easyBtnH) {

Manually checks whether a tap landed inside the Easy button's rectangle so the tap doesn't also trigger startGameDefault()

conditional Tap to jump if (!chicken.isJumping) {

Makes any tap during gameplay act as a jump, mirroring the desktop spacebar control

if (easyButton.elt.offsetParent) { // Check if button is visible
offsetParent is null when an element is hidden via CSS display:none, so this confirms the button is actually visible before testing taps against it.
let easyBtnX = easyButton.position().x;
Gets the button's on-screen pixel position so it can be compared against the touch/mouse coordinates.
startGameDefault();
If the tap wasn't on either button, it's treated as 'just start the game' with default speed.
if (!chicken.isJumping) {
Prevents spamming jump() while already airborne, though jump() itself already guards against this too.
return false; // Prevent default browser behavior (scrolling)
Stops the browser's default touch behavior (like page scrolling or zooming) from interfering with the game.

touchEnded()

This function is mostly commented-out placeholder code documenting a feature (tap-and-hold to duck) that wasn't built. It's a useful example of how real projects often leave notes about future improvements directly in the code.

function touchEnded() {
  // Currently, touchEnded doesn't trigger stand() because ducking by touch isn't implemented.
  // The chicken will stand automatically once it lands from a jump.
  // If you want a ducking mechanism on touch, we'd need to differentiate between tap and tap-and-hold.
  // For now, it's tap-to-jump.
  // if (gameState === 'playing') {
  //   chicken.stand(); // Stop ducking when touch ends (if ducking was implemented by touch)
  // }
  return false; // Prevent default browser behavior (scrolling)
}
Line-by-line explanation (1 lines)
return false; // Prevent default browser behavior (scrolling)
This is the only active line - it stops the browser's default touch-end behavior, since the actual duck-on-touch feature was never implemented (see the comments).

windowResized()

windowResized() is a p5.js callback fired automatically whenever the browser window size changes, letting responsive sketches recalculate layout-dependent values like groundY and button positions.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  groundY = height - 50;
  chicken.y = groundY - chicken.h; // Adjust chicken position
  // Removed: restartButton.position(width / 2 - 60, height / 2 + 50);
  easyButton.position(width / 2 - 120, height / 2 + 80); // Reposition difficulty buttons
  hardButton.position(width / 2 + 20, height / 2 + 80);  // Reposition difficulty buttons
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window whenever it changes size, e.g. rotating a phone.
groundY = height - 50;
Recalculates the ground's vertical position for the new canvas height.
chicken.y = groundY - chicken.h; // Adjust chicken position
Snaps the chicken back onto the newly repositioned ground so it doesn't appear to float or sink after a resize.

📦 Key Variables

chicken object

Holds the player-controlled Chicken instance, including its position, physics state, and drawing/update methods.

let chicken;
obstacles array

Stores all currently active Bomb and Bullet objects on screen; items are added when spawned and removed via splice() when off-screen or finished.

let obstacles = [];
groundY number

The fixed y-coordinate where the ground begins, used to position the chicken and obstacles consistently.

let groundY;
groundX1 number

The x-position of the first scrolling ground rectangle; wraps back to width when it scrolls off-screen.

let groundX1;
groundX2 number

The x-position of the second scrolling ground rectangle, offset from groundX1 to create seamless looping scenery.

let groundX2;
score number

Counts how many obstacles the player has successfully dodged; displayed on-screen and used to scale up gameSpeed.

let score = 0;
gameState string

Tracks which 'screen' the game is on ('start', 'playing', or 'gameOver') and controls which function draw() calls each frame.

let gameState = 'start';
easyButton object

Reference to the DOM button element for starting the game in Easy mode.

let easyButton;
hardButton object

Reference to the DOM button element for starting the game in Hard mode.

let hardButton;
easyGameSpeed number

The baseline scroll/obstacle speed used when the player selects Easy mode.

let easyGameSpeed = 3;
defaultGameSpeed number

The baseline speed used if the player starts the game without choosing a difficulty.

let defaultGameSpeed = 5;
hardGameSpeed number

The baseline speed used when the player selects Hard mode; also caps how high gameSpeed can scale up.

let hardGameSpeed = 7;
gameSpeed number

The current live scroll/obstacle speed, which starts at one of the difficulty presets and gradually increases with score.

let gameSpeed = defaultGameSpeed;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG touchStarted()

The Hard button's hit-test mistakenly compares mouseY against easyBtnY/easyBtnH instead of hardBtnY/hardBtnH: `mouseY >= easyBtnY && mouseY <= easyBtnY + easyBtnH`. Since both buttons sit at the same y-position this happens to work visually, but if their vertical positions ever differ, tapping the Hard button would fail to register correctly.

💡 Change the two lines inside the hard button check to use hardBtnY and hardBtnH instead of easyBtnY/easyBtnH so the hit-test is self-consistent.

BUG Chicken collision hitbox

The chicken's bounding box in checkCollision() doesn't account for the beak, comb, or legs that extend beyond this.w and this.h, so visually the chicken can look like it touched an obstacle without the game registering a hit (or vice versa).

💡 Either tighten the drawn shape to fit within this.w/this.h, or slightly enlarge the hitbox to better match the visible sprite for more intuitive collisions.

PERFORMANCE playGame()

map() and int() are recalculated every single frame just to check the obstacle spawn timing, even though gameSpeed only changes slowly (once per frame via score/100).

💡 This is cheap enough at 60fps to not matter in practice, but for clarity it could be extracted into a named variable like `spawnInterval` computed once per frame, making the modulo condition easier to read.

STYLE Global scope

Magic numbers like 40 (chicken height), 50 (ground thickness), and 120/60 (bullet height range) are scattered across multiple functions, making them hard to tune consistently.

💡 Extract these into named constants near the top of the file (e.g. CHICKEN_HEIGHT, GROUND_THICKNESS) so future tweaks only require changing one place.

FEATURE Overall game

There's no sound feedback for jumping, ducking, scoring, or explosions, which reduces the game-feel impact of key moments.

💡 Use the p5.sound library to add short jump/score/explosion sound effects, triggered from chicken.jump() and the collision-handling block in playGame().

🔄 Code Flow

Code flow showing chicken, bomb, bullet, checkcollision, setup, draw, startscreen, gameoverscreen, startgameeasy, startgamehard, startgamedefault, hideallbuttons, resetgame, playgame, keypressed, keyreleased, touchstarted, touchended, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> drawstate[draw-state-switch] drawstate --> startscreen[startScreen] drawstate --> playgame[playGame] drawstate --> gameoverscreen[gameOverScreen] click setup href "#fn-setup" click draw href "#fn-draw" click startscreen href "#fn-startscreen" click playgame href "#fn-playgame" click gameoverscreen href "#fn-gameoverscreen" playgame --> playgame-scroll[playgame-scroll-wrap] playgame --> playgame-spawn[playgame-spawn-check] playgame --> playgame-obstacles[playgame-obstacle-loop] playgame --> playgame-collision[playgame-collision-check] playgame --> playgame-cleanup[playgame-cleanup] click playgame-scroll href "#sub-playgame-scroll-wrap" click playgame-spawn href "#sub-playgame-spawn-check" click playgame-obstacles href "#sub-playgame-obstacle-loop" click playgame-collision href "#sub-playgame-collision-check" click playgame-cleanup href "#sub-playgame-cleanup" playgame-obstacles --> bullet[bBullet()] bullet --> bullet-sine[bullet-sine-motion] click bullet href "#fn-bullet" click bullet-sine href "#sub-bullet-sine-motion" playgame-collision --> collision[checkCollision()] collision --> collision-duck[collision-duck-box] collision --> collision-aabb[collision-aabb-check] click collision href "#fn-checkcollision" click collision-duck href "#sub-collision-duck-box" click collision-aabb href "#sub-collision-aabb-check" chicken[chicken()] --> chicken-show[chicken-show-branch] chicken --> chicken-jump[chicken-jump-guard] chicken --> chicken-ground[chicken-ground-state] click chicken href "#fn-chicken" click chicken-show href "#sub-chicken-show-branch" click chicken-jump href "#sub-chicken-jump-guard" click chicken-ground href "#sub-chicken-ground-state" bomb[bomb()] --> bomb-show[bomb-show-state] bomb --> bomb-update[bomb-update-state] click bomb href "#fn-bomb" click bomb-show href "#sub-bomb-show-state" click bomb-update href "#sub-bomb-update-state" keypressed[keyPressed()] --> keypressed-start[keypressed-start] keypressed --> keypressed-jump[keypressed-jump] click keypressed href "#fn-keypressed" click keypressed-start href "#sub-keypressed-start" click keypressed-jump href "#sub-keypressed-jump" touchstarted[touchStarted()] --> touch-button[touch-button-hit-test] touchstarted --> touch-jump[touch-jump] click touchstarted href "#fn-touchstarted" click touch-button href "#sub-touch-button-hit-test" click touch-jump href "#sub-touch-jump" windowresized[windowResized()] --> windowresize[Window Resize Logic] click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What does the Offline game that works offline chicken run sketch create visually?

The sketch visually creates a cute cartoon chicken running across a scrolling ground, avoiding obstacles. The chicken changes shape when ducking and features a colorful design with a yellow body and orange beak, set against a simple background.

How can users interact with the chicken run game?

Users can interact with the game by controlling the chicken's actions through keyboard inputs to jump or duck, depending on the obstacles approaching. The game also offers difficulty selection between easy and hard modes to enhance the challenge.

What creative coding technique does the chicken run sketch demonstrate?

This sketch demonstrates the use of object-oriented programming in p5.js by defining a Chicken object that encapsulates properties and behaviors. It also incorporates game state management to handle transitions between starting, playing, and game-over states.

How can someone recreate a similar jumping and ducking effect in p5.js?

To recreate a similar effect in p5.js, you can use keyboard events to trigger changes in an object's properties for jumping and ducking actions. Utilize gravity and velocity to simulate realistic movement, and implement a scrolling background to enhance the sense of motion.

Preview

Offline game that works offline chicken run - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Offline game that works offline chicken run - Code flow showing chicken, bomb, bullet, checkcollision, setup, draw, startscreen, gameoverscreen, startgameeasy, startgamehard, startgamedefault, hideallbuttons, resetgame, playgame, keypressed, keyreleased, touchstarted, touchended, windowresized
Code Flow Diagram