Repository navigation
feat(physics): add collision filtering, sensors and per-entity contacts - #709
Merged
Merged
Conversation
… 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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
# Conflicts: # CHANGELOG.md
`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
# Conflicts: # CHANGELOG.md # documentation-site/src/pages/demos/space-shooter/_create-player.ts
… pairs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVTLatSvTvhsw2P788Dcjj
stormmuller
enabled auto-merge (squash)
October 6, 2026 20:37
…compassionate-dijkstra-nfewh6
3 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
addColliderComponenttakescategory(default1) andmask(defaultallCollisionCategories). 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.ColliderEcsComponent.aabbis an output field, written only by the broad phase.AabbEcsComponent,aabbIdandaddAabbComponentare 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(world, start, end, { sort, mask, includeSensors })replaces the positionalsortboolean. Itsmasktest is one-sided, like Godot's and Unity's ray layer masks.Phase 2: sensors and contacts
Sensors.
sensor: truecolliders are detected and never resolved. The narrow phase reports their overlaps through contacts only and never writes them tocollisionManifolds, so the resolution system is unchanged. Raycasts pass through sensors unlessincludeSensors: true, the same default as Godot'scollide_with_areas.Contacts.
ContactsEcsComponent(addContactsComponent) is opt-in, like Avian'sCollidingEntities. 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.startedandended: computed against the previous tick'stouching.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
/demo/src/game.tsread contacts. No docs demo scanscollisionManifoldsto find what an entity touched any more.world.isAlivefor a bullet that touches two asteroids in the same tick./demos/sensors, in thephysicscategory). Balls bounce off solid ramps and fall through two sensor trigger zones that tint them onstartedand reset them onended. A sensor drain removes them. The zones and the drain use a mask that only accepts balls.physics/collisions.mdcovering filtering, contacts (including removed entities and when to read manifolds instead) and sensors.rigid-bodies.md,raycasting.md,terrain.mdandindex.mdare updated to match.continuous-collision-detection.mdnow refers tocollider.aabb, per §4.1 of the design.solution-reviewer verdict: it approved the plan with changes, all applied:
includeSensorstogether withsensor;Aabb;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-typespasses with 0 errorsnpm testpasses (230 files, 1856 tests)npm run lintpasses with 0 errorsnpm run cspellpasses with 0 errorsnpm run check-exportspassesContactsEcsComponent,contactsId,addContactsComponent,allCollisionCategories,RaycastOptions) is exported from the physics module/documentation-site/docs/docsis updated/dist, then ran the docs site'stypecheckandbuild;New tests:
mask,includeSensors, and bounds the broad phase hasn't computed yet;Changelog
## [Unreleased]:mask, sensors and contacts.raycastoptions object, with migration.AabbEcsComponent, with migration.🤖 Generated with Claude Code
https://claude.ai/code/session_01LVTLatSvTvhsw2P788Dcjj