Cowboy gunslinger!

This sketch creates an action-packed cowboy shooter game where you defend yourself against waves of incoming outlaws. You aim and shoot at targets moving across the screen while they shoot back—lose three lives and it's game over.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make targets spawn faster — Lower spawn intervals create a more chaotic, challenging game with targets appearing more frequently.
  2. Start with more lives — Increasing starting lives makes the game more forgiving, giving you more chances to learn and survive longer.
  3. Make targets easier to hit — Increasing the hitbox from half the target size to the full size makes hits much more forgiving.
  4. Make targets move slower — Lower movement speeds give you more time to aim and react, reducing difficulty.
  5. Change the sky color — The background color instantly transforms the visual mood—try orange for sunset or dark blue for night.
  6. Make targets shoot from farther away — Widening the vertical range where targets can shoot increases danger and difficulty.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a fully playable cowboy gunslinger game where you control a cowboy standing on the ground and tap anywhere on screen to shoot at incoming targets (sheriffs and bounty hunters). The targets move toward you, aim their guns, and fire back—you must destroy them before they reach you or shoot you down. The visual core combines simple silhouettes drawn with basic shapes (rectangles and ellipses), touch/mouse input handling, and dynamic rotation to aim the gun at your tap location.

The code is structured around a game loop in draw() that updates player position, spawns and moves targets, handles collisions, and manages the game state (score, lives, game over). By studying it you will learn how to build interactive games in p5.js: tracking multiple moving objects in an array, detecting collisions between shapes, responding to touch input, and managing game flow with conditional logic.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and initializes game variables (score = 0, lives = 3, empty targets array).
  2. Every frame, draw() clears the canvas, draws the blue sky background and brown ground, and positions the cowboy player at the bottom based on mouse/touch x-position (constrained to stay on screen).
  3. The gun is drawn above the cowboy and rotated to aim at wherever you tap or move your mouse using atan2() to calculate the angle.
  4. Targets spawn at random x-positions on the ground at regular intervals (every 60 frames by default) and are stored in the targets array as objects.
  5. Each target moves upward and horizontally toward the player, and when it enters a certain vertical range it begins shooting at you at random intervals—if it hits you, you lose a life.
  6. When you tap the screen, shoot() checks if the tap hit any active target by measuring distance—if you hit, the target dies and score increases; if you miss, you lose a life.
  7. The game continues until lives reach zero, at which point the screen shows 'DEAD!' and your final score, and tapping restarts the game.

🎓 Concepts You'll Learn

Game state managementArray iteration and object storageCollision detection (distance and rectangular overlap)Touch and mouse input handlingTrigonometry for aiming (atan2)Transformations (translate, rotate, push/pop)Conditional game flow

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It prepares the canvas, text styling, and initial game state. Any variables you want initialized before the game begins should be set here.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textSize(24);
  textAlign(CENTER, CENTER);
  noStroke();
  restartGame(); // Initialize game state
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the game responsive to screen size.
textSize(24);
Sets the font size to 24 pixels for score and lives display.
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically when drawn, making score/lives labels appear properly positioned.
noStroke();
Disables outlines on all shapes by default, giving the silhouettes a clean filled look.
restartGame();
Calls the restartGame() function to initialize all game variables (score, lives, targets array) so the game is ready to play.

draw()

draw() is the game loop—it runs 60 times per second and handles everything visible: clearing the canvas, positioning the player, updating targets, checking collisions, and displaying the UI. The if (!gameOver) check is the key to controlling game flow: when lives run out, gameOver flips to true and the game over screen appears instead of active gameplay.

🔬 This checks if enough frames have passed to spawn a new target. What happens if you add another target spawn right after this one? Try copy-pasting spawnTarget(); again inside the if-block to spawn two targets at once.

    if (frameCount - lastTargetSpawnTime > targetSpawnInterval) {
      spawnTarget();
      lastTargetSpawnTime = frameCount;
    }
function draw() {
  background(100, 150, 200); // Blue sky background

  if (!gameOver) {
    // --- Ground ---
    fill(150, 100, 50); // Brown ground
    rectMode(CORNERS); // https://p5js.org/reference/#/p5/rectMode
    rect(0, height * 0.8, width, height); // https://p5js.org/reference/#/p5/rect

    // --- Player Position ---
    playerY = height * 0.9; // Cowboy stands on the ground
    // Use touch or mouse input for horizontal aiming
    playerX = constrain(
      (touches.length > 0 ? touches[0].x : mouseX),
      width * 0.1, // Don't let cowboy go too far left
      width * 0.9  // Don't let cowboy go too far right
    );

    // --- Draw Cowboy (simple silhouette) ---
    drawCowboy(playerX, playerY);

    // --- Draw Gun (simple shape) ---
    drawGun(playerX, playerY);

    // --- Spawn Targets ---
    if (frameCount - lastTargetSpawnTime > targetSpawnInterval) {
      spawnTarget();
      lastTargetSpawnTime = frameCount;
    }

    // --- Update and Draw Targets ---
    drawTargets();

    // --- Display Score and Lives ---
    fill(0);
    text(`Score: ${score}`, width / 2, 30);
    text(`Lives: ${lives}`, width / 2, 60);

    // --- Player's Shot Effect (visual feedback) ---
    if (shotEffectAlpha > 0) {
      fill(255, 255, 0, shotEffectAlpha); // Yellow flash
      rectMode(CENTER);
      rect(width / 2, height / 2, width, height);
      shotEffectAlpha -= 20; // Fade out
    }
  } else {
    // --- Game Over Screen ---
    fill(255, 0, 0);
    textSize(48);
    text("DEAD!", width / 2, height / 2 - 50); // Changed to "DEAD!"
    fill(0);
    textSize(32);
    text(`Final Score: ${score}`, width / 2, height / 2 + 10);
    text("Tap to Restart", width / 2, height / 2 + 60);
  }
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

calculation Player Position Update playerX = constrain( (touches.length > 0 ? touches[0].x : mouseX), width * 0.1, width * 0.9 );

Updates the player's horizontal position based on touch or mouse input, while keeping them constrained within safe bounds (10%-90% of screen width).

conditional Target Spawn Check if (frameCount - lastTargetSpawnTime > targetSpawnInterval) { spawnTarget(); lastTargetSpawnTime = frameCount; }

Spawns a new target every targetSpawnInterval frames by checking if enough time has passed since the last spawn.

conditional Shot Effect Fade if (shotEffectAlpha > 0) { fill(255, 255, 0, shotEffectAlpha); // Yellow flash rectMode(CENTER); rect(width / 2, height / 2, width, height); shotEffectAlpha -= 20; // Fade out }

Creates a brief yellow flash effect when you shoot, fading away by reducing alpha each frame.

background(100, 150, 200); // Blue sky background
Clears the entire canvas with a light blue color every frame, creating the sky and erasing the previous frame.
if (!gameOver) {
Only runs the active game logic if gameOver is false; if true, the else block shows the game over screen instead.
fill(150, 100, 50); // Brown ground
Sets the fill color to brown for drawing the ground.
rect(0, height * 0.8, width, height);
Draws a brown rectangle from the 80% vertical mark to the bottom of the canvas, creating the ground where the cowboy stands.
playerY = height * 0.9; // Cowboy stands on the ground
Positions the cowboy vertically at 90% down the screen (on the ground).
playerX = constrain( (touches.length > 0 ? touches[0].x : mouseX), width * 0.1, width * 0.9 );
Updates the cowboy's horizontal position to follow your touch or mouse, but constrains it to stay between 10% and 90% of screen width so the cowboy doesn't move off-screen.
drawCowboy(playerX, playerY);
Calls the drawCowboy() function to draw the cowboy silhouette at the current player position.
drawGun(playerX, playerY);
Calls the drawGun() function to draw and rotate the gun based on where you're aiming.
if (frameCount - lastTargetSpawnTime > targetSpawnInterval) {
Checks if enough frames have passed since the last target spawn; if so, it's time to spawn a new target.
spawnTarget();
Creates a new target object and adds it to the targets array.
lastTargetSpawnTime = frameCount;
Records the current frame number as the time of the most recent spawn, so the next spawn check can compare against it.
drawTargets();
Calls the drawTargets() function to update and draw all active targets, handle their movement and shooting, and check collisions.
text(`Score: ${score}`, width / 2, 30);
Displays the current score at the top center of the screen.
text(`Lives: ${lives}`, width / 2, 60);
Displays the current number of lives just below the score.
if (shotEffectAlpha > 0) {
Only draws the shot flash effect if shotEffectAlpha is greater than 0 (meaning a shot was just fired).
fill(255, 255, 0, shotEffectAlpha); // Yellow flash
Sets the fill color to yellow with transparency based on shotEffectAlpha, creating a fading flash effect.
rect(width / 2, height / 2, width, height);
Draws a full-screen rectangle centered on the canvas with the fading yellow color.
shotEffectAlpha -= 20; // Fade out
Reduces the alpha value each frame so the yellow flash fades away quickly.
text("DEAD!", width / 2, height / 2 - 50);
When the game is over, displays 'DEAD!' in red at the center of the screen.
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Displays your final score below the 'DEAD!' message so you can see how you did.

drawCowboy(x, y)

drawCowboy() is a helper function that draws the cowboy silhouette using basic shapes (rectangles and an ellipse). It takes x and y parameters so the cowboy can be drawn at any position on screen. Each shape is positioned relative to y using subtraction, which moves them upward to create the stacked body, head, and hat.

function drawCowboy(x, y) {
  fill(50, 25, 0); // Dark brown
  rectMode(CENTER);
  // Body
  rect(x, y - 30, 60, 80);
  // Head
  ellipse(x, y - 90, 50);
  // Hat
  rect(x, y - 120, 80, 20); // Brim
  rect(x, y - 135, 40, 30); // Crown
}
Line-by-line explanation (6 lines)
fill(50, 25, 0); // Dark brown
Sets the fill color to dark brown so all the cowboy's body parts are drawn in that color.
rectMode(CENTER);
Changes rectangle drawing mode so that rect() positions by the center of the rectangle rather than the top-left corner, making it easier to position body parts accurately.
rect(x, y - 30, 60, 80);
Draws the cowboy's body as a rectangle 60 pixels wide and 80 pixels tall, centered 30 pixels above the y position (which is at the ground).
ellipse(x, y - 90, 50);
Draws the cowboy's head as a circle 50 pixels in diameter, positioned 90 pixels above the ground.
rect(x, y - 120, 80, 20); // Brim
Draws the wide brim of the cowboy hat as a rectangle 80 pixels wide and 20 pixels tall, positioned 120 pixels above the ground.
rect(x, y - 135, 40, 30); // Crown
Draws the crown (top part) of the cowboy hat as a rectangle 40 pixels wide and 30 pixels tall, positioned 135 pixels above the ground, creating the classic cowboy hat silhouette.

drawGun(playerX, playerY)

drawGun() demonstrates two crucial p5.js techniques: using atan2() to calculate angles toward a target, and using push/translate/rotate to transform the canvas coordinate system. The gun doesn't actually move—the canvas rotates around it, making the barrel point in the right direction. This is a core technique for rotating objects in any direction.

🔬 These three lines set up a coordinate system where the gun points toward your aim. What happens if you remove the rotate(gunAngle) line? The gun will no longer rotate—try it and move your mouse around to see the effect.

  push(); // https://p5js.org/reference/#/p5/push
  translate(gunX, gunY); // Move origin to gun position: https://p5js.org/reference/#/p5/translate
  rotate(gunAngle); // Rotate canvas for gun aiming: https://p5js.org/reference/#/p5/rotate
function drawGun(playerX, playerY) {
  // Position the gun relative to the cowboy
  gunX = playerX + 30;
  gunY = playerY - 50;

  // Calculate gun angle based on touch/mouse position relative to the gun
  // This makes the gun point towards where you tap/click
  let aimX = touches.length > 0 ? touches[0].x : mouseX;
  let aimY = touches.length > 0 ? touches[0].y : mouseY;
  gunAngle = atan2(aimY - gunY, aimX - gunX); // https://p5js.org/reference/#/p5/atan2

  push(); // https://p5js.org/reference/#/p5/push
  translate(gunX, gunY); // Move origin to gun position: https://p5js.org/reference/#/p5/translate
  rotate(gunAngle); // Rotate canvas for gun aiming: https://p5js.org/reference/#/p5/rotate

  fill(70); // Dark gray for gun
  rect(0, 0, 80, 20, 5); // Barrel
  rect(60, 0, 20, 30, 5); // Handle
  fill(100);
  ellipse(0, 0, 10); // Sight

  pop(); // https://p5js.org/reference/#/p5/pop
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Gun Angle Calculation gunAngle = atan2(aimY - gunY, aimX - gunX);

Uses atan2() to calculate the angle between the gun position and the touch/mouse position, allowing the gun to point at where you're aiming.

calculation Transform and Rotate push(); translate(gunX, gunY); rotate(gunAngle);

Saves the canvas state, moves the origin to the gun position, and rotates the canvas so the gun barrel points in the correct direction before drawing.

gunX = playerX + 30;
Positions the gun 30 pixels to the right of the cowboy's center so it appears at his hip.
gunY = playerY - 50;
Positions the gun 50 pixels above the ground, roughly at the cowboy's waist level.
let aimX = touches.length > 0 ? touches[0].x : mouseX;
Reads the x-coordinate of a touch if the screen is being touched, otherwise reads the mouse x-position, so aiming works on both touch and mouse devices.
let aimY = touches.length > 0 ? touches[0].y : mouseY;
Reads the y-coordinate of a touch if the screen is being touched, otherwise reads the mouse y-position.
gunAngle = atan2(aimY - gunY, aimX - gunX);
Uses atan2() to calculate the angle from the gun position to your touch/mouse position. The result is stored in gunAngle so the gun barrel points at your aim target.
push();
Saves the current canvas state (position, rotation, etc.) so that transformations below only affect the gun and don't mess up the rest of the drawing.
translate(gunX, gunY);
Moves the origin (0, 0 point) of the canvas to the gun's position, so all drawing after this happens relative to the gun.
rotate(gunAngle);
Rotates the canvas by gunAngle radians, so the gun barrel points in the direction of your aim.
fill(70); // Dark gray for gun
Sets the fill color to dark gray for drawing the gun parts.
rect(0, 0, 80, 20, 5); // Barrel
Draws the gun barrel as a rounded rectangle 80 pixels long and 20 pixels wide, starting at the origin (which is now at gunX, gunY due to translate).
rect(60, 0, 20, 30, 5); // Handle
Draws the gun handle as a rounded rectangle positioned 60 pixels along the barrel, creating the grip of the gun.
ellipse(0, 0, 10); // Sight
Draws a small circle at the very tip of the gun to represent a sight, helping visualize where the gun is pointing.
pop();
Restores the canvas to its previous state before the push(), undoing all the translations and rotations so subsequent drawing is unaffected.

drawTargetGun(x, y, angle, targetSize, isFiring)

drawTargetGun() is similar to drawGun() but scales all dimensions based on targetSize so guns appear proportionally correct on different-sized targets. The isFiring parameter enables the muzzle flash effect, which only appears when the target shoots—a nice visual detail that helps the player understand what's happening.

function drawTargetGun(x, y, angle, targetSize, isFiring) {
  push();
  translate(x, y);
  rotate(angle);

  fill(50); // Darker gray for target gun
  rect(0, 0, targetSize * 0.4, targetSize * 0.1, 2); // Barrel
  rect(targetSize * 0.3, 0, targetSize * 0.1, targetSize * 0.15, 2); // Handle

  // Muzzle flash when firing
  if (isFiring) {
    fill(255, 200, 0, 150); // Orange-yellow
    ellipse(targetSize * 0.5, 0, targetSize * 0.2, targetSize * 0.1);
  }

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

🔧 Subcomponents:

conditional Muzzle Flash Effect if (isFiring) { fill(255, 200, 0, 150); // Orange-yellow ellipse(targetSize * 0.5, 0, targetSize * 0.2, targetSize * 0.1); }

Draws an orange-yellow flash at the end of the gun barrel when the target is shooting, creating visual feedback that they've fired.

push();
Saves the current canvas state so transformations only affect this gun.
translate(x, y);
Moves the origin to the target gun's position.
rotate(angle);
Rotates the canvas by the target gun's angle, making it point toward the player.
fill(50); // Darker gray for target gun
Sets the fill color to a darker gray than the player's gun to distinguish target guns visually.
rect(0, 0, targetSize * 0.4, targetSize * 0.1, 2); // Barrel
Draws the target gun barrel as a rectangle whose size scales with the target size, keeping guns proportional to their bodies.
rect(targetSize * 0.3, 0, targetSize * 0.1, targetSize * 0.15, 2); // Handle
Draws the target gun handle, also scaled to the target size.
if (isFiring) {
Only draws the muzzle flash if the target is currently shooting (isFiring is true).
fill(255, 200, 0, 150); // Orange-yellow
Sets the fill to orange-yellow with transparency for the muzzle flash effect.
ellipse(targetSize * 0.5, 0, targetSize * 0.2, targetSize * 0.1);
Draws an ellipse at the tip of the gun barrel to represent a flash of light from the gunshot.
pop();
Restores the canvas state, undoing all transformations.

drawPerson(x, y, size)

drawPerson() is the function that draws targets as simple human silhouettes. All dimensions are scaled based on the size parameter, so targets can be any size and still maintain proper proportions. Notice how it uses translate and local coordinates (relative to x, y) to make the code cleaner and more portable.

function drawPerson(x, y, size) {
  push();
  translate(x, y);
  rectMode(CENTER);
  fill(150, 75, 0); // Brown for the person

  // Head
  ellipse(0, -size * 0.4, size * 0.6, size * 0.6);
  // Body
  rect(0, 0, size * 0.5, size * 0.8);
  // Legs
  rect(-size * 0.15, size * 0.6, size * 0.2, size * 0.4);
  rect(size * 0.15, size * 0.6, size * 0.2, size * 0.4);

  pop();
}
Line-by-line explanation (9 lines)
push();
Saves the current canvas state.
translate(x, y);
Moves the origin to the target's position so all body parts are drawn relative to the target.
rectMode(CENTER);
Sets rectangle positioning to center mode, making body positioning easier with consistent centering.
fill(150, 75, 0); // Brown for the person
Sets the fill color to brown for the target person's body.
ellipse(0, -size * 0.4, size * 0.6, size * 0.6);
Draws the target's head as a circle positioned above the origin, sized proportionally to the target's size.
rect(0, 0, size * 0.5, size * 0.8);
Draws the target's body as a rectangle at the origin, with proportional width and height.
rect(-size * 0.15, size * 0.6, size * 0.2, size * 0.4);
Draws the left leg, positioned to the left and below the body.
rect(size * 0.15, size * 0.6, size * 0.2, size * 0.4);
Draws the right leg, positioned to the right and below the body at the same vertical level as the left leg.
pop();
Restores the canvas state.

touchStarted()

touchStarted() is a p5.js function that handles both mouse clicks and touch input. By returning false, we prevent the browser's default behaviors and let the game respond to all input. This is essential for mobile games. The function demonstrates a common pattern: check game state first (are we in the menu, in-game, or game-over?) and respond differently based on context.

function touchStarted() {
  if (gameOver) {
    restartGame();
  } else {
    // Pass the touch/mouse coordinates to the shoot function
    shoot(touches.length > 0 ? touches[0].x : mouseX, touches.length > 0 ? touches[0].y : mouseY);
    shotEffectAlpha = 255; // Trigger visual shot effect for player's gun
  }
  return false; // Prevent default touch behavior (like scrolling)
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Game Over Restart Check if (gameOver) { restartGame(); }

Checks if the game is over and restarts it if the player taps while on the game over screen.

conditional Shoot Logic } else { shoot(touches.length > 0 ? touches[0].x : mouseX, touches.length > 0 ? touches[0].y : mouseY); shotEffectAlpha = 255; }

During active gameplay, sends the tap/click coordinates to shoot() and triggers the visual shot effect.

function touchStarted() {
Defines a special p5.js function that runs whenever the user touches the screen or clicks the mouse.
if (gameOver) {
Checks if the game has ended (gameOver is true).
restartGame();
If the game is over and the user taps, restart the game by resetting score, lives, and targets.
shoot(touches.length > 0 ? touches[0].x : mouseX, touches.length > 0 ? touches[0].y : mouseY);
During active gameplay, calls the shoot() function and passes the x and y coordinates of the touch or mouse click so the game knows where you shot.
shotEffectAlpha = 255; // Trigger visual shot effect for player's gun
Sets shotEffectAlpha to 255 (fully opaque) to trigger the yellow flash effect, which fades over the next few frames in draw().
return false; // Prevent default touch behavior (like scrolling)
Returns false to prevent the browser from executing its default touch behavior (like scrolling or zooming), so all touch input goes to the game.

spawnTarget()

spawnTarget() creates a new target object and adds it to the targets array. Each target is a JavaScript object with many properties (position, size, shooting schedule, etc.). This demonstrates a key game development pattern: storing game entities (in this case, targets) as objects in an array, so they can be updated and drawn in a loop. The random() function ensures variety so the game feels less repetitive.

function spawnTarget() {
  let size = random(targetSizeMin, targetSizeMax);
  let x = random(size / 2, width - size / 2);
  let y = height * 0.8; // Start on the ground
  let lifetime = targetLifetime; // How long target stays active
  let id = frameCount; // Unique ID for target
  targets.push({
    id, x, y, size, lifetime, isHit: false, spawnFrame: frameCount,
    targetGunAngle: 0,
    targetGunX: 0,
    targetGunY: 0,
    nextShotFrame: frameCount + random(targetShootIntervalMin, targetShootIntervalMax),
    isFiring: false
  });
}
Line-by-line explanation (10 lines)
let size = random(targetSizeMin, targetSizeMax);
Generates a random size between the minimum and maximum target sizes (40 to 80 pixels by default).
let x = random(size / 2, width - size / 2);
Generates a random x position for the target on the ground, keeping it within screen bounds by accounting for the target's size.
let y = height * 0.8; // Start on the ground
Positions the target at the top of the ground area (80% down the screen) so they spawn at the back and move toward the player.
let lifetime = targetLifetime; // How long target stays active
Stores the lifetime value (how many frames this target can exist) from the global variable.
let id = frameCount; // Unique ID for target
Assigns a unique ID to this target using the current frame count, useful for tracking individual targets.
targets.push({
Creates a new target object with all necessary properties and adds it to the targets array using push().
id, x, y, size, lifetime, isHit: false, spawnFrame: frameCount,
The first properties of the target object: its position, size, lifetime, spawn frame, and a flag indicating it hasn't been hit yet.
targetGunAngle: 0, targetGunX: 0, targetGunY: 0,
Initialize the target's gun position and angle, which will be updated each frame as the target moves and aims.
nextShotFrame: frameCount + random(targetShootIntervalMin, targetShootIntervalMax),
Calculates when this target should fire its first shot by adding a random interval (between 60 and 180 frames) to the current frame count.
isFiring: false
Initializes a flag that indicates whether the target is currently shooting (used for muzzle flash animation).

drawTargets()

drawTargets() is the core game loop for handling all targets. It demonstrates several essential game programming patterns: looping through entities in an array, checking multiple collision conditions, updating state (movement, aiming, shooting schedules), and removing entities when needed. The backward loop (from last to first) is a common trick to safely remove items during iteration. This function also shows how to use atan2() for AI aiming—targets always point their guns at you.

🔬 Targets only shoot when they're between 30% and 70% down the screen. What happens if you change these ranges to target.y < height * 0.9 and target.y > height * 0.1? Targets will shoot from much farther away, making the game much harder.

      // Target shoots only when in a certain vertical range and not hit yet
      if (target.y < height * 0.7 && target.y > height * 0.3) {
        if (frameCount > target.nextShotFrame) {
          lives--; // Player loses a life!
          target.nextShotFrame = frameCount + random(targetShootIntervalMin, targetShootIntervalMax);
          target.isFiring = true; // Trigger muzzle flash
          checkGameOver();
        }
      }
function drawTargets() {
  for (let i = targets.length - 1; i >= 0; i--) {
    let target = targets[i];

    if (!target.isHit) {
      // Move target upwards
      target.y -= targetMoveSpeed;

      // Also move horizontally towards the player's x position
      let targetHorizontalSpeed = targetMoveSpeed * 0.5; // Adjust horizontal speed
      if (target.x < playerX) {
        target.x += targetHorizontalSpeed;
      } else if (target.x > playerX) {
        target.x -= targetHorizontalSpeed;
      }
      target.x = constrain(target.x, target.size / 2, width - target.size / 2);

      // Check if target reached the top (miss)
      if (target.y + target.size / 2 < 0 || frameCount - target.spawnFrame > target.lifetime) {
        lives--;
        targets.splice(i, 1); // Remove missed target
        checkGameOver();
        continue; // Go to next target
      }

      // Check for target-player collision (run into player)
      // Player body approx: x: playerX-30, y: playerY-70, w:60, h:80
      // Target body approx: x: target.x - target.size*0.25, y: target.y - target.size*0.4, w: target.size*0.5, h: target.size*0.8
      if (
        target.y - target.size * 0.4 <= playerY - 70 && // Target's head/upper body reaches player's chest
        dist(target.x, target.y, playerX, playerY) < (target.size * 0.4 + playerCollisionRadius) // Horizontal proximity
      ) {
        lives--;
        targets.splice(i, 1); // Remove target that ran into player
        checkGameOver();
        continue;
      }

      // Draw active target as a person
      drawPerson(target.x, target.y, target.size);

      // --- Target Shooting Logic ---
      // Target gun position relative to target body
      target.targetGunX = target.x - target.size * 0.15;
      target.targetGunY = target.y - target.size * 0.1;

      // Aim target gun at player's head (approx playerY - 90)
      target.targetGunAngle = atan2((playerY - 90) - target.targetGunY, playerX - target.targetGunX);

      // Target shoots only when in a certain vertical range and not hit yet
      if (target.y < height * 0.7 && target.y > height * 0.3) {
        if (frameCount > target.nextShotFrame) {
          lives--; // Player loses a life!
          target.nextShotFrame = frameCount + random(targetShootIntervalMin, targetShootIntervalMax);
          target.isFiring = true; // Trigger muzzle flash
          checkGameOver();
        }
      }

      // Draw target's gun
      drawTargetGun(target.targetGunX, target.targetGunY, target.targetGunAngle, target.size, target.isFiring);

      // Reset firing state for muzzle flash (only lasts one frame)
      if (target.isFiring) {
        target.isFiring = false;
      }

    } else {
      // Draw hit target (e.g., faded or with a cross)
      fill(100, 100, 100, 150); // Faded gray for hit targets
      ellipse(target.x, target.y, target.size); // Still using ellipse for hit visual
      fill(255);
      stroke(255);
      strokeWeight(2);
      line(target.x - target.size / 4, target.y - target.size / 4, target.x + target.size / 4, target.y + target.size / 4);
      line(target.x + target.size / 4, target.y - target.size / 4, target.x - target.size / 4, target.y + target.size / 4);
      noStroke();

      // Remove hit target after a short delay for visual feedback
      if (frameCount - target.spawnFrame > 30) { // Keep hit target on screen for 30 frames
        targets.splice(i, 1);
      }
    }
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

calculation Target Movement target.y -= targetMoveSpeed; // Also move horizontally towards the player's x position let targetHorizontalSpeed = targetMoveSpeed * 0.5; if (target.x < playerX) { target.x += targetHorizontalSpeed; } else if (target.x > playerX) { target.x -= targetHorizontalSpeed; } target.x = constrain(target.x, target.size / 2, width - target.size / 2);

Moves each target upward and horizontally toward the player, creating the sense that they're advancing across the screen.

conditional Missed Target Check if (target.y + target.size / 2 < 0 || frameCount - target.spawnFrame > target.lifetime) { lives--; targets.splice(i, 1); checkGameOver(); continue; }

Removes targets that reach the top of the screen without being shot, and deducts a life from the player.

conditional Collision Check if ( target.y - target.size * 0.4 <= playerY - 70 && dist(target.x, target.y, playerX, playerY) < (target.size * 0.4 + playerCollisionRadius) ) { lives--; targets.splice(i, 1); checkGameOver(); continue; }

Detects when a target touches the player, removing the target and deducting a life.

conditional Target Shooting Logic if (target.y < height * 0.7 && target.y > height * 0.3) { if (frameCount > target.nextShotFrame) { lives--; target.nextShotFrame = frameCount + random(targetShootIntervalMin, targetShootIntervalMax); target.isFiring = true; checkGameOver(); } }

Allows targets to shoot at the player when they're in the middle vertical range, deducting a life if hit.

conditional Hit Target Visual } else { fill(100, 100, 100, 150); ellipse(target.x, target.y, target.size); fill(255); stroke(255); strokeWeight(2); line(target.x - target.size / 4, target.y - target.size / 4, target.x + target.size / 4, target.y + target.size / 4); line(target.x + target.size / 4, target.y - target.size / 4, target.x - target.size / 4, target.y + target.size / 4); noStroke();

Draws a faded gray circle with an X through it for targets you've already shot, then removes them after a brief delay.

for (let i = targets.length - 1; i >= 0; i--) {
Loops through the targets array backwards (from last to first), which is important because we might remove targets during the loop and don't want to skip any.
let target = targets[i];
Gets the current target object from the array for easier access.
if (!target.isHit) {
Only processes living targets; hit targets are handled in the else block.
target.y -= targetMoveSpeed;
Moves the target upward each frame by subtracting the movement speed from its y position.
let targetHorizontalSpeed = targetMoveSpeed * 0.5;
Sets horizontal movement to half the vertical speed so targets move up and toward the player simultaneously.
if (target.x < playerX) { target.x += targetHorizontalSpeed; } else if (target.x > playerX) { target.x -= targetHorizontalSpeed; }
Moves the target left or right depending on whether it's to the left or right of the player.
target.x = constrain(target.x, target.size / 2, width - target.size / 2);
Keeps the target within the horizontal screen bounds even as it moves toward the player.
if (target.y + target.size / 2 < 0 || frameCount - target.spawnFrame > target.lifetime) {
Checks if the target has left the top of the screen or exceeded its lifetime in frames.
lives--;
Deducts one life because the target reached the player without being shot.
targets.splice(i, 1);
Removes the target from the array using splice(), which deletes one element at position i.
checkGameOver();
Checks if lives have reached zero and ends the game if so.
continue;
Skips to the next iteration of the loop, preventing further processing of this target since it's been removed.
if ( target.y - target.size * 0.4 <= playerY - 70 && dist(target.x, target.y, playerX, playerY) < (target.size * 0.4 + playerCollisionRadius) ) {
Checks if the target has reached the player's vertical level AND is close enough horizontally (using dist()) to collide.
drawPerson(target.x, target.y, target.size);
Draws the target as a person silhouette at its current position.
target.targetGunX = target.x - target.size * 0.15; target.targetGunY = target.y - target.size * 0.1;
Positions the target's gun at its hip area, offset from the target's center.
target.targetGunAngle = atan2((playerY - 90) - target.targetGunY, playerX - target.targetGunX);
Calculates the angle from the target's gun to the player's head, making the gun point at you.
if (target.y < height * 0.7 && target.y > height * 0.3) {
Only allows targets to shoot when they're in the middle vertical range of the screen (not too far away, not too close).
if (frameCount > target.nextShotFrame) {
Checks if it's time for this target to fire (if the current frame count exceeds the scheduled shot frame).
target.nextShotFrame = frameCount + random(targetShootIntervalMin, targetShootIntervalMax);
Schedules the next shot for this target at a random time in the future.
target.isFiring = true;
Sets a flag to trigger the muzzle flash for this frame.
if (target.isFiring) { target.isFiring = false; }
Resets the firing flag after drawing, so the muzzle flash only appears for one frame.
fill(100, 100, 100, 150); ellipse(target.x, target.y, target.size);
Draws a faded gray circle to represent a hit target.
line(target.x - target.size / 4, target.y - target.size / 4, target.x + target.size / 4, target.y + target.size / 4); line(target.x + target.size / 4, target.y - target.size / 4, target.x - target.size / 4, target.y + target.size / 4);
Draws an X through the hit target using two diagonal lines.
if (frameCount - target.spawnFrame > 30) { targets.splice(i, 1); }
Removes the hit target from the array after 30 frames, giving the player time to see that they hit something.

shoot(shotX, shotY)

shoot() handles the core mechanic of the game: detecting whether a shot hit a target. It uses the dist() function to calculate distance, a fundamental tool in interactive sketches. The loop searches through all targets and breaks after finding the first hit, preventing shot overlap. The miss penalty (losing a life) makes every shot count, increasing tension.

function shoot(shotX, shotY) {
  let hit = false;
  for (let i = targets.length - 1; i >= 0; i--) {
    let target = targets[i];
    if (!target.isHit) {
      // Check if shot hits the target (simple distance check)
      let d = dist(shotX, shotY, target.x, target.y); // https://p5js.org/reference/#/p5/dist
      if (d < target.size / 2) { // Collision is still based on the target's original circular bounding box
        score++;
        target.isHit = true; // Mark target as hit
        hit = true;
        break; // Only hit one target per shot
      }
    }
  }

  // If no target was hit, lose a life
  if (!hit) {
    lives--;
    checkGameOver();
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Hit Detection Loop for (let i = targets.length - 1; i >= 0; i--) { let target = targets[i]; if (!target.isHit) { let d = dist(shotX, shotY, target.x, target.y); if (d < target.size / 2) { score++; target.isHit = true; hit = true; break; } } }

Loops through all targets to check if the shot hit any of them using distance calculation, marks the first hit target, and exits the loop.

conditional Miss Penalty if (!hit) { lives--; checkGameOver(); }

If no target was hit, the player loses a life for missing.

function shoot(shotX, shotY) {
Defines the shoot function that takes the x and y coordinates of where the shot was fired.
let hit = false;
Initializes a flag to track whether the shot hit any target.
for (let i = targets.length - 1; i >= 0; i--) {
Loops through the targets array backwards to check each target.
let target = targets[i];
Gets the current target object from the array.
if (!target.isHit) {
Only checks living targets; ignores targets that have already been hit.
let d = dist(shotX, shotY, target.x, target.y);
Calculates the distance between the shot position and the target's position using the dist() function.
if (d < target.size / 2) {
Checks if the distance is less than the target's radius, meaning the shot is inside the target's hitbox.
score++;
Increments the score by 1 for successfully hitting a target.
target.isHit = true;
Marks this target as hit so it won't be targeted again and will be drawn as a faded X instead.
hit = true;
Sets the hit flag to true so we know the shot landed.
break;
Exits the loop immediately after hitting the first target, preventing the same shot from hitting multiple targets.
if (!hit) {
If hit is still false, no target was struck.
lives--;
Deducts one life for a missed shot.
checkGameOver();
Checks if lives have reached zero after the miss.

checkGameOver()

checkGameOver() is a simple but crucial function that marks the end of gameplay. It's called after any event that costs a life (missing a shot, being hit by a target, or a target reaching you). By centralizing this check in one function, the game state remains consistent.

function checkGameOver() {
  if (lives <= 0) {
    gameOver = true;
  }
}
Line-by-line explanation (2 lines)
if (lives <= 0) {
Checks if the player's lives have reached zero or gone below.
gameOver = true;
Sets the gameOver flag to true, which causes draw() to display the game over screen instead of active gameplay.

restartGame()

restartGame() resets all game variables to their initial states, allowing the player to play again without reloading the page. It's called from both setup() (at startup) and touchStarted() (when the player taps to restart after game over). This demonstrates a common game development pattern: separating game initialization from game logic.

function restartGame() {
  score = 0;
  lives = 3;
  gameOver = false;
  targets = [];
  targetSpawnInterval = 60; // Reset difficulty
  targetMoveSpeed = 1.5; // Reset difficulty
  lastTargetSpawnTime = frameCount;
}
Line-by-line explanation (7 lines)
score = 0;
Resets the score to zero for a fresh game.
lives = 3;
Restores the player to 3 lives.
gameOver = false;
Sets gameOver back to false so draw() shows active gameplay instead of the game over screen.
targets = [];
Clears all targets from the array so no targets from the previous game carry over.
targetSpawnInterval = 60; // Reset difficulty
Resets the spawn interval to its starting value, ensuring the game starts at the original difficulty.
targetMoveSpeed = 1.5; // Reset difficulty
Resets the target movement speed to its starting value.
lastTargetSpawnTime = frameCount;
Records the current frame as the last spawn time so the spawn interval countdown starts fresh.

windowResized()

windowResized() is a special p5.js function that runs whenever the browser window is resized. Calling resizeCanvas() ensures the game always fills the entire window. This is essential for mobile and responsive design, making the game playable at any screen size. Without this function, the game would be stuck at its initial resolution.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-center player/gun position based on new canvas size
  playerY = height * 0.9;
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window dimensions when the window is resized.
playerY = height * 0.9;
Updates the player's vertical position relative to the new canvas height so they stay proportionally positioned on the ground.

📦 Key Variables

score number

Tracks how many targets the player has successfully shot

let score = 0;
lives number

Tracks how many hits the player can take before game over (starts at 3)

let lives = 3;
gameOver boolean

Flag that tracks whether the game has ended (when true, the game over screen displays)

let gameOver = false;
targets array

Stores all active target objects in the game, each with position, size, and shooting behavior

let targets = [];
targetSizeMin number

Minimum size in pixels for randomly generated targets

let targetSizeMin = 40;
targetSizeMax number

Maximum size in pixels for randomly generated targets

let targetSizeMax = 80;
targetSpawnInterval number

How many frames pass between each new target spawn (lower = more frequent spawns)

let targetSpawnInterval = 60;
lastTargetSpawnTime number

Stores the frame count of the last time a target was spawned, used to determine when to spawn the next one

let lastTargetSpawnTime = 0;
targetMoveSpeed number

How many pixels per frame targets move upward and toward the player

let targetMoveSpeed = 1.5;
targetLifetime number

How many frames a target can exist before it automatically disappears (causing you to lose a life)

let targetLifetime = 180;
targetShootIntervalMin number

Minimum number of frames between target shots (60 frames ≈ 1 second)

let targetShootIntervalMin = 60;
targetShootIntervalMax number

Maximum number of frames between target shots (180 frames ≈ 3 seconds)

let targetShootIntervalMax = 180;
shotEffectAlpha number

Transparency value for the yellow flash effect when the player shoots (fades each frame)

let shotEffectAlpha = 0;
playerX number

Horizontal position of the cowboy player on the canvas

let playerX;
playerY number

Vertical position of the cowboy player on the canvas (always on the ground)

let playerY;
gunX number

Horizontal position of the gun, offset from the player's center

let gunX;
gunY number

Vertical position of the gun, positioned at the player's hip

let gunY;
gunAngle number

Angle in radians that the gun barrel is rotated toward the player's aim point

let gunAngle = 0;
playerCollisionRadius number

Radius in pixels used for collision detection between targets and the player's body

const playerCollisionRadius = 30;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG shoot() function

The hitbox for targets is circular but the collision detection in drawTargets() uses rectangular approximation for visual checks, creating a mismatch between where you can hit and what looks hittable.

💡 Use consistent collision detection: either switch shoot() to rectangular bounding box collision or update drawTargets() to also use circular collision for target-player collisions.

PERFORMANCE drawTargets() function

The function recalculates target gun angles and positions every frame even for targets that are far away or won't shoot for a while, wasting computation on invisible or inactive targets.

💡 Add a distance check before calculating targetGunAngle and only update it when the target is in the dangerous vertical range (between height * 0.7 and height * 0.3).

FEATURE restartGame() and game progression

The game has no difficulty scaling—targets spawn at the same rate and speed regardless of score, so it never feels like the threat is increasing.

💡 Add difficulty scaling by reducing targetSpawnInterval and increasing targetMoveSpeed based on the player's score: e.g., targetSpawnInterval = max(20, 60 - score/5) to spawn faster as you get better.

STYLE Variable naming

Global variables like targetShootIntervalMin and targetShootIntervalMax are long and not marked as constants, making the code harder to scan and potentially suggesting they can change (they don't).

💡 Define these as constants at the top with const: const MIN_SHOOT_INTERVAL = 60; const MAX_SHOOT_INTERVAL = 180; to clarify they don't change and improve readability.

BUG drawTargets() collision check

The player collision detection uses an approximation of the player body bounds (playerY - 70) hardcoded, so if drawCowboy() changes, the collision won't update accordingly.

💡 Instead of hardcoding playerY - 70, either document the cowboy's visual bounds clearly in drawCowboy() or calculate collision bounds inside drawCowboy() and store them in global variables that drawTargets() can use.

🔄 Code Flow

Code flow showing setup, draw, drawcowboy, drawgun, drawtargetgun, drawperson, touchstarted, spawntarget, drawtargets, shoot, checkgameover, restartgame, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> player-position-update[player-position-update] draw --> target-spawn-check[target-spawn-check] draw --> shot-effect-fade[shot-effect-fade] draw --> gun-angle-calculation[gun-angle-calculation] draw --> transform-and-rotate[transform-and-rotate] draw --> muzzle-flash[muzzle-flash] draw --> game-over-restart[game-over-restart] draw --> shoot-logic[shoot-logic] draw --> drawtargets[drawtargets] drawtargets --> target-movement[target-movement] drawtargets --> missed-target-check[missed-target-check] drawtargets --> collision-check[collision-check] drawtargets --> target-shooting[target-shooting] drawtargets --> hit-target-visual[hit-target-visual] shoot-logic --> hit-detection-loop[hit-detection-loop] hit-detection-loop --> miss-penalty[miss-penalty] click setup href "#fn-setup" click draw href "#fn-draw" click player-position-update href "#sub-player-position-update" click target-spawn-check href "#sub-target-spawn-check" click shot-effect-fade href "#sub-shot-effect-fade" click gun-angle-calculation href "#sub-gun-angle-calculation" click transform-and-rotate href "#sub-transform-and-rotate" click muzzle-flash href "#sub-muzzle-flash" click game-over-restart href "#sub-game-over-restart" click shoot-logic href "#sub-shoot-logic" click drawtargets href "#fn-drawtargets" click target-movement href "#sub-target-movement" click missed-target-check href "#sub-missed-target-check" click collision-check href "#sub-collision-check" click target-shooting href "#sub-target-shooting" click hit-target-visual href "#sub-hit-target-visual" click hit-detection-loop href "#sub-hit-detection-loop" click miss-penalty href "#sub-miss-penalty"

❓ Frequently Asked Questions

What visual elements can I expect in the Cowboy Gunslinger sketch?

The sketch features a blue sky background, a brown ground, and a simple silhouette representation of a cowboy and his gun, creating a vibrant wild west atmosphere.

How can I interact with the Cowboy Gunslinger game?

Users can interact by moving the mouse or touching the screen to aim the cowboy's gun, while trying to avoid targets and survive against increasing law enforcement.

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

The sketch demonstrates techniques like sprite movement, collision detection, and user input handling using p5.js, making it an engaging example of interactive game development.

Preview

Cowboy gunslinger! - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Cowboy gunslinger! - Code flow showing setup, draw, drawcowboy, drawgun, drawtargetgun, drawperson, touchstarted, spawntarget, drawtargets, shoot, checkgameover, restartgame, windowresized
Code Flow Diagram