red vs blue

This sketch creates a two-player capture-the-flag game where a red player and blue player compete to steal each other's flags and return them to their own bases to score points. Players move with keyboard controls, pick up flags on collision, and can tag opponents to force them to drop the flag and respawn.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the game twice as fast — Players move faster when keys are held down, making the game more action-packed and skillful
  2. Make flags much bigger and easier to grab — Larger flags are easier to see and pick up, making gameplay more forgiving and accessible
  3. Switch player controls from WASD and arrows to both using arrow keys — Both players can now use arrow keys instead of one using WASD and one using arrows—easier for teaching or shared controllers
  4. Make bases twice as tall (easier to score) — Taller bases give a bigger target zone for returning the flag, making scoring less precise but faster
  5. Make players smaller and harder to tag — Smaller players are harder to see and tag, shifting the game balance toward flag carriers and away from defenders
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch implements a playable capture-the-flag game for two players on the same keyboard. Red controls use W/A/S/D while Blue uses arrow keys, and the goal is to steal the opponent's flag and return it to your base to score. The game combines several advanced p5.js concepts: custom classes to represent game objects (Player, Flag, Base), distance-based collision detection with dist(), circle-rectangle collision helpers, keyboard input handling with keyIsDown(), and state management to track which player is carrying which flag.

The code is organized into three custom classes that model real game entities—a Player that moves and carries flags, a Flag that can be picked up and dropped, and a Base that scores points when enemies return their flags. By reading this sketch you will learn how to structure a game with multiple objects that interact with each other, how collision detection enables core game mechanics like picking up items and tagging opponents, and how to track and display game state like scores.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas, places two bases (red on the left, blue on the right) with their flags positioned above each base, and spawns two players—red at the bottom of the red base, blue at the bottom of the blue base.
  2. Every frame, draw() clears the background and displays all game objects: the two bases, flags, and players.
  3. The move() method on each player reads keyboard input with keyIsDown() and updates the player's x and y position, then constrains them within the canvas boundaries to prevent escape.
  4. Flag objects update their position to follow their carrier if they are being held, otherwise they stay put or return to their original spawn location.
  5. Game logic checks for three types of collisions: if an uncarried flag touches an enemy player, that player picks it up and hasFlag becomes true; if a player carrying the opponent's flag reaches their own base, their score increases and all flags reset; if two players collide, the one carrying a flag is tagged and forced to respawn, dropping their flag back at the opponent's base.
  6. At the top of the screen, the current scores for each team update in real-time as points are scored.

🎓 Concepts You'll Learn

Object-oriented programming with classesCollision detectionKeyboard input and continuous movementGame state managementPlayer respawning and item carrying

📝 Code Breakdown

Player constructor()

The constructor is called once per player in setup(). It stores all the data about that player as properties (this.x, this.team, etc.). This data persists across frames and gets updated by move(), display(), and game logic in draw().

  constructor(x, y, teamColor, controls) {
    this.x = x;
    this.y = y;
    this.size = 30; // Player circle size
    this.speed = 4; // Player movement speed
    this.team = teamColor;
    this.hasFlag = false;
    this.flagCarried = null; // Reference to the flag object if carrying
    this.controls = controls; // Object like { up: 'W', down: 'S', left: 'A', right: 'D' }
    this.originalX = x;
    this.originalY = y;
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Position initialization this.x = x; this.y = y;

Stores the player's starting position on the canvas

assignment Team assignment this.team = teamColor;

Stores the player's team color (red or blue) used for display

assignment Flag carrying state this.hasFlag = false; this.flagCarried = null;

Tracks whether this player is carrying a flag and which one

assignment Control mapping this.controls = controls;

Stores the keyboard key mappings (W/A/S/D for red, arrows for blue)

constructor(x, y, teamColor, controls) {
Constructor function that runs when a new Player object is created; receives spawn position, team color, and keyboard controls
this.x = x;
Stores the player's horizontal position; this.x will be updated each frame to create movement
this.y = y;
Stores the player's vertical position; will be updated each frame to create movement
this.size = 30;
Stores the player's visual diameter in pixels; used for drawing and collision detection
this.speed = 4;
Stores how many pixels the player moves per frame when a movement key is pressed
this.team = teamColor;
Stores the player's team color (passed in from setup); used to draw them the correct color and identify which flag they can score
this.hasFlag = false;
Boolean flag that starts as false; becomes true when the player picks up the opponent's flag
this.flagCarried = null;
Stores a reference to the actual Flag object this player is carrying; null means they are not carrying anything
this.controls = controls;
Stores the control mapping object (e.g., {up: 'W', down: 'S'} for red) so move() knows which keys control this player
this.originalX = x;
Saves the spawn position so respawn() can send the player back to their base when tagged or after scoring
this.originalY = y;
Saves the spawn position's y-coordinate for the same respawn purpose

move()

move() is called once per frame for each player. It reads keyboard state with keyIsDown() (which only works for certain keys) and updates position before constraining. This is called BEFORE display(), so the player is always drawn at their updated position.

🔬 These four lines handle movement for all four directions. What happens if you change this.speed to this.speed * 2 in one of them? Can you make the player move faster horizontally than vertically?

    // Check for continuous movement based on controls
    if (keyIsDown(this.controls.up)) this.y -= this.speed;
    if (keyIsDown(this.controls.down)) this.y += this.speed;
    if (keyIsDown(this.controls.left)) this.x -= this.speed;
    if (keyIsDown(this.controls.right)) this.x += this.speed;
  move() {
    // Check for continuous movement based on controls
    if (keyIsDown(this.controls.up)) this.y -= this.speed;
    if (keyIsDown(this.controls.down)) this.y += this.speed;
    if (keyIsDown(this.controls.left)) this.x -= this.speed;
    if (keyIsDown(this.controls.right)) this.x += this.speed;

    // Keep player within canvas bounds
    this.x = constrain(this.x, this.size / 2, width - this.size / 2);
    this.y = constrain(this.y, this.size / 2, height - this.size / 2);
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Keyboard input handling if (keyIsDown(this.controls.up)) this.y -= this.speed; if (keyIsDown(this.controls.down)) this.y += this.speed; if (keyIsDown(this.controls.left)) this.x -= this.speed; if (keyIsDown(this.controls.right)) this.x += this.speed;

Checks which keys are currently held down and updates position accordingly

calculation Boundary constraint this.x = constrain(this.x, this.size / 2, width - this.size / 2); this.y = constrain(this.y, this.size / 2, height - this.size / 2);

Prevents the player from moving outside the canvas by clamping x and y to valid ranges

if (keyIsDown(this.controls.up)) this.y -= this.speed;
keyIsDown() returns true if a key is currently held; if the 'up' key for this player is down, subtract speed from y (moving up means smaller y in p5.js)
if (keyIsDown(this.controls.down)) this.y += this.speed;
If the 'down' key is held, add speed to y to move downward
if (keyIsDown(this.controls.left)) this.x -= this.speed;
If the 'left' key is held, subtract speed from x to move left
if (keyIsDown(this.controls.right)) this.x += this.speed;
If the 'right' key is held, add speed to x to move right
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
constrain() clamps x between two bounds: the player's radius (size/2) on the left and canvas width minus radius on the right, preventing escape
this.y = constrain(this.y, this.size / 2, height - this.size / 2);
Same clamping for y: between the player's radius on top and canvas height minus radius on the bottom

display()

display() is called once per frame for each player. It draws the player as a colored circle, and if they are carrying a flag, adds a small square badge in their team's opponent's color. This visual feedback is crucial for the player to understand the game state at a glance.

  display() {
    noStroke();
    fill(this.team);
    ellipse(this.x, this.y, this.size); // Draw player as a circle

    if (this.hasFlag) {
      // Draw a small square indicator on the player to show they have a flag
      fill(this.flagCarried.team);
      rectMode(CENTER);
      rect(this.x + this.size / 2, this.y - this.size / 2, 10, 10);
    }
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Draw player circle fill(this.team); ellipse(this.x, this.y, this.size);

Fills with the player's team color and draws a circle at their current position

conditional Flag indicator on carrier if (this.hasFlag) { fill(this.flagCarried.team); rectMode(CENTER); rect(this.x + this.size / 2, this.y - this.size / 2, 10, 10); }

If the player is carrying a flag, draws a small colored square on their circle to show what flag they have

noStroke();
Disables outlines for shapes, so the player circle and flag indicator will be solid colors
fill(this.team);
Sets the fill color to this player's team color (red or blue)
ellipse(this.x, this.y, this.size);
Draws a circle centered at the player's current (x, y) position with diameter equal to this.size
if (this.hasFlag) {
Checks if this player is currently carrying a flag; only draw the indicator if true
fill(this.flagCarried.team);
Changes fill color to the color of the flag being carried (the opponent's team color)
rectMode(CENTER);
Sets rectMode to CENTER so the following rect() is drawn centered at the given coordinates (not from the top-left)
rect(this.x + this.size / 2, this.y - this.size / 2, 10, 10);
Draws a 10x10 pixel square at the top-right of the player's circle to visually indicate they are carrying a flag

respawn()

respawn() is called in two game situations: when a player is tagged by the opponent (in the player vs player collision check), or after they successfully score. It teleports them back to their base's spawn point.

  // Resets player to their original base position and drops flag if carrying
  respawn() {
    this.x = this.originalX;
    this.y = this.originalY;
    if (this.hasFlag) {
      this.dropFlag();
    }
  }
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Reset position this.x = this.originalX; this.y = this.originalY;

Moves the player back to their spawn position at their team's base

conditional Conditional flag drop if (this.hasFlag) { this.dropFlag(); }

If the player is carrying a flag when respawning, calls dropFlag() to place it back at the opponent's base

this.x = this.originalX;
Resets the player's x position to the original spawn location saved in the constructor
this.y = this.originalY;
Resets the player's y position to the original spawn location
if (this.hasFlag) {
Checks if the player is carrying a flag; only drop it if they have one
this.dropFlag();
Calls the dropFlag() method to place the carried flag at the player's current position before respawning

dropFlag()

dropFlag() synchronizes the state of both the Player and Flag objects. It breaks the carrying relationship by setting two booleans and two references to null, then repositions the flag. This is called when a player is tagged or respawns while carrying a flag.

  // Drops the carried flag at the player's current position
  dropFlag() {
    if (this.hasFlag) {
      this.flagCarried.isCarried = false;
      this.flagCarried.carrier = null;
      // Flag is dropped at current player position
      this.flagCarried.x = this.x;
      this.flagCarried.y = this.y;
      this.hasFlag = false;
    }
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Flag state update if (this.hasFlag) { this.flagCarried.isCarried = false; this.flagCarried.carrier = null; this.flagCarried.x = this.x; this.flagCarried.y = this.y; this.hasFlag = false; }

Updates both the player and flag objects to break the carrying relationship and position the flag on the ground

if (this.hasFlag) {
Checks if this player is actually carrying a flag before attempting to drop it
this.flagCarried.isCarried = false;
Updates the flag's isCarried property so it knows it is no longer being held
this.flagCarried.carrier = null;
Clears the flag's carrier reference; the flag no longer knows which player was holding it
this.flagCarried.x = this.x;
Sets the flag's x position to the player's current x, dropping it where the player currently is
this.flagCarried.y = this.y;
Sets the flag's y position to the player's current y
this.hasFlag = false;
Updates the player's hasFlag to false so they are no longer marked as carrying anything

checkPlayerCollision()

This method uses dist(), one of the most important p5.js functions for interactive sketches. Any game mechanic that requires 'are these two things close?' uses this pattern: calculate distance, compare to a threshold.

🔬 This function returns true when players touch. What if you multiply the collision threshold by 2, like d < (this.size / 2 + otherPlayer.size / 2) * 2? Players can tag from farther away—is that more or less fair?

  checkPlayerCollision(otherPlayer) {
    let d = dist(this.x, this.y, otherPlayer.x, otherPlayer.y);
    return d < (this.size / 2 + otherPlayer.size / 2);
  }
  // Checks if this player is colliding with another player
  checkPlayerCollision(otherPlayer) {
    let d = dist(this.x, this.y, otherPlayer.x, otherPlayer.y);
    return d < (this.size / 2 + otherPlayer.size / 2);
  }
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Distance calculation let d = dist(this.x, this.y, otherPlayer.x, otherPlayer.y);

Calculates the straight-line distance between the two players' centers using the p5.js dist() function

conditional Collision threshold check return d < (this.size / 2 + otherPlayer.size / 2);

Returns true if the distance is less than the sum of both players' radii (collision), false otherwise

let d = dist(this.x, this.y, otherPlayer.x, otherPlayer.y);
dist() is a p5.js function that calculates the Euclidean distance between two points; here it measures the gap between the centers of the two players
return d < (this.size / 2 + otherPlayer.size / 2);
If d is smaller than the sum of both radii, the circles overlap, so return true; otherwise return false. This is the core of circle-circle collision detection.

Flag constructor()

Like Player, Flag has a constructor that initializes all its properties. The key pattern here is storing the original position so reset() can use it, and maintaining a carrier reference so updatePosition() knows which player to follow.

  constructor(x, y, teamColor) {
    this.originalX = x;
    this.originalY = y;
    this.x = x;
    this.y = y;
    this.size = 20; // Flag square size
    this.team = teamColor;
    this.isCarried = false;
    this.carrier = null; // Reference to player carrying it
  }
Line-by-line explanation (8 lines)
this.originalX = x;
Saves the flag's spawn position (its base location) so reset() can return it there later
this.originalY = y;
Saves the flag's spawn y-coordinate
this.x = x;
Sets the flag's current x position to the spawn point
this.y = y;
Sets the flag's current y position to the spawn point
this.size = 20;
The flag is drawn as a 20x20 pixel square
this.team = teamColor;
Stores the flag's team color (red or blue) for display and identification
this.isCarried = false;
Starts as false; becomes true when a player picks it up
this.carrier = null;
Starts as null; stores a reference to the player carrying the flag when isCarried is true

Flag display()

display() draws the flag as a colored square. If the flag is being carried, its x and y will be updated each frame by updatePosition() to follow the carrier, so the flag will appear to move with the player.

  display() {
    noStroke();
    fill(this.team);
    rectMode(CENTER);
    rect(this.x, this.y, this.size, this.size); // Draw flag as a square
  }
Line-by-line explanation (4 lines)
noStroke();
Removes the outline so the flag square appears solid
fill(this.team);
Sets the fill color to the flag's team color (red for red flag, blue for blue flag)
rectMode(CENTER);
Sets rectangle drawing mode to CENTER so the rectangle is centered at (x, y) instead of drawn from the top-left corner
rect(this.x, this.y, this.size, this.size);
Draws a square at the flag's current position with width and height both equal to this.size (20 pixels)

updatePosition()

updatePosition() is called every frame in draw(). Because it always copies the carrier's coordinates, the flag smoothly follows the player without any extra code needed in the game logic.

🔬 Right now the flag is drawn exactly at the carrier's position. What if you offset it slightly, like this.x = this.carrier.x + 20? The flag would float to the side of the carrier instead of being centered on them.

  updatePosition() {
    if (this.isCarried && this.carrier) {
      this.x = this.carrier.x;
      this.y = this.carrier.y;
    }
  }
  // Updates flag position to follow its carrier
  updatePosition() {
    if (this.isCarried && this.carrier) {
      this.x = this.carrier.x;
      this.y = this.carrier.y;
    }
  }
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Follow carrier if (this.isCarried && this.carrier) { this.x = this.carrier.x; this.y = this.carrier.y; }

If the flag is being carried, constantly updates its position to match the carrier's position each frame

if (this.isCarried && this.carrier) {
Checks two conditions: isCarried is true AND carrier is not null. Both must be true to prevent errors.
this.x = this.carrier.x;
Sets the flag's x position to the carrier's x position so it stays with the player horizontally
this.y = this.carrier.y;
Sets the flag's y position to the carrier's y position so it stays with the player vertically

Flag reset()

reset() is called after a successful score or when a player is tagged while carrying the flag. It returns the flag to the opponent's base and breaks the carrying relationship.

  // Resets flag to its original base position
  reset() {
    this.x = this.originalX;
    this.y = this.originalY;
    this.isCarried = false;
    this.carrier = null;
  }
Line-by-line explanation (4 lines)
this.x = this.originalX;
Moves the flag back to its original spawn x position at the base
this.y = this.originalY;
Moves the flag back to its original spawn y position
this.isCarried = false;
Sets isCarried to false so the flag is no longer marked as being held
this.carrier = null;
Clears the carrier reference so the flag forgets who was holding it

Flag checkPlayerCollision()

This method uses the exact same collision formula as Player.checkPlayerCollision(). It is called in draw() to check if the red player touches the blue flag or vice versa.

  // Checks if a player is colliding with this flag
  checkPlayerCollision(player) {
    let d = dist(this.x, this.y, player.x, player.y);
    return d < (this.size / 2 + player.size / 2);
  }
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Distance to player let d = dist(this.x, this.y, player.x, player.y);

Calculates the distance between the flag and a player

conditional Flag collision detection return d < (this.size / 2 + player.size / 2);

Returns true if the distance is less than the sum of the flag and player radii (they are touching)

let d = dist(this.x, this.y, player.x, player.y);
dist() measures the gap between the flag's center and the player's center
return d < (this.size / 2 + player.size / 2);
Returns true if the distance is less than the combined radii, meaning the flag square and player circle are overlapping

Base constructor()

Base is the simplest of the three game object classes. It stores a rectangle's position, size, and color. It doesn't move or change state—it is just a scoring zone.

  constructor(x, y, w, h, teamColor) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.team = teamColor;
  }
Line-by-line explanation (5 lines)
this.x = x;
Stores the base's center x position
this.y = y;
Stores the base's center y position
this.w = w;
Stores the base's width
this.h = h;
Stores the base's height
this.team = teamColor;
Stores the base's team color (a semi-transparent red or blue)

Base display()

display() draws the base and a label. The ternary operator (condition ? trueValue : falseValue) is a compact way to choose between two options based on a condition.

  display() {
    noStroke();
    fill(this.team);
    rectMode(CENTER);
    rect(this.x, this.y, this.w, this.h); // Draw base as a rectangle
    fill(255);
    textAlign(CENTER, CENTER);
    textSize(20);
    text(this.team === color(255, 0, 0, 100) ? 'RED BASE' : 'BLUE BASE', this.x, this.y);
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Draw base rectangle fill(this.team); rectMode(CENTER); rect(this.x, this.y, this.w, this.h);

Fills with a semi-transparent team color and draws the base as a centered rectangle

conditional Draw base label fill(255); textAlign(CENTER, CENTER); textSize(20); text(this.team === color(255, 0, 0, 100) ? 'RED BASE' : 'BLUE BASE', this.x, this.y);

Determines which team owns the base and draws the appropriate text label at its center

noStroke();
Disables outlines
fill(this.team);
Sets the fill to the base's team color (semi-transparent red or blue)
rectMode(CENTER);
Sets rectangle mode to CENTER
rect(this.x, this.y, this.w, this.h);
Draws the base rectangle centered at (x, y) with width w and height h
fill(255);
Changes fill to white for the text
textAlign(CENTER, CENTER);
Centers the text both horizontally and vertically
textSize(20);
Sets the text size to 20 pixels
text(this.team === color(255, 0, 0, 100) ? 'RED BASE' : 'BLUE BASE', this.x, this.y);
Uses a ternary operator: if team color matches the red color, draw 'RED BASE'; otherwise draw 'BLUE BASE'. This text is drawn at the base's center.

Base checkPlayerCollision()

This method delegates to the circleRectCollision() helper. It is called in draw() to detect when a player enters their own base (to score) or the opponent's base (no effect). The actual collision logic is separate, making the code more readable.

  // Checks if a player is inside this base (using a circle-rectangle collision helper)
  checkPlayerCollision(player) {
    return circleRectCollision(player.x, player.y, player.size / 2, this.x, this.y, this.w, this.h);
  }
Line-by-line explanation (1 lines)
return circleRectCollision(player.x, player.y, player.size / 2, this.x, this.y, this.w, this.h);
Calls the circleRectCollision() helper function to check if the player's circle is overlapping the base's rectangle, passing the player's position and radius, and the base's position and dimensions

circleRectCollision()

This is an advanced collision detection algorithm: separating axis theorem for circles and rectangles. It is not built into p5.js, so the sketch defines it as a helper. Understanding this function teaches you how to detect collisions between different shapes, which powers many games.

🔬 This code finds the closest point on the rectangle's edge to the circle's center. It works by clamping the circle's center coordinates to the rectangle's bounds. What would happen if you removed the 'else if' for the right edge, so a circle could only collide with the left or top edge?

  // Find the closest point on the rectangle to the center of the circle
  let testX = circleX;
  let testY = circleY;

  if (circleX < rectX - rectW / 2) testX = rectX - rectW / 2;      // left edge
  else if (circleX > rectX + rectW / 2) testX = rectX + rectW / 2; // right edge
  if (circleY < rectY - rectH / 2) testY = rectY - rectH / 2;      // top edge
  else if (circleY > rectY + rectH / 2) testY = rectY + rectH / 2; // bottom edge
// Helper function for circle-rectangle collision detection
// (Not part of p5.js core, so we define it here)
function circleRectCollision(circleX, circleY, circleRadius, rectX, rectY, rectW, rectH) {
  // Find the closest point on the rectangle to the center of the circle
  let testX = circleX;
  let testY = circleY;

  if (circleX < rectX - rectW / 2) testX = rectX - rectW / 2;      // left edge
  else if (circleX > rectX + rectW / 2) testX = rectX + rectW / 2; // right edge
  if (circleY < rectY - rectH / 2) testY = rectY - rectH / 2;      // top edge
  else if (circleY > rectY + rectH / 2) testY = rectY + rectH / 2; // bottom edge

  // Calculate the distance between the closest point and the circle's center
  let distX = circleX - testX;
  let distY = circleY - testY;
  let distance = sqrt((distX * distX) + (distY * distY));

  // Check if the distance is less than the circle's radius
  return distance <= circleRadius;
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Find closest rectangle point if (circleX < rectX - rectW / 2) testX = rectX - rectW / 2; else if (circleX > rectX + rectW / 2) testX = rectX + rectW / 2; if (circleY < rectY - rectH / 2) testY = rectY - rectH / 2; else if (circleY > rectY + rectH / 2) testY = rectY + rectH / 2;

Determines which edge or corner of the rectangle is closest to the circle's center

calculation Calculate distance let distX = circleX - testX; let distY = circleY - testY; let distance = sqrt((distX * distX) + (distY * distY));

Computes the straight-line distance from the circle's center to the closest point on the rectangle

conditional Collision test return distance <= circleRadius;

Returns true if the closest point is within the circle's radius (collision)

function circleRectCollision(circleX, circleY, circleRadius, rectX, rectY, rectW, rectH) {
Helper function that takes a circle's position and radius, and a rectangle's position and dimensions
let testX = circleX;
Initializes testX to the circle's x, which we will adjust if it is outside the rectangle
let testY = circleY;
Initializes testY to the circle's y
if (circleX < rectX - rectW / 2) testX = rectX - rectW / 2;
If the circle's center is to the left of the rectangle, set testX to the rectangle's left edge
else if (circleX > rectX + rectW / 2) testX = rectX + rectW / 2;
If the circle's center is to the right of the rectangle, set testX to the rectangle's right edge
if (circleY < rectY - rectH / 2) testY = rectY - rectH / 2;
If the circle's center is above the rectangle, set testY to the rectangle's top edge
else if (circleY > rectY + rectH / 2) testY = rectY + rectH / 2;
If the circle's center is below the rectangle, set testY to the rectangle's bottom edge
let distX = circleX - testX;
Calculates the horizontal distance from the circle center to the closest point
let distY = circleY - testY;
Calculates the vertical distance from the circle center to the closest point
let distance = sqrt((distX * distX) + (distY * distY));
Uses the Pythagorean theorem to calculate the straight-line distance
return distance <= circleRadius;
Returns true if the distance is less than or equal to the circle's radius, meaning the circle and rectangle are touching or overlapping

setup()

setup() runs once when the sketch starts. It creates the canvas and all game objects (bases, flags, players) and initializes them with starting positions. Because setup() uses width and height variables (which only exist after createCanvas() is called), we must call createCanvas() first.

🔬 Flags spawn 50 pixels above the base center. What if you change -50 to -150 so flags spawn much farther from the bases? Or change it to 0 so they spawn in the center of the bases?

  // Initialize flags at their bases (flags are opaque)
  redFlag = new Flag(baseWidth / 2, height / 2 - 50, color(255, 0, 0));
  blueFlag = new Flag(width - baseWidth / 2, height / 2 - 50, color(0, 0, 255));
function setup() {
  createCanvas(windowWidth, windowHeight);

  // Define player controls
  let redControls = { up: 'W', down: 'S', left: 'A', right: 'D' };
  let blueControls = { up: UP_ARROW, down: DOWN_ARROW, left: LEFT_ARROW, right: RIGHT_ARROW };

  // Initialize bases (transparent red/blue rectangles)
  let baseWidth = width * 0.2;
  let baseHeight = height * 0.4;
  redBase = new Base(baseWidth / 2, height / 2, baseWidth, baseHeight, color(255, 0, 0, 100));
  blueBase = new Base(width - baseWidth / 2, height / 2, baseWidth, baseHeight, color(0, 0, 255, 100));

  // Initialize flags at their bases (flags are opaque)
  redFlag = new Flag(baseWidth / 2, height / 2 - 50, color(255, 0, 0));
  blueFlag = new Flag(width - baseWidth / 2, height / 2 - 50, color(0, 0, 255));

  // Initialize players
  redPlayer = new Player(baseWidth / 2, height / 2 + 50, color(255, 0, 0), redControls);
  bluePlayer = new Player(width - baseWidth / 2, height / 2 + 50, color(0, 0, 255), blueControls);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Canvas creation createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire window

assignment Control definitions let redControls = { up: 'W', down: 'S', left: 'A', right: 'D' }; let blueControls = { up: UP_ARROW, down: DOWN_ARROW, left: LEFT_ARROW, right: RIGHT_ARROW };

Creates two objects that map keyboard keys to movement directions for each player

calculation Base dimensions let baseWidth = width * 0.2; let baseHeight = height * 0.4;

Calculates the base size as percentages of the canvas so they scale to any window size

assignment Base creation redBase = new Base(baseWidth / 2, height / 2, baseWidth, baseHeight, color(255, 0, 0, 100)); blueBase = new Base(width - baseWidth / 2, height / 2, baseWidth, baseHeight, color(0, 0, 255, 100));

Creates two bases: red on the left side, blue on the right side, positioned vertically in the center

assignment Flag creation redFlag = new Flag(baseWidth / 2, height / 2 - 50, color(255, 0, 0)); blueFlag = new Flag(width - baseWidth / 2, height / 2 - 50, color(0, 0, 255));

Creates two flags at their respective bases, positioned 50 pixels above the base center

assignment Player creation redPlayer = new Player(baseWidth / 2, height / 2 + 50, color(255, 0, 0), redControls); bluePlayer = new Player(width - baseWidth / 2, height / 2 + 50, color(0, 0, 255), blueControls);

Creates two players at their respective bases, positioned 50 pixels below the base center

createCanvas(windowWidth, windowHeight);
Creates a canvas that stretches to fill the entire window, making the game responsive
let redControls = { up: 'W', down: 'S', left: 'A', right: 'D' };
Defines an object mapping movement directions to keys for the red player (WASD layout)
let blueControls = { up: UP_ARROW, down: DOWN_ARROW, left: LEFT_ARROW, right: RIGHT_ARROW };
Defines an object mapping movement directions to arrow keys for the blue player
let baseWidth = width * 0.2;
Calculates the base width as 20% of the canvas width, making bases scale with window size
let baseHeight = height * 0.4;
Calculates the base height as 40% of the canvas height
redBase = new Base(baseWidth / 2, height / 2, baseWidth, baseHeight, color(255, 0, 0, 100));
Creates the red base on the left side of the canvas at x = baseWidth / 2, centered vertically, with semi-transparent red color (the 100 is the alpha/transparency value)
blueBase = new Base(width - baseWidth / 2, height / 2, baseWidth, baseHeight, color(0, 0, 255, 100));
Creates the blue base on the right side of the canvas at x = width - baseWidth / 2, also centered vertically, with semi-transparent blue
redFlag = new Flag(baseWidth / 2, height / 2 - 50, color(255, 0, 0));
Creates the red flag at the red base's x position, but 50 pixels ABOVE the base's vertical center (negative offset means up)
blueFlag = new Flag(width - baseWidth / 2, height / 2 - 50, color(0, 0, 255));
Creates the blue flag at the blue base's x position, also 50 pixels above its base center
redPlayer = new Player(baseWidth / 2, height / 2 + 50, color(255, 0, 0), redControls);
Creates the red player at the red base's x position, 50 pixels BELOW the base center, and assigns redControls to them
bluePlayer = new Player(width - baseWidth / 2, height / 2 + 50, color(0, 0, 255), blueControls);
Creates the blue player at the blue base's x position, 50 pixels below the base center, and assigns blueControls

draw()

draw() runs 60 times per second and is the game's main loop. It handles all visual updates and game logic: moving players, displaying objects, checking collisions, scoring, and respawning. The order matters: we move and display objects first, then check game logic, then display the HUD (heads-up display) like scores.

🔬 This block handles red picking up blue's flag. What happens if you remove the && !blueFlag.isCarried check? Can red player pick up the blue flag even if blue is already carrying it?

  // Check Red Player interactions
  if (!redPlayer.hasFlag) { // Red player does not have a flag
    // Check if Red player collides with Blue flag
    if (blueFlag.checkPlayerCollision(redPlayer) && !blueFlag.isCarried) {
      redPlayer.hasFlag = true;
      redPlayer.flagCarried = blueFlag;
      blueFlag.isCarried = true;
      blueFlag.carrier = redPlayer;
      print("Red player picked up blue flag!");
    }
  }
function draw() {
  background(220); // Light gray background

  // Display bases
  redBase.display();
  blueBase.display();

  // Display and update flags
  redFlag.display();
  blueFlag.display();
  redFlag.updatePosition();
  blueFlag.updatePosition();

  // Move and display players
  redPlayer.move();
  bluePlayer.move();
  redPlayer.display();
  bluePlayer.display();

  // --- Game Logic ---

  // Check Red Player interactions
  if (!redPlayer.hasFlag) { // Red player does not have a flag
    // Check if Red player collides with Blue flag
    if (blueFlag.checkPlayerCollision(redPlayer) && !blueFlag.isCarried) {
      redPlayer.hasFlag = true;
      redPlayer.flagCarried = blueFlag;
      blueFlag.isCarried = true;
      blueFlag.carrier = redPlayer;
      print("Red player picked up blue flag!");
    }
  } else { // Red player is carrying a flag
    // Check if Red player returns Blue flag to Red base
    if (redBase.checkPlayerCollision(redPlayer)) {
      redScore++;
      print("Red player scored! Score: Red " + redScore + ", Blue " + blueScore);
      blueFlag.reset(); // Reset blue flag to its base
      redPlayer.respawn(); // Respawn red player after scoring
    }
  }

  // Check Blue Player interactions (same logic as Red player, but reversed)
  if (!bluePlayer.hasFlag) { // Blue player does not have a flag
    // Check if Blue player collides with Red flag
    if (redFlag.checkPlayerCollision(bluePlayer) && !redFlag.isCarried) {
      bluePlayer.hasFlag = true;
      bluePlayer.flagCarried = redFlag;
      redFlag.isCarried = true;
      redFlag.carrier = bluePlayer;
      print("Blue player picked up red flag!");
    }
  } else { // Blue player is carrying a flag
    // Check if Blue player returns Red flag to Blue base
    if (blueBase.checkPlayerCollision(bluePlayer)) {
      blueScore++;
      print("Blue player scored! Score: Red " + redScore + ", Blue " + blueScore);
      redFlag.reset(); // Reset red flag to its base
      bluePlayer.respawn(); // Respawn blue player after scoring
    }
  }

  // Check Player vs Player Tagging
  // If players collide:
  if (redPlayer.checkPlayerCollision(bluePlayer)) {
    // If Red player was carrying a flag, they drop it and respawn
    if (redPlayer.hasFlag) {
      blueFlag.reset(); // Blue flag returns to its base
      redPlayer.respawn(); // Red player respawns
      print("Blue player tagged red player! Blue flag returned.");
    }
    // If Blue player was carrying a flag, they drop it and respawn
    if (bluePlayer.hasFlag) {
      redFlag.reset(); // Red flag returns to its base
      bluePlayer.respawn(); // Blue player respawns
      print("Red player tagged blue player! Red flag returned.");
    }
  }

  // Display Score
  fill(0); // Black text
  textSize(24);
  textAlign(LEFT, TOP);
  text("Red Score: " + redScore, 10, 10);
  textAlign(RIGHT, TOP);
  text("Blue Score: " + blueScore, width - 10, 10);
}
Line-by-line explanation (34 lines)

🔧 Subcomponents:

calculation Clear and display game objects background(220); redBase.display(); blueBase.display(); redFlag.display(); blueFlag.display(); redFlag.updatePosition(); blueFlag.updatePosition(); redPlayer.move(); bluePlayer.move(); redPlayer.display(); bluePlayer.display();

Clears the canvas and draws all game objects each frame

conditional Red player game logic if (!redPlayer.hasFlag) { if (blueFlag.checkPlayerCollision(redPlayer) && !blueFlag.isCarried) { redPlayer.hasFlag = true; redPlayer.flagCarried = blueFlag; blueFlag.isCarried = true; blueFlag.carrier = redPlayer; print("Red player picked up blue flag!"); } } else { if (redBase.checkPlayerCollision(redPlayer)) { redScore++; print("Red player scored! Score: Red " + redScore + ", Blue " + blueScore); blueFlag.reset(); redPlayer.respawn(); } }

Checks if red player picks up the blue flag or if they score by returning it to the red base

conditional Blue player game logic if (!bluePlayer.hasFlag) { if (redFlag.checkPlayerCollision(bluePlayer) && !redFlag.isCarried) { bluePlayer.hasFlag = true; bluePlayer.flagCarried = redFlag; redFlag.isCarried = true; redFlag.carrier = bluePlayer; print("Blue player picked up red flag!"); } } else { if (blueBase.checkPlayerCollision(bluePlayer)) { blueScore++; print("Blue player scored! Score: Red " + redScore + ", Blue " + blueScore); redFlag.reset(); bluePlayer.respawn(); } }

Same logic as red player but for the blue player and red flag

conditional Player vs player tagging if (redPlayer.checkPlayerCollision(bluePlayer)) { if (redPlayer.hasFlag) { blueFlag.reset(); redPlayer.respawn(); print("Blue player tagged red player! Blue flag returned."); } if (bluePlayer.hasFlag) { redFlag.reset(); bluePlayer.respawn(); print("Red player tagged blue player! Red flag returned."); } }

If two players collide, any flag being carried is dropped back at its base and the flag carrier respawns

calculation Score display fill(0); textSize(24); textAlign(LEFT, TOP); text("Red Score: " + redScore, 10, 10); textAlign(RIGHT, TOP); text("Blue Score: " + blueScore, width - 10, 10);

Displays the current score for both teams in the top corners of the screen

background(220);
Clears the entire canvas with a light gray color, erasing all previous frames' drawings
redBase.display();
Calls the display method of the red base to draw it
blueBase.display();
Calls the display method of the blue base
redFlag.display();
Draws the red flag at its current position (which may be in a player's hands or on the ground)
blueFlag.display();
Draws the blue flag
redFlag.updatePosition();
Updates the red flag's position to follow its carrier if it is being held; otherwise it stays put
blueFlag.updatePosition();
Updates the blue flag's position
redPlayer.move();
Calls move() on the red player to update their position based on currently held keys
bluePlayer.move();
Calls move() on the blue player
redPlayer.display();
Draws the red player as a circle at their current position
bluePlayer.display();
Draws the blue player
if (!redPlayer.hasFlag) {
Checks if red player is NOT carrying a flag (hasFlag is false)
if (blueFlag.checkPlayerCollision(redPlayer) && !blueFlag.isCarried) {
Checks two conditions: red player is touching the blue flag AND the blue flag is not already being carried
redPlayer.hasFlag = true;
Sets red player's hasFlag to true so they are marked as carrying
redPlayer.flagCarried = blueFlag;
Stores a reference to the blue flag object in red player's flagCarried property
blueFlag.isCarried = true;
Updates the flag to mark it as being carried
blueFlag.carrier = redPlayer;
Stores a reference to red player in the flag's carrier property so updatePosition() knows who to follow
print("Red player picked up blue flag!");
Prints a debug message to the browser console
} else {
If red player IS carrying a flag, execute this block instead
if (redBase.checkPlayerCollision(redPlayer)) {
Checks if red player is inside their own base (red base)
redScore++;
Increments red score by 1
print("Red player scored! Score: Red " + redScore + ", Blue " + blueScore);
Prints the updated score to the console
blueFlag.reset();
Calls reset() on the blue flag to return it to its spawn position at the blue base
redPlayer.respawn();
Calls respawn() on red player to teleport them back to the red base
if (redPlayer.checkPlayerCollision(bluePlayer)) {
Checks if the two players are touching
if (redPlayer.hasFlag) {
If red player is carrying a flag, they are tagged
blueFlag.reset();
Returns the blue flag to its base
redPlayer.respawn();
Respawns the red player
fill(0);
Sets text color to black
textSize(24);
Sets text size to 24 pixels
textAlign(LEFT, TOP);
Aligns text to the left and top for the next text() call
text("Red Score: " + redScore, 10, 10);
Draws the red score text at position (10, 10) in the top-left corner, concatenating the string with the current redScore value
textAlign(RIGHT, TOP);
Aligns text to the right and top for the next text() call
text("Blue Score: " + blueScore, width - 10, 10);
Draws the blue score text at position (width - 10, 10) in the top-right corner

windowResized()

windowResized() is a built-in p5.js callback that runs whenever the user resizes their browser window. By calling setup() here, the game readjusts itself to fit the new window size. The side effect is that scores and positions reset—this could be improved by saving scores instead.

// Handles canvas resizing to make the sketch responsive
function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize bases and flags based on new window size
  // This will also reset the game (scores, flag positions, player positions)
  setup();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Canvas resize resizeCanvas(windowWidth, windowHeight);

Resizes the canvas to match the new window dimensions

assignment Game reinitialization setup();

Calls setup() to recreate all game objects based on the new canvas size

function windowResized() {
This special p5.js function runs automatically whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight);
p5.js function that resizes the canvas to match the new window width and height
setup();
Calls setup() again to reinitialize all game objects (bases, flags, players, scores) based on the new canvas size

📦 Key Variables

redPlayer object (Player)

Stores the Player object for the red team; holds position, controls, flag state, and team color

let redPlayer = new Player(x, y, color(255, 0, 0), redControls);
bluePlayer object (Player)

Stores the Player object for the blue team

let bluePlayer = new Player(x, y, color(0, 0, 255), blueControls);
redFlag object (Flag)

Stores the red team's flag object; tracks position, whether it is being carried, and which player carries it

let redFlag = new Flag(x, y, color(255, 0, 0));
blueFlag object (Flag)

Stores the blue team's flag object

let blueFlag = new Flag(x, y, color(0, 0, 255));
redBase object (Base)

Stores the red team's base object; a scoring zone where red must return the blue flag to score

let redBase = new Base(x, y, w, h, color(255, 0, 0, 100));
blueBase object (Base)

Stores the blue team's base object

let blueBase = new Base(x, y, w, h, color(0, 0, 255, 100));
redScore number

Tracks the red team's current score; incremented each time red successfully returns the blue flag to the red base

let redScore = 0;
blueScore number

Tracks the blue team's current score

let blueScore = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() - game logic

Score and player positions reset every time the window is resized, losing game progress

💡 Instead of calling setup() in windowResized(), only call resizeCanvas(windowWidth, windowHeight) and recalculate base/flag positions without reinitializing players and scores. Store scores in separate variables that are never reset.

BUG draw() - player vs player collision

If both players collide while both carrying flags, both flags get reset and both players respawn—but a player cannot carry their opponent's flag and their own flag simultaneously, so one case is impossible

💡 The logic is safe but redundant. Add a comment clarifying that only one player per collision can be carrying a flag at a time, or restructure with else-if to make the exclusivity explicit.

PERFORMANCE draw()

dist() is called multiple times per frame (player-flag collisions, player-base collisions, player-player collision), but the results are not cached; if players were objects in an array, this could become expensive quickly

💡 For this small sketch it is fine, but good practice: store collision results in variables before using them multiple times, or restructure game logic to minimize redundant calculations.

STYLE draw() - base label display

The ternary operator for choosing 'RED BASE' or 'BLUE BASE' compares colors using ===, which may fail if color objects are created fresh each time

💡 Instead, use a boolean property on the Base object (like this.isRed) to determine which label to display, making the code more readable and more reliable.

FEATURE Game mechanics

There is no way to pause, restart, or change difficulty settings mid-game

💡 Add keyboard listeners for 'P' to pause, 'R' to restart (which resets scores and respawns players without resizing the canvas), or 'D' to toggle difficulty by adjusting base size or player speed.

FEATURE Visual feedback

When a flag is dropped or reset, the player may not immediately notice where the flag went

💡 Add a temporary visual effect (like a bright flash, a shrinking circle, or a text message at the flag's location) when the flag is reset, making the game easier to follow.

🔄 Code Flow

Code flow showing player_constructor, move, display, respawn, dropflag, checkplayercollision, flag_constructor, flag_display, updateposition, flag_reset, flag_checkplayercollision, base_constructor, base_display, base_checkplayercollision, circlerectcollision, setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" draw --> clear-and-display[clear-and-display] clear-and-display --> player-logic[Player Logic] player-logic --> red-player-logic[red-player-logic] red-player-logic --> tagging-logic[tagging-logic] tagging-logic --> blue-player-logic[blue-player-logic] blue-player-logic --> flag-check[flag-checkplayercollision] flag-check --> base-check[base-checkplayercollision] base-check --> score-display[score-display] click clear-and-display href "#sub-clear-and-display" click player-logic href "#sub-player-logic" click red-player-logic href "#sub-red-player-logic" click tagging-logic href "#sub-tagging-logic" click blue-player-logic href "#sub-blue-player-logic" click flag-check href "#sub-flag-checkplayercollision" click base-check href "#sub-base-checkplayercollision" click score-display href "#sub-score-display" setup --> canvas-creation[canvas-creation] canvas-creation --> controls-definition[controls-definition] controls-definition --> base-dimensions[base-dimensions] base-dimensions --> base-creation[base-creation] base-creation --> flag-creation[flag-creation] flag-creation --> player-creation[player-creation] click canvas-creation href "#sub-canvas-creation" click controls-definition href "#sub-controls-definition" click base-dimensions href "#sub-base-dimensions" click base-creation href "#sub-base-creation" click flag-creation href "#sub-flag-creation" click player-creation href "#sub-player-creation" draw --> position-init[position-init] position-init --> team-color[team-color] team-color --> flag-state[flag-state] flag-state --> controls-mapping[controls-mapping] controls-mapping --> keyboard-input[keyboard-input] keyboard-input --> boundary-constraint[boundary-constraint] boundary-constraint --> move[move] move --> draw-player-circle[draw-player-circle] draw-player-circle --> draw-flag-indicator[draw-flag-indicator] draw-flag-indicator --> display[display] click position-init href "#sub-position-init" click team-color href "#sub-team-color" click flag-state href "#sub-flag-state" click controls-mapping href "#sub-controls-mapping" click keyboard-input href "#sub-keyboard-input" click boundary-constraint href "#sub-boundary-constraint" click move href "#fn-move" click draw-player-circle href "#sub-draw-player-circle" click draw-flag-indicator href "#sub-draw-flag-indicator" click display href "#fn-display" display --> respawn[respawn] respawn --> reset-position[reset-position] reset-position --> conditional-flag-drop[conditional-flag-drop] conditional-flag-drop --> dropflag[dropflag] click respawn href "#fn-respawn" click reset-position href "#sub-reset-position" click conditional-flag-drop href "#sub-conditional-flag-drop" click dropflag href "#fn-dropflag" draw --> flag-display[flag_display] flag-display --> updateposition[updateposition] updateposition --> carrier-follow[carrier-follow] click flag-display href "#fn-flag_display" click updateposition href "#fn-updateposition" click carrier-follow href "#sub-carrier-follow" draw --> flag-reset[flag_reset] flag-reset --> flag-state-update[flag-state-update] flag-state-update --> flag-collision-check[flag-collision-check] flag-collision-check --> flag-distance[flag-distance] click flag-reset href "#fn-flag_reset" click flag-state-update href "#sub-flag-state-update" click flag-collision-check href "#sub-flag-collision-check" click flag-distance href "#sub-flag-distance" draw --> base-display[base_display] base-display --> draw-base-rect[draw-base-rect] draw-base-rect --> draw-base-label[draw-base-label] click base-display href "#fn-base_display" click draw-base-rect href "#sub-draw-base-rect" click draw-base-label href "#sub-draw-base-label" draw --> circlerectcollision[circlerectcollision] circlerectcollision --> find-closest-point[find-closest-point] find-closest-point --> calculate-distance[calculate-distance] calculate-distance --> collision-test[collision-test] click circlerectcollision href "#fn-circlerectcollision" click find-closest-point href "#sub-find-closest-point" click calculate-distance href "#sub-calculate-distance" click collision-test href "#sub-collision-test" windowresized --> game-reinit[game-reinit] game-reinit --> canvas-resize[canvas-resize] click windowresized href "#fn-windowresized" click game-reinit href "#sub-game-reinit" click canvas-resize href "#sub-canvas-resize"

❓ Frequently Asked Questions

What visual elements are present in the red vs blue p5.js sketch?

The sketch features two player teams represented by colored circles (red and blue) and a square flag that players can capture and carry.

How can users interact with the red vs blue capture the flag game?

Users can control their player using keyboard keys, with specified controls for movement, to navigate the canvas and capture the flag.

What creative coding concepts are demonstrated in the red vs blue sketch?

This sketch showcases object-oriented programming through player classes, as well as basic game mechanics like movement, flag capture, and respawning.

Preview

red vs blue - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of red vs blue - Code flow showing player_constructor, move, display, respawn, dropflag, checkplayercollision, flag_constructor, flag_display, updateposition, flag_reset, flag_checkplayercollision, base_constructor, base_display, base_checkplayercollision, circlerectcollision, setup, draw, windowresized
Code Flow Diagram