Skip to main content

bevy_rapier3d/plugin/systems/
multiple_rapier_contexts.rs

1//! systems to support multiple physics contexts, and changes between them.
2
3use crate::dynamics::{
4    RapierImpulseJointHandle, RapierMultibodyJointHandle, RapierRigidBodyHandle,
5    RapierSoftBodyHandle,
6};
7use crate::geometry::RapierColliderHandle;
8use crate::plugin::context::RapierRigidBodySet;
9use crate::plugin::context::{
10    DefaultRapierContext, RapierContextColliders, RapierContextEntityLink, RapierContextJoints,
11};
12use bevy::ecs::system::SystemParam;
13use bevy::prelude::*;
14
15/// Finds the Rapier context of the entities being initialized without a
16/// [`RapierContextEntityLink`].
17///
18/// Such an entity joins the context of its closest ancestor with a [`RapierContextEntityLink`],
19/// or the [`DefaultRapierContext`] if it has none.
20#[derive(SystemParam)]
21pub struct RapierContextLinkResolver<'w, 's> {
22    default_context: Query<'w, 's, Entity, With<DefaultRapierContext>>,
23    child_of: Query<'w, 's, &'static ChildOf>,
24    links: Query<'w, 's, &'static RapierContextEntityLink>,
25}
26
27impl RapierContextLinkResolver<'_, '_> {
28    /// The context of `entity`, given its current `link`.
29    ///
30    /// If `link` is `None`, the resolved context is inserted as the entity's
31    /// [`RapierContextEntityLink`]. Returns `None` if no context was found.
32    pub fn resolve(
33        &self,
34        entity: Entity,
35        link: Option<&RapierContextEntityLink>,
36        commands: &mut Commands,
37    ) -> Option<Entity> {
38        if let Some(link) = link {
39            return Some(link.0);
40        }
41
42        let context = self
43            .child_of
44            .iter_ancestors(entity)
45            .find_map(|ancestor| self.links.get(ancestor).ok().map(|link| link.0))
46            .or_else(|| self.default_context.single().ok())?;
47        commands
48            .entity(entity)
49            .insert(RapierContextEntityLink(context));
50        Some(context)
51    }
52}
53
54/// If an entity is turned into the child of something with a physics context link,
55/// the child should become a part of that physics context
56///
57/// If this fails to happen, weirdness will ensue.
58pub fn on_add_entity_with_parent(
59    q_add_entity_without_parent: Query<
60        (Entity, &ChildOf),
61        (
62            With<RapierContextEntityLink>,
63            Or<(Changed<RapierContextEntityLink>, Changed<ChildOf>)>,
64        ),
65    >,
66    q_child_of: Query<&ChildOf>,
67    q_physics_world: Query<&RapierContextEntityLink>,
68    mut commands: Commands,
69) {
70    for (ent, child_of) in &q_add_entity_without_parent {
71        let mut parent = Some(child_of.parent());
72        while let Some(parent_entity) = parent {
73            if let Ok(pw) = q_physics_world.get(parent_entity) {
74                // Change rapier context link only if the existing link isn't the correct one.
75                if q_physics_world.get(ent).map(|x| x != pw).unwrap_or(true) {
76                    remove_old_physics(ent, &mut commands);
77                    commands.entity(ent).insert(*pw);
78                }
79                break;
80            }
81            parent = q_child_of.get(parent_entity).ok().map(|x| x.parent());
82        }
83    }
84}
85
86/// Flags the entity to have its old physics removed
87fn remove_old_physics(entity: Entity, commands: &mut Commands) {
88    commands
89        .entity(entity)
90        .remove::<RapierColliderHandle>()
91        .remove::<RapierRigidBodyHandle>()
92        .remove::<RapierMultibodyJointHandle>()
93        .remove::<RapierImpulseJointHandle>()
94        .remove::<RapierSoftBodyHandle>();
95}
96
97/// Reacts to modifications to [`RapierContextEntityLink`]
98/// to move an entity's physics data from a context to another.
99///
100/// Also recursively bubbles down context changes to children & flags them to apply any needed physics changes
101pub fn on_change_context(
102    q_changed_contexts: Query<
103        (Entity, Ref<RapierContextEntityLink>),
104        Changed<RapierContextEntityLink>,
105    >,
106    q_children: Query<&Children>,
107    q_physics_context: Query<&RapierContextEntityLink>,
108    q_context: Query<(
109        &RapierContextColliders,
110        &RapierContextJoints,
111        &RapierRigidBodySet,
112    )>,
113    mut commands: Commands,
114) {
115    for (entity, new_physics_context) in &q_changed_contexts {
116        let context = q_context.get(new_physics_context.0);
117        // Ensure the context actually changed before removing them from the context
118        if !context
119            .map(|(colliders, joints, rigidbody_set)| {
120                // They are already apart of this context if any of these are true
121                colliders.entity2collider.contains_key(&entity)
122                    || rigidbody_set.entity2body.contains_key(&entity)
123                    || rigidbody_set.entity2soft_body.contains_key(&entity)
124                    || joints.entity2impulse_joint.contains_key(&entity)
125                    || joints.entity2multibody_joint.contains_key(&entity)
126            })
127            .unwrap_or(false)
128        {
129            remove_old_physics(entity, &mut commands);
130            bubble_down_context_change(
131                &mut commands,
132                entity,
133                &q_children,
134                *new_physics_context,
135                &q_physics_context,
136            );
137        }
138    }
139}
140
141fn bubble_down_context_change(
142    commands: &mut Commands,
143    entity: Entity,
144    q_children: &Query<&Children>,
145    new_physics_context: RapierContextEntityLink,
146    q_physics_context: &Query<&RapierContextEntityLink>,
147) {
148    let Ok(children) = q_children.get(entity) else {
149        return;
150    };
151
152    children.iter().for_each(|child| {
153        if q_physics_context
154            .get(child)
155            .map(|x| *x == new_physics_context)
156            .unwrap_or(false)
157        {
158            return;
159        }
160
161        remove_old_physics(child, commands);
162        commands.entity(child).insert(new_physics_context);
163
164        bubble_down_context_change(
165            commands,
166            child,
167            q_children,
168            new_physics_context,
169            q_physics_context,
170        );
171    });
172}
173
174#[cfg(test)]
175mod test {
176    use crate::plugin::{
177        context::{
178            RapierContextColliders, RapierContextEntityLink, RapierContextSimulation,
179            RapierRigidBodySet,
180        },
181        NoUserData, PhysicsSet, RapierPhysicsPlugin,
182    };
183    use crate::prelude::{ActiveEvents, Collider, ContactForceEventThreshold, RigidBody, Sensor};
184    use bevy::prelude::*;
185    use bevy::time::{TimePlugin, TimeUpdateStrategy};
186    use rapier::math::Real;
187
188    #[test]
189    pub fn multi_context_hierarchy_update() {
190        let mut app = App::new();
191        app.add_plugins((
192            TransformPlugin,
193            TimePlugin,
194            RapierPhysicsPlugin::<NoUserData>::default(),
195        ))
196        .add_systems(
197            PostUpdate,
198            setup_physics
199                .run_if(run_once)
200                .before(PhysicsSet::SyncBackend),
201        );
202        // Simulates 60 updates per seconds
203        app.insert_resource(TimeUpdateStrategy::ManualDuration(
204            std::time::Duration::from_secs_f32(1f32 / 60f32),
205        ));
206        app.finish();
207        app.update();
208        // Verify all rapier entities have a `RapierContextEntityLink`.
209        let world = app.world_mut();
210        let mut query = world.query_filtered::<Entity, With<Marker<'R'>>>();
211        for entity in query.iter(world) {
212            world
213                .get::<RapierContextEntityLink>(entity)
214                .unwrap_or_else(|| panic!("no link to rapier context entity from {entity}."));
215        }
216        // Verify link is correctly updated for children.
217        let new_rapier_context = world.spawn((RapierContextSimulation::default(),)).id();
218        // FIXME: We need to wait 1 frame when creating a context.
219        // Ideally we should be able to order the systems so that we don't have to wait.
220        app.update();
221        let world = app.world_mut();
222        let mut query = world.query_filtered::<&mut RapierContextEntityLink, With<Marker<'P'>>>();
223        let mut link_parent = query.single_mut(world).unwrap();
224        link_parent.0 = new_rapier_context;
225        app.update();
226        let world = app.world_mut();
227        let mut query = world.query_filtered::<&RapierContextEntityLink, With<Marker<'C'>>>();
228        let link_child = query.single_mut(world).unwrap();
229        assert_eq!(link_child.0, new_rapier_context);
230        return;
231
232        #[derive(Component)]
233        pub struct Marker<const MARKER: char>;
234
235        #[cfg(feature = "dim3")]
236        fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
237            Collider::cuboid(hx, hy, hz)
238        }
239        #[cfg(feature = "dim2")]
240        fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
241            Collider::cuboid(hx, hy)
242        }
243        pub fn setup_physics(mut commands: Commands) {
244            commands.spawn((
245                Transform::from_xyz(0.0, -1.2, 0.0),
246                cuboid(4.0, 1.0, 1.0),
247                Marker::<'R'>,
248            ));
249
250            commands.spawn((
251                Transform::from_xyz(0.0, 5.0, 0.0),
252                cuboid(4.0, 1.5, 1.0),
253                Sensor,
254                Marker::<'R'>,
255            ));
256
257            commands
258                .spawn((
259                    Transform::from_xyz(0.0, 13.0, 0.0),
260                    RigidBody::Dynamic,
261                    cuboid(0.5, 0.5, 0.5),
262                    ActiveEvents::COLLISION_EVENTS,
263                    ContactForceEventThreshold(30.0),
264                    Marker::<'P'>,
265                    Marker::<'R'>,
266                ))
267                .with_children(|child_builder| {
268                    child_builder.spawn((
269                        Transform::from_xyz(0.0, -1.2, 0.0),
270                        cuboid(4.0, 1.0, 1.0),
271                        Marker::<'C'>,
272                        Marker::<'R'>,
273                    ));
274                });
275        }
276    }
277
278    #[test]
279    pub fn children_inherit_the_parent_context_at_initialization() {
280        let mut app = App::new();
281        app.add_plugins((
282            TransformPlugin,
283            TimePlugin,
284            RapierPhysicsPlugin::<NoUserData>::default(),
285        ));
286        app.insert_resource(TimeUpdateStrategy::ManualDuration(
287            std::time::Duration::from_secs_f32(1f32 / 60f32),
288        ));
289        app.finish();
290        app.update();
291
292        let other_context = app
293            .world_mut()
294            .spawn(RapierContextSimulation::default())
295            .id();
296        app.update();
297
298        let mut child = Entity::PLACEHOLDER;
299        let mut grandchild = Entity::PLACEHOLDER;
300        let parent = app
301            .world_mut()
302            .spawn((
303                Transform::default(),
304                RigidBody::Dynamic,
305                RapierContextEntityLink(other_context),
306            ))
307            .with_children(|builder| {
308                child = builder
309                    .spawn((Transform::default(), Collider::ball(0.5)))
310                    .with_children(|builder| {
311                        grandchild = builder
312                            .spawn((Transform::default(), Collider::ball(0.5)))
313                            .id();
314                    })
315                    .id();
316            })
317            .id();
318        app.update();
319
320        let world = app.world();
321        for entity in [child, grandchild] {
322            assert_eq!(
323                world.get::<RapierContextEntityLink>(entity).unwrap().0,
324                other_context
325            );
326        }
327        let colliders = world.get::<RapierContextColliders>(other_context).unwrap();
328        assert!(colliders.entity2collider().contains_key(&child));
329        assert!(colliders.entity2collider().contains_key(&grandchild));
330        assert_eq!(
331            colliders.collider_parent(
332                world.get::<RapierRigidBodySet>(other_context).unwrap(),
333                child
334            ),
335            Some(parent)
336        );
337    }
338}