← Teacher guide

Final challenge: Catch the circle

Students play a click-to-catch game, learn that mousePressed runs on its own and that touching means a small distance, fix a catch that scores for every miss, add lives with a game over and a restart key, and make a power-up ball.

Drawing and animation with p5.js · Final challenge: Catch the circle · about 60 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 15 min · Modify 15 min · Make 20 min

Before class

Read the unit primer first. This is the Python Catch the turtle challenge in JavaScript, and it completes the course. Everything has been taught except events and joining text: mousePressed() and keyPressed() are functions p5 calls for you. Click coordinates are the sketch's mouseX and mouseY. The common slip is > where < belongs, so the bug task is a logic error with no red line: students must read the condition and play. The game needs the sketch to be clicked first for keys to work. This lesson marks "Challenge complete".

Running the lesson

  1. Predict: draw a 3-4-5 triangle on the board.
  2. Run: let students play for a minute and call out their scores.
  3. Investigate: for the bug, ask 'when is the distance big?'
  4. Modify: ask where lives are taken away, and where the check for zero lives belongs.
  5. Make: pairs list the power-up's property and the two lines that change before coding.

The hard part (on purpose)

Close means a small distance. Play the buggy version and say what is happening before reading the code.

When students get stuck

What you will seeA question to askThe fix
Every click scores, even misses.Is the distance supposed to be bigger or smaller than 20 for a catch?Use dist(...) < 20.
Pressing R does nothing.Has the sketch been clicked first, and is lives zero?Click the picture, play until GAME OVER, then press R.
Lives go below zero or the game keeps moving after GAME OVER.Is the movement inside if (lives > 0)?Wrap the loop in if (lives > 0) { ... } else { ... }.
The power-up looks the same as the others.Where is its fill chosen?Choose fill with an if on the property before drawing the circle.

Answers to the checks

  1. Predict: What does console.log(dist(10, 10, 13, 14)) show?
    5. The two points are 3 apart across and 4 apart down, and the straight-line distance is 5.
  2. Which part does what?: Match each piece with what it does.
    mousePressed() → A. p5 runs it each time the mouse is clicked; dist(mouseX, mouseY, ball.x, ball.y) < 20 → C. the click is within 20 pixels of this ball; restart(ball); → D. sends this ball back to the top at a new x; "Score: " + score → B. joins the words and the number into one piece of text
  3. When does the score go up?: Where does the score go up?
    Inside mousePressed, in the if that checks the click is close
    The score should change when a catch happens, once. mousePressed runs once per click, so the if inside it decides whether the click was a catch.
  4. Spot the bug: In this game every click that misses scores. Click the line that is wrong.
    line 3
    The condition is true when the click is more than 20 away. A catch is when it is less than 20 away: dist(...) < 20.
  5. Where this leads: You have 4 balls and want to add 10 more. What is the smallest change?
    Change the number in the setup loop
    The balls live in a list and a loop visits each one. More balls is one number in the loop that fills the list.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › Lives

let balls = [];
let score = 0;
let lives = 3;

function setup() {
  createCanvas(300, 200);
  for (let i = 0; i < 4; i++) {
    balls.push({ x: random(20, 280), y: random(-100, 0), speed: random(1, 2.5) });
  }
}

function draw() {
  background(20, 30, 60);
  if (lives > 0) {
    fill(255, 210, 0);
    for (let ball of balls) {
      circle(ball.x, ball.y, 24);
      ball.y = ball.y + ball.speed;
      if (ball.y > 210) {
        lives = lives - 1;
        restart(ball);
      }
    }
    fill(255);
    text("Score: " + score + "   Lives: " + lives, 10, 20);
  } else {
    fill(255, 100, 100);
    textSize(24);
    text("GAME OVER", 80, 100);
    textSize(12);
    fill(255);
    text("Score: " + score, 120, 125);
  }
}

function restart(ball) {
  ball.x = random(20, 280);
  ball.y = -10;
}

function mousePressed() {
  for (let ball of balls) {
    if (dist(mouseX, mouseY, ball.x, ball.y) < 20) {
      score = score + 1;
      restart(ball);
    }
  }
}

Modify › Play again

let balls = [];
let score = 0;
let lives = 3;

function setup() {
  createCanvas(300, 200);
  for (let i = 0; i < 4; i++) {
    balls.push({ x: random(20, 280), y: random(-100, 0), speed: random(1, 2.5) });
  }
}

function draw() {
  background(20, 30, 60);
  if (lives > 0) {
    fill(255, 210, 0);
    for (let ball of balls) {
      circle(ball.x, ball.y, 24);
      ball.y = ball.y + ball.speed;
      if (ball.y > 210) {
        lives = lives - 1;
        restart(ball);
      }
    }
    fill(255);
    text("Score: " + score + "   Lives: " + lives, 10, 20);
  } else {
    fill(255, 100, 100);
    textSize(24);
    text("GAME OVER", 80, 100);
    textSize(12);
    fill(255);
    text("Score: " + score + "   Press R to play again", 60, 125);
  }
}

function restart(ball) {
  ball.x = random(20, 280);
  ball.y = -10;
}

function mousePressed() {
  for (let ball of balls) {
    if (dist(mouseX, mouseY, ball.x, ball.y) < 20) {
      score = score + 1;
      restart(ball);
    }
  }
}

function keyPressed() {
  if (key === "r" && lives === 0) {
    score = 0;
    lives = 3;
  }
}

a power-up

let balls = [];
let score = 0;

function setup() {
  createCanvas(300, 200);
  for (let i = 0; i < 4; i++) {
    balls.push({ x: random(20, 280), y: random(-100, 0), speed: random(1, 2.5), gold: false });
  }
  balls.push({ x: 150, y: -50, speed: 3.5, gold: true });
}

function draw() {
  background(20, 30, 60);
  for (let ball of balls) {
    if (ball.gold) {
      fill(255, 255, 255);
    } else {
      fill(255, 210, 0);
    }
    circle(ball.x, ball.y, 24);
    ball.y = ball.y + ball.speed;
    if (ball.y > 210) {
      restart(ball);
    }
  }
  fill(255);
  text("Score: " + score, 10, 20);
}

function restart(ball) {
  ball.x = random(20, 280);
  ball.y = -10;
}

function mousePressed() {
  for (let ball of balls) {
    if (dist(mouseX, mouseY, ball.x, ball.y) < 20) {
      if (ball.gold) {
        score = score + 5;
      } else {
        score = score + 1;
      }
      restart(ball);
    }
  }
}

Stretch and support

Stretch: Make the balls get faster as the score rises, or add a high score kept in a variable.

Support: Give the lives code and ask only for the restart key, or give the gold ball and ask for a second power-up.

Exit ticket

Why is the catch test dist(...) < 20 and not > 20?

Two things touch when the distance between them is small.

What each step is for

Where it leads

Optional next lesson: moving art with sin, cos and map.