Skip to main content

bevy_rapier3d/plugin/systems/
character_controller.rs

1use crate::control::controller_filter::ControllerExclusions;
2use crate::control::CharacterCollision;
3use crate::control::ControllerIgnored;
4use crate::dynamics::RapierRigidBodyHandle;
5use crate::geometry::RapierColliderHandle;
6use crate::plugin::context::systemparams::RAPIER_CONTEXT_EXPECT_ERROR;
7use crate::plugin::context::RapierContextEntityLink;
8use crate::plugin::RapierConfiguration;
9use crate::prelude::context::RapierContextColliders;
10use crate::prelude::context::RapierContextSimulation;
11use crate::prelude::context::RapierRigidBodySet;
12use crate::prelude::KinematicCharacterController;
13use crate::prelude::KinematicCharacterControllerOutput;
14use crate::utils;
15use bevy::ecs::entity::EntityHashSet;
16use bevy::prelude::*;
17use rapier::geometry::{Collider as RapierCollider, ColliderHandle};
18use rapier::math::Pose;
19use rapier::pipeline::QueryFilter;
20
21/// System responsible for applying the character controller translation to the underlying
22/// collider.
23pub fn update_character_controls(
24    mut commands: Commands,
25    config: Query<&RapierConfiguration>,
26    mut context_access: Query<(
27        &mut RapierContextSimulation,
28        &mut RapierContextColliders,
29        &mut RapierRigidBodySet,
30    )>,
31    mut character_controllers: Query<(
32        Entity,
33        &RapierContextEntityLink,
34        &mut KinematicCharacterController,
35        Option<&mut KinematicCharacterControllerOutput>,
36        Option<&RapierColliderHandle>,
37        Option<&RapierRigidBodyHandle>,
38        Option<&GlobalTransform>,
39    )>,
40    mut transforms: Query<&mut Transform>,
41    ignored: Query<Entity, With<ControllerIgnored>>,
42) {
43    let ignored: EntityHashSet = ignored.iter().collect();
44    for (
45        entity,
46        rapier_context_link,
47        mut controller,
48        output,
49        collider_handle,
50        body_handle,
51        glob_transform,
52    ) in character_controllers.iter_mut()
53    {
54        if let (Some(raw_controller), Some(translation)) =
55            (controller.to_raw(), controller.translation)
56        {
57            let config = config
58                .get(rapier_context_link.0)
59                .expect("Could not get [`RapierConfiguration`]");
60            let (mut context, mut context_colliders, mut rigidbody_set) = context_access
61                .get_mut(rapier_context_link.0)
62                .expect(RAPIER_CONTEXT_EXPECT_ERROR);
63
64            let context = &mut *context;
65            let rigidbody_set = &mut *rigidbody_set;
66            let scaled_custom_shape =
67                controller
68                    .custom_shape
69                    .as_ref()
70                    .map(|(custom_shape, tra, rot)| {
71                        // TODO: avoid the systematic scale somehow?
72                        let mut scaled_shape = custom_shape.clone();
73                        scaled_shape.set_scale(custom_shape.scale, config.scaled_shape_subdivision);
74
75                        (scaled_shape, *tra, *rot)
76                    });
77
78            let parent_rigid_body = body_handle.map(|h| h.0).or_else(|| {
79                collider_handle
80                    .and_then(|h| context_colliders.colliders.get(h.0))
81                    .and_then(|c| c.parent())
82            });
83            let entity_to_move = parent_rigid_body
84                .and_then(|rb| rigidbody_set.rigid_body_entity(rb))
85                .unwrap_or(entity);
86
87            let (character_shape, character_pos) = if let Some((scaled_shape, tra, rot)) =
88                &scaled_custom_shape
89            {
90                let mut shape_pos: Pose = utils::pose_from(*tra, *rot);
91
92                if let Some(body) = body_handle.and_then(|h| rigidbody_set.bodies.get(h.0)) {
93                    shape_pos = body.position() * shape_pos
94                } else if let Some(gtransform) = glob_transform {
95                    shape_pos = utils::transform_to_iso(&gtransform.compute_transform()) * shape_pos
96                }
97
98                (&*scaled_shape.raw, shape_pos)
99            } else if let Some(collider) =
100                collider_handle.and_then(|h| context_colliders.colliders.get(h.0))
101            {
102                (collider.shape(), *collider.position())
103            } else {
104                continue;
105            };
106            let character_shape = character_shape.clone_dyn();
107
108            let exclude_collider = collider_handle.map(|h| h.0);
109
110            let character_mass = controller
111                .custom_mass
112                .or_else(|| {
113                    parent_rigid_body
114                        .and_then(|h| rigidbody_set.bodies.get(h))
115                        .map(|rb| rb.mass())
116                })
117                .unwrap_or(0.0);
118
119            let exclusions = ControllerExclusions::new(
120                &controller.exclude_colliders,
121                &controller.exclude_rigid_bodies,
122                &ignored,
123                controller.filter_predicate.as_ref(),
124                rigidbody_set,
125            );
126            let predicate =
127                |_: ColliderHandle, collider: &RapierCollider| exclusions.test(collider);
128            let mut filter = QueryFilter {
129                flags: controller.filter_flags,
130                groups: controller.filter_groups.map(|g| g.into()),
131                exclude_collider: None,
132                exclude_rigid_body: None,
133                predicate: (!exclusions.is_empty())
134                    .then_some(&predicate as &dyn Fn(ColliderHandle, &RapierCollider) -> bool),
135            };
136
137            if let Some(parent) = parent_rigid_body {
138                filter = filter.exclude_rigid_body(parent);
139            } else if let Some(excl_co) = exclude_collider {
140                filter = filter.exclude_collider(excl_co)
141            };
142
143            let collisions = &mut context.character_collisions_collector;
144            collisions.clear();
145
146            let mut query_pipeline = context.broad_phase.as_query_pipeline_mut(
147                context.narrow_phase.query_dispatcher(),
148                &mut rigidbody_set.bodies,
149                &mut context_colliders.colliders,
150                filter,
151            );
152
153            let movement = raw_controller.move_shape(
154                context.integration_parameters.dt,
155                &query_pipeline.as_ref(),
156                &*character_shape,
157                &character_pos,
158                translation,
159                |c| collisions.push(c),
160            );
161
162            if controller.apply_impulse_to_dynamic_bodies {
163                raw_controller.solve_character_collision_impulses(
164                    context.integration_parameters.dt,
165                    &mut query_pipeline,
166                    &*character_shape,
167                    character_mass,
168                    collisions.iter(),
169                )
170            }
171
172            if let Ok(mut transform) = transforms.get_mut(entity_to_move) {
173                // TODO: take the parent’s GlobalTransform rotation into account?
174                transform.translation.x += movement.translation.x;
175                transform.translation.y += movement.translation.y;
176                #[cfg(feature = "dim3")]
177                {
178                    transform.translation.z += movement.translation.z;
179                }
180            }
181
182            let converted_collisions = context
183                .character_collisions_collector
184                .iter()
185                .filter_map(|c| CharacterCollision::from_raw(&context_colliders, c));
186
187            if let Some(mut output) = output {
188                output.desired_translation = controller.translation.unwrap();
189                output.effective_translation = movement.translation;
190                output.grounded = movement.grounded;
191                output.collisions.clear();
192                output.collisions.extend(converted_collisions);
193                output.is_sliding_down_slope = movement.is_sliding_down_slope;
194            } else {
195                commands
196                    .entity(entity)
197                    .insert(KinematicCharacterControllerOutput {
198                        desired_translation: controller.translation.unwrap(),
199                        effective_translation: movement.translation,
200                        grounded: movement.grounded,
201                        collisions: converted_collisions.collect(),
202                        is_sliding_down_slope: movement.is_sliding_down_slope,
203                    });
204            }
205
206            controller.translation = None;
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use crate::control::{
214        ControllerFilterPredicate, ControllerIgnored, KinematicCharacterController,
215    };
216    use crate::geometry::Collider;
217    use crate::math::Vect;
218    use crate::plugin::{NoUserData, RapierPhysicsPlugin};
219    use crate::prelude::RigidBody;
220    use bevy::ecs::entity::EntityHashSet;
221    use bevy::prelude::*;
222    use bevy::time::{TimePlugin, TimeUpdateStrategy};
223
224    #[cfg(feature = "dim2")]
225    fn wall() -> Collider {
226        Collider::cuboid(0.25, 2.0)
227    }
228
229    #[cfg(feature = "dim3")]
230    fn wall() -> Collider {
231        Collider::cuboid(0.25, 2.0, 2.0)
232    }
233
234    #[test]
235    fn character_controller_exclusions() {
236        let mut app = App::new();
237        app.add_plugins((
238            TransformPlugin,
239            TimePlugin,
240            RapierPhysicsPlugin::<NoUserData>::default(),
241        ))
242        .insert_resource(TimeUpdateStrategy::ManualDuration(
243            std::time::Duration::from_secs_f32(1.0 / 60.0),
244        ));
245        app.finish();
246
247        // One row per scenario, each with its own wall in the character's path.
248        let mut spawn_row = |row: usize, wall_is_body: bool| {
249            let y = row as f32 * 10.0;
250            let mut wall_entity = app
251                .world_mut()
252                .spawn((Transform::from_xyz(3.0, y, 0.0), wall()));
253            if wall_is_body {
254                wall_entity.insert(RigidBody::Fixed);
255            }
256            let wall_entity = wall_entity.id();
257            let character = app
258                .world_mut()
259                .spawn((
260                    Transform::from_xyz(0.0, y, 0.0),
261                    Collider::ball(0.5),
262                    KinematicCharacterController {
263                        snap_to_ground: None,
264                        ..Default::default()
265                    },
266                ))
267                .id();
268            (wall_entity, character)
269        };
270        let (_, blocked) = spawn_row(0, false);
271        let (wall1, excluded_collider) = spawn_row(1, false);
272        let (wall2, excluded_body) = spawn_row(2, true);
273        let (wall3, predicate_filtered) = spawn_row(3, false);
274        let (wall4, ignored_collider) = spawn_row(4, false);
275        let (wall5, ignored_body) = spawn_row(5, true);
276
277        // Let the colliders be initialized and inserted into the broad-phase.
278        app.update();
279        app.update();
280
281        let world = app.world_mut();
282        world
283            .get_mut::<KinematicCharacterController>(excluded_collider)
284            .unwrap()
285            .exclude_colliders = EntityHashSet::from_iter([wall1]);
286        world
287            .get_mut::<KinematicCharacterController>(excluded_body)
288            .unwrap()
289            .exclude_rigid_bodies = EntityHashSet::from_iter([wall2]);
290        world
291            .get_mut::<KinematicCharacterController>(predicate_filtered)
292            .unwrap()
293            .filter_predicate = Some(ControllerFilterPredicate::new(move |e, _| e != wall3));
294        world.entity_mut(wall4).insert(ControllerIgnored);
295        world.entity_mut(wall5).insert(ControllerIgnored);
296        for character in [
297            blocked,
298            excluded_collider,
299            excluded_body,
300            predicate_filtered,
301            ignored_collider,
302            ignored_body,
303        ] {
304            world
305                .get_mut::<KinematicCharacterController>(character)
306                .unwrap()
307                .translation = Some(Vect::X * 6.0);
308        }
309
310        app.update();
311
312        let x = |entity: Entity| app.world().get::<Transform>(entity).unwrap().translation.x;
313        assert!(x(blocked) < 2.5, "The wall should block the character");
314        for character in [
315            excluded_collider,
316            excluded_body,
317            predicate_filtered,
318            ignored_collider,
319            ignored_body,
320        ] {
321            assert!(
322                (x(character) - 6.0).abs() < 1.0e-3,
323                "The character should pass through the excluded wall: {}",
324                x(character)
325            );
326        }
327    }
328}