Skip to content

feat(physics): add collision filtering, sensors and per-entity contacts - #709

Merged
stormmuller merged 6 commits into
devfrom
claude/compassionate-dijkstra-nfewh6
Oct 6, 2026
Merged

stormmuller merged 6 commits into
devfrom
claude/compassionate-dijkstra-nfewh6

Conversation

@stormmuller

@stormmuller stormmuller commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements design/collision-events.md, both phases. Phase 2 builds on the generational entity handles from #715, which are merged into this branch.

Phase 1: filtering and the AABB on the collider

  • Filtering. addColliderComponent takes category (default 1) and mask (default allCollisionCategories). The broad phase skips a pair unless (a.category & b.mask) !== 0 && (b.category & a.mask) !== 0, Box2D's symmetric rule (option (a) in the design's open question). The check runs before the AABB test.
  • The AABB lives on the collider. ColliderEcsComponent.aabb is an output field, written only by the broad phase. AabbEcsComponent, aabbId and addAabbComponent are removed, so a collider works without a separate component. A new collider's bounds start empty instead of as a zero-size box at the origin, so a ray through the origin no longer hits a collider the broad phase hasn't seen yet.
  • Raycast options. raycast(world, start, end, { sort, mask, includeSensors }) replaces the positional sort boolean. Its mask test is one-sided, like Godot's and Unity's ray layer masks.

Phase 2: sensors and contacts

  • Sensors. sensor: true colliders are detected and never resolved. The narrow phase reports their overlaps through contacts only and never writes them to collisionManifolds, so the resolution system is unchanged. Raycasts pass through sensors unless includeSensors: true, the same default as Godot's collide_with_areas.

  • Contacts. ContactsEcsComponent (addContactsComponent) is opt-in, like Avian's CollidingEntities. The narrow phase queries [contactsId] and is the only writer of the component. Each tick it writes:

    • touching: each entity once, even when a pair produced several manifolds, for example one per terrain edge.
    • started and ended: computed against the previous tick's touching.

    Contacts end when the other entity is removed or loses its collider, or when this entity loses its own collider. Generational handles keep a removed entity distinct from a new one in the same slot, and a test covers that case.

  • The narrow phase skips detection for a sensor pair when neither entity records contacts, and entities with no contacts this tick share one frozen empty array.

Docs and demos

  • The space shooter, brick breaker, rolling ball and car docs demos and /demo/src/game.ts read contacts. No docs demo scans collisionManifolds to find what an entity touched any more.
  • The space shooter uses categories, so asteroids and bullets are no longer tested against each other. It uses world.isAlive for a bullet that touches two asteroids in the same tick.
  • New Sensors and Contacts docs demo (/demos/sensors, in the physics category). Balls bounce off solid ramps and fall through two sensor trigger zones that tint them on started and reset them on ended. A sensor drain removes them. The zones and the drain use a mask that only accepts balls.
  • New guide physics/collisions.md covering filtering, contacts (including removed entities and when to read manifolds instead) and sensors. rigid-bodies.md, raycasting.md, terrain.md and index.md are updated to match.
  • continuous-collision-detection.md now refers to collider.aabb, per §4.1 of the design.

solution-reviewer verdict: it approved the plan with changes, all applied:

  • keep the pair filter internal;
  • add includeSensors together with sensor;
  • give each collider its own Aabb;
  • skip sensor pairs nobody records;
  • use a shared empty array.

It also confirmed that the narrow phase owning both outputs keeps one writer per value. It flagged that Phase 2 needed generational ids, and #715 has since landed.

The separate Galactic Journey repo installs Forge from npm, so its migration waits for a release.

Related issue(s)

Closes #559 (sensors), closes #560 (layers and masks).

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (230 files, 1856 tests)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • New public API (ContactsEcsComponent, contactsId, addContactsComponent, allCollisionCategories, RaycastOptions) is exported from the physics module
  • Documentation under /documentation-site/docs/docs is updated
  • Demos updated and verified:
    • built /dist, then ran the docs site's typecheck and build;
    • loaded sensors, space-shooter, brick-breaker, rolling-ball, car, physics and raycasting (and, for Phase 1, every other physics demo) in Chromium with no engine errors;
    • checked that balls are tinted in the zones and the walls stay untinted, that shooting while strafing destroys asteroids, that bricks break, that the car sits on the terrain, and that the ray stops at the first target.

New tests:

  • the filter matrix, including bit 31;
  • the raycast mask, includeSensors, and bounds the broad phase hasn't computed yet;
  • contacts started, touching, ended and listed once against terrain with several edges;
  • contacts ending on entity removal and slot reuse, and when either side loses its collider;
  • sensors never producing manifolds, including against a static collider and another sensor;
  • a sensor never pushing a dynamic body.

Changelog

  • Bullets under ## [Unreleased]:
    • Added: filtering, the raycast mask, sensors and contacts.
    • Changed: the raycast options object, with migration.
    • Removed: AabbEcsComponent, with migration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVTLatSvTvhsw2P788Dcjj

… the collider

Colliders get a `category` and `mask`; the broad phase skips a pair unless
each collider's category is in the other's mask (Box2D's symmetric rule).
`raycast` takes an options object with `sort` and a `mask`.

The broad phase now writes each collider's bounds to `collider.aabb`, so a
collider takes part in collision detection and raycasts without a separate
component. `AabbEcsComponent`, `aabbId` and `addAabbComponent` are removed,
and every caller in /src tests, /demo and the docs site is migrated. A new
collider's bounds start empty instead of a zero-size box at the origin, so
a ray through the origin no longer hits colliders the broad phase hasn't
seen yet.

Implements Phase 1 of design/collision-events.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVTLatSvTvhsw2P788Dcjj
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

claude added 2 commits October 6, 2026 17:42
`addColliderComponent` takes `sensor: true` for a collider that's detected
but never resolved. The narrow phase reports sensor overlaps through
contacts only and never writes them to `collisionManifolds`, so the
resolution system is unchanged. `raycast` passes through sensors unless
given `includeSensors: true`.

New `ContactsEcsComponent` (opt-in via `addContactsComponent`): the narrow
phase queries it and, each tick, writes the entities it's touching (each
listed once, however many manifolds the pair produced) and which contacts
started and ended since the last tick. An entity that loses its collider,
or whose partner is removed, gets its contacts ended; generational handles
keep a removed entity distinct from a new one in its slot.

The space shooter, brick breaker, rolling ball and car docs demos and
/demo now read contacts instead of scanning `collisionManifolds`, and the
space shooter filters asteroids, bullets and the player with categories.
A new Sensors and Contacts docs demo shows trigger zones, and the physics
guide gains a Collisions page covering filtering, sensors and contacts.

Implements Phase 2 of design/collision-events.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVTLatSvTvhsw2P788Dcjj
@stormmuller stormmuller changed the title feat(physics): add collision categories and masks; move the AABB onto the collider feat(physics): add collision filtering, sensors and per-entity contacts Oct 6, 2026
claude added 2 commits October 6, 2026 18:17
# Conflicts:
#	CHANGELOG.md
#	documentation-site/src/pages/demos/space-shooter/_create-player.ts
@stormmuller
stormmuller enabled auto-merge (squash) October 6, 2026 20:37
@stormmuller
stormmuller merged commit 8576630 into dev Oct 6, 2026
12 checks passed
@stormmuller
stormmuller deleted the claude/compassionate-dijkstra-nfewh6 branch October 6, 2026 20:43
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.

Physics: add collision layers/masks for selective collision filtering Physics: add triggers/sensors (non-solid overlap colliders)

2 participants