jack and cappie

This sketch creates an interactive 2D adventure game where you control Jack, exploring two houses and interacting with family members and objects. Jack can move left and right, enter houses, and discover messages from parents, his girlfriend Cappie, and retro gaming consoles, all set in a nostalgic 2001 setting.

🧪 Try This!

Experiment with the code by making these changes:

  1. Add a third house — By creating a third house X position and duplicating the house-drawing and house-entry code, you can expand the world for Jack to explore
  2. Change Jack's color — Jack starts as gold (#FFD700)—change this hex value to any color you prefer, like red, green, or purple
  3. Add a new character to Jack's house — Insert another family member or friend by pushing a new object to the characters array with a unique name, position, and message
  4. Adjust interaction range
  5. Speed up Jack's movement — Increase jackSpeed to make Jack move faster; try 10 for zippy movement or keep it at 5 for a leisurely pace
  6. Make houses taller — Increase houseHeight to make the houses stretch taller and affect interior space—try 200 for mansion-sized homes
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a playable point-and-click adventure game using p5.js. Jack, a character born in 2001, can walk around an outdoor scene with two houses, enter either house by pressing the up arrow, and interact with family members and objects by getting close to them. The sketch demonstrates scene management (switching between outside and inside views), character collision detection for interactions, constrained movement, and responsive UI with dynamic text messages.

The code is organized around three main scenes (outside, inside Jack's house, inside Cappie's house) managed by the currentScene variable. A characters array stores all NPCs and objects with their properties, positions, and messages. The draw loop continuously checks which scene is active and calls the appropriate drawing and interaction functions. By studying this sketch, you'll learn how to build multi-room games, manage game state with conditionals, use arrays to organize entities, and create engaging interactive narratives.

⚙️ How It Works

  1. When the sketch loads, setup() initializes the canvas, defines colors and house positions, and populates a characters array with Jack, Cappie, parents, and the old consoles object—each with their outside positions, inside positions, and unique messages.
  2. In the draw loop, the sketch checks currentScene and calls drawOutsideScene() or drawInsideScene() to render either the outdoor landscape with two houses or the interior of one house. All character positions are drawn based on their current scene.
  3. Every frame, handleOutsideMovementAndInteractions() or handleInsideMovementAndInteractions() processes keyboard input (arrow keys or WASD) to move Jack left and right, checking if Jack is close enough to another character to trigger their message.
  4. When Jack approaches a house door, pressing UP_ARROW enters that house and changes currentScene. Inside, Jack can move horizontally within the house bounds and interact with family members who live there.
  5. Pressing DOWN_ARROW or walking to the edge of a house exits back to the outside scene. The windowResized() function keeps all positions and sizes proportional when the browser resizes, ensuring the game stays playable on any screen.
  6. Interactive messages appear at the bottom of the screen whenever Jack gets close to a character or object, creating a narrative experience that rewards exploration.

🎓 Concepts You'll Learn

Scene management and state machinesArrays for managing multiple entitiesCollision detection and proximity checkingConstrained movement within boundariesKeyboard input handlingRelative coordinate systems with translate()

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to initialize canvas size, define colors, set up coordinate systems, and populate data structures like the characters array. All the variables you set up here (groundY, jackHouseX, the characters array) are then used throughout draw() and the interaction functions.

function setup() {
  createCanvas(windowWidth, windowHeight);
  rectMode(CENTER); // Draw rectangles from their center
  textAlign(CENTER, BOTTOM); // Align text for character names
  textSize(12); // Default text size for character names
  groundY = height * 0.7; // 70% down from the top is the ground level
  insideFloorY = height * 0.8; // 80% down from the top is the floor inside houses

  // Initialize house positions
  jackHouseX = width * 0.2; // Jack's house on the left
  cappieHouseX = width * 0.8; // Cappie's house on the right

  // Define all characters and interactive objects
  // Name, outsideX, outsideY, insideX, insideY, size, color, home, type, message
  characters.push({ name: "Jack", outsideX: jackHouseX, outsideY: groundY, insideX: 0, insideY: 0, size: jackSize, color: '#FFD700', home: "jackHouse", type: "player" });
  characters.push({ name: "Jack's Mom", outsideX: jackHouseX - 60, outsideY: groundY, insideX: -60, insideY: 0, size: 25, color: '#FF69B4', home: "jackHouse", type: "parent", message: "Jack's mom loves him so much!" });
  characters.push({ name: "Jack's Dad", outsideX: jackHouseX + 60, outsideY: groundY, insideX: 60, insideY: 0, size: 25, color: '#1E90FF', home: "jackHouse", type: "parent", message: "Jack's dad is proud of him!" });
  characters.push({ name: "Old Consoles", outsideX: jackHouseX, outsideY: groundY, insideX: 0, insideY: -15, size: 40, color: '#696969', home: "jackHouse", type: "object", message: "Jack loves his old Nintendo consoles: N64, GameCube, PS2!" });
  characters.push({ name: "Cappie's Mom", outsideX: cappieHouseX - 60, outsideY: groundY, insideX: -60, insideY: 0, size: 25, color: '#FF6347', home: "cappieHouse", type: "parent", message: "Cappie's mom thinks Jack is a sweet boy!" });
  characters.push({ name: "Cappie's Dad", outsideX: cappieHouseX + 60, outsideY: groundY, insideX: 60, insideY: 0, size: 25, color: '#32CD32', home: "cappieHouse", type: "parent", message: "Cappie's dad likes Jack's spirit!" });
  characters.push({ name: "Cappie", outsideX: cappieHouseX, outsideY: groundY, insideX: 0, insideY: 0, size: 30, color: '#FFC0CB', home: "cappieHouse", type: "girlfriend", message: "Cappie loves Jack! She's so happy to see him!" });
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Canvas and text configuration createCanvas(windowWidth, windowHeight);

Creates a full-screen canvas and sets up drawing modes for rectangles and text alignment

calculation Ground and floor positioning groundY = height * 0.7; // 70% down from the top is the ground level

Calculates where characters stand outside and inside based on canvas height

calculation House X positions jackHouseX = width * 0.2; // Jack's house on the left

Places houses at 20% and 80% of canvas width so they scale with screen size

for-loop Character array initialization characters.push({ name: "Jack", outsideX: jackHouseX, outsideY: groundY, insideX: 0, insideY: 0, size: jackSize, color: '#FFD700', home: "jackHouse", type: "player" });

Populates the characters array with all NPCs, setting their initial positions, colors, and messages

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the game responsive to window size
rectMode(CENTER);
Changes rectangle drawing so the x,y position is at the center instead of the top-left corner—makes positioning and transforms cleaner
textAlign(CENTER, BOTTOM);
Centers text horizontally and aligns it to the bottom of its position, useful for name tags below characters
groundY = height * 0.7;
Calculates where the ground is by using 70% of the canvas height; this keeps the ground proportional to any screen size
jackHouseX = width * 0.2;
Places Jack's house at 20% across from the left; using multiplication of width makes it scale when the window resizes
characters.push({ name: "Jack", outsideX: jackHouseX, outsideY: groundY, ... });
Adds Jack to the characters array as an object with properties for position, size, color, home, type, and message

draw()

draw() runs 60 times per second and is the heartbeat of the game. It's a state machine—currentScene acts as a switch that determines which drawing and interaction functions run. This pattern (checking a state variable and dispatching to different functions) is fundamental to games with multiple rooms or menus. Notice how interactionText is cleared and re-set every frame—this is a simple way to make messages disappear unless Jack is close to someone.

🔬 The draw loop uses currentScene to decide which functions to call. What happens if you add a third scene by duplicating an else-if block and changing 'insideJackHouse' to 'insideLibrary'?

  if (currentScene === 'outside') {
    drawOutsideScene();
    handleOutsideMovementAndInteractions();
  } else if (currentScene === 'insideJackHouse') {
function draw() {
  // Clear interaction text each frame unless interacting
  interactionText = "";

  if (currentScene === 'outside') {
    drawOutsideScene();
    handleOutsideMovementAndInteractions();
  } else if (currentScene === 'insideJackHouse') {
    drawInsideScene(jackHouseColor, jackRoofColor, true);
    handleInsideMovementAndInteractions(jackHouseX, 'insideJackHouse');
  } else if (currentScene === 'insideCappieHouse') {
    drawInsideScene(cappieHouseColor, cappieRoofColor, false);
    handleInsideMovementAndInteractions(cappieHouseX, 'insideCappieHouse');
  }

  // Display interaction text at the bottom
  if (interactionText) {
    fill(0);
    noStroke();
    textSize(20); // Larger text for interactions
    text(interactionText, width / 2, height - 30);
    textSize(12); // Reset text size for other UI
  }

  // Display game title
  fill(0);
  noStroke();
  textSize(30);
  text("Jack's 2001 Adventure", width / 2, 50);
  textSize(12); // Reset text size
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Scene state machine if (currentScene === 'outside') {

Routes the draw and interaction logic to the correct functions based on which scene Jack is currently in

conditional Interaction text rendering if (interactionText) {

Only draws the interaction message if one has been set during handleOutsideMovementAndInteractions() or handleInsideMovementAndInteractions()

interactionText = "";
Clears the interaction message at the start of each frame so old messages don't persist; it gets reset by the next interaction check
if (currentScene === 'outside') {
Checks if Jack is in the outside scene; if true, draws the outside view and handles outside-specific controls
drawOutsideScene();
Renders the sky, ground, houses, and all characters at their outside positions
handleOutsideMovementAndInteractions();
Processes keyboard input for movement, checks proximity to other characters for interactions, and detects if Jack should enter a house
} else if (currentScene === 'insideJackHouse') {
If Jack is inside his house, calls the inside drawing and interaction functions with Jack's house colors and the true flag
if (interactionText) {
Only renders the interaction message if one was set; prevents empty text from cluttering the screen
text(interactionText, width / 2, height - 30);
Displays the interaction message at the bottom center of the screen, 30 pixels from the bottom
text("Jack's 2001 Adventure", width / 2, 50);
Displays the game title at the top center of the screen, 50 pixels down

drawOutsideScene()

drawOutsideScene() renders the entire outdoor environment: sky, ground, buildings, and characters. It's called once per frame when currentScene === 'outside'. Notice that it loops through the characters array and uses a for-of loop (for (let char of characters)) instead of a traditional for loop—this is more readable when you care about the values rather than the indices. The function shows a complete scene rendering pattern: background first, static scenery next, then entities (characters and houses).

🔬 This loop currently draws all characters the same way, with an if-statement that does nothing. What happens if you change the condition to char.type === "player" but make the player's color different inside the if-block?

  // Draw all characters at their outside positions
  for (let char of characters) {
    if (char.type === "player") {
      drawCharacter(char.outsideX, char.outsideY, char.size, char.color, char.name);
    } else {
      drawCharacter(char.outsideX, char.outsideY, char.size, char.color, char.name);
    }
  }
function drawOutsideScene() {
  background(135, 206, 235); // Sky Blue

  // Draw ground
  noStroke();
  fill(107, 142, 35); // Olive Drab
  rect(width / 2, groundY + (height - groundY) / 2, width, height - groundY);

  // Draw Jack's house
  drawHouse(jackHouseX, groundY, jackHouseColor, jackRoofColor);
  // Draw Cappie's house
  drawHouse(cappieHouseX, groundY, cappieHouseColor, cappieRoofColor);

  // Draw all characters at their outside positions
  for (let char of characters) {
    if (char.type === "player") {
      drawCharacter(char.outsideX, char.outsideY, char.size, char.color, char.name);
    } else {
      drawCharacter(char.outsideX, char.outsideY, char.size, char.color, char.name);
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Environment setup background(135, 206, 235); // Sky Blue

Clears the canvas with a sky blue color and draws the ground rectangle below it

for-loop House rendering drawHouse(jackHouseX, groundY, jackHouseColor, jackRoofColor);

Calls drawHouse() twice with different coordinates and colors to render both buildings

for-loop Character rendering loop for (let char of characters) {

Iterates through the characters array and draws each character at their outside position using drawCharacter()

background(135, 206, 235);
Fills the entire canvas with sky blue, erasing whatever was drawn last frame and creating the background
noStroke();
Removes outlines from shapes, so the ground and houses have solid colors with no borders
rect(width / 2, groundY + (height - groundY) / 2, width, height - groundY);
Draws the ground as a rectangle starting at groundY and extending to the bottom of the screen; positioned at width/2 because rectMode(CENTER) is active
drawHouse(jackHouseX, groundY, jackHouseColor, jackRoofColor);
Calls drawHouse() to render Jack's house at its X position with brown colors
for (let char of characters) {
Loops through every character in the characters array, one at a time
drawCharacter(char.outsideX, char.outsideY, char.size, char.color, char.name);
Calls drawCharacter() to render each character at their outside position with their assigned size, color, and name

drawInsideScene()

drawInsideScene() is called from draw() when Jack is inside a house. It receives the house color (unused here), roof color (unused), and isJackHouse boolean to know which house to render. The key difference from drawOutsideScene() is that characters are positioned relative to the house center (width / 2 + insideJackX) instead of using their absolute outsideX values. The isJackHouse parameter demonstrates a pattern for house-specific content: only Jack's house has old consoles drawn, while Cappie's house stays simple.

🔬 This condition filters characters to only show those in the current house. What happens if you remove the home check entirely and just draw all characters inside whenever Jack enters a house?

  // Draw characters inside the current house
  for (let char of characters) {
    if ((isJackHouse && char.home === "jackHouse") || (!isJackHouse && char.home === "cappieHouse")) {
function drawInsideScene(houseColor, roofColor, isJackHouse) {
  background(176, 224, 230); // Lighter sky blue for interior walls

  // Draw house interior walls (simple rectangle covering the top part)
  noStroke();
  fill(255); // White interior walls
  rect(width / 2, height / 2, houseWidth, insideFloorY - houseHeight / 2);

  // Draw the floor inside
  fill(139, 69, 19); // SaddleBrown for floor
  rect(width / 2, insideFloorY, houseWidth, (height - insideFloorY) * 2); // Extend floor down

  // Draw window (simple rectangle)
  fill(173, 216, 230); // LightBlue
  rect(width / 2, insideFloorY - houseHeight / 2 - 30, 40, 40);

  // Draw old consoles specifically in Jack's house
  if (isJackHouse) {
    drawOldConsoles();
  }

  // Draw characters inside the current house
  for (let char of characters) {
    if ((isJackHouse && char.home === "jackHouse") || (!isJackHouse && char.home === "cappieHouse")) {
      if (char.type === "player") {
        drawCharacter(width / 2 + insideJackX, insideFloorY + char.insideY, char.size, char.color, char.name);
      } else {
        drawCharacter(width / 2 + char.insideX, insideFloorY + char.insideY, char.size, char.color, char.name);
      }
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Interior room walls rect(width / 2, height / 2, houseWidth, insideFloorY - houseHeight / 2);

Draws the white rectangular walls of the house interior centered on the screen

conditional Console rendering for Jack's house only if (isJackHouse) {

Only draws the old consoles if Jack is inside his own house, not when he visits Cappie's

for-loop Inside character rendering for (let char of characters) {

Draws only the characters who live in the current house, using their relative insideX positions

background(176, 224, 230);
Colors the background with a lighter blue to indicate an interior space
rect(width / 2, height / 2, houseWidth, insideFloorY - houseHeight / 2);
Draws the white interior walls as a tall rectangle centered on screen, creating the illusion of being inside a room
rect(width / 2, insideFloorY, houseWidth, (height - insideFloorY) * 2);
Draws the brown floor below the walls; the height calculation extends it off the bottom of the screen for a complete look
if (isJackHouse) {
The isJackHouse parameter is true only when Jack is inside his own house, allowing us to draw house-specific objects
drawOldConsoles();
Calls drawOldConsoles() to render the N64, GameCube, and PS2 inside Jack's house interior
if ((isJackHouse && char.home === "jackHouse") || (!isJackHouse && char.home === "cappieHouse")) {
Only renders characters who belong in the current house; filters out characters from the other house
drawCharacter(width / 2 + insideJackX, insideFloorY + char.insideY, char.size, char.color, char.name);
Draws Jack using his relative insideJackX position (which changes with arrow keys) centered at the house's X position

handleOutsideMovementAndInteractions()

handleOutsideMovementAndInteractions() is the controller for the outside scene. It processes three things: (1) movement—reading arrow/WASD keys and updating Jack's position while keeping him on-screen, (2) interaction—checking proximity to all other characters and setting interactionText if Jack is close, and (3) house entry—detecting UP presses near houses and switching scenes. The dist() function is crucial here: it calculates the straight-line distance between two points, which is perfect for proximity checks in games. Notice the loop uses 'break' to stop checking other characters once an interaction is found; this prevents overlapping messages.

🔬 These two if-statements let Jack move left and right with arrow keys or WASD. What happens if you change -= to += in the LEFT_ARROW block so pressing left makes him move right instead?

  // Jack's movement
  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT_ARROW or 'A'
    jackChar.outsideX -= jackSpeed;
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT_ARROW or 'D'
    jackChar.outsideX += jackSpeed;
  }
function handleOutsideMovementAndInteractions() {
  let jackChar = characters[0];

  // Jack's movement
  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT_ARROW or 'A'
    jackChar.outsideX -= jackSpeed;
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT_ARROW or 'D'
    jackChar.outsideX += jackSpeed;
  }
  jackChar.outsideX = constrain(jackChar.outsideX, 0, width); // Keep Jack within canvas bounds

  // Check interaction with other characters/objects
  let interactionDistance = 60; // How close Jack needs to be to interact
  for (let i = 1; i < characters.length; i++) { // Start from index 1 to skip Jack
    let otherChar = characters[i];
    let d = dist(jackChar.outsideX, jackChar.outsideY, otherChar.outsideX, otherChar.outsideY);
    if (d < interactionDistance) {
      interactionText = otherChar.message;
      break; // Only one interaction message at a time
    }
  }

  // Check for entering houses
  let houseDoorDistance = 30; // How close Jack needs to be to a door
  if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // UP_ARROW or 'W'
    if (dist(jackChar.outsideX, jackChar.outsideY, jackHouseX, groundY) < houseDoorDistance) {
      currentScene = 'insideJackHouse';
      insideJackX = 0; // Reset Jack to center of house
      insideJackY = 0; // Reset Jack to floor level
      jackChar.outsideX = jackHouseX; // Keep track of outside position for exiting
      return; // Exit function to avoid multiple state changes
    }
    if (dist(jackChar.outsideX, jackChar.outsideY, cappieHouseX, groundY) < houseDoorDistance) {
      currentScene = 'insideCappieHouse';
      insideJackX = 0; // Reset Jack to center of house
      insideJackY = 0; // Reset Jack to floor level
      jackChar.outsideX = cappieHouseX; // Keep track of outside position for exiting
      return; // Exit function to avoid multiple state changes
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Left and right movement if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT_ARROW or 'A'

Responds to arrow keys or WASD to move Jack left and right across the outdoor scene

calculation Constrain to canvas jackChar.outsideX = constrain(jackChar.outsideX, 0, width);

Prevents Jack from walking off the edges of the canvas by clamping his position

for-loop Proximity-based interaction loop for (let i = 1; i < characters.length; i++) {

Loops through all other characters and triggers a message if Jack is close enough to one

conditional House entry detection if (dist(jackChar.outsideX, jackChar.outsideY, jackHouseX, groundY) < houseDoorDistance) {

Checks if Jack is close to a house door and enters it when UP is pressed

let jackChar = characters[0];
Gets Jack from the characters array (he's always at index 0) so we can update his position
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
Checks if either the left arrow or the 'A' key is pressed (keyCode 65 is 'A'); keyIsDown() checks continuous holding, not single presses
jackChar.outsideX -= jackSpeed;
Moves Jack left by subtracting jackSpeed (5) from his X position each frame; the -= operator is shorthand for outsideX = outsideX - jackSpeed
jackChar.outsideX = constrain(jackChar.outsideX, 0, width);
Clamps Jack's X position between 0 and width, preventing him from going off-screen; constrain(value, min, max) returns the value if it's in range, or the boundary if it's out of range
let interactionDistance = 60;
Defines how many pixels close Jack needs to be to another character to see their message; this is a tunable variable
for (let i = 1; i < characters.length; i++) {
Loops through the characters array starting at index 1 (skipping Jack at index 0); i++ increments i by 1 each iteration
let d = dist(jackChar.outsideX, jackChar.outsideY, otherChar.outsideX, otherChar.outsideY);
Calculates the distance between Jack and the current character using p5.js's dist() function; dist(x1, y1, x2, y2) returns the Euclidean distance
if (d < interactionDistance) {
If the distance is less than 60 pixels, Jack is close enough to trigger an interaction
interactionText = otherChar.message;
Sets the interaction message to the character's message; this will be displayed at the bottom of the screen in draw()
break;
Exits the loop early so only one interaction message appears at a time, even if Jack is close to multiple characters
if (dist(jackChar.outsideX, jackChar.outsideY, jackHouseX, groundY) < houseDoorDistance) {
Checks if Jack is within 30 pixels of Jack's house (at jackHouseX, groundY); if so, pressing UP can enter
currentScene = 'insideJackHouse';
Changes the scene state from 'outside' to 'insideJackHouse', which will trigger drawInsideScene() in the next draw() call
insideJackX = 0;
Resets Jack's position inside the house to the center (0 is relative to the house center, which is width / 2)
return;
Exits the function early to prevent checking the second house; this avoids entering both houses simultaneously

handleInsideMovementAndInteractions()

handleInsideMovementAndInteractions() mirrors the outside handler but operates in relative coordinates. Inside a house, Jack's position is insideJackX (relative to house center), not his absolute outsideX. The key difference is that interactions only happen with characters in the same house (checked with otherChar.home === jackChar.home), and exiting can happen by walking to an edge or pressing DOWN. The boundary calculation (insideJackX constrained between -houseWidth/2 and +houseWidth/2) keeps Jack inside the house walls. This function demonstrates how game state changes as Jack moves between scenes and how the same controls (arrow keys) can mean different things depending on context.

🔬 This condition has four parts joined by || (or), so exiting happens three ways: walk left, walk right, or press DOWN. What happens if you remove the last two conditions (the DOWN_ARROW and keyCode 83 checks) so Jack can only exit by walking to an edge?

  // Check for exiting houses
  // Exit by moving to the edge of the house or pressing DOWN_ARROW/S
  if (insideJackX <= -houseWidth / 2 + jackSize || insideJackX >= houseWidth / 2 - jackSize || keyIsDown(DOWN_ARROW) || keyIsDown(83)) {
function handleInsideMovementAndInteractions(houseCenterX, sceneName) {
  let jackChar = characters[0];

  // Jack's movement inside
  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT_ARROW or 'A'
    insideJackX -= jackSpeed;
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT_ARROW or 'D'
    insideJackX += jackSpeed;
  }
  // Constrain Jack's movement to be within the house walls
  insideJackX = constrain(insideJackX, -houseWidth / 2 + jackSize / 2, houseWidth / 2 - jackSize / 2);

  // Check interaction with other characters/objects inside
  let interactionDistance = 60; // How close Jack needs to be to interact
  for (let i = 1; i < characters.length; i++) { // Start from index 1 to skip Jack
    let otherChar = characters[i];
    if (otherChar.home === jackChar.home) { // Only interact with characters in the current house
      let d = dist(insideJackX, 0, otherChar.insideX, otherChar.insideY); // Y is fixed at 0 for inside floor
      if (d < interactionDistance) {
        interactionText = otherChar.message;
        break; // Only one interaction message at a time
      }
    }
  }

  // Check for exiting houses
  // Exit by moving to the edge of the house or pressing DOWN_ARROW/S
  if (insideJackX <= -houseWidth / 2 + jackSize || insideJackX >= houseWidth / 2 - jackSize || keyIsDown(DOWN_ARROW) || keyIsDown(83)) { // DOWN_ARROW or 'S'
    currentScene = 'outside';
    // Jack's outsideX is already stored, no need to reset it here
    return; // Exit function to avoid multiple state changes
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Inside house movement controls if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {

Moves Jack left or right inside the house using the same controls as outside

calculation House boundary constraint insideJackX = constrain(insideJackX, -houseWidth / 2 + jackSize / 2, houseWidth / 2 - jackSize / 2);

Prevents Jack from walking through the house walls by limiting his relative X position

for-loop Inside interaction checking for (let i = 1; i < characters.length; i++) {

Checks distance to characters who live in the same house and triggers their messages

conditional House exit detection if (insideJackX <= -houseWidth / 2 + jackSize || insideJackX >= houseWidth / 2 - jackSize || keyIsDown(DOWN_ARROW) || keyIsDown(83)) {

Exits the house when Jack reaches an edge or presses DOWN

let jackChar = characters[0];
Gets Jack from the characters array so we can check which house he belongs to
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
Checks for left arrow or 'A' key; inside a house, this moves Jack left relative to the house center
insideJackX -= jackSpeed;
Moves Jack left by subtracting speed; insideJackX is a relative coordinate (0 = house center)
insideJackX = constrain(insideJackX, -houseWidth / 2 + jackSize / 2, houseWidth / 2 - jackSize / 2);
Clamps Jack between the left and right walls of the house; the calculations account for Jack's size so he doesn't overlap walls
if (otherChar.home === jackChar.home) {
Only checks interactions with characters who live in the current house; this prevents seeing Cappie's parents while inside Jack's house
let d = dist(insideJackX, 0, otherChar.insideX, otherChar.insideY);
Calculates distance using relative coordinates; the Y value 0 represents the floor level inside the house
if (insideJackX <= -houseWidth / 2 + jackSize || insideJackX >= houseWidth / 2 - jackSize || keyIsDown(DOWN_ARROW) || keyIsDown(83)) {
Checks three exit conditions: Jack walked to the left wall, Jack walked to the right wall, or Jack pressed DOWN; any one triggers the exit
currentScene = 'outside';
Changes the scene back to 'outside', which will render the outdoor view on the next draw() call

drawHouse()

drawHouse() is a helper function that draws a complete house at a given position with customizable colors. The push()/translate()/pop() pattern is essential to p5.js graphics: translate() moves the origin so we can draw relative to the house position, and pop() restores the previous state so the translation doesn't affect other drawings. All measurements (houseHeight, houseWidth, houseGroundOffset) are defined as global variables, making it easy to change house sizes throughout the game. Notice how the roof uses a triangle—an excellent example of p5.js's shape drawing functions working together.

function drawHouse(x, y, houseColor, roofColor) {
  push();
  translate(x, y); // Translate origin to the house's ground center

  // House body
  fill(houseColor);
  noStroke();
  rect(0, -houseHeight / 2 + houseGroundOffset, houseWidth, houseHeight);

  // Roof
  fill(roofColor);
  triangle(
    -houseWidth / 2, -houseHeight + houseGroundOffset,
    houseWidth / 2, -houseHeight + houseGroundOffset,
    0, -houseHeight - 50 + houseGroundOffset // Peak of the roof
  );

  // Window (simple rectangle)
  fill(173, 216, 230); // LightBlue
  rect(0, -houseHeight / 2 + houseGroundOffset - 30, 40, 40);

  // Door (rectangle)
  fill(139, 69, 19); // SaddleBrown
  rect(0, houseGroundOffset, 60, 80);

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

🔧 Subcomponents:

calculation Coordinate system setup push(); translate(x, y);

Saves the current transformation matrix and moves the origin to the house's position, simplifying all relative drawing

calculation House geometry rect(0, -houseHeight / 2 + houseGroundOffset, houseWidth, houseHeight);

Draws the main rectangular body, roof triangle, window, and door using relative coordinates

push();
Saves the current transformation state (coordinate system, fill color, stroke settings); allows us to reset after drawing the house
translate(x, y);
Moves the origin to the house's position (x, y); all subsequent coordinates are relative to this point, making the code clearer
rect(0, -houseHeight / 2 + houseGroundOffset, houseWidth, houseHeight);
Draws the house body as a rectangle centered horizontally at x=0, starting above ground level and extending downward
triangle(
Creates the roof as a triangle with two points at the top of the house and one point at the peak, creating a classic pitched roof shape
rect(0, -houseHeight / 2 + houseGroundOffset - 30, 40, 40);
Draws the window as a light blue square on the front of the house, 30 pixels above the midpoint
rect(0, houseGroundOffset, 60, 80);
Draws the door as a brown rectangle at the bottom of the house where Jack can stand to enter
pop();
Restores the previous transformation state, returning the coordinate system to normal and resetting colors

drawOldConsoles()

drawOldConsoles() is an example of a decorative object function that adds thematic detail to Jack's house. It draws three retro gaming consoles (N64, GameCube, PS2) as colored rectangles with labels, visible only when Jack is inside his house. The function uses translate() to position the consoles relative to the house interior, and fill() changes before each console to give them distinct colors. This pattern—a helper function that draws related objects in one place—makes the code organized and easy to modify. If you wanted to add more consoles or change their appearance, you'd only edit this one function.

🔬 This block draws three consoles with different colors, sizes, and positions. What happens if you add a fourth console (like a Sega Genesis) by copying one of these blocks and changing the fill color, text, and position?

  // N64
  rect(-houseWidth / 2 + 50, 0, 50, 30); // N64 box
  fill(255);
  noStroke();
  text("N64", -houseWidth / 2 + 50, -20);

  // GameCube
  fill(100, 0, 100); // Purple
  rect(-houseWidth / 2 + 120, 0, 40, 40); // GameCube box
  fill(255);
  text("GC", -houseWidth / 2 + 120, -25);

  // PS2 (tall and thin)
  fill(0);
  rect(houseWidth / 2 - 50, -5, 30, 50); // PS2 box
  fill(255);
  text("PS2", houseWidth / 2 - 50, -30);
function drawOldConsoles() {
  fill(69, 69, 69); // Dark grey
  stroke(30);
  strokeWeight(2);

  // Place consoles relative to the house center (0, insideFloorY)
  // InsideFloorY is the actual y-coordinate, so we just use the insideX offset
  let consoleBaseY = insideFloorY - 15; // Slightly above the floor

  textSize(10); // Smaller text for console labels

  push();
  translate(width / 2, consoleBaseY); // Translate to the center of the house interior floor

  // N64
  rect(-houseWidth / 2 + 50, 0, 50, 30); // N64 box
  fill(255);
  noStroke();
  text("N64", -houseWidth / 2 + 50, -20);

  // GameCube
  fill(100, 0, 100); // Purple
  rect(-houseWidth / 2 + 120, 0, 40, 40); // GameCube box
  fill(255);
  text("GC", -houseWidth / 2 + 120, -25);

  // PS2 (tall and thin)
  fill(0);
  rect(houseWidth / 2 - 50, -5, 30, 50); // PS2 box
  fill(255);
  text("PS2", houseWidth / 2 - 50, -30);

  pop(); // Restore previous transformation

  noFill();
  noStroke(); // Reset for other drawings
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Console display initialization fill(69, 69, 69); // Dark grey

Sets the fill color and stroke style for drawing console boxes

calculation House coordinate system push(); translate(width / 2, consoleBaseY);

Sets up a local coordinate system centered on the house interior for drawing consoles

calculation Three console drawings rect(-houseWidth / 2 + 50, 0, 50, 30);

Draws three separate console boxes (N64, GameCube, PS2) with different colors and sizes

fill(69, 69, 69); // Dark grey
Sets the fill color to dark grey; this will be used for the console boxes
stroke(30);
Sets the outline color to a very dark gray (30 out of 255), creating thin borders around shapes
strokeWeight(2);
Sets the outline thickness to 2 pixels, making the console boxes more visible
let consoleBaseY = insideFloorY - 15;
Calculates where the consoles should sit relative to the floor (15 pixels above it)
textSize(10);
Sets text size to 10 pixels, smaller than the default 12, for console labels
push(); translate(width / 2, consoleBaseY);
Saves the state and moves the origin to the center of the house floor, so all console positions are relative to that point
rect(-houseWidth / 2 + 50, 0, 50, 30);
Draws the N64 console as a grey box positioned 50 pixels from the left edge of the house
fill(255); noStroke(); text("N64", -houseWidth / 2 + 50, -20);
Changes to white fill and no stroke, then draws the text "N64" above the grey box; the negative y-value (-20) places it above
fill(100, 0, 100); // Purple
Sets fill to purple for the GameCube console, creating visual distinction between the three systems
pop();
Restores the previous transformation and drawing settings
noFill(); noStroke();
Resets fill and stroke so subsequent drawings don't inherit the console colors and styles

drawCharacter()

drawCharacter() is a reusable helper function that draws a simple stick-figure character at any position with any color and name. By parameterizing size, color, and name, the same function renders Jack (size 30, gold), parents (size 25, pink/blue), and others. The function demonstrates the power of helper functions in game code: instead of repeating the same drawing code 7 times (once per character), you define it once and call it with different parameters. The push()/pop() pattern protects the main coordinate system, so each character's translate() doesn't affect others. The black name tag creates a readable contrast over any character color.

function drawCharacter(x, y, size, color, name) {
  push();
  translate(x, y); // Translate origin to the character's ground center

  fill(color);
  noStroke();

  // Head
  ellipse(0, -size / 2, size, size);

  // Body
  rect(0, size / 2, size / 2, size);

  // Arms
  rect(-size / 4, 0, size / 8, size / 2);
  rect(size / 4, 0, size / 8, size / 2);

  // Legs
  rect(-size / 8, size, size / 8, size / 2);
  rect(size / 8, size, size / 8, size / 2);

  // Name tag
  fill(0);
  noStroke();
  textSize(12);
  text(name, 0, -size - 10);

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

🔧 Subcomponents:

calculation Head drawing ellipse(0, -size / 2, size, size);

Draws a circular head centered above the body

calculation Body and limb drawing rect(0, size / 2, size / 2, size);

Draws a rectangular body and four rectangular limbs positioned relative to the body

calculation Name label text(name, 0, -size - 10);

Draws the character's name in black text above their head

push();
Saves the current transformation state so the translate() won't affect other drawings
translate(x, y);
Moves the origin to the character's ground position, so all body parts are drawn relative to that point
fill(color);
Sets the fill color to the character's color, which is passed as a parameter
ellipse(0, -size / 2, size, size);
Draws the head as a circle centered at (0, -size/2), positioned above the body; ellipse(x, y, w, h) draws at that center
rect(0, size / 2, size / 2, size);
Draws the body as a narrow rectangle below the head; positioning uses rectMode(CENTER) so it's centered horizontally
rect(-size / 4, 0, size / 8, size / 2);
Draws the left arm as a thin vertical rectangle positioned to the left of center, at the shoulder height
rect(size / 8, size, size / 8, size / 2);
Draws a left leg as a thin vertical rectangle below the body; the y-position (size) is the waist level
fill(0);
Changes fill to black (0) for the name text; this ensures the name is readable regardless of the character's color
text(name, 0, -size - 10);
Draws the character's name above their head; the negative y-value positions it above, and textAlign(CENTER, BOTTOM) centers it horizontally
pop();
Restores the previous transformation and fill color, preventing this character's style from affecting subsequent drawings

windowResized()

windowResized() is a p5.js built-in function that runs automatically when the browser window changes size. By calling resizeCanvas() and recalculating all proportional positions, the game remains playable at any screen size. This is essential for responsive design: rather than hardcoding positions (e.g., jackHouseX = 100), the code uses proportions (jackHouseX = width * 0.2) so houses and characters scale smoothly. The function also updates every character's position to ensure they move with the new screen size. This is a pattern worth copying in your own sketches: always make positions and sizes proportional to canvas dimensions, not absolute values.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  groundY = height * 0.7; // Recalculate ground position
  insideFloorY = height * 0.8; // Recalculate inside floor position

  // Update house positions relative to new canvas size
  jackHouseX = width * 0.2;
  cappieHouseX = width * 0.8;

  // Update character positions relative to new canvas size and groundY/insideFloorY
  for (let char of characters) {
    char.outsideY = groundY; // All characters stand on the ground outside
    char.insideY = 0; // All characters stand on the floor inside (relative to insideFloorY)

    if (char.type === "player") {
      // Jack's outsideX is updated dynamically, his insideX is relative to house center
      // If he was inside, reset his insideX to center
      if (currentScene === 'insideJackHouse' || currentScene === 'insideCappieHouse') {
        insideJackX = 0;
        insideJackY = 0;
      }
    } else if (char.home === "jackHouse") {
      if (char.name === "Jack's Mom") char.outsideX = jackHouseX - 60;
      else if (char.name === "Jack's Dad") char.outsideX = jackHouseX + 60;
      else if (char.name === "Old Consoles") char.outsideX = jackHouseX; // Interaction point is at house center outside
      char.insideX = (char.name === "Jack's Mom") ? -60 : (char.name === "Jack's Dad") ? 60 : 0;
      if (char.name === "Old Consoles") char.insideY = -15; // Consoles are slightly off the floor
    } else if (char.home === "cappieHouse") {
      if (char.name === "Cappie's Mom") char.outsideX = cappieHouseX - 60;
      else if (char.name === "Cappie's Dad") char.outsideX = cappieHouseX + 60;
      else if (char.name === "Cappie") char.outsideX = cappieHouseX;
      char.insideX = (char.name === "Cappie's Mom") ? -60 : (char.name === "Cappie's Dad") ? 60 : 0;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Canvas resizing resizeCanvas(windowWidth, windowHeight);

Adjusts the canvas to match the new browser window dimensions

calculation Position recalculation groundY = height * 0.7;

Recalculates ground and floor positions based on the new canvas height

for-loop Character position updates for (let char of characters) {

Loops through all characters and updates their positions proportionally to the new screen size

resizeCanvas(windowWidth, windowHeight);
p5.js calls this function automatically when the browser window is resized; resizeCanvas() adjusts the canvas size to match
groundY = height * 0.7;
Recalculates groundY using the new height; this keeps the ground proportional to the screen size
jackHouseX = width * 0.2;
Recalculates Jack's house X position as 20% across; this keeps houses proportional to the new width
for (let char of characters) {
Loops through all characters to update their positions for the new screen size
char.outsideY = groundY;
Updates every character's outside Y position to the newly calculated ground level
if (char.name === "Jack's Mom") char.outsideX = jackHouseX - 60;
Updates Jack's mom's position to be 60 pixels left of the newly positioned Jack's house
if (currentScene === 'insideJackHouse' || currentScene === 'insideCappieHouse') {
Checks if Jack is currently inside a house; if so, resets his inside position to the center to avoid jarring movement during resize

📦 Key Variables

jackSize number

Controls the visual size of Jack and Cappie in pixels; used in drawCharacter() to scale head, body, and limbs proportionally

let jackSize = 30;
jackSpeed number

How many pixels Jack moves per frame when arrow keys are pressed; controls game responsiveness

let jackSpeed = 5;
groundY number

The Y-coordinate where outdoor characters stand and where the ground is drawn; calculated as 70% down the canvas

let groundY;
insideFloorY number

The Y-coordinate where characters stand inside houses; calculated as 80% down the canvas

let insideFloorY;
jackHouseColor string

Hex color code for Jack's house walls (brown/saddle brown) used in drawHouse()

let jackHouseColor = '#8B4513';
cappieHouseColor string

Hex color code for Cappie's house walls (light steel blue) used in drawHouse()

let cappieHouseColor = '#B0C4DE';
jackRoofColor string

Hex color code for Jack's house roof (sienna/reddish brown) used in drawHouse()

let jackRoofColor = '#A0522D';
cappieRoofColor string

Hex color code for Cappie's house roof (light slate gray) used in drawHouse()

let cappieRoofColor = '#778899';
houseWidth number

The width in pixels of both houses when drawn outside; affects interior movement boundaries

let houseWidth = 200;
houseHeight number

The height in pixels of both houses; affects roof shape and interior wall positioning

let houseHeight = 150;
jackHouseX number

The X-coordinate where Jack's house is drawn (20% from the left); updated on resize

let jackHouseX;
cappieHouseX number

The X-coordinate where Cappie's house is drawn (80% from the left); updated on resize

let cappieHouseX;
houseGroundOffset number

How many pixels of each house appears 'underground'; affects visual placement of houses relative to ground

let houseGroundOffset = 10;
interactionText string

Stores the current message to display at the bottom of the screen when Jack is close to a character

let interactionText = "";
characters array

Array of objects storing all NPCs and entities: Jack, parents, Cappie, and the old consoles; each has position, color, home, and message

let characters = [];
currentScene string

Tracks which scene is active: 'outside', 'insideJackHouse', or 'insideCappieHouse'; controls which drawing and interaction functions run

let currentScene = 'outside';
insideJackX number

Jack's position relative to the house center when inside; ranges from -houseWidth/2 to +houseWidth/2

let insideJackX = 0;
insideJackY number

Jack's vertical position when inside (currently unused; kept for potential future height mechanics)

let insideJackY = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG handleInsideMovementAndInteractions() line: insideJackX = constrain(...)

The boundary calculation uses jackSize in the constrain formula but jackSize is a global variable that could theoretically be larger than houseWidth/2, causing the constraint to fail or allow Jack to escape the bounds

💡 Either ensure jackSize is always much smaller than houseWidth, or parameterize the constrain to use a fixed safe margin: insideJackX = constrain(insideJackX, -houseWidth / 2 + 50, houseWidth / 2 - 50);

STYLE setup() and windowResized()

Character position assignments are repeated in two places (setup and windowResized); if you add a new character, you must update both locations

💡 Create a helper function called initializeCharacters() that runs in both setup() and windowResized(), eliminating duplication and reducing bugs

PERFORMANCE draw() and drawOutsideScene()

The background is cleared every frame, which is correct, but fill() and noStroke() are called repeatedly in multiple functions; this is inefficient

💡 Set default fill/stroke colors once in setup() and only change them when needed, rather than resetting them in every draw function

FEATURE handleInsideMovementAndInteractions()

Jack can exit a house by walking to the edge OR pressing DOWN, but there is no visual feedback (like a door prompt) showing which edge leads outside

💡 Draw a 'EXIT' label or highlight on the left and right edges of the house interior to guide players, or only allow exiting via a specific door location

BUG drawOutsideScene() character loop

The loop checks if char.type === 'player' but then does the exact same thing in both branches (draws with drawCharacter), making the condition meaningless

💡 Remove the if-else and just call drawCharacter() once per character, or use the condition to draw Jack differently (e.g., add a halo or different size)

STYLE drawOldConsoles()

The console positions are hardcoded with magic numbers like -houseWidth / 2 + 50, making it difficult to reposition consoles without understanding the math

💡 Define console positions as named variables at the top of the function (e.g., let n64X = -houseWidth / 2 + 50;) so they're easy to tweak

🔄 Code Flow

Code flow showing setup, draw, drawoutsidescene, drawinsidescene, handleoutsideMovementAndInteractions, handleinsideMovementAndInteractions, drawhouse, drawoldconsoles, drawcharacter, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Setup] setup --> ground-positioning[Ground Positioning] setup --> house-positioning[House Positioning] setup --> character-population[Character Population] setup --> draw[draw loop] draw --> scene-dispatch[Scene Dispatch] draw --> interaction-display[Interaction Display] draw --> sky-and-ground[Sky and Ground] draw --> house-drawing[House Drawing] draw --> character-loop[Character Loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click ground-positioning href "#sub-ground-positioning" click house-positioning href "#sub-house-positioning" click character-population href "#sub-character-population" click draw href "#fn-draw" click scene-dispatch href "#sub-scene-dispatch" click interaction-display href "#sub-interaction-display" click sky-and-ground href "#sub-sky-and-ground" click house-drawing href "#sub-house-drawing" click character-loop href "#sub-character-loop" scene-dispatch --> drawoutsidescene[drawOutsideScene] scene-dispatch --> drawinsidescene[drawInsideScene] click drawoutsidescene href "#fn-drawoutsidescene" click drawinsidescene href "#fn-drawinsidescene" drawoutsidescene --> horizontal-movement[Horizontal Movement] drawoutsidescene --> movement-boundary[Movement Boundary] drawoutsidescene --> interaction-check[Interaction Check] drawoutsidescene --> house-entry[House Entry] click horizontal-movement href "#sub-horizontal-movement" click movement-boundary href "#sub-movement-boundary" click interaction-check href "#sub-interaction-check" click house-entry href "#sub-house-entry" drawinsidescene --> inside-movement[Inside Movement] drawinsidescene --> inside-boundary[Inside Boundary] drawinsidescene --> inside-interaction[Inside Interaction] drawinsidescene --> house-exit[House Exit] click inside-movement href "#sub-inside-movement" click inside-boundary href "#sub-inside-boundary" click inside-interaction href "#sub-inside-interaction" click house-exit href "#sub-house-exit" house-drawing --> drawhouse[drawHouse] click drawhouse href "#fn-drawhouse" character-loop --> drawcharacter[drawCharacter] click drawcharacter href "#fn-drawcharacter" drawinsidescene --> interior-walls[Interior Walls] drawinsidescene --> console-conditional[Console Conditional] drawinsidescene --> inside-character-loop[Inside Character Loop] click interior-walls href "#sub-interior-walls" click console-conditional href "#sub-console-conditional" click inside-character-loop href "#sub-inside-character-loop" drawhouse --> coordinate-transform[Coordinate Transform] drawhouse --> house-parts[House Parts] click coordinate-transform href "#sub-coordinate-transform" click house-parts href "#sub-house-parts" house-parts --> head[Head] house-parts --> body-limbs[Body and Limbs] house-parts --> name-tag[Name Tag] click head href "#sub-head" click body-limbs href "#sub-body-limbs" click name-tag href "#sub-name-tag" drawoldconsoles[drawOldConsoles] --> console-setup[Console Setup] drawoldconsoles --> three-consoles[Three Consoles] click drawoldconsoles href "#fn-drawoldconsoles" click console-setup href "#sub-console-setup" click three-consoles href "#sub-three-consoles" windowresized[windowResized] --> canvas-resize[Canvas Resize] windowresized --> recalculate-positions[Recalculate Positions] windowresized --> character-loop-resize[Character Loop Resize] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click recalculate-positions href "#sub-recalculate-positions" click character-loop-resize href "#sub-character-loop-resize"

❓ Frequently Asked Questions

What visual elements are featured in the 'jack and cappie' p5.js sketch?

The sketch visually represents two houses belonging to characters Jack and Cappie, with distinct colors and a detailed background that includes the ground and interiors.

How can users interact with the 'jack and cappie' sketch?

Users can interact with the sketch by controlling Jack's movement and exploring different scenes, such as transitioning between the outside and inside of the houses.

What creative coding techniques are showcased in the 'jack and cappie' project?

The project demonstrates object-oriented programming by using arrays to manage character properties and scene transitions, along with dynamic visual rendering based on user input.

Preview

jack and cappie - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of jack and cappie - Code flow showing setup, draw, drawoutsidescene, drawinsidescene, handleoutsideMovementAndInteractions, handleinsideMovementAndInteractions, drawhouse, drawoldconsoles, drawcharacter, windowresized
Code Flow Diagram