-
-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework Contact Pair Management #683
Merged
Merged
Conversation
This file contains 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
- The broad phase now emits new collision pairs and stores all pairs in a HashSet - Contact status and kind is now tracked with `ContactPairFlags` instead of booleans - The narrow phase adds new collision pairs, updates existing pairs, and responds to state changes separately instead of overwriting and doing extra work for persistent contact - State changes are tracked with bit vectors (bit sets), which are fast to iterate serially - The narrow phase is responsible for collision events instead of the `ContactReportingPlugin`
- Renamed `BroadCollisionPairs` to `BroadPhasePairSet` - Added `BroadPhasePairSet` for fast pair lookup with new `PairKey` - Improve broad phase docs
…pt-in - Removed `BroadPhaseAddedPairs` - Renamed `BroadPhasePairSet` to `BroadPhasePairs` - Moved contact creation to broad phase to improve persistence - Removed some graph querying overhead from contact pair removal by using the `EdgeIndex` directly - Made collision events opt-in with `CollisionEventsEnabled` component - Improved a lot of docs
Also moved contact types to `contact_types` module and improved some docs.
Jondolf
added a commit
that referenced
this pull request
Mar 31, 2025
# Objective Especially after #683, we use the term "contact pair" a lot. However, the contact pair type is called `Contacts`. This name is very ambiguous: does it represent all contacts in the world, all contacts between two entities, contacts belonging to a specific contact surface, or something else? ## Solution Rename `Contacts` to `ContactPair`. It much more accurately describes what the type represents: contact data for a pair of entities that may be in contact. This is also the name used by Rapier. --- ## Migration Guide `Contacts` has been renamed to `ContactPair`.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
A-Collision
Relates to the broad phase, narrow phase, colliders, or other collision functionality
C-Breaking-Change
This change removes or changes behavior or APIs, requiring users to adapt
C-Enhancement
New feature or request
C-Performance
Improvements or questions related to performance
D-Complex
Challenging from a design or technical perspective. Ask for help if you'd like to help tackle this!
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.
Objective
Currently, contact pair management works as follows:
BroadCollisionPairs
resource.during_previous_frame
andduring_current_frame
of each existing contact totrue
andfalse
respectively.BroadCollisionPairs
in parallel. For each pair:Collisions
, if they exist, and setduring_previous_frame
accordingly.Collisions
.CollidingEntities
).during_current_frame
isfalse
.There are a lot of inefficiencies here.
Vec
every frame.BroadCollisionPairs
collects collisions into new vectors every time.Overall, there is an excessive amount of iteration and allocations, and the logic for managing contact statuses is very confusing.
In addition, the
Collisions
resource itself is not efficient for our purposes. There are many cases where you may need to iterate over contacts associated with a specific entity, but this currently requires iterating through all collisions because collisions are just stored in anIndexMap
. To resolve this, we need a more graph-like structure.Solution
Change
Collisions
to aContactGraph
, and rework contact pair management to look like the following:ContactGraph
directly. Duplicate pairs are avoided with fast lookups into aHashSet<PairKey>
.ContactGraph
in parallel, maintaining thread-local bit vectors to track contact status changes. For each contact pair:ContactPairFlags::DISJOINT_AABB
and the status change bit for this contact pair. Continue to the next pair.CollisionEnded
event (if events are enabled), updateCollidingEntities
, and remove the pair fromCollisions
.CollisionStared
event (if events are enabled) and updateCollidingEntities
.CollisionEnded
event (if events are enabled) and updateCollidingEntities
.Contact removal for removed or disabled colliders is now also handled with observers.
This improves several aspects:
ContactGraph
directly.ContactGraph
directly, and don't need to do separate lookups for previous contacts or do any extra allocations.As you may have noticed, a contact pair now exists between two colliders if their AABBs are touching, even if the actual shapes aren't. This is important for the pair management logic, though it does mean that the
ContactGraph
can now have a lot more contact pairs in some cases.Contact Graph
Previously,
Collisions
used anIndexMap
to store collisions, keyed by(Entity, Entity)
. The motivation was that we get vec-like iteration speed, with preserved insertion order and fast lookups by entity pairs.However, there are scenarios where you may need to iterate over the entities colliding with a given entity, such as for simulation islands or even gameplay logic. With just an
IndexMap
, this requires iterating over all pairs.This PR adds an undirected graph data structure called
UnGraph
, based on petgraph, simplified and tailored for our use cases.This is used for the new
ContactGraph
to provide faster and more powerful queries over contact pairs. The following methods are available:get(&self, entity1: Entity, entity2: Entity)
get_mut(&mut self, entity1: Entity, entity2: Entity)
contains(&self, entity1: Entity, entity2: Entity)
contains_key(&self, pair_key: &PairKey)
iter(&self)
iter_touching(&self)
iter_mut(&mut self)
iter_touching_mut(&mut self)
collisions_with(&self, entity: Entity)
collisions_with_mut(&mut self, entity: Entity)
entities_colliding_with(&self, entity: Entity)
and a few ones primarily for internals:
add_pair(&mut self, contacts: Contacts)
add_pair_with_key(&mut self, contacts: Contacts, pair_key: PairKey)
insert_pair(&mut self, contacts: Contacts)
insert_pair_with_key(&mut self, contacts: Contacts, pair_key: PairKey)
remove_pair(&mut self, entity1: Entity, entity2: Entity)
remove_collider_with(&mut self, entity: Entity, pair_callback: F)
The graph doesn't let us directly get nodes or edges by
Entity
ID. However, a newEntityDataIndex
is used to mapEntity
IDs to graph nodes.This is modeled after Rapier's
Coarena
.Collisions
System ParameterThe
ContactGraph
resource contains both touching and non-touching contacts. This may be inconvenient and confusing for new users.To provide a simpler, more user-friendly API, a
Collisions
SystemParam
has been added. It is similar to the oldCollisions
resource, and only provides access to touching contacts. It doesn't allow mutation, as contact modification and filtering should typically be handled viaCollisionHooks
.Contact Reporting
Previously, the
ContactReportingPlugin
sent theCollisionStarted
,CollisionEnded
, andCollision
events and updatedCollidingEntities
for all contact pairs after the solver. This required iterating through all contacts and performing lots of queries, which had meaningful overhead, even for apps that don't need collision events, or only need them for a few entities.Very few applications actually need collision events for all entities. In most engines, contact reporting/monitoring is entirely optional, and typically opt-in. Thus, a new
CollisionEventsEnabled
component has been added. Collision events are only sent if either entity in a collision has the component.The
ContactReportingPlugin
has also been entirely removed, and contact reporting is now handled directly by the narrow phase when processing contact status changes. This removes the need for extra iteration or queries.Finally, the
Collision
event has been removed. It was largely unnecessary, as the collision data can be accessed throughCollisions
directly. And semantically, it didn't feel like an "event" as it was sent every frame during continuous contact.Performance
In the new
pyramid_2d
example, with a pyramid that has a base of 50 boxes, 1276 total colliders, and 6 substeps, the old timings with theparallel
feature looked like the following after 500 steps:Now, they look like this:
Notably:
wake_on_collision_ended
being removed in favor of much more efficient logic integrated into the narrow phaseThe total step time in this scene is reduced by 1.76 ms. The difference should be larger the more collisions there are.
Single-threaded performance is also improved, though not quite as much. In the same test scene, the old timings looked like this:
Now, they look like this:
reducing the total step time in this scene by 0.7 ms.
The changes in this PR also unlock many future optimizations:
It is worth noting that we are currently using Bevy's built-in
ComputeTaskPool
for parallelism, which limits the number of available threads. Using it, we are still slightly behind Rapier's narrow phase performance. However, I have measured that if we manually increase the size of the thread pool, or use rayon, we now match or even slightly outperform Rapier's narrow phase.Future Work
Contacts
toContactPair
ContactManifold
types)Migration Guide
PostProcessCollisions
The
PostProcessCollisions
schedule andNarrowPhaseSet::PostProcess
system set have been removed, as it is incompatible with new optimizations to narrow phase collision detection. Instead, useCollisionHooks
for contact modification.Contact Reporting
The
ContactReportingPlugin
andPhysicsStepSet
have been removed. Contact reporting is now handled by theNarrowPhasePlugin
directly.The
Collision
event no longer exists. Instead, useCollisions
directly, or get colliding entities using theCollidingEntities
component.The
CollisionStarted
andCollisionEnded
events are now only sent if either entity in the collision has theCollisionEventsEnabled
component. If you'd like to revert to the old behavior of having collision events for all entities, consider makingCollisionEventsEnabled
a required component forCollider
:Collisions
The
Collisions
resource is now aSystemParam
.Internally,
Collisions
now stores aContactGraph
that stores both touching and non-touching contact pairs. TheCollisions
system parameter is just a wrapper that provides a simpler API and only returns touching contacts.The
collisions_with_entity
method has also been renamed tocollisions_with
, and all methods that mutatate, add, or remove contact pairs have been removed fromCollisions
. However, the following mutating methods are available onContactGraph
:get_mut
iter_mut
iter_touching_mut
collisions_with_mut
add_pair
/add_pair_with_key
insert_pair
/insert_pair_with_key
remove_pair
remove_collider_with
For most scenarios, contact modification and removal are intended to be handled with
CollisionHooks
.Contacts
The
is_sensor
,during_current_frame
, andduring_previous_frame
properties ofContacts
have been removed in favor of aflags
property storing information in a more compact bitflag format. Theis_sensor
,is_touching
,collision_started
, andcollision_ended
helper methods can be used instead.ContactManifold
Methods such as
AnyCollider::contact_manifolds_with_context
now take&mut Vec<ContactManifold>
instead of returning a new vector every time. This allows manifolds to be persisted more effectively, and reduces unnecessary allocations.BroadCollisionPairs
The
BroadCollisionPairs
resource has been removed. Use theContactGraph
resource instead.AabbIntersections
The
AabbIntersections
component has been removed. UseContactGraph::entities_colliding_with
instead.