6.7 Advanced Input
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Learning Objectives
By the end of this section, you will be able to:
- Detect which sprite the mouse is over using
world.getSpriteAt(). - Implement a clickable button by hit-testing a mouse press against a sprite.
- Build a drag-and-drop system for sprites.
§6.6's title screen currently reads "Press space to start" — keyboard-only. This section adds a real clickable button using hit testing, then uses the same technique to let the player drag coins around before the level even begins.
6.7.1 Hit Testing with getSpriteAt
world.getSpriteAt(x, y) returns the top-most (highest layer) sprite at that world position, or undefined if nothing is there. This is called hit testing — checking whether a point (like the mouse cursor) overlaps with any sprite. It's the foundation of all mouse interaction in games.
The process of checking whether a point (such as the mouse cursor position) overlaps with any sprite in the world. world.getSpriteAt(x, y) performs hit testing.
On the title screen, add a rectangle sprite labeled as a button (draw 'Start' text over it) and log "clicked!" when mouse.presses() fires while world.getSpriteAt(mouse.x, mouse.y) returns that button sprite.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Click anywhere on the green rectangle and the console logs "clicked!". Click anywhere else on the canvas and nothing happens — getSpriteAt returned undefined, which never equals startButton.
6.7.2 A Clickable Start Button
The click-to-button pattern has three steps: (1) on mouse.presses(), call world.getSpriteAt(mouse.x, mouse.y); (2) compare the result to the button sprite you're checking; (3) if it matches, do whatever pressing that button should do.
A button is just a sprite with collider = 'none' (so it never affects physics) that you hit-test against. Everything from §6.7.1 already is one — "button" describes how you're using a sprite, not a different kind of sprite.
Replace §6.6's keyboard-only title screen with this button: clicking 'Start' calls startGame() and switches to 'play', exactly like pressing space did before. Keep the space-bar path working too — offer both.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Both paths now start the game: clicking the green 'Start' rectangle, or pressing space like before. The clicked || kb.presses(' ') line is the whole feature — everything else was already built in §6.6.
6.7.3 Drag and Drop
Building on hit-testing, a drag is: on mouse.presses(), check whether the cursor landed on the sprite you want to drag; while the button stays down, snap the sprite's position to the cursor each frame and zero its velocity (so physics doesn't fight the snap); release ends the drag.
An interaction where the user presses the mouse on a sprite, moves it to a new position while holding the button, and releases to drop it.
Before the level starts, let the player customize it: on the title screen, spawn the 8 coins where they'll appear in 'play', and let the player click-and-drag any one of them to a new position before clicking Start. When startGame() runs, it should keep the coins where the player put them instead of resetting to the default row.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Each coin can be picked up and dropped anywhere on the canvas; coins.includes(hit) confirms the clicked sprite belongs to the group before starting a drag, so clicking the Start button (which isn't in coins) never accidentally grabs it. Because the coins are the very same sprites startGame() would use, nothing needs to "remember" the new positions separately — they're already sitting where the player left them.
6.7.4 Challenge: Extend It Yourself
No starter code this time — you build it. Add a second button next to Start: 'Reset Layout', which returns every coin to its original row position (30 + i * 45, 260) when clicked, undoing any dragging.
Hint (try the Challenge yourself first!)
You'll need to remember each coin's original (x, y) at the moment it's created — store it as a plain array of {x, y} objects alongside the group, or as extra properties directly on each sprite (c.homeX = ...), then loop over coins and restore from that.
Problem Set 6.7
Problem 1. What does world.getSpriteAt(x, y) return? What does it return if no sprite is at that position?
Solution
Step 1 — Recall the definition:
world.getSpriteAt(x, y) performs hit testing at a world position and returns the top-most (highest layer) sprite whose shape covers the point \((x, y)\).
Step 2 — The empty case:
If no sprite overlaps that position, it returns undefined. This is why hit-test code usually compares the result against a specific sprite (=== startButton) or checks truthiness (if (hit)) before using it.
Answer: world.getSpriteAt(x, y) returns the top-most sprite at that world position, or undefined if there is no sprite there.
Problem 2. What is hit testing? Why is it important for mouse interaction in games?
Solution
Step 1 — Define hit testing:
Hit testing is the process of checking whether a point — such as the mouse cursor position \((\text{mouse.x}, \text{mouse.y})\) — overlaps with any sprite in the world. world.getSpriteAt(x, y) is the function that performs it.
Step 2 — Why it matters: Mouse interaction needs to translate "the user clicked at pixel \((x, y)\)" into "the user clicked on this object." Without hit testing, a click handler would fire identically everywhere on the canvas with no way to know which sprite (if any) was under the cursor. Hit testing is what makes buttons clickable, sprites selectable, and drag-and-drop possible — every mouse interaction in a game builds on it.
Answer: Hit testing is checking whether a point (like the cursor) overlaps any sprite; it's essential because it lets the game determine which object the mouse is interacting with, enabling clicks, selection, and dragging.
Problem 3. In §6.7.2, why does the code check world.getSpriteAt(mouse.x, mouse.y) === startButton instead of just checking mouse.presses() alone?
Solution
Step 1 — What mouse.presses() alone tells you:
mouse.presses() only reports that the mouse button went down somewhere on the canvas. It carries no information about where the click landed or what was under the cursor.
Step 2 — What the comparison adds: world.getSpriteAt(mouse.x, mouse.y) === startButton answers two questions at once:
- Was anything clicked? (
getSpriteAtreturnsundefinedif not.) - Was it specifically the Start button? (Strict equality against
startButton.)
Step 3 — Why both are needed together:
Combining them with && means startGame() runs only when the press happened and the press landed on the button. Checking mouse.presses() alone would start the game no matter where the player clicked — even empty background — making the button meaningless.
Answer: Because mouse.presses() only detects that a click occurred anywhere; the hit test confirms the click actually landed on the Start button before triggering the action.
Problem 4. Describe the three phases of a drag interaction. What code runs in each phase?
Solution
Phase 1 — Press (grab):
Runs once when mouse.presses() fires. Code: hit-test the cursor with world.getSpriteAt(mouse.x, mouse.y), check the result belongs to the draggable set (e.g., coins.includes(hit)), and store it as the currently dragged sprite (dragging = hit).
Phase 2 — Hold (drag):
Runs every frame while the button stays down (mouse.pressing()). Code: snap the dragged sprite to the cursor each frame (dragging.x = mouse.x; dragging.y = mouse.y;) and zero its velocity (dragging.vel.x = 0; dragging.vel.y = 0;) so physics doesn't fight the snap — without it, a sprite still carrying momentum keeps drifting after each frame's snap. The coin example skips those two lines because its coins run collider = 'none' at world.gravity.y = 0, so nothing is pushing them; any draggable sprite with a real collider needs them.
Phase 3 — Release (drop):
Detected when mouse.pressing() becomes false. Code: clear the drag reference (dragging = null), leaving the sprite wherever it was dropped.
Answer: Press → grab via hit test and store the sprite; Hold → snap its position to the cursor each frame while the button is down; Release → stop dragging by clearing the reference when the button comes up.
Problem 5. In the coin-dragging example, why does coins.includes(hit) matter before starting a drag?
Solution
Step 1 — What could go wrong without it:
On the title screen there are multiple sprites: the coins and the Start button. If the code started a drag whenever world.getSpriteAt returned any sprite, clicking the Start button would set dragging = startButton, and the button itself would get dragged around the screen instead of being clicked.
Step 2 — What the check guarantees:
coins.includes(hit) verifies the hit sprite is actually a member of the coins group before assigning it to dragging. Only genuine coins can be picked up; everything else (button, background) is ignored as a drag target.
Step 3 — Bonus benefit:
Because the check filters drags to coins, the same mouse.presses() event can safely also be used for the Start-button click logic without the two features interfering.
Answer: It ensures the grabbed sprite really belongs to the coin group, so clicking other sprites (like the Start button) never accidentally starts a drag of the wrong object.
Problem 6. Modify the button example so startButton changes to a lighter green while the mouse is hovering over it (hint: you'll need to hit-test every frame, not just on mouse.presses()).
Solution
Step 1 — Understand hover vs. press:
Hovering means the cursor is over the button without clicking. So we must hit-test every frame in draw(), not just inside an if (mouse.presses()) block — a press happens once per click, but hover state changes continuously.
Step 2 — Write the code:
▶ Press Run to see the output…
Step 3 — Explain the key line:
The ternary sets the color each frame based on the current hover state: lighter green ('mediumseagreen') while the cursor is over the button, normal 'seagreen' otherwise. Since this runs in draw(), the color updates instantly as the mouse moves in and out.
Answer: Store the per-frame result of world.getSpriteAt(mouse.x, mouse.y) === startButton and use it to set startButton.color to a lighter green when true and the original color when false — see the code above.
Problem 7. Write code that lets the user click on any of three sprites to select it, then press the Delete key to remove the selected sprite from the world.
Solution
Step 1 — Track the selection: We need a variable holding whichever sprite the user last clicked. Clicking one of the three sprites selects it; clicking elsewhere deselects.
▶ Press Run to see the output…
Step 2 — Select on click:
On mouse.presses(), hit-test and accept the click only if it landed on one of our three sprites:
function draw() {
background('#112');
if (mouse.presses()) {
const hit = world.getSpriteAt(mouse.x, mouse.y);
if (hit === a || hit === b || hit === c) {
selected = hit;
} else {
selected = null;
}
}
Step 3 — Delete on keypress: When Delete is pressed and something is selected, remove it from the world and clear the selection:
if (kb.presses('Delete') && selected) {
selected.remove();
selected = null;
}
}
Step 4 — Verify the flow:
Click a sprite → it's stored in selected; press Delete → selected.remove() destroys exactly that sprite; pressing Delete with nothing selected does nothing because of the truthiness guard.
Answer: See the full code above — click to select via world.getSpriteAt, then kb.presses('Delete') calls selected.remove() and clears the selection.
Problem 8. What is the difference between mouse.presses() and mouse.pressing()? Why does the drag code use both?
Solution
Step 1 — mouse.presses():
Returns true for exactly one frame, at the moment the mouse button transitions from up to down. Use it for one-time events: grabbing a sprite, registering a click.
Step 2 — mouse.pressing():
Returns true continuously for every frame the button is held down. Use it for ongoing states: checking whether a drag is still active.
Step 3 — Why the drag code uses both:
mouse.presses()starts the drag: it fires once, so the grab logic (hit test + storingdragging) runs a single time rather than repeatedly.!mouse.pressing()ends the drag: since it's true every held frame, the moment it flips to false we know the button was released, so we cleardragging.
Using presses() for the release check wouldn't work (it doesn't detect release), and using pressing() for the grab would re-run the grab logic every frame.
Answer: mouse.presses() is true only on the single frame the button goes down (edge-triggered); mouse.pressing() is true the whole time it's held (level-triggered). The drag code uses presses() to begin the drag once and pressing() to detect when the button is released and end it.
Key Terms
| Term | Definition |
|---|---|
| Drag | An interaction where the user presses on a sprite, moves it while holding the button, and releases to drop it |
| Hit testing | Checking whether a point (like the mouse cursor) overlaps with any sprite in the world |
| world.getSpriteAt | A function that returns the top-most sprite at a given world position, or undefined if none exists |