avian3d/collision/collider/collider_hierarchy/plugin.rs
1use crate::{prelude::*, sync::ancestor_marker::AncestorMarker};
2use bevy::prelude::*;
3
4/// A plugin for managing [`ColliderOf`] relationships based on the entity hierarchy.
5///
6/// By default, colliders are automatically attached to the nearest rigid body ancestor.
7///
8/// This plugin requires that colliders have the [`ColliderMarker`] component,
9/// which is added automatically for colliders if the [`ColliderBackendPlugin`] is enabled.
10pub struct ColliderHierarchyPlugin;
11
12impl Plugin for ColliderHierarchyPlugin {
13 fn build(&self, app: &mut App) {
14 // Imsert `ColliderOf` for colliders that are added to a rigid body.
15 app.add_observer(
16 |trigger: Trigger<OnAdd, (RigidBody, ColliderMarker)>,
17 mut commands: Commands,
18 query: Query<(), With<ColliderMarker>>,
19 rb_query: Query<Entity, With<RigidBody>>,
20 parent_query: Query<&ChildOf>| {
21 let entity = trigger.target();
22
23 // Make sure the entity is a collider.
24 if !query.contains(entity) {
25 return;
26 }
27
28 // Find the closest rigid body ancestor.
29 if let Some(body) = rb_query.get(entity).ok().map_or_else(
30 || {
31 parent_query
32 .iter_ancestors(trigger.target())
33 .find(|&entity| rb_query.contains(entity))
34 },
35 Some,
36 ) {
37 commands.entity(entity).insert(ColliderOf { body });
38 }
39 },
40 );
41
42 // Remove `ColliderOf` when the rigid body or collider is removed.
43 app.add_observer(
44 |trigger: Trigger<OnRemove, (RigidBody, ColliderMarker)>,
45 mut commands: Commands,
46 query: Query<(), With<ColliderMarker>>| {
47 let entity = trigger.target();
48
49 // Make sure the collider is on the same entity as the rigid body.
50 if query.contains(entity) {
51 commands.entity(entity).try_remove::<ColliderOf>();
52 }
53 },
54 );
55
56 // Update `ColliderOf` components for colliders when their ancestors change.
57 app.add_observer(on_collider_body_changed);
58
59 // Remove `ColliderOf` from colliders when their rigid bodies are removed.
60 app.add_observer(on_body_removed);
61 }
62}
63
64/// Updates [`ColliderOf`] components for colliders when their ancestors change
65/// or when a rigid body is added to the hierarchy.
66fn on_collider_body_changed(
67 trigger: Trigger<OnInsert, (ChildOf, RigidBody, AncestorMarker<ColliderMarker>)>,
68 mut commands: Commands,
69 query: Query<
70 Has<ColliderMarker>,
71 Or<(With<ColliderMarker>, With<AncestorMarker<ColliderMarker>>)>,
72 >,
73 child_query: Query<&Children>,
74 parent_query: Query<&ChildOf>,
75 body_query: Query<Entity, With<RigidBody>>,
76 collider_query: Query<(), With<ColliderMarker>>,
77) {
78 let entity = trigger.target();
79
80 // Skip if the entity is not a collider or an ancestor of a collider.
81 let Ok(is_collider) = query.get(entity) else {
82 return;
83 };
84
85 // Find the closest rigid body ancestor.
86 let Some(body) = body_query.get(entity).ok().map_or_else(
87 || {
88 parent_query
89 .iter_ancestors(trigger.target())
90 .find(|&entity| body_query.contains(entity))
91 },
92 Some,
93 ) else {
94 return;
95 };
96
97 // Insert `ColliderOf` for the new child entity.
98 if is_collider {
99 commands.entity(entity).insert(ColliderOf { body });
100 }
101
102 // Iterate over all descendants starting from the new child entity,
103 // and update the `ColliderOf` component to point to the closest rigid body ancestor.
104 for child in child_query.iter_descendants(entity) {
105 // Stop traversal if we reach another rigid body.
106 // FIXME: This might lead to siblings of the rigid body being skipped.
107 if body_query.contains(child) {
108 break;
109 }
110
111 // Update the `ColliderOf` component.
112 if collider_query.contains(child) {
113 commands.entity(child).insert(ColliderOf { body });
114 }
115 }
116}
117
118/// Removes [`ColliderOf`] from colliders when their rigid bodies are removed.
119fn on_body_removed(
120 trigger: Trigger<OnRemove, RigidBody>,
121 mut commands: Commands,
122 body_collider_query: Query<&RigidBodyColliders>,
123) {
124 // TODO: Here we assume that rigid bodies are not nested, so `ColliderOf` is simply removed
125 // instead of being updated to point to a new rigid body in the hierarchy.
126
127 let body = trigger.target();
128
129 // Remove `ColliderOf` from all colliders attached to the rigid body.
130 if let Ok(colliders) = body_collider_query.get(body) {
131 for collider in colliders.iter() {
132 commands
133 .entity(collider)
134 .try_remove::<(ColliderOf, ColliderTransform)>();
135 }
136 }
137}