bevy_rapier2d/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};
6use crate::geometry::RapierColliderHandle;
7use crate::plugin::context::RapierRigidBodySet;
8use crate::plugin::context::{
9    RapierContextColliders, RapierContextEntityLink, RapierContextJoints,
10};
11use bevy::prelude::*;
12
13/// If an entity is turned into the child of something with a physics context link,
14/// the child should become a part of that physics context
15///
16/// If this fails to happen, weirdness will ensue.
17pub fn on_add_entity_with_parent(
18    q_add_entity_without_parent: Query<
19        (Entity, &ChildOf),
20        (
21            With<RapierContextEntityLink>,
22            Or<(Changed<RapierContextEntityLink>, Changed<ChildOf>)>,
23        ),
24    >,
25    q_child_of: Query<&ChildOf>,
26    q_physics_world: Query<&RapierContextEntityLink>,
27    mut commands: Commands,
28) {
29    for (ent, child_of) in &q_add_entity_without_parent {
30        let mut parent = Some(child_of.parent());
31        while let Some(parent_entity) = parent {
32            if let Ok(pw) = q_physics_world.get(parent_entity) {
33                // Change rapier context link only if the existing link isn't the correct one.
34                if q_physics_world.get(ent).map(|x| x != pw).unwrap_or(true) {
35                    remove_old_physics(ent, &mut commands);
36                    commands.entity(ent).insert(*pw);
37                }
38                break;
39            }
40            parent = q_child_of.get(parent_entity).ok().map(|x| x.parent());
41        }
42    }
43}
44
45/// Flags the entity to have its old physics removed
46fn remove_old_physics(entity: Entity, commands: &mut Commands) {
47    commands
48        .entity(entity)
49        .remove::<RapierColliderHandle>()
50        .remove::<RapierRigidBodyHandle>()
51        .remove::<RapierMultibodyJointHandle>()
52        .remove::<RapierImpulseJointHandle>();
53}
54
55/// Reacts to modifications to [`RapierContextEntityLink`]
56/// to move an entity's physics data from a context to another.
57///
58/// Also recursively bubbles down context changes to children & flags them to apply any needed physics changes
59pub fn on_change_context(
60    q_changed_contexts: Query<
61        (Entity, Ref<RapierContextEntityLink>),
62        Changed<RapierContextEntityLink>,
63    >,
64    q_children: Query<&Children>,
65    q_physics_context: Query<&RapierContextEntityLink>,
66    q_context: Query<(
67        &RapierContextColliders,
68        &RapierContextJoints,
69        &RapierRigidBodySet,
70    )>,
71    mut commands: Commands,
72) {
73    for (entity, new_physics_context) in &q_changed_contexts {
74        let context = q_context.get(new_physics_context.0);
75        // Ensure the context actually changed before removing them from the context
76        if !context
77            .map(|(colliders, joints, rigidbody_set)| {
78                // They are already apart of this context if any of these are true
79                colliders.entity2collider.contains_key(&entity)
80                    || rigidbody_set.entity2body.contains_key(&entity)
81                    || joints.entity2impulse_joint.contains_key(&entity)
82                    || joints.entity2multibody_joint.contains_key(&entity)
83            })
84            .unwrap_or(false)
85        {
86            remove_old_physics(entity, &mut commands);
87            bubble_down_context_change(
88                &mut commands,
89                entity,
90                &q_children,
91                *new_physics_context,
92                &q_physics_context,
93            );
94        }
95    }
96}
97
98fn bubble_down_context_change(
99    commands: &mut Commands,
100    entity: Entity,
101    q_children: &Query<&Children>,
102    new_physics_context: RapierContextEntityLink,
103    q_physics_context: &Query<&RapierContextEntityLink>,
104) {
105    let Ok(children) = q_children.get(entity) else {
106        return;
107    };
108
109    children.iter().for_each(|child| {
110        if q_physics_context
111            .get(child)
112            .map(|x| *x == new_physics_context)
113            .unwrap_or(false)
114        {
115            return;
116        }
117
118        remove_old_physics(child, commands);
119        commands.entity(child).insert(new_physics_context);
120
121        bubble_down_context_change(
122            commands,
123            child,
124            q_children,
125            new_physics_context,
126            q_physics_context,
127        );
128    });
129}
130
131#[cfg(test)]
132mod test {
133    use crate::plugin::{
134        context::{RapierContextEntityLink, RapierContextSimulation},
135        NoUserData, PhysicsSet, RapierPhysicsPlugin,
136    };
137    use crate::prelude::{ActiveEvents, Collider, ContactForceEventThreshold, RigidBody, Sensor};
138    use bevy::prelude::*;
139    use bevy::time::{TimePlugin, TimeUpdateStrategy};
140    use rapier::math::Real;
141
142    #[test]
143    pub fn multi_context_hierarchy_update() {
144        let mut app = App::new();
145        app.add_plugins((
146            TransformPlugin,
147            TimePlugin,
148            RapierPhysicsPlugin::<NoUserData>::default(),
149        ))
150        .add_systems(
151            PostUpdate,
152            setup_physics
153                .run_if(run_once)
154                .before(PhysicsSet::SyncBackend),
155        );
156        // Simulates 60 updates per seconds
157        app.insert_resource(TimeUpdateStrategy::ManualDuration(
158            std::time::Duration::from_secs_f32(1f32 / 60f32),
159        ));
160        app.finish();
161        app.update();
162        // Verify all rapier entities have a `RapierContextEntityLink`.
163        let world = app.world_mut();
164        let mut query = world.query_filtered::<Entity, With<Marker<'R'>>>();
165        for entity in query.iter(world) {
166            world
167                .get::<RapierContextEntityLink>(entity)
168                .unwrap_or_else(|| panic!("no link to rapier context entity from {entity}."));
169        }
170        // Verify link is correctly updated for children.
171        let new_rapier_context = world.spawn((RapierContextSimulation::default(),)).id();
172        // FIXME: We need to wait 1 frame when creating a context.
173        // Ideally we should be able to order the systems so that we don't have to wait.
174        app.update();
175        let world = app.world_mut();
176        let mut query = world.query_filtered::<&mut RapierContextEntityLink, With<Marker<'P'>>>();
177        let mut link_parent = query.single_mut(world).unwrap();
178        link_parent.0 = new_rapier_context;
179        app.update();
180        let world = app.world_mut();
181        let mut query = world.query_filtered::<&RapierContextEntityLink, With<Marker<'C'>>>();
182        let link_child = query.single_mut(world).unwrap();
183        assert_eq!(link_child.0, new_rapier_context);
184        return;
185
186        #[derive(Component)]
187        pub struct Marker<const MARKER: char>;
188
189        #[cfg(feature = "dim3")]
190        fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
191            Collider::cuboid(hx, hy, hz)
192        }
193        #[cfg(feature = "dim2")]
194        fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
195            Collider::cuboid(hx, hy)
196        }
197        pub fn setup_physics(mut commands: Commands) {
198            commands.spawn((
199                Transform::from_xyz(0.0, -1.2, 0.0),
200                cuboid(4.0, 1.0, 1.0),
201                Marker::<'R'>,
202            ));
203
204            commands.spawn((
205                Transform::from_xyz(0.0, 5.0, 0.0),
206                cuboid(4.0, 1.5, 1.0),
207                Sensor,
208                Marker::<'R'>,
209            ));
210
211            commands
212                .spawn((
213                    Transform::from_xyz(0.0, 13.0, 0.0),
214                    RigidBody::Dynamic,
215                    cuboid(0.5, 0.5, 0.5),
216                    ActiveEvents::COLLISION_EVENTS,
217                    ContactForceEventThreshold(30.0),
218                    Marker::<'P'>,
219                    Marker::<'R'>,
220                ))
221                .with_children(|child_builder| {
222                    child_builder.spawn((
223                        Transform::from_xyz(0.0, -1.2, 0.0),
224                        cuboid(4.0, 1.0, 1.0),
225                        Marker::<'C'>,
226                        Marker::<'R'>,
227                    ));
228                });
229        }
230    }
231}