6.2 Overlaps and Collisions

Aligned outcomes:

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:

In this section, you will learn to:
  • Explain the difference between an overlap and a collision.
  • Use overlaps() to detect when two sprites occupy the same space.
  • Use the callback form of overlaps() to act on the specific sprite that was touched.
  • Use colliding() to detect solid physical contact.
  • Explain why colliding() returns a number rather than true or false.
  • Build a pickup that scores and disappears when the player touches it.
  • Safely remove sprites from a group while iterating it, and explain why group.remove() and sprite.delete() are different operations.

6.2.1 Two Different Questions

Section 6.1 gave you a group of sprites. The obvious next question is did the player touch one? — and moSHion answers it two different ways, because "touch" means two different things in a game.

Think about a platformer. The player lands on a platform and stops — the platform is solid, it pushes back, the physics engine resolves it. The player also runs through a coin and collects it — the coin does not push back, does not slow the player down, and vanishes. Both are "touching". Only one is a collision.

moSHion keeps them apart:

Definition 6.2.1: Overlap

Two sprites occupying the same space on screen. Detected with overlaps(), which compares positions directly and does not require either sprite to participate in physics.

Definition 6.2.2: Collision

A solid physical contact between two sprites, resolved by the physics engine so that they push each other apart. Detected with colliding().

Read the two words as the question each one asks. overlaps is a geometry question: are you in the same place? colliding is a physics question: did you hit each other? A sprite with collider = 'none' can answer the first and can never answer the second, because there is nothing there to hit.

These are not two names for the same thing, and using the wrong one is the most common reason a pickup "doesn't work". A coin with collider = 'none' — the setting Section 6.1.3 recommended for pickups — has no physics body for the engine to make contact with, so colliding() will never report it. Only overlaps() sees it.

Try It Now 6.2.1

Before writing any code, decide which method each of these needs:

  1. A coin the player runs through to collect.
  2. A wall the player cannot walk past.
  3. A trigger zone that starts a cutscene when entered.
  4. A crate the player pushes across the floor.
Solution
  1. overlaps() — the coin should not stop the player.
  2. colliding() — the wall is solid and pushes back.
  3. overlaps() — a trigger zone is invisible and passable; it only reports.
  4. colliding() — the crate is solid and moves when pushed.

The pattern: if the object stops or moves the player, it is a collision. If the object only notices the player, it is an overlap.

6.2.2 Detecting an Overlap

overlaps() is called on one sprite and passed another. It returns true or false.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Drive the blue circle into the gold one with the arrow keys and the console fills with messages.

That last detail matters: the message repeats every frame you are touching. overlaps() reports a state — "we are overlapping right now" — not an event. At 60 frames a second, a coin you sit on top of for one second prints sixty times.

That is exactly what you want for something continuous, like standing in a damage zone. It is not what you want for collecting a coin, and the fix is to make the coin stop existing:

  if (player.overlaps(coin)) {
    coin.remove();
    score = score + 1;
  }

Once the coin is removed, there is nothing left to overlap, so the branch cannot run twice. Removing the thing you just collected is both the game behaviour you wanted and the way you stop the repeat.

6.2.3 Overlaps and Groups

Checking one coin is not useful; a game has twenty. overlaps() also accepts a Group, and returns true if the sprite overlaps any member of it:

This is the same distinction Section 5.2 drew between kb.pressing() and kb.presses() — a state that is true for as long as something holds, versus a one-off event. overlaps() is a pressing-style question. When you need it to fire once, you have to arrange that yourself, and removing the sprite is the simplest arrangement there is.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

One line covers all eight coins, and it would cover eight hundred. The group form is what makes overlap detection scale — without it you would need a loop and an if for every sprite, which is the repetition Section 3.1.3 warned about.

But notice what this version cannot do: it knows you touched a coin, and not which coin. There is nothing to remove and nothing to score. That is what the callback form is for.

Try It Now 6.2.2

Build a scene with a player circle and a group of 5 stationary red squares with collider set to 'none'. Print "Danger" whenever the player is inside any of them.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

The hazards do not stop the player, because their collider is 'none' — they only notice. Try removing that line and you will feel the difference immediately: the squares become solid and shove you around.

6.2.4 Reacting to the Sprite You Touched

overlaps() takes an optional second argument: a function to run for each overlapping pair. moSHion calls it with two sprites — the one you asked about, and the one it touched.

  player.overlaps(coins, (self, coin) => {
    coin.remove();
    score = score + 1;
  });

That is the callback from Section 3.4.5, doing real work. player.overlaps(coins, ...) walks the group, and for every coin actually being touched it calls your arrow with self set to the player and coin set to that specific coin. Now you have the one thing the boolean form could not give you: a handle on the sprite to remove.

Here is the whole pickup game:

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Three things worth pointing at:

The boolean form and the callback form answer different questions, and choosing between them is the same decision every time. Did anything happen? → boolean. What exactly did I hit, and what should I do to it? → callback. Reaching for the boolean form and then hunting for the sprite with a loop is rewriting the walk moSHion already did.

Try It Now 6.2.3

Extend the pickup game: make half the coins 'crimson' bombs worth minus 5 points. (Hint: two groups, two overlaps calls — the callback tells you which group was hit because you called it on that group.)

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

You never have to ask what kind of thing you hit. Each group gets its own call, so the group you asked about is the answer.

6.2.5 Solid Contact with colliding()

colliding() is the other question: did the physics engine record a real contact? It needs both sprites to have physics bodies, so a sprite with collider = 'none' is invisible to it.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

You met this in Section 6.3's jumping code, where landing on the ground is what makes another jump legal. The important part here is what it hands back.

colliding() returns a number

overlaps() gives you true or false. colliding() gives you a number — how many frames the contact has lasted so far.

  if (player.colliding(ground)) {
    console.log(player.colliding(ground));   // 1, then 2, then 3, ...
  }

That works in an if because any number except 0 is truthy — Section 2.1.2's rule doing real work in a game. But it means this is wrong:

  if (player.colliding(ground) === true) {   // NEVER true
    player.vel.y = -10;
  }

1 === true is false. The comparison fails on the very first frame of contact and every frame after, the jump never fires, and nothing anywhere reports an error.

Definition 6.2.3: Contact frame count

colliding() returns the number of consecutive frames two sprites have been in solid contact, starting at 1. It is truthy while contact lasts and 0 when it ends, so it works directly in a condition but must never be compared with === true.

The number is genuinely useful once you stop fighting it. player.colliding(ground) === 1 is true on exactly the frame of landing — the event, not the state — which is how you play a landing sound once instead of sixty times a second:

  if (player.colliding(ground) === 1) {
    console.log('Landed!');
  }

Three times now the same shape has appeared: kb.pressing vs kb.presses, an overlaps() that repeats every frame, and a contact count whose first frame is the landing. A game loop runs 60 times a second, so every question you ask it is a state by default, and every event has to be built out of a state by noticing the frame it changed on. Expect it, and the next engine you use will hold no surprises here.

Try It Now 6.2.4

Build a scene with a falling ball and a static floor. Print "bounce" exactly once each time the ball lands, not every frame it rests there.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Press space to drop the ball again. === 1 catches the first frame of each contact, so you get one message per landing however long the ball then sits there. Change it to if (ball.colliding(floor)) and watch the console fill instead.

6.2.6 Projectiles: Spawning and Cleaning Up a Group

A Group extends JavaScript's own Array — you get .length, indexing, for..of, and array methods for free. That makes it the natural home for a stream of short-lived sprites like bullets: spawn one into the group on an edge-triggered fire key, and remove each one once it leaves the canvas so they don't pile up forever.

There is one trap. Removing a sprite while iterating the group directly shifts the array and skips the next entryfor (const b of bullets) { if (offscreen) b.delete(); } will silently miss some bullets, because deleting index 2 shifts index 3 into its place while the loop has already moved on to what is now index 4. The fix is to iterate a copy: for (const b of [...bullets]). The spread makes a snapshot, so the original group can shrink safely underneath it without the loop losing its place.

Definition: Safe Iteration

Looping over [...group] (a spread-copied array) instead of group directly, so that deleting members during the loop cannot shift the group's own indices out from under the iteration and cause an entry to be skipped.

§6.2.4's overlaps(group, callback) never needs this trick — the method finishes its own internal walk before calling your callback, so deleting inside that callback is already safe. The [...group] pattern only matters when you're writing the loop yourself, as this section does.

There's also a second removal operation worth telling apart from sprite.delete(): group.remove(sprite) pulls a sprite out of the group but leaves it in the world — it still draws and still runs physics, it just stops being a member of that particular group. sprite.delete() (§5.2.5) destroys the sprite entirely, everywhere. Reaching for group.remove() when you meant delete() leaves a ghost sprite behind that still draws and still collides, just quietly untracked by whatever group code was watching it.

Definition: group.remove() vs sprite.delete()

group.remove(sprite) unparents a sprite from one specific group — the sprite still exists, still draws, still runs physics. sprite.delete() destroys the sprite completely and removes it from every group it belonged to. The two are not interchangeable.

Try It Now 6.2.5

Add a projectile mechanic to your scene: pressing 'f' spawns a bullet into a bullets group at the player's position, moving right at a fixed speed. Every frame, safely remove any bullet that has flown past the right edge of the canvas.

Editor
runs in a sandboxed frame
▶ Press Run to see the output…
Solution
Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Hold 'f' down and watch bullets: N — it climbs by exactly one per press, never more, because kb.presses is edge-triggered. Fire several and watch the count fall back as each one crosses x > 420 and gets safely deleted, even though the loop is removing entries out from under itself — because it's walking [...bullets], a snapshot, rather than bullets directly.

6.2.7 Challenge: Extend It Yourself

No starter code this time — you build it. Take the coins-and-bombs pickup game from Try It Now 6.2.3 and add a third group: gems, worth 10 points each (more than a coin), but only 2 of them on screen. Keep this sketch — §6.3 adds gravity and a ground to this exact game, so a player, a coins group, a bombs group, and a running score variable are what the rest of the chapter builds on.

Hint (try the Challenge yourself first!)

Same shape as bombs: a new Group, a color, a collider = 'none', a short spawn loop, and its own player.overlaps(gems, ...) callback that adds 10 instead of subtracting 5.

Problem Set 6.2

Problem 1. Explain the difference between an overlap and a collision in one sentence each.

Solution

Step 1 — Define the overlap: An overlap is a geometry question — two sprites occupy the same space on screen, detected with overlaps(), which compares positions directly and does not require either sprite to have any physics at all.

Step 2 — Define the collision: A collision is a physics question — a solid contact between two sprites with physics bodies, resolved by the engine so that they push each other apart, detected with colliding().

Step 3 — State the practical difference: An overlap only notices the player (the coin); a collision stops or moves the player (the wall or crate). That is why a sprite with collider = 'none' can be overlapped but can never be collided with.

Answer: An overlap is two sprites sharing the same space, detected by position alone with overlaps(); a collision is a solid physical contact resolved by the physics engine so the sprites push apart, detected with colliding().

Problem 2. For each, say whether you would use overlaps() or colliding(): a coin, a wall, a cutscene trigger, a pushable crate.

Solution

Step 1 — Apply the test to each object: Ask of each one: does it stop or move the player (collision), or does it only notice the player (overlap)?

  • Coin: overlaps() — you run through it and collect it, so it must not push back.
  • Wall: colliding() — it is solid and must stop the player.
  • Cutscene trigger: overlaps() — an invisible, passable zone that only reports being entered.
  • Pushable crate: colliding() — it is solid, and the push is a physics contact that moves it.

Answer: Coin → overlaps(); wall → colliding(); cutscene trigger → overlaps(); pushable crate → colliding().

Problem 3. Why can colliding() never detect a sprite whose collider is 'none'?

Solution

Step 1 — Recall what colliding() asks: It asks the physics engine whether it recorded a solid contact between two bodies.

Step 2 — Recall what collider = 'none' does: It removes the sprite from physics entirely — there is no body for the engine to track, touch, or resolve.

Step 3 — Put the two together: Since the engine never sees the sprite, it can never record contact with it, so colliding() reports 0 for it forever. Only overlaps(), which compares positions directly, can find it.

Answer: A collider = 'none' sprite has no physics body, so the physics engine never registers a contact with it — and colliding() only reports contacts the engine registered. Detect such sprites with overlaps() instead.

Problem 4. Why does if (player.overlaps(coin)) print sixty times a second, and what is the simplest fix when collecting a coin?

Solution

Step 1 — Recall how the draw loop runs: It executes about 60 times per second, and every if inside it is re-tested on every frame.

Step 2 — Recognize overlaps() as a state: overlaps() answers "are we overlapping right now?", so while the player sits on the coin the answer stays true frame after frame — roughly sixty prints per second of contact. It is a kb.pressing()-style state question, not a one-off event.

Step 3 — Apply the simplest fix: Remove the coin inside the branch:

if (player.overlaps(coin)) {
  coin.remove();
  score = score + 1;
}

Once the coin is removed there is nothing left to overlap, so the branch cannot run twice — removing the collected thing is both the game behaviour you wanted and the repeat-stopper.

Answer: Because overlaps() is a state that stays true for every frame of contact, and the loop re-tests it 60 times a second; the simplest fix is coin.remove() inside the branch, so the coin stops existing after the first touch.

Problem 5. What does player.overlaps(coins) return when coins is a Group, and what can it not tell you?

Solution

Step 1 — Recall the group form: player.overlaps(coins) with a Group returns true if the player overlaps any member of the group, and false otherwise — one line covers all the coins.

Step 2 — Note what is missing: The boolean tells you that something was touched but not which one — you get no handle on the specific coin, so you cannot remove it, score against it individually, or act on it in any way.

Step 3 — Name the tool that fills the gap: The callback form, player.overlaps(coins, (self, coin) => { ... }), runs once per actually-touched coin and hands you that specific sprite.

Answer: It returns a single boolean — true when the player touches any coin in the group. It cannot tell you which coin was touched, so it gives you nothing to remove or act on.

Problem 6. Rewrite this so the coin actually disappears when collected.

if (player.overlaps(coins)) {
  score = score + 1;
}
Solution

Step 1 — Diagnose the original: The boolean group form cannot tell you which coin was touched, so there is no sprite to remove — and the score would keep climbing every frame the player touches any coin.

Step 2 — Switch to the callback form: The callback runs once per actually-touched coin and hands you that specific sprite, which is the handle you need in order to remove it:

player.overlaps(coins, (self, coin) => {
  coin.remove();
  score = score + 1;
});

Step 3 — Check the behaviour: coin.remove() deletes the coin from the world and from the group, so each coin can be collected exactly once, the score rises by 1 per coin, and the overlap cannot repeat on a coin that no longer exists.

Answer: Use the callback form and remove the specific coin:

player.overlaps(coins, (self, coin) => {
  coin.remove();
  score = score + 1;
});

Problem 7. In player.overlaps(coins, (self, coin) => { ... }), what is self and what is coin?

Solution

Step 1 — Identify self: It is the sprite you called the method on — here, the player. moSHion always passes that as the first argument.

Step 2 — Identify coin: It is the specific member of the coins group that the player is currently overlapping. The callback runs once per touched coin, and each run gets its own coin bound to that run's sprite.

Step 3 — Note that the names are yours: (self, coin) could equally be (a, b) — only the order is fixed: first the sprite you asked about, second the group member it touched.

Answer: self is the player (the sprite the method was called on); coin is the particular coin from the group that self is touching on that callback call.

Problem 8. Why is if (player.colliding(ground) === true) always false?

Solution

Step 1 — Recall the return type: colliding() returns a number — the count of consecutive frames of solid contact, starting at 1 — not a boolean.

Step 2 — Apply strict equality: === compares without any type conversion, and a number is never strictly equal to a boolean, so 1 === true evaluates to false.

Step 3 — Trace the consequence: On the first frame of contact colliding() returns 1, and 1 === true fails; on every later frame it returns 2, 3, 4…, which also fails. The branch never runs, the jump never fires, and nothing anywhere reports an error.

Answer: Because colliding() returns a frame count (a number), and 1 === true is false under strict equality — the comparison can never succeed. Test truthiness directly with if (player.colliding(ground)), or compare against a number such as === 1.

Problem 9. What is the difference between colliding(ground) and colliding(ground) === 1, and when do you want each?

Solution

Step 1 — Characterize the bare call: player.colliding(ground) is truthy for the entire contact — every frame from the first until the sprites separate — because any number except 0 is truthy. It answers the state question "am I touching the ground right now?"

Step 2 — Characterize the === 1 test: player.colliding(ground) === 1 is true only on the very first frame of contact. It answers the event question "did I just land this frame?"

Step 3 — Match each to its use: Use the bare state for continuous conditions — for example, allowing a jump only while grounded. Use === 1 for one-off actions — for example, playing a landing sound once instead of sixty times a second.

Answer: colliding(ground) is a state, truthy for the whole contact; colliding(ground) === 1 is an event, true only on the landing frame. Use the state for "while grounded" logic and the event for "on landing" actions like a sound effect.

Problem 10. Write the condition that plays a sound on the exact frame a player lands.

Solution

Step 1 — Pick the event test: The landing frame is the first frame of contact, and colliding() counts contact frames starting at 1, so comparing with === 1 isolates exactly that frame.

Step 2 — Write the condition:

if (player.colliding(ground) === 1) {
  landSound.play();
}

Step 3 — Verify it fires once per landing: On later frames of the same contact the count is 2, 3, 4…, so the condition is false and the sound cannot repeat; a fresh landing restarts the count at 1, so the sound plays again exactly once.

Answer:

if (player.colliding(ground) === 1) {
  landSound.play();
}

Problem 11. Build a pickup game with a player, 6 gold coins, and a score that rises as they are collected.

Solution

Step 1 — Plan the pieces: A player sprite, a coins Group with collider = 'none' (so the coins are passable pickups that only overlaps() can see), 6 coins spawned in a loop, arrow-key movement, and the callback form of overlaps() to remove each coin and raise the score.

Step 2 — Write the code:

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Step 3 — Check the mechanics: collider = 'none' means the player passes through the coins, so detection must use overlaps(); the callback removes each touched coin (which also stops the state repeating) and adds 1 to the score; the score is drawn every frame.

Answer: The game above — 6 gold, passable coins worth 1 point each, collected through player.overlaps(coins, ...) with coin.remove() inside the callback.

Problem 12. Extend 6.2.11 with a win message that appears when coins.length reaches 0.

Solution

Step 1 — Identify the signal: coin.remove() takes the sprite out of the world and out of the group, so coins.length falls by one per pickup and reaches 0 exactly when every coin has been collected.

Step 2 — Add the check to draw(): After the overlaps callback, test the length and draw the message when the board is clear:

  if (coins.length === 0) {
    text('All collected!', 150, 200);
  }

Step 3 — Full extended code:

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Answer: Watch coins.length — it falls as coins are removed — and draw the win message when it reaches 0:

if (coins.length === 0) {
  text('All collected!', 150, 200);
}

Problem 13. Add a group of crimson bombs worth minus 5 points to the game from 6.2.11.

Solution

Step 1 — Mirror the coin group: A bomb is the same shape as a coin with a different colour and effect: a bombs Group, 'crimson', collider = 'none', and its own short spawn loop.

Step 2 — Give it its own callback: Because you call overlaps() on a group, the group you ask about is the answer — so a second call on bombs subtracts 5 for each bomb touched, and you never have to ask what kind of thing you hit:

  player.overlaps(bombs, (self, bomb) => {
    bomb.remove();
    score = score - 5;
  });

Step 3 — Full code:

Editor
runs in a sandboxed frame
▶ Press Run to see the output…

Answer: A bombs group with collider = 'none' and its own overlaps callback that removes the bomb and subtracts 5 — one group per object type means the call site itself tells you what was hit.

Problem 14. Name the three places so far where a per-frame state had to be turned into a one-off event, and describe the trick each one used.

Solution

Step 1 — Keyboard input: kb.pressing() is a state — true the whole time a key is held. The event version is kb.presses(), and the trick is the engine's built-in edge detection: it fires exactly once, on the frame the key goes down.

Step 2 — Overlap pickup: overlaps() is a state — true every frame of contact. The trick is to remove the sprite inside the branch or callback, so after the first frame there is nothing left to overlap and the condition cannot be true again. You build the one-off yourself by destroying the thing you touched.

Step 3 — Collision landing: colliding() is a state — a count that stays nonzero for the whole contact. The trick is comparing with === 1, which is true only on the first frame of contact, turning the ongoing contact into a single landing event.

Answer: (1) kb.presses() vs kb.pressing() — the engine's edge detection fires once per key-down; (2) the repeating overlaps() — remove the overlapped sprite so the state cannot recur; (3) the contact count — test colliding() === 1 to catch only the first frame. In every case a per-frame state had to be converted into an event by noticing the frame it started.

Problem 15. Why does looping over bullets directly and deleting inside the loop risk skipping an entry, while looping over [...bullets] does not?

Solution

Step 1 — Remember a Group is an array: bullets extends JavaScript's Array, so deleting a member splices it out of the sequence and every later bullet shifts down one index.

Step 2 — Trace the skip: Suppose the loop is at index 2 and deletes that bullet. The bullet that was at index 3 now moves into index 2 — but the loop has already finished with index 2 and advances to index 3, which now holds what used to be at index 4. The bullet that shifted into index 2 is never visited, silently and with no error.

Step 3 — Explain the snapshot: [...bullets] evaluates the spread once, making a plain copy of the group's current contents. The loop walks that unchanging snapshot while the original group shrinks underneath it, so shifting indices in the group cannot move anything inside the snapshot — every bullet captured at the start is visited exactly once.

Answer: Deleting during a direct loop shifts later elements down one index while the loop counter keeps advancing, so the element that moved into the just-deleted slot gets skipped; [...bullets] loops over an unchanging copy, so the group can shrink safely underneath the loop without losing its place.

Problem 16. What is the difference between group.remove(sprite) and sprite.delete()? Which one would leave a sprite still drawing on screen?

Solution

Step 1 — Define group.remove(sprite): It unparents the sprite from that one group only — the sprite still exists in the world, still draws, still runs physics, and remains a member of any other group it belongs to.

Step 2 — Define sprite.delete(): It destroys the sprite entirely — removed from the world and from every group it belonged to. Beware the near-identical names: sprite.remove() is an alias for sprite.delete(), while group.remove(sprite) is the genuinely different operation.

Step 3 — Answer the screen question: The ghost-drawing operation is group.remove(sprite) — the sprite keeps drawing and keeps colliding, it is just quietly untracked by whatever group code was watching it.

Answer: group.remove(sprite) detaches the sprite from one group but leaves it alive in the world; sprite.delete() destroys it everywhere. group.remove(sprite) is the one that leaves a sprite still drawing on screen.

Key Terms

Overlap -- Two sprites occupying the same space, detected with overlaps(). Works regardless of physics.

Collision -- A solid physical contact resolved by the physics engine, detected with colliding().

overlaps(other) -- Returns true or false. Accepts a Sprite or a Group; with a Group it is true if any member overlaps.

Callback form -- overlaps(group, (self, other) => { ... }), which runs your function once per touched sprite and hands you that sprite.

colliding(other) -- Returns the number of consecutive frames of solid contact, not a boolean. Truthy during contact, 0 otherwise.

=== 1 -- The test for the first frame of a contact, turning an ongoing state into a landing event.

collider = 'none' -- Removes a sprite from physics entirely; it can still be found by overlaps() but never by colliding().

sprite.remove() -- An alias for sprite.delete(): deletes a sprite from the world and from every group, which both collects the pickup and stops the overlap repeating.

group.remove(sprite) -- A different operation with a similar name: unparents a sprite from one group only. The sprite itself is untouched — still drawing, still in physics, still a member of any other group it belongs to.

Safe iteration -- Looping over [...group] instead of group directly so that deleting members mid-loop cannot skip an entry.