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}