Skip to content

Commit fb0158d

Browse files
authored
Change ColliderParent to a ColliderOf relationship and rework ColliderHierarchyPlugin (#671)
# Objective `ColliderParent` should be a relationship for robustness and semantics, and to take advantage of automatic source tracking for rigid bodies. The `ColliderHierarchyPlugin` also currently deals with both attaching colliders to rigid bodies and managing transforms. This would be good to split into separate plugins. ## Solution Rename `ColliderParent` to `ColliderOf`, make it a `Relationship`, and add a `RigidBodyColliders` component for the `RelationshipTarget`. Collider hierarchies are now managed via observers and hooks instead of systems. Note that `ColliderOf` requires a custom `Relationship` implementation, because Bevy's default implementation does not allow relationships to point to themselves. I also split the `ColliderHierarchyPlugin` into a `ColliderHierarchyPlugin` (manages `ColliderOf` relationships) and `ColliderTransformPlugin` (manages and propagates collider transforms). --- ## Migration Guide `ColliderParent` has been renamed to `ColliderOf`, and it is now a `Relationship`. The transform management in `ColliderHierarchyPlugin` has been extracted into a new `ColliderTransformPlugin`. The `ColliderHierarchyPlugin` no longer takes a schedule.
1 parent 24a3a14 commit fb0158d

File tree

19 files changed

+475
-341
lines changed

19 files changed

+475
-341
lines changed

crates/avian2d/examples/kinematic_character_2d/plugin.rs

+9-13
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearV
267267
fn kinematic_controller_collisions(
268268
collisions: Res<Collisions>,
269269
bodies: Query<&RigidBody>,
270-
collider_parents: Query<&ColliderParent, Without<Sensor>>,
270+
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
271271
mut character_controllers: Query<
272272
(&mut Position, &mut LinearVelocity, Option<&MaxSlopeAngle>),
273273
(With<RigidBody>, With<CharacterController>),
@@ -277,8 +277,8 @@ fn kinematic_controller_collisions(
277277
// Iterate through collisions and move the kinematic body to resolve penetration
278278
for contacts in collisions.iter() {
279279
// Get the rigid body entities of the colliders (colliders could be children)
280-
let Ok([collider_parent1, collider_parent2]) =
281-
collider_parents.get_many([contacts.entity1, contacts.entity2])
280+
let Ok([&ColliderOf { rigid_body: rb1 }, &ColliderOf { rigid_body: rb2 }]) =
281+
collider_rbs.get_many([contacts.entity1, contacts.entity2])
282282
else {
283283
continue;
284284
};
@@ -291,19 +291,15 @@ fn kinematic_controller_collisions(
291291
let is_other_dynamic: bool;
292292

293293
let (mut position, mut linear_velocity, max_slope_angle) =
294-
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
294+
if let Ok(character) = character_controllers.get_mut(rb1) {
295295
is_first = true;
296-
character_rb = *bodies.get(collider_parent1.get()).unwrap();
297-
is_other_dynamic = bodies
298-
.get(collider_parent2.get())
299-
.is_ok_and(|rb| rb.is_dynamic());
296+
character_rb = *bodies.get(rb1).unwrap();
297+
is_other_dynamic = bodies.get(rb2).is_ok_and(|rb| rb.is_dynamic());
300298
character
301-
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
299+
} else if let Ok(character) = character_controllers.get_mut(rb2) {
302300
is_first = false;
303-
character_rb = *bodies.get(collider_parent2.get()).unwrap();
304-
is_other_dynamic = bodies
305-
.get(collider_parent1.get())
306-
.is_ok_and(|rb| rb.is_dynamic());
301+
character_rb = *bodies.get(rb2).unwrap();
302+
is_other_dynamic = bodies.get(rb1).is_ok_and(|rb| rb.is_dynamic());
307303
character
308304
} else {
309305
continue;

crates/avian3d/examples/kinematic_character_3d/plugin.rs

+9-13
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearV
283283
fn kinematic_controller_collisions(
284284
collisions: Res<Collisions>,
285285
bodies: Query<&RigidBody>,
286-
collider_parents: Query<&ColliderParent, Without<Sensor>>,
286+
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
287287
mut character_controllers: Query<
288288
(&mut Position, &mut LinearVelocity, Option<&MaxSlopeAngle>),
289289
(With<RigidBody>, With<CharacterController>),
@@ -293,8 +293,8 @@ fn kinematic_controller_collisions(
293293
// Iterate through collisions and move the kinematic body to resolve penetration
294294
for contacts in collisions.iter() {
295295
// Get the rigid body entities of the colliders (colliders could be children)
296-
let Ok([collider_parent1, collider_parent2]) =
297-
collider_parents.get_many([contacts.entity1, contacts.entity2])
296+
let Ok([&ColliderOf { rigid_body: rb1 }, &ColliderOf { rigid_body: rb2 }]) =
297+
collider_rbs.get_many([contacts.entity1, contacts.entity2])
298298
else {
299299
continue;
300300
};
@@ -307,19 +307,15 @@ fn kinematic_controller_collisions(
307307
let is_other_dynamic: bool;
308308

309309
let (mut position, mut linear_velocity, max_slope_angle) =
310-
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
310+
if let Ok(character) = character_controllers.get_mut(rb1) {
311311
is_first = true;
312-
character_rb = *bodies.get(collider_parent1.get()).unwrap();
313-
is_other_dynamic = bodies
314-
.get(collider_parent2.get())
315-
.is_ok_and(|rb| rb.is_dynamic());
312+
character_rb = *bodies.get(rb1).unwrap();
313+
is_other_dynamic = bodies.get(rb2).is_ok_and(|rb| rb.is_dynamic());
316314
character
317-
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
315+
} else if let Ok(character) = character_controllers.get_mut(rb2) {
318316
is_first = false;
319-
character_rb = *bodies.get(collider_parent2.get()).unwrap();
320-
is_other_dynamic = bodies
321-
.get(collider_parent1.get())
322-
.is_ok_and(|rb| rb.is_dynamic());
317+
character_rb = *bodies.get(rb2).unwrap();
318+
is_other_dynamic = bodies.get(rb1).is_ok_and(|rb| rb.is_dynamic());
323319
character
324320
} else {
325321
continue;

src/collision/broad_phase.rs

+21-14
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub struct AabbIntersections(pub Vec<Entity>);
105105
struct AabbIntervals(
106106
Vec<(
107107
Entity,
108-
ColliderParent,
108+
ColliderOf,
109109
ColliderAabb,
110110
CollisionLayers,
111111
AabbIntervalFlags,
@@ -139,7 +139,7 @@ fn update_aabb_intervals(
139139
aabbs: Query<
140140
(
141141
&ColliderAabb,
142-
Option<&ColliderParent>,
142+
Option<&ColliderOf>,
143143
Option<&CollisionLayers>,
144144
Option<&ActiveCollisionHooks>,
145145
Has<AabbIntersections>,
@@ -152,10 +152,10 @@ fn update_aabb_intervals(
152152
) {
153153
intervals
154154
.0
155-
.retain_mut(|(collider_entity, collider_parent, aabb, layers, flags)| {
155+
.retain_mut(|(collider_entity, collider_of, aabb, layers, flags)| {
156156
if let Ok((
157157
new_aabb,
158-
new_parent,
158+
new_collider_of,
159159
new_layers,
160160
hooks,
161161
new_store_intersections,
@@ -167,11 +167,18 @@ fn update_aabb_intervals(
167167
}
168168

169169
*aabb = *new_aabb;
170-
*collider_parent = new_parent.map_or(ColliderParent(*collider_entity), |p| *p);
170+
*collider_of = new_collider_of.map_or(
171+
ColliderOf {
172+
rigid_body: *collider_entity,
173+
},
174+
|p| *p,
175+
);
171176
*layers = new_layers.map_or(CollisionLayers::default(), |layers| *layers);
172177

173-
let is_static =
174-
new_parent.is_some_and(|p| rbs.get(p.get()).is_ok_and(RigidBody::is_static));
178+
let is_static = new_collider_of.is_some_and(|collider_of| {
179+
rbs.get(collider_of.rigid_body)
180+
.is_ok_and(RigidBody::is_static)
181+
});
175182

176183
flags.set(AabbIntervalFlags::IS_INACTIVE, is_static || is_sleeping);
177184
flags.set(
@@ -192,7 +199,7 @@ fn update_aabb_intervals(
192199

193200
type AabbIntervalQueryData = (
194201
Entity,
195-
Option<Read<ColliderParent>>,
202+
Option<Read<ColliderOf>>,
196203
Read<ColliderAabb>,
197204
Option<Read<RigidBody>>,
198205
Option<Read<CollisionLayers>>,
@@ -210,7 +217,7 @@ fn add_new_aabb_intervals(
210217
) {
211218
let re_enabled_aabbs = aabbs.iter_many(re_enabled_colliders.read());
212219
let aabbs = added_aabbs.iter().chain(re_enabled_aabbs).map(
213-
|(entity, parent, aabb, rb, layers, hooks, store_intersections)| {
220+
|(entity, collider_of, aabb, rb, layers, hooks, store_intersections)| {
214221
let mut flags = AabbIntervalFlags::empty();
215222
flags.set(AabbIntervalFlags::STORE_INTERSECTIONS, store_intersections);
216223
flags.set(
@@ -223,7 +230,7 @@ fn add_new_aabb_intervals(
223230
);
224231
(
225232
entity,
226-
parent.map_or(ColliderParent(entity), |p| *p),
233+
collider_of.map_or(ColliderOf { rigid_body: entity }, |p| *p),
227234
*aabb,
228235
layers.map_or(CollisionLayers::default(), |layers| *layers),
229236
flags,
@@ -280,20 +287,20 @@ fn sweep_and_prune<H: CollisionHooks>(
280287
broad_collision_pairs.clear();
281288

282289
// Find potential collisions by checking for AABB intersections along all axes.
283-
for (i, (entity1, parent1, aabb1, layers1, flags1)) in intervals.0.iter().enumerate() {
284-
for (entity2, parent2, aabb2, layers2, flags2) in intervals.0.iter().skip(i + 1) {
290+
for (i, (entity1, collider_of1, aabb1, layers1, flags1)) in intervals.0.iter().enumerate() {
291+
for (entity2, collider_of2, aabb2, layers2, flags2) in intervals.0.iter().skip(i + 1) {
285292
// x doesn't intersect; check this first so we can discard as soon as possible.
286293
if aabb2.min.x > aabb1.max.x {
287294
break;
288295
}
289296

290297
// No collisions between bodies that haven't moved or colliders with incompatible layers
291-
// or colliders with the same parent.
298+
// or colliders attached to the same rigid body.
292299
if flags1
293300
.intersection(*flags2)
294301
.contains(AabbIntervalFlags::IS_INACTIVE)
295302
|| !layers1.interacts_with(*layers2)
296-
|| parent1 == parent2
303+
|| collider_of1 == collider_of2
297304
{
298305
continue;
299306
}

src/collision/collider/backend.rs

+22-50
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
188188
let entity_ref = world.entity_mut(ctx.entity);
189189

190190
// Get the rigid body entity that the collider is attached to.
191-
let Some(parent) = entity_ref.get::<ColliderParent>().copied() else {
191+
let Some(collider_of) = entity_ref.get::<ColliderOf>().copied() else {
192192
return;
193193
};
194194

@@ -197,22 +197,24 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
197197
*world.resource::<ColliderRemovalSystem>().to_owned();
198198

199199
// Handle collider removal.
200-
world.commands().run_system_with(system_id, parent);
200+
world.commands().run_system_with(system_id, collider_of);
201201
});
202202

203203
// When the `Sensor` component is added to a collider, queue its rigid body for a mass property update.
204204
app.add_observer(
205205
|trigger: Trigger<OnAdd, Sensor>,
206206
mut commands: Commands,
207-
query: Query<(&ColliderMassProperties, &ColliderParent)>| {
208-
if let Ok((collider_mass_properties, parent)) = query.get(trigger.target()) {
207+
query: Query<(&ColliderMassProperties, &ColliderOf)>| {
208+
if let Ok((collider_mass_properties, &ColliderOf { rigid_body })) =
209+
query.get(trigger.target())
210+
{
209211
// If the collider mass properties are zero, there is nothing to subtract.
210212
if *collider_mass_properties == ColliderMassProperties::ZERO {
211213
return;
212214
}
213215

214-
// Queue the parent rigid body for a mass property update.
215-
if let Ok(mut entity_commands) = commands.get_entity(parent.get()) {
216+
// Queue the rigid body for a mass property update.
217+
if let Ok(mut entity_commands) = commands.get_entity(rigid_body) {
216218
entity_commands.insert(RecomputeMassProperties);
217219
}
218220
}
@@ -252,12 +254,6 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
252254
),
253255
);
254256

255-
// Update collider parents for colliders that are on the same entity as the rigid body.
256-
app.add_systems(
257-
self.schedule,
258-
update_root_collider_parents::<C>.before(PrepareSet::Finalize),
259-
);
260-
261257
let physics_schedule = app
262258
.get_schedule_mut(PhysicsSchedule)
263259
.expect("add PhysicsSchedule first");
@@ -290,29 +286,6 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
290286
#[reflect(Component, Debug, Default)]
291287
pub struct ColliderMarker;
292288

293-
/// Updates [`ColliderParent`] for colliders that are on the same entity as the [`RigidBody`].
294-
///
295-
/// The [`ColliderHierarchyPlugin`] should be used to handle hierarchies.
296-
fn update_root_collider_parents<C: AnyCollider>(
297-
mut commands: Commands,
298-
mut bodies: Query<
299-
(Entity, Option<&mut ColliderParent>),
300-
(With<RigidBody>, With<C>, Or<(Added<RigidBody>, Added<C>)>),
301-
>,
302-
) {
303-
for (entity, collider_parent) in &mut bodies {
304-
if let Some(mut collider_parent) = collider_parent {
305-
collider_parent.0 = entity;
306-
} else {
307-
commands.entity(entity).try_insert((
308-
ColliderParent(entity),
309-
// TODO: This probably causes a one frame delay. Compute real value?
310-
ColliderTransform::default(),
311-
));
312-
}
313-
}
314-
}
315-
316289
/// Generates [`Collider`]s based on [`ColliderConstructor`]s.
317290
///
318291
/// If a [`ColliderConstructor`] requires a mesh, the system keeps running
@@ -505,7 +478,7 @@ fn update_aabb<C: AnyCollider>(
505478
&mut ColliderAabb,
506479
&Position,
507480
&Rotation,
508-
Option<&ColliderParent>,
481+
Option<&ColliderOf>,
509482
Option<&CollisionMargin>,
510483
Option<&SpeculativeMargin>,
511484
Has<SweptCcd>,
@@ -520,7 +493,7 @@ fn update_aabb<C: AnyCollider>(
520493
Changed<C>,
521494
)>,
522495
>,
523-
parent_velocity: Query<
496+
rb_velocities: Query<
524497
(
525498
&Position,
526499
&ComputedCenterOfMass,
@@ -544,7 +517,7 @@ fn update_aabb<C: AnyCollider>(
544517
mut aabb,
545518
pos,
546519
rot,
547-
collider_parent,
520+
collider_of,
548521
collision_margin,
549522
speculative_margin,
550523
has_swept_ccd,
@@ -571,16 +544,16 @@ fn update_aabb<C: AnyCollider>(
571544
// Expand the AABB based on the body's velocity and CCD speculative margin.
572545
let (lin_vel, ang_vel) = if let (Some(lin_vel), Some(ang_vel)) = (lin_vel, ang_vel) {
573546
(*lin_vel, *ang_vel)
574-
} else if let Some(Ok((parent_pos, center_of_mass, Some(lin_vel), Some(ang_vel)))) =
575-
collider_parent.map(|p| parent_velocity.get(p.get()))
547+
} else if let Some(Ok((rb_pos, center_of_mass, Some(lin_vel), Some(ang_vel)))) =
548+
collider_of.map(|&ColliderOf { rigid_body }| rb_velocities.get(rigid_body))
576549
{
577550
// If the rigid body is rotating, off-center colliders will orbit around it,
578551
// which affects their linear velocities. We need to compute the linear velocity
579552
// at the offset position.
580553
// TODO: This assumes that the colliders would continue moving in the same direction,
581554
// but because they are orbiting, the direction will change. We should take
582555
// into account the uniform circular motion.
583-
let offset = pos.0 - parent_pos.0 - center_of_mass.0;
556+
let offset = pos.0 - rb_pos.0 - center_of_mass.0;
584557
#[cfg(feature = "2d")]
585558
let vel_at_offset =
586559
lin_vel.0 + Vector::new(-ang_vel.0 * offset.y, ang_vel.0 * offset.x) * 1.0;
@@ -661,26 +634,24 @@ pub fn update_collider_scale<C: ScalableCollider>(
661634

662635
/// A resource that stores the system ID for the system that reacts to collider removals.
663636
#[derive(Resource)]
664-
struct ColliderRemovalSystem(SystemId<In<ColliderParent>>);
637+
struct ColliderRemovalSystem(SystemId<In<ColliderOf>>);
665638

666639
/// Updates the mass properties of bodies and wakes bodies up when an attached collider is removed.
667640
///
668-
/// Takes the removed collider's entity, parent, mass properties, and transform as input.
641+
/// Takes the removed collider's entity, rigid body entity, mass properties, and transform as input.
669642
fn collider_removed(
670-
In(parent): In<ColliderParent>,
643+
In(ColliderOf { rigid_body }): In<ColliderOf>,
671644
mut commands: Commands,
672645
mut sleep_query: Query<&mut TimeSleeping>,
673646
) {
674-
let parent = parent.get();
675-
676-
let Ok(mut entity_commands) = commands.get_entity(parent) else {
647+
let Ok(mut entity_commands) = commands.get_entity(rigid_body) else {
677648
return;
678649
};
679650

680-
// Queue the parent entity for mass property recomputation.
651+
// Queue the rigid body for mass property recomputation.
681652
entity_commands.insert(RecomputeMassProperties);
682653

683-
if let Ok(mut time_sleeping) = sleep_query.get_mut(parent) {
654+
if let Ok(mut time_sleeping) = sleep_query.get_mut(rigid_body) {
684655
// Wake up the rigid body since removing the collider could also remove active contacts.
685656
entity_commands.remove::<Sleeping>();
686657
time_sleeping.0 = 0.0;
@@ -720,8 +691,9 @@ mod tests {
720691
app.add_plugins((
721692
PreparePlugin::new(FixedPostUpdate),
722693
MassPropertyPlugin::new(FixedPostUpdate),
694+
ColliderHierarchyPlugin,
695+
ColliderTransformPlugin::default(),
723696
ColliderBackendPlugin::<Collider>::new(FixedPostUpdate),
724-
ColliderHierarchyPlugin::new(FixedPostUpdate),
725697
));
726698

727699
let collider = Collider::capsule(0.5, 2.0);

0 commit comments

Comments
 (0)