fly and race 2 player

This sketch creates a competitive 2-player airplane racing game where players accelerate their planes across the screen using keyboard controls. The game features a dynamic day-night sky cycle that transitions between dawn, day, and night, a countdown timer before races start, and declares a winner when an airplane reaches the finish line.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make airplane 1 red and airplane 2 blue — Change the fill color inside drawAirplaneShape() to paint each airplane differently by editing this function twice
  2. Speed up the day-night cycle 10x — Multiply daySpeed by 10 so dawn, day, and night cycle rapidly—watch the sky change colors throughout the race
  3. Make friction much stronger so planes stop quickly — Increase frictionRate so planes decelerate aggressively when you release the key—adds a power/precision feel to the race
  4. Change player keys to 'A' and 'L' — Swap the keyboard controls so player 1 uses 'A' and player 2 uses 'L' instead of 'Q' and 'P'
  5. Make the countdown start at 5 instead of 3 — Give players more time to prepare before the race by extending the countdown sequence
  6. Double the acceleration rate for dramatic speed — Players will reach max speed much faster, making the race feel more responsive and explosive
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a thrilling 2-player airplane race where each player accelerates their plane across the screen by holding a keyboard key, racing to reach the opposite edge first. The visual experience is enhanced by a smooth day-night sky cycle that transitions from dawn through day to night using gradient backgrounds and color lerping. The game state flows logically from instructions to a 3-2-1 countdown to the active race to a winner display, teaching learners how to structure multi-state games with clear state variables and transitions.

The code is organized around three core systems: the draw loop handles visual rendering and game logic, keyboard and mouse input handlers detect player actions, and a resetRace() function manages state transitions. By studying it you will learn how to manage multiple game states, implement smooth color transitions with lerpColor(), use push/pop for coordinate transformations, handle continuous keyboard input for gameplay, and structure a complete interactive game from start to finish.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas and calls resetRace() to initialize both airplanes at the starting line (off-screen left) with zero speed, and the game enters the instructions state.
  2. Every frame, draw() updates the day-night cycle by incrementing dayProgress and calculating current sky colors using lerpColor(), then draws a smooth gradient sky from top to bottom with setGradient().
  3. Both airplanes are drawn at their current X positions using translate() and scale() to position and size them, with drawAirplaneShape() rendering the plane geometry.
  4. When the game is not racing, clicking the mouse starts a 3-2-1 countdown using countdownTimer and countdownValue; when the countdown reaches zero, isRacing becomes true and the race begins.
  5. During the race, keyPressed() sets player1IsAccelerating or player2IsAccelerating to true when Q or P are held, and keyReleased() sets them false when released.
  6. In each frame of the race, acceleration increases speed by accelerationRate (capped at maxSpeed), friction decreases speed by frictionRate (floored at 0), then airplane positions move by their speeds.
  7. When either airplane's X position exceeds the right edge (width + 150), the winner is declared, isRacing stops, and clicking the mouse calls resetRace() to play again.

🎓 Concepts You'll Learn

Game state managementKeyboard input handlingColor lerping and gradientsCoordinate transforms (translate/scale)Conditional logic flowAnimation and physics (velocity, friction)

📝 Code Breakdown

setGradient(c1, c2)

setGradient() is a helper function that creates smooth color transitions by computing an intermediate color for each row. The key technique is map() which converts pixel positions into blend factors, and lerpColor() which smoothly interpolates between two colors. This is how the sky fades from one color to another.

function setGradient(c1, c2) {
  // Loop through each pixel row from top to bottom
  for (let y = 0; y < height; y++) {
    // Calculate the interpolation factor (0 at top, 1 at bottom)
    let inter = map(y, 0, height, 0, 1);
    
    // Lerp (linear interpolate) between the two colors based on the factor
    let c = lerpColor(c1, c2, inter);
    
    // Set the stroke color to the interpolated color and draw a line
    // This draws a thin line across the canvas for each row, creating the gradient
    stroke(c);
    line(0, y, width, y);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Row-by-row gradient loop for (let y = 0; y < height; y++) {

Iterates through every horizontal line from top to bottom, allowing each row to have a slightly different interpolated color

calculation Color interpolation let c = lerpColor(c1, c2, inter);

Blends smoothly between the top color (c1) and bottom color (c2) based on the row's position

for (let y = 0; y < height; y++) {
Loop counter y starts at 0 (top of canvas) and increments until it reaches the canvas height, so we process every row
let inter = map(y, 0, height, 0, 1);
map() converts y from the range [0, height] into a 0-to-1 range; at the top y=0 so inter=0, at the bottom y=height so inter=1
let c = lerpColor(c1, c2, inter);
lerpColor() blends between color c1 and c2; when inter=0 it returns c1 (top), when inter=1 it returns c2 (bottom), in between it mixes
stroke(c);
Sets the line color to the interpolated color c so the line at this row will be the right blend
line(0, y, width, y);
Draws a horizontal line from the left edge (0, y) to the right edge (width, y) at the current row, in the interpolated color

drawAirplaneShape()

drawAirplaneShape() is drawn with its origin (0,0) at the center of the airplane, making it easy to position and scale using translate() and scale() in draw(). All coordinates are relative to this center point, which is why the body is at (0,0) and other parts are positioned relative to it.

🔬 These two rectangles ARE the wings. What happens if you make the main wing taller (change 10 to 20) or move the rear wing higher (change 15 to 5)?

  // Wings
  rect(-40, -15, 80, 10); // Main wing (relative to body center)
  rect(-20, 15, 40, 10); // Rear wing
function drawAirplaneShape() {
  // Main body
  fill(150, 150, 150); // Gray color
  noStroke();
  ellipse(0, 0, 100, 30); // Body (width 100, height 30)

  // Wings
  rect(-40, -15, 80, 10); // Main wing (relative to body center)
  rect(-20, 15, 40, 10); // Rear wing

  // Tail
  triangle(40, -15, 60, 0, 40, 15); // Tail fin

  // Cockpit window (optional detail)
  fill(100, 100, 200, 150); // Blueish transparent
  ellipse(-30, 0, 15, 10);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Main body ellipse(0, 0, 100, 30);

Draws the main fuselage of the airplane as a gray ellipse centered at the origin

calculation Wings rect(-40, -15, 80, 10); // Main wing

Draws the main wing and rear wing as rectangles positioned relative to the body center

calculation Tail fin triangle(40, -15, 60, 0, 40, 15);

Draws the tail fin as a triangle pointing right from the back of the fuselage

fill(150, 150, 150);
Sets the fill color to gray (RGB: 150, 150, 150) for all shapes drawn after this until fill() is called again
noStroke();
Removes the outline/border from shapes so they have no black edges
ellipse(0, 0, 100, 30);
Draws the airplane body as an ellipse centered at (0,0) with width 100 and height 30 pixels
rect(-40, -15, 80, 10);
Draws the main wing as a rectangle starting at (-40, -15) with width 80 and height 10, positioned above the body center
rect(-20, 15, 40, 10);
Draws the rear wing as a smaller rectangle below the body center
triangle(40, -15, 60, 0, 40, 15);
Draws a triangle with three corners: (40, -15), (60, 0), (40, 15)—this creates the tail fin pointing to the right
fill(100, 100, 200, 150);
Changes the fill color to a semi-transparent blue (the fourth number 150 is the alpha/opacity, making it see-through)
ellipse(-30, 0, 15, 10);
Draws the cockpit window as a small transparent blue ellipse on the left side of the body

setup()

setup() runs once when the sketch starts. It prepares the canvas and delegates initialization to resetRace() to avoid code duplication.

function setup() {
  // Make the canvas full screen
  createCanvas(windowWidth, windowHeight);
  resetRace(); // Initialize race state and positions, including countdown
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window using windowWidth and windowHeight
resetRace();
Calls resetRace() to initialize all variables needed for the first game (airplane positions, speeds, race state, countdown)

resetRace()

resetRace() is a critical helper function called whenever the game needs to return to a clean state—at startup, after a race ends, and when the window is resized. It groups all initialization in one place to keep the code DRY (Don't Repeat Yourself).

function resetRace() {
  // Initialize airplane positions to the starting line (off-screen left)
  airplane1X = -150; 
  airplane2X = -150; // Both start at the same place

  // Reset speeds and acceleration flags
  airplane1Speed = 0;
  airplane2Speed = 0;
  player1IsAccelerating = false;
  player2IsAccelerating = false;

  isRacing = false; // Race is not active yet
  winner = ""; // Clear any previous winner
  
  // Reset countdown
  countdownValue = 3;
  countdownTimer = 0; // Ensure timer is not running initially
  
  // Set text properties for displaying messages
  textAlign(CENTER, CENTER);
  textSize(32);
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Position initialization airplane1X = -150;

Sets both airplanes to the left side of the screen, off-screen so they appear to race onto the canvas

calculation Speed and state reset airplane1Speed = 0;

Clears all movement and acceleration state from any previous race

calculation Countdown reset countdownValue = 3;

Restores the countdown to 3 and clears the timer so a new countdown can start

airplane1X = -150;
Places airplane 1 at x = -150 (150 pixels off the left edge of the screen)
airplane2X = -150;
Places airplane 2 at the same starting position so both begin the race evenly
airplane1Speed = 0;
Resets airplane 1's speed to zero so it doesn't coast from the previous race
airplane2Speed = 0;
Resets airplane 2's speed to zero
player1IsAccelerating = false;
Clears the flag that tracks if player 1's key is held, so no acceleration happens at the start
player2IsAccelerating = false;
Clears player 2's acceleration flag
isRacing = false;
Sets the race state to inactive so the draw loop enters the pre-race instructions phase
winner = "";
Clears the winner string so the winner message doesn't persist to the next race
countdownValue = 3;
Resets the countdown number to 3 for the next race's countdown sequence
countdownTimer = 0;
Clears the timer that tracks when each countdown number started, ensuring the next countdown begins fresh
textAlign(CENTER, CENTER);
Configures text to be centered horizontally and vertically around its (x, y) position
textSize(32);
Sets the default font size to 32 pixels for messages

draw()

draw() is the engine of the game—it runs 60 times per second and handles three major tasks: rendering the animated sky, drawing both airplanes, and executing game logic. During races, it updates physics; during pre-race and post-race states, it displays instructions and messages. The if(isRacing) conditional is the key gate that separates these behaviors.

🔬 These two blocks ARE the acceleration system—they only increase speed if a player holds their key. What happens if you remove the if-statements so planes ALWAYS accelerate even when no key is held?

    if (player1IsAccelerating) {
      airplane1Speed += accelerationRate;
      airplane1Speed = min(airplane1Speed, maxSpeed); // Cap speed at maxSpeed
    }
    if (player2IsAccelerating) {
      airplane2Speed += accelerationRate;
      airplane2Speed = min(airplane2Speed, maxSpeed); // Cap speed at maxSpeed
    }

🔬 These lines apply friction (drag) that slows planes down every frame. What happens if you increase frictionRate from 0.1 to 0.5 (you'd need to edit the global variable)? Will planes feel more sluggish or responsive?

    // Apply friction to slow down airplanes
    airplane1Speed -= frictionRate;
    airplane1Speed = max(airplane1Speed, 0); // Speed cannot go below zero
    airplane2Speed -= frictionRate;
    airplane2Speed = max(airplane2Speed, 0); // Speed cannot go below zero
function draw() {
  // --- Day-Night Cycle Logic ---
  // Update dayProgress (loops from 0 to 1)
  dayProgress += daySpeed;
  if (dayProgress > 1) {
    dayProgress = 0; // Loop back to morning/dawn
  }

  // Define colors for different times of day
  let dawnTop = color(255, 180, 100); // Warm orange for dawn top
  let dawnBottom = color(255, 220, 180); // Lighter orange for dawn horizon
  let dayTop = color(100, 150, 255); // A medium blue for the top of the day sky
  let dayBottom = color(200, 220, 255); // A lighter blue/white for the day horizon
  let nightTop = color(20, 30, 80); // Dark blue for night top
  let nightBottom = color(50, 60, 120); // Slightly lighter dark blue for night horizon

  let currentSkyTopColor;
  let currentSkyBottomColor;

  if (dayProgress < 0.5) { // Morning to Day
    // Transition from Dawn to Day colors
    let inter = map(dayProgress, 0, 0.5, 0, 1);
    currentSkyTopColor = lerpColor(dawnTop, dayTop, inter);
    currentSkyBottomColor = lerpColor(dawnBottom, dayBottom, inter);
  } else { // Day to Night
    // Transition from Day to Night colors
    let inter = map(dayProgress, 0.5, 1, 0, 1);
    currentSkyTopColor = lerpColor(dayTop, nightTop, inter);
    currentSkyBottomColor = lerpColor(dayBottom, nightBottom, inter);
  }

  // Draw the sky gradient using the current colors
  setGradient(currentSkyTopColor, currentSkyBottomColor);
  
  // --- Draw Airplane 1 (flying right) ---
  push(); // Start an isolated drawing state
  translate(airplane1X, height * 0.3); // Position the airplane horizontally and vertically
  scale(airplaneSize1); // Scale its size
  drawAirplaneShape(); // Draw the airplane shape
  pop(); // Restore the previous drawing state

  // --- Draw Airplane 2 (flying right) ---
  push(); // Start another isolated drawing state
  translate(airplane2X, height * 0.5); // Position Airplane 2
  scale(airplaneSize2); // Scale its size (no flip, both fly right for the race)
  drawAirplaneShape(); // Draw the airplane shape
  pop(); // Restore the previous drawing state

  // --- Game State Logic ---
  if (isRacing) {
    // Race is active - apply acceleration, friction, move planes, check winner
    if (player1IsAccelerating) {
      airplane1Speed += accelerationRate;
      airplane1Speed = min(airplane1Speed, maxSpeed); // Cap speed at maxSpeed
    }
    if (player2IsAccelerating) {
      airplane2Speed += accelerationRate;
      airplane2Speed = min(airplane2Speed, maxSpeed); // Cap speed at maxSpeed
    }

    // Apply friction to slow down airplanes
    airplane1Speed -= frictionRate;
    airplane1Speed = max(airplane1Speed, 0); // Speed cannot go below zero
    airplane2Speed -= frictionRate;
    airplane2Speed = max(airplane2Speed, 0); // Speed cannot go below zero

    // Move airplanes based on their current speeds
    airplane1X += airplane1Speed; 
    airplane2X += airplane2Speed; 

    // Check for a winner
    // The `winner === ""` check ensures only the first airplane to cross is declared the winner
    if (airplane1X > width + 150 && winner === "") { 
      winner = "Player 1 Wins!";
      isRacing = false; // Stop the race
    } else if (airplane2X > width + 150 && winner === "") { 
      winner = "Player 2 Wins!";
      isRacing = false; // Stop the race
    }
  } else {
    // Race is not active - display instructions, countdown, or winner
    fill(0); // Black text color
    if (winner === "") {
      // Pre-race idle state (instructions)
      if (countdownTimer === 0) { // Countdown hasn't started yet
        text("GO FLY AND WIN!", width / 2, height / 2 - 60); // Changed text here
        textSize(24);
        text(`Player 1: Press '${player1Key}' to accelerate`, width / 2, height / 2);
        text(`Player 2: Press '${player2Key}' to accelerate`, width / 2, height / 2 + 30);
        textSize(32); // Reset text size
      } else {
        // Countdown is active
        textSize(100); // Larger text for countdown numbers
        text(countdownValue, width / 2, height / 2);
        textSize(32); // Reset text size for other messages

        if (frameCount - countdownTimer >= countdownDuration) {
          countdownValue--; // Decrement the countdown number
          countdownTimer = frameCount; // Reset timer for the next number

          if (countdownValue === 0) {
            isRacing = true; // Start the race!
            countdownTimer = 0; // Clear timer as countdown is over
          }
        }
      }
    } else {
      // Post-race idle state (winner)
      text(winner, width / 2, height / 2 - 30); // Display the winner
      textSize(24);
      text("Click to Race Again!", width / 2, height / 2 + 10); // Prompt to restart
      textSize(32); // Reset text size for next race message
    }
  }
}
Line-by-line explanation (27 lines)

🔧 Subcomponents:

conditional Day-night color transition if (dayProgress < 0.5) {

Determines whether to transition from dawn→day (first half) or day→night (second half)

calculation Sky gradient rendering setGradient(currentSkyTopColor, currentSkyBottomColor);

Draws the sky background using the interpolated colors for the current time of day

calculation Airplane 1 rendering push(); translate(airplane1X, height * 0.3); scale(airplaneSize1); drawAirplaneShape(); pop();

Positions, scales, and draws airplane 1 in the upper third of the canvas

calculation Airplane 2 rendering push(); translate(airplane2X, height * 0.5); scale(airplaneSize2); drawAirplaneShape(); pop();

Positions, scales, and draws airplane 2 in the middle of the canvas

conditional Active race physics if (isRacing) {

When the race is active, applies acceleration, friction, movement, and winner detection

conditional Countdown sequence if (frameCount - countdownTimer >= countdownDuration) {

Tracks elapsed frames and decrements the countdown number when enough time has passed

dayProgress += daySpeed;
Increments dayProgress each frame by daySpeed (0.0005), making the day-night cycle advance slowly
if (dayProgress > 1) {
Checks if dayProgress has exceeded 1 (the end of a full cycle)
dayProgress = 0;
Resets dayProgress back to 0 (dawn) so the cycle loops infinitely
let inter = map(dayProgress, 0, 0.5, 0, 1);
Converts dayProgress (0 to 0.5) into a blend factor (0 to 1) for the dawn→day transition
currentSkyTopColor = lerpColor(dawnTop, dayTop, inter);
Interpolates between the dawn sky top color and day sky top color based on the blend factor
setGradient(currentSkyTopColor, currentSkyBottomColor);
Calls setGradient() to draw the background with smooth color transitions from top to bottom
push();
Saves the current drawing state (position, rotation, scale) so changes don't affect later drawing
translate(airplane1X, height * 0.3);
Moves the origin to airplane 1's position: airplane1X horizontally, and 30% down the canvas vertically
scale(airplaneSize1);
Scales airplane 1 by airplaneSize1 (0.8), making it 80% of its original size
drawAirplaneShape();
Draws the airplane shape, which will appear at the translated and scaled location
pop();
Restores the drawing state to before the push(), undoing the translate and scale for subsequent drawing
if (isRacing) {
Checks if the race is currently active; if true, update physics and check for winner
if (player1IsAccelerating) {
If player 1's key is held (player1IsAccelerating is true), increase their speed
airplane1Speed += accelerationRate;
Increases airplane 1's speed by 0.2 each frame while the key is held
airplane1Speed = min(airplane1Speed, maxSpeed);
Caps airplane 1's speed at the maximum value (10) so it can't accelerate infinitely
airplane1Speed -= frictionRate;
Applies friction by subtracting 0.1 from speed each frame, simulating air resistance
airplane1Speed = max(airplane1Speed, 0);
Prevents speed from going below zero, so planes don't reverse backward
airplane1X += airplane1Speed;
Moves airplane 1 by adding its current speed to its position
if (airplane1X > width + 150 && winner === "") {
Checks if airplane 1 has crossed the finish line (past the right edge) and no winner has been declared yet
winner = "Player 1 Wins!";
Sets the winner string to declare player 1 as the winner
isRacing = false;
Stops the race so physics updates halt and the winner message displays
if (frameCount - countdownTimer >= countdownDuration) {
Checks if enough frames have passed (60 frames = 1 second) since the countdown timer started
countdownValue--;
Decrements the countdown number (3→2→1→0) so the next number displays
countdownTimer = frameCount;
Resets the timer to the current frame count so the next countdown number lasts another 60 frames
if (countdownValue === 0) {
Checks if the countdown has reached 0 (finished)
isRacing = true;
Sets isRacing to true, enabling physics updates and starting the actual race
text(countdownValue, width / 2, height / 2);
Displays the current countdown number (3, 2, or 1) centered on the screen in large text

keyPressed()

keyPressed() is called once each time a key goes down. It sets acceleration flags that persist until keyReleased() is called. This pattern (flag-based input) is essential for smooth gameplay because the draw loop can read these flags every frame, not just when keys are pressed.

function keyPressed() {
  if (isRacing) { // Only allow acceleration during an active race
    if (key.toUpperCase() === player1Key) {
      player1IsAccelerating = true;
    } else if (key.toUpperCase() === player2Key) {
      player2IsAccelerating = true;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Key identity check if (key.toUpperCase() === player1Key) {

Determines which player's key was pressed and sets the appropriate acceleration flag

if (isRacing) {
Only processes key input during an active race; ignores keypresses during countdown and instructions
if (key.toUpperCase() === player1Key) {
Converts the pressed key to uppercase and checks if it matches player1Key ('Q'); toUpperCase() makes 'q' equal 'Q'
player1IsAccelerating = true;
Sets the flag to true so draw() knows to increase airplane 1's speed
} else if (key.toUpperCase() === player2Key) {
If the key wasn't player 1's, check if it matches player 2's key ('P')
player2IsAccelerating = true;
Sets player 2's acceleration flag to true

keyReleased()

keyReleased() is the counterpart to keyPressed()—it's called once each time a key goes up. Together, these two functions enable continuous input: a flag stays true while a key is held and becomes false when released, allowing the draw loop to apply acceleration for as many frames as the key is held.

function keyReleased() {
  if (isRacing) { // Only affect acceleration during an active race
    if (key.toUpperCase() === player1Key) {
      player1IsAccelerating = false;
    } else if (key.toUpperCase() === player2Key) {
      player2IsAccelerating = false;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Key release detection if (key.toUpperCase() === player1Key) {

Determines which player released their key and clears the acceleration flag

if (isRacing) {
Only processes key releases during an active race
if (key.toUpperCase() === player1Key) {
Checks if the released key was player 1's key ('Q')
player1IsAccelerating = false;
Sets the flag to false so draw() stops increasing airplane 1's speed and friction takes over
} else if (key.toUpperCase() === player2Key) {
If it wasn't player 1's key, check if it was player 2's key ('P')
player2IsAccelerating = false;
Clears player 2's acceleration flag

mousePressed()

mousePressed() is the game's state machine control—a single click triggers different actions depending on what state the game is in (instructions → countdown → winner → reset). The nested if-else structure carefully distinguishes between these states to prevent unintended behavior.

🔬 This function branches based on game state. What happens if you remove the countdownTimer === 0 check so clicking always restarts the countdown, even if one is already running?

  if (!isRacing && winner === "") { // If not racing and no winner displayed (either initial or countdown phase)
    if (countdownTimer === 0) { // If countdown hasn't started yet (initial instruction phase)
      // Start the countdown
      countdownTimer = frameCount;
      countdownValue = 3; // Ensure it starts from 3
    }
    // If countdown is already running, a click should not affect it.
  } else if (!isRacing && winner !== "") { // If race finished and winner is displayed
    resetRace(); // Reset for a new game, which includes resetting countdownValue and countdownTimer
  }
function mousePressed() {
  if (!isRacing && winner === "") { // If not racing and no winner displayed (either initial or countdown phase)
    if (countdownTimer === 0) { // If countdown hasn't started yet (initial instruction phase)
      // Start the countdown
      countdownTimer = frameCount;
      countdownValue = 3; // Ensure it starts from 3
    }
    // If countdown is already running, a click should not affect it.
  } else if (!isRacing && winner !== "") { // If race finished and winner is displayed
    resetRace(); // Reset for a new game, which includes resetting countdownValue and countdownTimer
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Countdown initiation if (countdownTimer === 0) {

Detects when no countdown is running and starts a new one by setting countdownTimer

conditional Race restart logic } else if (!isRacing && winner !== "") {

Detects when a race has finished and clicking the mouse triggers resetRace()

if (!isRacing && winner === "") {
Checks if the race is NOT active (!isRacing) AND no winner has been declared (winner === "")—this means we're in pre-race state
if (countdownTimer === 0) {
If countdownTimer is 0, no countdown is running yet, so a click should start one
countdownTimer = frameCount;
Sets countdownTimer to the current frame number to mark when the countdown started
countdownValue = 3;
Ensures the countdown begins at 3 (in case countdownValue was left in an unexpected state)
} else if (!isRacing && winner !== "") {
If the race is NOT active AND a winner HAS been declared, we're in post-race state
resetRace();
Calls resetRace() to clear the winner, reset positions, and prepare for a new game

windowResized()

windowResized() is automatically called by p5.js whenever the browser window is resized. Without this function, the canvas wouldn't adjust, creating a poor experience. By calling resetRace() here, the code ensures the game stays playable even if the window is resized mid-game.

function windowResized() {
  // Make the canvas full screen on resize too
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize airplane positions and race state for a fresh start
  resetRace();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions when the browser is resized
resetRace();
Calls resetRace() to reinitialize everything after the resize, ensuring positions are still valid for the new canvas size

📦 Key Variables

airplane1X number

Stores the horizontal position of airplane 1 on the canvas; updated every frame during races

let airplane1X = -150;
airplane2X number

Stores the horizontal position of airplane 2 on the canvas; updated every frame during races

let airplane2X = -150;
airplane1Speed number

Tracks how many pixels airplane 1 moves per frame; increases with acceleration, decreases with friction

let airplane1Speed = 0;
airplane2Speed number

Tracks how many pixels airplane 2 moves per frame; increases with acceleration, decreases with friction

let airplane2Speed = 0;
maxSpeed number

The maximum speed airplanes can reach; prevents unlimited acceleration

const maxSpeed = 10;
accelerationRate number

How much speed increases per frame when a player holds their key

const accelerationRate = 0.2;
frictionRate number

How much speed decreases per frame due to air resistance, slowing planes when no key is held

const frictionRate = 0.1;
isRacing boolean

Tracks whether the race is currently active; when true, physics updates run; when false, instructions or winner message displays

let isRacing = false;
winner string

Stores the name of the winner once a race finishes; empty string ("") if no winner yet

let winner = "";
countdownValue number

The current countdown number (3, 2, 1, 0); displayed on screen before races

let countdownValue = 3;
countdownTimer number

Stores the frameCount when the countdown started; used to measure how many frames have passed for each number

let countdownTimer = 0;
countdownDuration number

How many frames each countdown number (3, 2, 1) displays; 60 frames ≈ 1 second at 60 fps

const countdownDuration = 60;
player1Key string

The keyboard key that player 1 presses to accelerate their airplane

const player1Key = 'Q';
player2Key string

The keyboard key that player 2 presses to accelerate their airplane

const player2Key = 'P';
player1IsAccelerating boolean

Flag that is true when player 1's key is held and false when released; tells draw() to apply acceleration

let player1IsAccelerating = false;
player2IsAccelerating boolean

Flag that is true when player 2's key is held and false when released; tells draw() to apply acceleration

let player2IsAccelerating = false;
dayProgress number

A value from 0 to 1 that represents progress through the day-night cycle (0=dawn, 0.5=day, 1=night)

let dayProgress = 0;
daySpeed number

How much dayProgress increases each frame; controls the speed of the day-night cycle

const daySpeed = 0.0005;
airplaneSize1 number

Scale multiplier for airplane 1; 0.8 means it's 80% of the base size

const airplaneSize1 = 0.8;
airplaneSize2 number

Scale multiplier for airplane 2; 0.6 means it's 60% of the base size

const airplaneSize2 = 0.6;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() countdown logic

Countdown can be restarted mid-sequence by clicking again, breaking the timing of the 3-2-1 sequence

💡 Add a third condition in mousePressed() to ignore clicks when a countdown is already running: check if (countdownTimer !== 0) && (countdownValue > 0) to block interruptions

STYLE draw() game state display

Text size is set to 32 globally but different messages need different sizes (32 for instructions, 100 for countdown), leading to repeated textSize() calls that are easy to forget

💡 Create helper variables like let instructionSize = 32; let countdownSize = 100; and set them in setup() to avoid magic numbers scattered throughout draw()

PERFORMANCE setGradient() function

Drawing a line for every pixel row (height iterations) is inefficient—if the canvas is 1080px tall, 1080 lines are drawn every frame

💡 Use createGraphics() to draw the gradient once and cache it, or use canvas pixel manipulation, or reduce iteration by drawing lines every 2-4 pixels and letting p5 interpolate between them

FEATURE draw() airplane movement

No visual feedback when an airplane reaches max speed—players might think they're accelerating when they're actually capped

💡 Add a check in draw() that displays a message or visual indicator (like color change) when player1Speed === maxSpeed or player2Speed === maxSpeed

BUG draw() countdown logic

If countdownDuration is changed but the code is running, the countdown might skip numbers or behave erratically because countdownValue is decremented before checking if it's 0

💡 Restructure the countdown: move the countdownValue === 0 check before the decrement, or ensure countdownValue never goes below 0 with a guard clause

STYLE mousePressed() function

Logic is hard to follow with nested if-else statements; a switch statement or separate handler functions would be clearer

💡 Create separate functions like startCountdown() and restartRace() and call them based on game state, making mousePressed() a cleaner dispatcher

🔄 Code Flow

Code flow showing setgradient, drawairplaneshape, setup, resetrace, draw, keypressed, keyreleased, mousepressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> skygradient[sky-gradient-render] skygradient --> gradientloop[gradient-loop] gradientloop --> colorinterp[color-interpolation] draw --> airplane1[airplane1-draw] airplane1 --> body[airplane-body] airplane1 --> wings[airplane-wings] airplane1 --> tail[airplane-tail] draw --> airplane2[airplane2-draw] airplane2 --> body2[airplane-body] airplane2 --> wings2[airplane-wings] airplane2 --> tail2[airplane-tail] draw --> racing[racing-logic] racing --> keycheck[key-check] racing --> speedreset[speed-reset] racing --> positionreset[position-reset] draw --> countdown[countdown-logic] countdown --> countdownstart[countdown-start] countdown --> countdownreset[countdown-reset] click setup href "#fn-setup" click draw href "#fn-draw" click skygradient href "#sub-sky-gradient-render" click gradientloop href "#sub-gradient-loop" click colorinterp href "#sub-color-interpolation" click airplane1 href "#sub-airplane1-draw" click body href "#sub-airplane-body" click wings href "#sub-airplane-wings" click tail href "#sub-airplane-tail" click airplane2 href "#sub-airplane2-draw" click body2 href "#sub-airplane-body" click wings2 href "#sub-airplane-wings" click tail2 href "#sub-airplane-tail" click racing href "#sub-racing-logic" click keycheck href "#sub-key-check" click speedreset href "#sub-speed-reset" click positionreset href "#sub-position-reset" click countdown href "#sub-countdown-logic" click countdownstart href "#sub-countdown-start" click countdownreset href "#sub-countdown-reset"

❓ Frequently Asked Questions

What visual elements are featured in the fly and race 2 player sketch?

The sketch visually represents two airplanes racing against each other, with a dynamic day-night cycle creating an engaging background atmosphere.

How can players interact with the fly and race 2 player sketch?

Users can control the acceleration of their respective airplanes using the 'Q' key for Player 1 and the 'P' key for Player 2, aiming to win the race.

What creative coding concepts does the fly and race 2 player sketch showcase?

This sketch demonstrates concepts such as object motion dynamics, user input handling, and the implementation of a day-night cycle through gradual color transitions.

Preview

fly and race 2 player - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of fly and race 2 player - Code flow showing setgradient, drawairplaneshape, setup, resetrace, draw, keypressed, keyreleased, mousepressed, windowresized
Code Flow Diagram