Skip to main content

avian3d/dynamics/joints/joint_graph/
plugin.rs

1use core::marker::PhantomData;
2
3use super::{JointGraph, JointGraphEdge, JointId};
4#[cfg(feature = "xpbd_joints")]
5use crate::dynamics::solver::xpbd::XpbdVelocityProjection;
6use crate::{
7    collision::contact_types::ContactId,
8    data_structures::pair_key::PairKey,
9    dynamics::joints::EntityConstraint,
10    prelude::{
11        ContactGraph, ContactStatusChange, ContactStatusChangeQueue, JointCollisionDisabled,
12        JointDisabled, PhysicsSchedule, PhysicsStepSystems, RigidBodyColliders,
13    },
14};
15use bevy::{
16    ecs::{
17        component::ComponentId, entity_disabling::Disabled, lifecycle::HookContext,
18        query::QueryFilter, world::DeferredWorld,
19    },
20    prelude::*,
21};
22
23/// A plugin that manages the [`JointGraph`] for a specific [joint] type.
24///
25/// [joint]: crate::dynamics::joints
26pub struct JointGraphPlugin<T: Component + EntityConstraint<2>>(PhantomData<T>);
27
28impl<T: Component + EntityConstraint<2>> Default for JointGraphPlugin<T> {
29    fn default() -> Self {
30        Self(PhantomData)
31    }
32}
33
34#[derive(Resource, Default)]
35struct JointGraphPluginInitialized;
36
37impl<T: Component + EntityConstraint<2>> Plugin for JointGraphPlugin<T> {
38    fn build(&self, app: &mut App) {
39        let already_initialized = app
40            .world()
41            .is_resource_added::<JointGraphPluginInitialized>();
42
43        app.init_resource::<JointGraph>();
44        app.add_message::<JointGraphChange>();
45        app.init_resource::<JointGraphPluginInitialized>();
46
47        // Automatically add the `JointComponentId` component when the joint is added.
48        app.register_required_components::<T, JointComponentId>();
49
50        // Register hooks for adding and removing joints.
51        app.world_mut()
52            .register_component_hooks::<T>()
53            .on_add(on_add_joint)
54            .on_remove(on_remove_joint);
55
56        // Add the joint to the joint graph when it is added and the joint is not disabled.
57        app.add_observer(
58            add_joint_to_graph::<T, Add<T>, (With<JointComponentId>, Without<JointDisabled>)>,
59        );
60
61        // Remove the joint from the joint graph when it is removed.
62        app.add_observer(remove_joint_from_graph::<Remove<T>>);
63
64        if !already_initialized {
65            // Remove the joint from the joint graph when it is disabled.
66            app.add_observer(remove_joint_from_graph::<Add<(Disabled, JointDisabled)>>);
67
68            // Remove contacts between bodies when the `JointCollisionDisabled` component is added.
69            app.add_observer(on_disable_joint_collision);
70        }
71
72        // Add the joint back to the joint graph when `Disabled` is removed.
73        app.add_observer(
74            add_joint_to_graph::<
75                T,
76                Remove<Disabled>,
77                (
78                    With<JointComponentId>,
79                    Or<(With<Disabled>, Without<Disabled>)>,
80                    Without<JointDisabled>,
81                ),
82            >,
83        );
84
85        // Add the joint back to the joint graph when `JointDisabled` is removed.
86        app.add_observer(add_joint_to_graph::<T, Remove<JointDisabled>, With<JointComponentId>>);
87
88        app.add_systems(
89            PhysicsSchedule,
90            on_change_joint_entities::<T>
91                .in_set(PhysicsStepSystems::First)
92                .ambiguous_with(PhysicsStepSystems::First),
93        );
94    }
95}
96
97/// A [`Message`] recording a change to the [`JointGraph`].
98///
99/// This is used to communicate changes to the joint graph to the physics solver,
100/// which may need to link and unlink joints from simulation islands or other structures.
101#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
102pub enum JointGraphChange {
103    /// A joint was added to the [`JointGraph`].
104    Added(JointId),
105    /// A joint was removed from the [`JointGraph`].
106    Removed(JointId),
107}
108
109/// A component that holds the [`ComponentId`] of the [joint] component on this entity, if any.
110///
111/// [joint]: crate::dynamics::joints
112#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
113pub struct JointComponentId(Option<ComponentId>);
114
115impl JointComponentId {
116    /// Creates a new [`JointComponentId`] component with no active joint.
117    pub fn new() -> Self {
118        Self(None)
119    }
120
121    /// Returns the [`ComponentId`] of the active joint component, if any.
122    pub fn id(&self) -> Option<ComponentId> {
123        self.0
124    }
125}
126
127fn add_joint_to_graph<
128    T: Component + EntityConstraint<2>,
129    E: EventPattern<Event: EntityEvent>,
130    F: QueryFilter,
131>(
132    trigger: On<E>,
133    query: Query<(&T, Has<JointCollisionDisabled>), F>,
134    mut joint_graph: ResMut<JointGraph>,
135    mut joint_graph_changes: MessageWriter<JointGraphChange>,
136    #[cfg(feature = "xpbd_joints")] mut commands: Commands,
137) {
138    let entity = trigger.event_target();
139
140    let Ok((joint, collision_disabled)) = query.get(entity) else {
141        return;
142    };
143
144    let [body1, body2] = joint.entities();
145
146    // Add the joint to the joint graph.
147    let joint_edge = JointGraphEdge::new(entity, body1, body2, collision_disabled);
148    let joint_id = joint_graph.add_joint(joint_edge);
149
150    // The bodies are now constrained, so the XPBD solver must project their velocities.
151    #[cfg(feature = "xpbd_joints")]
152    for body in [body1, body2] {
153        commands.entity(body).try_insert(XpbdVelocityProjection);
154    }
155
156    // Record the change.
157    joint_graph_changes.write(JointGraphChange::Added(joint_id));
158}
159
160fn remove_joint_from_graph<E: EventPattern<Event: EntityEvent>>(
161    trigger: On<E>,
162    mut joint_graph: ResMut<JointGraph>,
163    mut joint_graph_changes: MessageWriter<JointGraphChange>,
164    #[cfg(feature = "xpbd_joints")] mut commands: Commands,
165) {
166    let entity = trigger.event_target();
167
168    let Some(joint) = joint_graph.get(entity) else {
169        return;
170    };
171
172    // Record the change.
173    joint_graph_changes.write(JointGraphChange::Removed(joint.id));
174    #[cfg(feature = "xpbd_joints")]
175    let bodies = [joint.body1, joint.body2];
176
177    // Remove the joint from the joint graph.
178    joint_graph.remove_joint(entity);
179
180    // Stop projecting XPBD velocities for bodies that are no longer constrained by any joint.
181    #[cfg(feature = "xpbd_joints")]
182    for body in bodies {
183        if joint_graph.joints_of(body).next().is_none() {
184            commands.entity(body).try_remove::<XpbdVelocityProjection>();
185        }
186    }
187}
188
189fn on_add_joint(mut world: DeferredWorld, ctx: HookContext) {
190    let entity = ctx.entity;
191    let component_id = ctx.component_id;
192
193    let mut joint = world.get_mut::<JointComponentId>(entity).unwrap();
194    let old_joint = joint.0;
195
196    // Update the joint component with the new component ID.
197    joint.0 = Some(component_id);
198
199    if let Some(old_joint) = old_joint {
200        // Joint already exists, remove the old one.
201        world.commands().entity(entity).remove_by_id(old_joint);
202
203        #[cfg(feature = "validate")]
204        {
205            use disqualified::ShortName;
206
207            // Log a warning about the joint replacement in case it was not intentional.
208            let components = world.components();
209            let old_joint_shortname = components.get_info(old_joint).unwrap().name();
210            let old_joint_name = ShortName(&old_joint_shortname);
211            let new_joint_shortname = components.get_info(component_id).unwrap().name();
212            let new_joint_name = ShortName(&new_joint_shortname);
213
214            warn!(
215                "{old_joint_name} was replaced with {new_joint_name} on entity {entity}. An entity can only hold one joint type at a time."
216            );
217        }
218    }
219}
220
221fn on_remove_joint(mut world: DeferredWorld, ctx: HookContext) {
222    let entity = ctx.entity;
223    let component_id = ctx.component_id;
224
225    // Remove the `JointComponentId` from the entity unless the component ID
226    // was changed, implying that the joint is being replaced by another one.
227    if let Some(mut joint) = world.get_mut::<JointComponentId>(entity)
228        && joint.0 == Some(component_id)
229    {
230        joint.0 = None;
231
232        // Remove the joint component.
233        world
234            .commands()
235            .entity(entity)
236            .try_remove::<JointComponentId>();
237    }
238}
239
240fn on_disable_joint_collision(
241    trigger: On<Add<JointCollisionDisabled>>,
242    query: Query<&RigidBodyColliders>,
243    joint_graph: Res<JointGraph>,
244    mut contact_graph: ResMut<ContactGraph>,
245    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
246) {
247    let entity = trigger.entity;
248
249    // Iterate through each collider of the body with fewer colliders,
250    // find contacts with the other body, and remove them.
251    let Some([body1, body2]) = joint_graph.bodies_of(entity) else {
252        return;
253    };
254    let Ok([colliders1, colliders2]) = query.get_many([body1, body2]) else {
255        return;
256    };
257
258    let (colliders, other_body) = if colliders1.len() < colliders2.len() {
259        (colliders1, body2)
260    } else {
261        (colliders2, body1)
262    };
263
264    let contacts_to_remove: Vec<ContactId> = colliders
265        .iter()
266        .flat_map(|collider| {
267            contact_graph
268                .contact_edges_with(collider)
269                .filter_map(|edge| {
270                    if edge.body1 == Some(other_body) || edge.body2 == Some(other_body) {
271                        Some(edge.id)
272                    } else {
273                        None
274                    }
275                })
276        })
277        .collect();
278
279    for contact_id in contacts_to_remove {
280        // Remove the contact from the contact graph.
281        let pair_key = PairKey::new(body1.index_u32(), body2.index_u32());
282        contact_graph.remove_edge_by_id(&pair_key, contact_id);
283
284        // Record the contact removal.
285        contact_status_changes.push(ContactStatusChange::StoppedGeneratingConstraints {
286            contact_id,
287            body1,
288            body2,
289        });
290    }
291}
292
293/// Update the joint graph when the entities of a joint change.
294fn on_change_joint_entities<T: Component + EntityConstraint<2>>(
295    query: Query<(Entity, &T), Changed<T>>,
296    mut joint_graph: ResMut<JointGraph>,
297    mut joint_graph_changes: MessageWriter<JointGraphChange>,
298    #[cfg(feature = "xpbd_joints")] mut commands: Commands,
299) {
300    for (entity, joint) in &query {
301        let [body1, body2] = joint.entities();
302        let Some(old_edge) = joint_graph.get(entity) else {
303            continue;
304        };
305
306        if body1 != old_edge.body1 || body2 != old_edge.body2 {
307            let old_id = old_edge.id;
308            #[cfg(feature = "xpbd_joints")]
309            let old_bodies = [old_edge.body1, old_edge.body2];
310
311            // Remove the old joint edge.
312            if let Some(mut edge) = joint_graph.remove_joint(entity) {
313                // Record the removal.
314                joint_graph_changes.write(JointGraphChange::Removed(old_id));
315
316                // Update the edge with the new bodies.
317                edge.body1 = body1;
318                edge.body2 = body2;
319
320                // Add the joint edge back with the new bodies.
321                let joint_id = joint_graph.add_joint(edge);
322
323                // Record the addition.
324                joint_graph_changes.write(JointGraphChange::Added(joint_id));
325
326                // Move XPBD velocity projection over to the new bodies.
327                #[cfg(feature = "xpbd_joints")]
328                {
329                    for body in [body1, body2] {
330                        commands.entity(body).try_insert(XpbdVelocityProjection);
331                    }
332                    for body in old_bodies {
333                        if joint_graph.joints_of(body).next().is_none() {
334                            commands.entity(body).try_remove::<XpbdVelocityProjection>();
335                        }
336                    }
337                }
338            }
339        }
340    }
341}
342
343// TODO: Tests