Project 2 — A clicking game
You will build: click the cat before the timer runs out. It jumps, your streak grows, a second sprite keeps score.
You will learn: globals vs sprite members, if/switch, while, broadcasts between
sprites, string interpolation, and how to reach a Scratch block the stdlib has not wrapped.
Make a file called clicker.knip.
Step 1 — A clickable sprite
Section titled “Step 1 — A clickable sprite”public score: num = 0;
sprite Cat { events.onFlag() { score = 0; motion.goTo(0, 0); looks.say("Click me!", 1); }
events.onClick() { score += 10; looks.say(f"Score: {score}", 1); }}Build and run. Clicking the cat scores.
f"Score: {score}" is an interpolated string. It compiles to Scratch’s join block —
and it is currently the practical way to put a number into a text slot, because the
Str() cast is declared but not yet lowered.
Step 2 — Make it move
Section titled “Step 2 — Make it move”A target that stays still is not a game. Scratch has a pick random block, but the
standard library has not wrapped it. Bind the opcode yourself:
proc random(@opcode = "operator_random", from: num, to: num) -> void {}Almost — that needs to return a number:
proc random(@opcode = "operator_random", from: num, to: num) -> num {}An empty body plus @opcode says “this procedure is that Scratch block”. The stdlib is
written entirely this way — it is a set of .knip files, not compiler special-cases, so
anything it declares you can declare too.
events.onClick() { score += 10; motion.goTo(random(-200, 200), random(-140, 140)); }Step 3 — Sprite state and a streak
Section titled “Step 3 — Sprite state and a streak”Add a member to the sprite:
sprite Cat { private streak: num = 0;
events.onClick() { streak += 1; score += 10 + streak; motion.goTo(random(-200, 200), random(-140, 140)); }}The difference between score and streak matters:
| Declared | Lives on | Seen by | |
|---|---|---|---|
score |
top level | the stage | every sprite |
streak |
inside Cat |
the Cat sprite | Cat only |
If you had named the member score too, Scratch could not represent it — stage and sprite
names resolve together. Katnip lets you write it and renames the sprite’s copy to
Cat_score in the output.
Step 4 — A procedure with a side effect
Section titled “Step 4 — A procedure with a side effect”Track a best score. This logic belongs in one place:
public score: num = 0;public best: num = 0;
proc award(points: num) -> void { score += points; if (score > best) { best = score; }} events.onClick() { streak += 1; award(10 + streak); motion.goTo(random(-200, 200), random(-140, 140)); }Step 5 — switch
Section titled “Step 5 — switch”Change the cat’s size with the streak, so the game gets visibly harder:
switch (streak % 3) { case (0) { looks.setSize(70); } case (1) { looks.setSize(100); } default { looks.setSize(130); } }switch compiles to an if/else chain, which has two consequences worth committing to
memory: there is no fallthrough, and there is no exhaustiveness check. Always write
a default.
A case can hold several values — case (0, 1) { ... } matches either.
Step 6 — A timer
Section titled “Step 6 — A timer”public timeLeft: num = 20; events.onFlag() { score = 0; streak = 0; looks.show(); looks.setSize(100); motion.goTo(0, 0); looks.say("Click me!", 1);
sensing.resetTimer(); while (sensing.timer() < timeLeft) { wait(0.1); }
looks.hide(); events.broadcast("over"); }The wait(0.1) is not optional. A while loop with no wait and no motion blocks pins
Scratch’s scheduler and the project stops responding — the same trap as a
run-without-screen-refresh loop in the block editor.
Step 7 — A second sprite
Section titled “Step 7 — A second sprite”A sprite cannot call another sprite’s procedure; custom blocks belong to a target. Broadcasts are the only cross-sprite control flow there is.
sprite Scoreboard { events.onFlag() { motion.goTo(0, 140); looks.show(); }
events.onBroadcast("scored") { looks.say(f"Score {score} - best {best}"); }}And in the Cat’s click handler, at the end:
events.broadcast("scored");The pattern is worth naming, because you will use it constantly:
- data travels through a top-level
publicvariable — a stage global - timing travels through the broadcast
events.broadcast fires and returns immediately. events.broadcastAndWait blocks until
every receiver has finished — use that when the next line depends on the result.
The finished file
Section titled “The finished file”public score: num = 0;public best: num = 0;public timeLeft: num = 20;
proc award(points: num) -> void { score += points; if (score > best) { best = score; }}
proc random(@opcode = "operator_random", from: num, to: num) -> num {}
sprite Cat { private streak: num = 0;
events.onFlag() { score = 0; streak = 0; looks.show(); looks.setSize(100); motion.goTo(0, 0); looks.say("Click me!", 1);
sensing.resetTimer(); while (sensing.timer() < timeLeft) { wait(0.1); }
looks.hide(); events.broadcast("over"); }
events.onClick() { streak += 1; award(10 + streak); motion.goTo(random(-200, 200), random(-140, 140));
switch (streak % 3) { case (0) { looks.setSize(70); } case (1) { looks.setSize(100); } default { looks.setSize(130); } }
events.broadcast("scored"); }
events.onBroadcast("over") { looks.show(); looks.setSize(100); motion.goTo(0, 0); looks.say(f"Final: {score} (best {best})", 3); }}
sprite Scoreboard { events.onFlag() { motion.goTo(0, 140); looks.show(); }
events.onBroadcast("scored") { looks.say(f"Score {score} - best {best}"); }}Try it yourself
Section titled “Try it yourself”- Reset
streakto 0 if the player misses — a click on the stage, not the cat. - Show the score with
showVariable(score)instead of a talking sprite. - Add a second, faster, smaller cat worth more points.
- Make
timeLeftshrink each round for a survival mode.
Project 3 — Falling treats, where you spawn clones and track state in lists.