Skip to content

Add collision enter and exit events, and collision layers and masks - #4

Merged
RohanFredriksson merged 1 commit into
masterfrom
claude/gameplay-api-batch3
Oct 16, 2026
Merged

RohanFredriksson merged 1 commit into
masterfrom
claude/gameplay-api-batch3

Conversation

@RohanFredriksson

Copy link
Copy Markdown
Owner

Batch 3 of ROADMAP.md. This is stacked on #3 (batch 2), so it targets that branch and the diff shows only batch 3. Merge #3 first, then change this PR's base to master (GitHub does it automatically if the batch 2 branch is deleted on merge).

Changes

  • Enter and exit events: CollisionListener gets collisionEnter(with, manifold) (the first step two bodies touch) and collisionExit(with) (the first step they don't), alongside the existing per-contact collision. Sensors get them too.
    • A body leaving the world, e.g. because its entity is killed, sends exits to the bodies it was touching, so counters like "is grounded" don't get stuck.
  • Deferred dispatch: all listener calls are now queued during the physics step and sent at its end, in body-id-pair order.
    • Listeners can kill entities safely. Before, a body killed in a callback was still narrow-phased and solved for the rest of that step.
    • A pair only counts as touching once its enter is sent. So a body removed mid-dispatch, like a coin that kills itself on enter, only sends exits for enters that were delivered.
  • Collision layers and masks: Rigidbody::setCollisionLayer and setCollisionMask. Two bodies collide only if each one's layer is in the other's mask (canCollideWith).
    • Checked in the broad phase, saved as optional fields so older scene files still load, and editable in the inspector as hex.
    • World::raycast takes an optional mask. World::isTouching(a, b) reports the current contact state. ALL_LAYERS is the default mask.
  • Teleport fix: bodies moved by game code (setPosition, the tp command) update their grid cells at the start of each step. Before, the broad phase saw them in their old cells for one step, so they missed collisions in their new place.
  • Teardown: Entity::kill and ~Entity mark the entity dead before ending components, and Scene::~Scene calls World::clearContacts() first, so dying entities and scene teardown get no exit events.
  • Demo: the impact flash uses collisionEnter and the previous frame's speed, because velocities read inside callbacks are now post-solve.
  • Docs: README gets a "Collisions" section and upgrade notes. CLAUDE.md's physics step list and ROADMAP.md are updated.

Behaviour changes

  • collision callbacks run after the solver rather than during the narrow phase, so a velocity read there is the resolved one.
  • A dying entity's own listeners no longer get callbacks while it is being killed.

Testing

  • New unit test tests/test_collision_events.cpp covers:
    • enter once, contact every step, and exit on separation, with re-entry after a teleport;
    • exits sent on removal, but not to the removed body itself;
    • a sensor coin that kills itself on enter while overlapping two players;
    • layers and masks in both directions, and raycast masks, bounded and unbounded;
    • saving and loading, including older files without the fields.
  • For the coin case, I checked that the test catches the bug: marking all current pairs as touching before dispatch makes it fail with an exit that had no enter.
  • All 10 tests pass locally in the ASan build and the Release build.

🤖 Generated with Claude Code

https://claude.ai/code/session_015cNAkNqoDWA5gFReD1AsqV


Generated by Claude Code

Batch 3 of ROADMAP.md.

- CollisionListener gets collisionEnter (the first step two bodies touch)
  and collisionExit (the first step they don't, or when one is removed from
  the world), alongside the existing per-contact collision.
- All listener calls are now queued during the step and sent at its end,
  in body-id-pair order. A listener can kill entities safely: before, a
  body killed in a callback was still narrow-phased and solved for the rest
  of the step. A pair only counts as touching once its enter is sent, so a
  body removed mid-dispatch only sends exits for enters that were
  delivered.
- Rigidbody collision layer and mask. Two bodies collide only if each one's
  layer is in the other's mask. Checked in the broad phase, serialised as
  optional fields, and editable in the inspector as hex. Raycasts take an
  optional mask. World::isTouching reports the current contact state.
- Bodies moved by game code (setPosition, the tp command) update their grid
  cells at the start of each step. Before, the broad phase saw them in their
  old cells for one step, so they missed collisions in their new place.
- Entity::kill and ~Entity mark the entity dead before ending components,
  and Scene::~Scene clears contacts first, so dying entities and scene
  teardown get no exit events.
- The demo's impact flash uses collisionEnter and last frame's speed, since
  velocities read in callbacks are now post-solve.

Tests: new unit test test_collision_events for enter, contact and exit
events, exits on removal, killing bodies from listeners, layers and
masks, raycast masks, and serialisation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cNAkNqoDWA5gFReD1AsqV
@RohanFredriksson
RohanFredriksson changed the base branch from claude/gameplay-api-batch2 to master October 16, 2026 02:19
@RohanFredriksson
RohanFredriksson merged commit cee048f into master Oct 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants