bevy_rapier3d/plugin/systems/
character_controller.rs1use 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
21pub 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 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(>ransform.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 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 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 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}