1use crate::geometry::{ColliderHandle, ContactManifold, Shape, ShapeCastHit};
2use crate::math::{Isometry, Point, Real, UnitVector, Vector};
3use crate::pipeline::{QueryFilterFlags, QueryPipeline, QueryPipelineMut};
4use crate::utils;
5use na::{RealField, Vector2};
6use parry::bounding_volume::BoundingVolume;
7use parry::math::Translation;
8use parry::query::details::ShapeCastOptions;
9use parry::query::{DefaultQueryDispatcher, PersistentQueryDispatcher};
10
11#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
12#[derive(Copy, Clone, Debug, PartialEq)]
13pub enum CharacterLength {
15 Relative(Real),
21 Absolute(Real),
27}
28
29impl CharacterLength {
30 pub fn map_absolute(self, f: impl FnOnce(Real) -> Real) -> Self {
33 if let Self::Absolute(value) = self {
34 Self::Absolute(f(value))
35 } else {
36 self
37 }
38 }
39
40 pub fn map_relative(self, f: impl FnOnce(Real) -> Real) -> Self {
43 if let Self::Relative(value) = self {
44 Self::Relative(f(value))
45 } else {
46 self
47 }
48 }
49
50 fn eval(self, value: Real) -> Real {
51 match self {
52 Self::Relative(x) => value * x,
53 Self::Absolute(x) => x,
54 }
55 }
56}
57
58#[derive(Debug)]
59struct HitInfo {
60 toi: ShapeCastHit,
61 is_wall: bool,
62 is_nonslip_slope: bool,
63}
64
65#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct CharacterAutostep {
69 pub max_height: CharacterLength,
71 pub min_width: CharacterLength,
73 pub include_dynamic_bodies: bool,
75}
76
77impl Default for CharacterAutostep {
78 fn default() -> Self {
79 Self {
80 max_height: CharacterLength::Relative(0.25),
81 min_width: CharacterLength::Relative(0.5),
82 include_dynamic_bodies: true,
83 }
84 }
85}
86
87#[derive(Debug)]
88struct HitDecomposition {
89 normal_part: Vector<Real>,
90 horizontal_tangent: Vector<Real>,
91 vertical_tangent: Vector<Real>,
92 }
95
96impl HitDecomposition {
97 pub fn unconstrained_slide_part(&self) -> Vector<Real> {
98 self.normal_part + self.horizontal_tangent + self.vertical_tangent
99 }
100}
101
102#[derive(Copy, Clone, Debug)]
104pub struct CharacterCollision {
105 pub handle: ColliderHandle,
107 pub character_pos: Isometry<Real>,
109 pub translation_applied: Vector<Real>,
111 pub translation_remaining: Vector<Real>,
113 pub hit: ShapeCastHit,
115}
116
117#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
119#[derive(Copy, Clone, Debug)]
120pub struct KinematicCharacterController {
121 pub up: UnitVector<Real>,
123 pub offset: CharacterLength,
128 pub slide: bool,
130 pub autostep: Option<CharacterAutostep>,
135 pub max_slope_climb_angle: Real,
138 pub min_slope_slide_angle: Real,
141 pub snap_to_ground: Option<CharacterLength>,
144 pub normal_nudge_factor: Real,
153}
154
155impl Default for KinematicCharacterController {
156 fn default() -> Self {
157 Self {
158 up: Vector::y_axis(),
159 offset: CharacterLength::Relative(0.01),
160 slide: true,
161 autostep: None,
162 max_slope_climb_angle: Real::frac_pi_4(),
163 min_slope_slide_angle: Real::frac_pi_4(),
164 snap_to_ground: Some(CharacterLength::Relative(0.2)),
165 normal_nudge_factor: 1.0e-4,
166 }
167 }
168}
169
170#[derive(Debug)]
172pub struct EffectiveCharacterMovement {
173 pub translation: Vector<Real>,
175 pub grounded: bool,
177 pub is_sliding_down_slope: bool,
179}
180
181impl KinematicCharacterController {
182 fn check_and_fix_penetrations(&self) {
183 }
211
212 #[profiling::function]
214 pub fn move_shape(
215 &self,
216 dt: Real,
217 queries: &QueryPipeline,
218 character_shape: &dyn Shape,
219 character_pos: &Isometry<Real>,
220 desired_translation: Vector<Real>,
221 mut events: impl FnMut(CharacterCollision),
222 ) -> EffectiveCharacterMovement {
223 let mut result = EffectiveCharacterMovement {
224 translation: Vector::zeros(),
225 grounded: false,
226 is_sliding_down_slope: false,
227 };
228 let dims = self.compute_dims(character_shape);
229
230 self.check_and_fix_penetrations();
232
233 let mut translation_remaining = desired_translation;
234
235 let grounded_at_starting_pos = self.detect_grounded_status_and_apply_friction(
236 dt,
237 queries,
238 character_shape,
239 character_pos,
240 &dims,
241 None,
242 None,
243 );
244
245 let mut max_iters = 20;
246 let mut kinematic_friction_translation = Vector::zeros();
247 let offset = self.offset.eval(dims.y);
248 let mut is_moving = false;
249
250 while let Some((translation_dir, translation_dist)) =
251 UnitVector::try_new_and_get(translation_remaining, 1.0e-5)
252 {
253 if max_iters == 0 {
254 break;
255 } else {
256 max_iters -= 1;
257 }
258 is_moving = true;
259
260 if let Some((handle, hit)) = queries.cast_shape(
262 &(Translation::from(result.translation) * character_pos),
263 &translation_dir,
264 character_shape,
265 ShapeCastOptions {
266 target_distance: offset,
267 stop_at_penetration: false,
268 max_time_of_impact: translation_dist,
269 compute_impact_geometry_on_penetration: true,
270 },
271 ) {
272 let allowed_dist = hit.time_of_impact;
274 let allowed_translation = *translation_dir * allowed_dist;
275 result.translation += allowed_translation;
276 translation_remaining -= allowed_translation;
277
278 events(CharacterCollision {
279 handle,
280 character_pos: Translation::from(result.translation) * character_pos,
281 translation_applied: result.translation,
282 translation_remaining,
283 hit,
284 });
285
286 let hit_info = self.compute_hit_info(hit);
287
288 if !self.handle_stairs(
290 *queries,
291 character_shape,
292 &(Translation::from(result.translation) * character_pos),
293 &dims,
294 handle,
295 &hit_info,
296 &mut translation_remaining,
297 &mut result,
298 ) {
299 translation_remaining = self.handle_slopes(
301 &hit_info,
302 &desired_translation,
303 &translation_remaining,
304 self.normal_nudge_factor,
305 &mut result,
306 );
307 }
308 } else {
309 result.translation += translation_remaining;
311 translation_remaining.fill(0.0);
312 result.grounded = self.detect_grounded_status_and_apply_friction(
313 dt,
314 queries,
315 character_shape,
316 &(Translation::from(result.translation) * character_pos),
317 &dims,
318 None,
319 None,
320 );
321 break;
322 }
323 result.grounded = self.detect_grounded_status_and_apply_friction(
324 dt,
325 queries,
326 character_shape,
327 &(Translation::from(result.translation) * character_pos),
328 &dims,
329 Some(&mut kinematic_friction_translation),
330 Some(&mut translation_remaining),
331 );
332
333 if !self.slide {
334 break;
335 }
336 }
337 if !is_moving {
340 result.grounded = self.detect_grounded_status_and_apply_friction(
341 dt,
342 queries,
343 character_shape,
344 &(Translation::from(result.translation) * character_pos),
345 &dims,
346 None,
347 None,
348 );
349 }
350 if grounded_at_starting_pos {
352 self.snap_to_ground(
353 queries,
354 character_shape,
355 &(Translation::from(result.translation) * character_pos),
356 &dims,
357 &mut result,
358 );
359 }
360
361 result
363 }
364
365 fn snap_to_ground(
366 &self,
367 queries: &QueryPipeline,
368 character_shape: &dyn Shape,
369 character_pos: &Isometry<Real>,
370 dims: &Vector2<Real>,
371 result: &mut EffectiveCharacterMovement,
372 ) -> Option<(ColliderHandle, ShapeCastHit)> {
373 if let Some(snap_distance) = self.snap_to_ground {
374 if result.translation.dot(&self.up) < -1.0e-5 {
375 let snap_distance = snap_distance.eval(dims.y);
376 let offset = self.offset.eval(dims.y);
377 if let Some((hit_handle, hit)) = queries.cast_shape(
378 character_pos,
379 &-self.up,
380 character_shape,
381 ShapeCastOptions {
382 target_distance: offset,
383 stop_at_penetration: false,
384 max_time_of_impact: snap_distance,
385 compute_impact_geometry_on_penetration: true,
386 },
387 ) {
388 result.translation -= *self.up * hit.time_of_impact;
390 result.grounded = true;
391 return Some((hit_handle, hit));
392 }
393 }
394 }
395
396 None
397 }
398
399 fn predict_ground(&self, up_extends: Real) -> Real {
400 self.offset.eval(up_extends) + 0.05
401 }
402
403 #[profiling::function]
404 fn detect_grounded_status_and_apply_friction(
405 &self,
406 dt: Real,
407 queries: &QueryPipeline,
408 character_shape: &dyn Shape,
409 character_pos: &Isometry<Real>,
410 dims: &Vector2<Real>,
411 mut kinematic_friction_translation: Option<&mut Vector<Real>>,
412 mut translation_remaining: Option<&mut Vector<Real>>,
413 ) -> bool {
414 let prediction = self.predict_ground(dims.y);
415
416 let dispatcher = DefaultQueryDispatcher;
418
419 let mut manifolds: Vec<ContactManifold> = vec![];
420 let character_aabb = character_shape
421 .compute_aabb(character_pos)
422 .loosened(prediction);
423
424 let mut grounded = false;
425
426 'outer: for (_, collider) in queries.intersect_aabb_conservative(character_aabb) {
427 manifolds.clear();
428 let pos12 = character_pos.inv_mul(collider.position());
429 let _ = dispatcher.contact_manifolds(
430 &pos12,
431 character_shape,
432 collider.shape(),
433 prediction,
434 &mut manifolds,
435 &mut None,
436 );
437
438 if let (Some(kinematic_friction_translation), Some(translation_remaining)) = (
439 kinematic_friction_translation.as_deref_mut(),
440 translation_remaining.as_deref_mut(),
441 ) {
442 let init_kinematic_friction_translation = *kinematic_friction_translation;
443 let kinematic_parent = collider
444 .parent
445 .and_then(|p| queries.bodies.get(p.handle))
446 .filter(|rb| rb.is_kinematic());
447
448 for m in &manifolds {
449 if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
450 grounded = true;
451 }
452
453 if let Some(kinematic_parent) = kinematic_parent {
454 let mut num_active_contacts = 0;
455 let mut manifold_center = Point::origin();
456 let normal = -(character_pos * m.local_n1);
457
458 for contact in &m.points {
459 if contact.dist <= prediction {
460 num_active_contacts += 1;
461 let contact_point = collider.position() * contact.local_p2;
462 let target_vel = kinematic_parent.velocity_at_point(&contact_point);
463
464 let normal_target_mvt = target_vel.dot(&normal) * dt;
465 let normal_current_mvt = translation_remaining.dot(&normal);
466
467 manifold_center += contact_point.coords;
468 *translation_remaining +=
469 normal * (normal_target_mvt - normal_current_mvt);
470 }
471 }
472
473 if num_active_contacts > 0 {
474 let target_vel = kinematic_parent.velocity_at_point(
475 &(manifold_center / num_active_contacts as Real),
476 );
477 let tangent_platform_mvt =
478 (target_vel - normal * target_vel.dot(&normal)) * dt;
479 kinematic_friction_translation.zip_apply(
480 &tangent_platform_mvt,
481 |y, x| {
482 if x.abs() > (*y).abs() {
483 *y = x;
484 }
485 },
486 );
487 }
488 }
489 }
490
491 *translation_remaining +=
492 *kinematic_friction_translation - init_kinematic_friction_translation;
493 } else {
494 for m in &manifolds {
495 if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
496 grounded = true;
497 break 'outer; }
499 }
500 }
501 }
502 grounded
503 }
504
505 fn is_grounded_at_contact_manifold(
506 &self,
507 manifold: &ContactManifold,
508 character_pos: &Isometry<Real>,
509 dims: &Vector2<Real>,
510 ) -> bool {
511 let normal = -(character_pos * manifold.local_n1);
512
513 if normal.dot(&self.up) >= 1.0e-3 {
516 let prediction = self.predict_ground(dims.y);
517 for contact in &manifold.points {
518 if contact.dist <= prediction {
519 return true;
520 }
521 }
522 }
523 false
524 }
525
526 fn handle_slopes(
527 &self,
528 hit: &HitInfo,
529 movement_input: &Vector<Real>,
530 translation_remaining: &Vector<Real>,
531 normal_nudge_factor: Real,
532 result: &mut EffectiveCharacterMovement,
533 ) -> Vector<Real> {
534 let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
535 let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi);
536 let decomp = self.decompose_hit(translation_remaining, &hit.toi);
537
538 let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0;
541 let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0;
543
544 let climbing_intent = self.up.dot(&_vertical_input) > 0.0;
546 let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0;
548
549 let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
550 decomp.horizontal_tangent + decomp.normal_part
552 } else if hit.is_nonslip_slope && slipping && !slipping_intent {
553 decomp.horizontal_tangent + decomp.normal_part
555 } else {
556 result.is_sliding_down_slope = true;
558 decomp.unconstrained_slide_part()
559 };
560
561 allowed_movement + *hit.toi.normal1 * normal_nudge_factor
562 }
563
564 fn split_into_components(&self, translation: &Vector<Real>) -> [Vector<Real>; 2] {
565 let vertical_translation = *self.up * (self.up.dot(translation));
566 let horizontal_translation = *translation - vertical_translation;
567 [vertical_translation, horizontal_translation]
568 }
569
570 fn compute_hit_info(&self, toi: ShapeCastHit) -> HitInfo {
571 let angle_with_floor = self.up.angle(&toi.normal1);
572 let is_ceiling = self.up.dot(&toi.normal1) < 0.0;
573 let is_wall = angle_with_floor >= self.max_slope_climb_angle && !is_ceiling;
574 let is_nonslip_slope = angle_with_floor <= self.min_slope_slide_angle;
575
576 HitInfo {
577 toi,
578 is_wall,
579 is_nonslip_slope,
580 }
581 }
582
583 fn decompose_hit(&self, translation: &Vector<Real>, hit: &ShapeCastHit) -> HitDecomposition {
584 let dist_to_surface = translation.dot(&hit.normal1);
585 let normal_part;
586 let penetration_part;
587
588 if dist_to_surface < 0.0 {
589 normal_part = Vector::zeros();
590 penetration_part = dist_to_surface * *hit.normal1;
591 } else {
592 penetration_part = Vector::zeros();
593 normal_part = dist_to_surface * *hit.normal1;
594 }
595
596 let tangent = translation - normal_part - penetration_part;
597 #[cfg(feature = "dim3")]
598 let horizontal_tangent_dir = hit.normal1.cross(&self.up);
599 #[cfg(feature = "dim2")]
600 let horizontal_tangent_dir = Vector::zeros();
601
602 let horizontal_tangent_dir = horizontal_tangent_dir
603 .try_normalize(1.0e-5)
604 .unwrap_or_default();
605 let horizontal_tangent = tangent.dot(&horizontal_tangent_dir) * horizontal_tangent_dir;
606 let vertical_tangent = tangent - horizontal_tangent;
607
608 HitDecomposition {
609 normal_part,
610 horizontal_tangent,
611 vertical_tangent,
612 }
613 }
614
615 fn compute_dims(&self, character_shape: &dyn Shape) -> Vector2<Real> {
616 let extents = character_shape.compute_local_aabb().extents();
617 let up_extent = extents.dot(&self.up.abs());
618 let side_extent = (extents - (*self.up).abs() * up_extent).norm();
619 Vector2::new(side_extent, up_extent)
620 }
621
622 #[profiling::function]
623 fn handle_stairs(
624 &self,
625 mut queries: QueryPipeline,
626 character_shape: &dyn Shape,
627 character_pos: &Isometry<Real>,
628 dims: &Vector2<Real>,
629 stair_handle: ColliderHandle,
630 hit: &HitInfo,
631 translation_remaining: &mut Vector<Real>,
632 result: &mut EffectiveCharacterMovement,
633 ) -> bool {
634 let Some(autostep) = self.autostep else {
635 return false;
636 };
637
638 if !hit.is_wall {
640 return false;
641 }
642
643 let offset = self.offset.eval(dims.y);
644 let min_width = autostep.min_width.eval(dims.x) + offset;
645 let max_height = autostep.max_height.eval(dims.y) + offset;
646
647 if !autostep.include_dynamic_bodies {
648 if queries
649 .colliders
650 .get(stair_handle)
651 .and_then(|co| co.parent)
652 .and_then(|p| queries.bodies.get(p.handle))
653 .map(|b| b.is_dynamic())
654 == Some(true)
655 {
656 return false;
658 }
659
660 queries.filter.flags |= QueryFilterFlags::EXCLUDE_DYNAMIC;
661 }
662
663 let shifted_character_pos = Translation::from(*self.up * max_height) * character_pos;
664
665 let Some(horizontal_dir) = (*translation_remaining
666 - *self.up * translation_remaining.dot(&self.up))
667 .try_normalize(1.0e-5) else {
668 return false;
669 };
670
671 if queries
672 .cast_shape(
673 character_pos,
674 &self.up,
675 character_shape,
676 ShapeCastOptions {
677 target_distance: offset,
678 stop_at_penetration: false,
679 max_time_of_impact: max_height,
680 compute_impact_geometry_on_penetration: true,
681 },
682 )
683 .is_some()
684 {
685 return false;
687 }
688
689 if queries
690 .cast_shape(
691 &shifted_character_pos,
692 &horizontal_dir,
693 character_shape,
694 ShapeCastOptions {
695 target_distance: offset,
696 stop_at_penetration: false,
697 max_time_of_impact: min_width,
698 compute_impact_geometry_on_penetration: true,
699 },
700 )
701 .is_some()
702 {
703 return false;
705 }
706
707 if let Some((_, hit)) = queries.cast_shape(
710 &(Translation::from(horizontal_dir * min_width) * shifted_character_pos),
711 &-self.up,
712 character_shape,
713 ShapeCastOptions {
714 target_distance: offset,
715 stop_at_penetration: false,
716 max_time_of_impact: max_height,
717 compute_impact_geometry_on_penetration: true,
718 },
719 ) {
720 let [vertical_slope_translation, horizontal_slope_translation] = self
721 .split_into_components(translation_remaining)
722 .map(|remaining| subtract_hit(remaining, &hit));
723
724 let slope_translation = horizontal_slope_translation + vertical_slope_translation;
725
726 let angle_with_floor = self.up.angle(&hit.normal1);
727 let climbing = self.up.dot(&slope_translation) >= 0.0;
728
729 if climbing && angle_with_floor > self.max_slope_climb_angle {
730 return false; }
732 }
733
734 let step_height = max_height
736 - queries
737 .cast_shape(
738 &(Translation::from(horizontal_dir * min_width) * shifted_character_pos),
739 &-self.up,
740 character_shape,
741 ShapeCastOptions {
742 target_distance: offset,
743 stop_at_penetration: false,
744 max_time_of_impact: max_height,
745 compute_impact_geometry_on_penetration: true,
746 },
747 )
748 .map(|hit| hit.1.time_of_impact)
749 .unwrap_or(max_height);
750
751 let step = *self.up * step_height;
753 *translation_remaining -= step;
754
755 let horizontal_nudge =
758 horizontal_dir * horizontal_dir.dot(translation_remaining).min(min_width);
759 *translation_remaining -= horizontal_nudge;
760
761 result.translation += step + horizontal_nudge;
762 true
763 }
764
765 #[profiling::function]
770 pub fn solve_character_collision_impulses<'a>(
771 &self,
772 dt: Real,
773 queries: &mut QueryPipelineMut,
774 character_shape: &dyn Shape,
775 character_mass: Real,
776 collisions: impl IntoIterator<Item = &'a CharacterCollision>,
777 ) {
778 for collision in collisions {
779 self.solve_single_character_collision_impulse(
780 dt,
781 queries,
782 character_shape,
783 character_mass,
784 collision,
785 );
786 }
787 }
788
789 #[profiling::function]
794 fn solve_single_character_collision_impulse(
795 &self,
796 dt: Real,
797 queries: &mut QueryPipelineMut,
798 character_shape: &dyn Shape,
799 character_mass: Real,
800 collision: &CharacterCollision,
801 ) {
802 let extents = character_shape.compute_local_aabb().extents();
803 let up_extent = extents.dot(&self.up.abs());
804 let movement_to_transfer =
805 *collision.hit.normal1 * collision.translation_remaining.dot(&collision.hit.normal1);
806 let prediction = self.predict_ground(up_extent);
807
808 let dispatcher = DefaultQueryDispatcher;
810
811 let mut manifolds: Vec<ContactManifold> = vec![];
812 let character_aabb = character_shape
813 .compute_aabb(&collision.character_pos)
814 .loosened(prediction);
815
816 for (_, collider) in queries.as_ref().intersect_aabb_conservative(character_aabb) {
817 if let Some(parent) = collider.parent {
818 if let Some(body) = queries.bodies.get(parent.handle) {
819 if body.is_dynamic() {
820 manifolds.clear();
821 let pos12 = collision.character_pos.inv_mul(collider.position());
822 let prev_manifolds_len = manifolds.len();
823 let _ = dispatcher.contact_manifolds(
824 &pos12,
825 character_shape,
826 collider.shape(),
827 prediction,
828 &mut manifolds,
829 &mut None,
830 );
831
832 for m in &mut manifolds[prev_manifolds_len..] {
833 m.data.rigid_body2 = Some(parent.handle);
834 m.data.normal = collision.character_pos * m.local_n1;
835 }
836 }
837 }
838 }
839 }
840
841 let velocity_to_transfer = movement_to_transfer * utils::inv(dt);
842
843 for manifold in &manifolds {
844 let body_handle = manifold.data.rigid_body2.unwrap();
845 let body = &mut queries.bodies[body_handle];
846
847 for pt in &manifold.points {
848 if pt.dist <= prediction {
849 let body_mass = body.mass();
850 let contact_point = body.position() * pt.local_p2;
851 let delta_vel_per_contact = (velocity_to_transfer
852 - body.velocity_at_point(&contact_point))
853 .dot(&manifold.data.normal);
854 let mass_ratio = body_mass * character_mass / (body_mass + character_mass);
855
856 body.apply_impulse_at_point(
857 manifold.data.normal * delta_vel_per_contact.max(0.0) * mass_ratio,
858 contact_point,
859 true,
860 );
861 }
862 }
863 }
864 }
865}
866
867fn subtract_hit(translation: Vector<Real>, hit: &ShapeCastHit) -> Vector<Real> {
868 let surface_correction = (-translation).dot(&hit.normal1).max(0.0);
869 let surface_correction = surface_correction * (1.0 + 1.0e-5);
871 translation + *hit.normal1 * surface_correction
872}
873
874#[cfg(all(feature = "dim3", feature = "f32"))]
875#[cfg(test)]
876mod test {
877 use crate::{
878 control::{CharacterLength, KinematicCharacterController},
879 prelude::*,
880 };
881
882 #[test]
883 fn character_controller_climb_test() {
884 let mut colliders = ColliderSet::new();
885 let mut impulse_joints = ImpulseJointSet::new();
886 let mut multibody_joints = MultibodyJointSet::new();
887 let mut pipeline = PhysicsPipeline::new();
888 let mut bf = BroadPhaseBvh::new();
889 let mut nf = NarrowPhase::new();
890 let mut islands = IslandManager::new();
891
892 let mut bodies = RigidBodySet::new();
893
894 let gravity = Vector::y() * -9.81;
895
896 let ground_size = 100.0;
897 let ground_height = 0.1;
898 let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]);
902 let floor_handle = bodies.insert(rigid_body);
903 let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
904 colliders.insert_with_parent(collider, floor_handle, &mut bodies);
905
906 let slope_angle = 0.2;
910 let slope_size = 2.0;
911 let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
912 .translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
913 .rotation(Vector::z() * slope_angle);
914 colliders.insert(collider);
915
916 let impossible_slope_angle = 0.6;
920 let impossible_slope_size = 2.0;
921 let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size)
922 .translation(vector![
923 0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
924 -ground_height + 1.7,
925 0.0
926 ])
927 .rotation(Vector::z() * impossible_slope_angle);
928 colliders.insert(collider);
929
930 let integration_parameters = IntegrationParameters::default();
931
932 let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
934 .additional_mass(1.0)
935 .build();
936 character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
937 let character_handle_can_climb = bodies.insert(character_body_can_climb);
938
939 let collider = ColliderBuilder::ball(0.5).build();
940 colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
941
942 let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
944 .additional_mass(1.0)
945 .build();
946 character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
947 let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
948
949 let collider = ColliderBuilder::ball(0.5).build();
950 let character_shape = collider.shape();
951 colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
952
953 for i in 0..200 {
954 pipeline.step(
956 &gravity,
957 &integration_parameters,
958 &mut islands,
959 &mut bf,
960 &mut nf,
961 &mut bodies,
962 &mut colliders,
963 &mut impulse_joints,
964 &mut multibody_joints,
965 &mut CCDSolver::new(),
966 &(),
967 &(),
968 );
969
970 let mut update_character_controller =
971 |controller: KinematicCharacterController, handle: RigidBodyHandle| {
972 let character_body = bodies.get(handle).unwrap();
973 let mut collisions = vec![];
976 let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
977 let query_pipeline = bf.as_query_pipeline(
978 nf.query_dispatcher(),
979 &bodies,
980 &colliders,
981 filter_character_controller,
982 );
983 let effective_movement = controller.move_shape(
984 integration_parameters.dt,
985 &query_pipeline,
986 character_shape,
987 character_body.position(),
988 Vector::new(0.1, -0.1, 0.0),
989 |collision| collisions.push(collision),
990 );
991 let character_body = bodies.get_mut(handle).unwrap();
992 let translation = character_body.translation();
993 assert!(
994 effective_movement.grounded,
995 "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
996 i,
997 translation + effective_movement.translation
998 );
999 character_body.set_next_kinematic_translation(
1000 translation + effective_movement.translation,
1001 );
1002 };
1003
1004 let character_controller_cannot_climb = KinematicCharacterController {
1005 max_slope_climb_angle: impossible_slope_angle - 0.001,
1006 ..Default::default()
1007 };
1008 let character_controller_can_climb = KinematicCharacterController {
1009 max_slope_climb_angle: impossible_slope_angle + 0.001,
1010 ..Default::default()
1011 };
1012 update_character_controller(
1013 character_controller_cannot_climb,
1014 character_handle_cannot_climb,
1015 );
1016 update_character_controller(character_controller_can_climb, character_handle_can_climb);
1017 }
1018 let character_body = bodies.get(character_handle_can_climb).unwrap();
1019 assert!(character_body.translation().x > 6.0);
1020 assert!(character_body.translation().y > 3.0);
1021 let character_body = bodies.get(character_handle_cannot_climb).unwrap();
1022 assert!(character_body.translation().x < 4.0);
1023 assert!(dbg!(character_body.translation().y) < 2.0);
1024 }
1025
1026 #[test]
1027 fn character_controller_ground_detection() {
1028 let mut colliders = ColliderSet::new();
1029 let mut impulse_joints = ImpulseJointSet::new();
1030 let mut multibody_joints = MultibodyJointSet::new();
1031 let mut pipeline = PhysicsPipeline::new();
1032 let mut bf = BroadPhaseBvh::new();
1033 let mut nf = NarrowPhase::new();
1034 let mut islands = IslandManager::new();
1035
1036 let mut bodies = RigidBodySet::new();
1037
1038 let gravity = Vector::y() * -9.81;
1039
1040 let ground_size = 1001.0;
1041 let ground_height = 1.0;
1042 let rigid_body =
1046 RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height / 2f32, 0.0]);
1047 let floor_handle = bodies.insert(rigid_body);
1048 let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1049 colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1050
1051 let integration_parameters = IntegrationParameters::default();
1052
1053 let character_controller_snap = KinematicCharacterController {
1055 snap_to_ground: Some(CharacterLength::Relative(0.2)),
1056 ..Default::default()
1057 };
1058 let mut character_body_snap = RigidBodyBuilder::kinematic_position_based()
1059 .additional_mass(1.0)
1060 .build();
1061 character_body_snap.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1062 let character_handle_snap = bodies.insert(character_body_snap);
1063
1064 let collider = ColliderBuilder::ball(0.5).build();
1065 colliders.insert_with_parent(collider.clone(), character_handle_snap, &mut bodies);
1066
1067 let character_controller_no_snap = KinematicCharacterController {
1069 snap_to_ground: None,
1070 ..Default::default()
1071 };
1072 let mut character_body_no_snap = RigidBodyBuilder::kinematic_position_based()
1073 .additional_mass(1.0)
1074 .build();
1075 character_body_no_snap.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1076 let character_handle_no_snap = bodies.insert(character_body_no_snap);
1077
1078 let collider = ColliderBuilder::ball(0.5).build();
1079 let character_shape = collider.shape();
1080 colliders.insert_with_parent(collider.clone(), character_handle_no_snap, &mut bodies);
1081
1082 for i in 0..10000 {
1083 pipeline.step(
1085 &gravity,
1086 &integration_parameters,
1087 &mut islands,
1088 &mut bf,
1089 &mut nf,
1090 &mut bodies,
1091 &mut colliders,
1092 &mut impulse_joints,
1093 &mut multibody_joints,
1094 &mut CCDSolver::new(),
1095 &(),
1096 &(),
1097 );
1098
1099 let mut update_character_controller =
1100 |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1101 let character_body = bodies.get(handle).unwrap();
1102 let mut collisions = vec![];
1105 let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1106 let query_pipeline = bf.as_query_pipeline(
1107 nf.query_dispatcher(),
1108 &bodies,
1109 &colliders,
1110 filter_character_controller,
1111 );
1112 let effective_movement = controller.move_shape(
1113 integration_parameters.dt,
1114 &query_pipeline,
1115 character_shape,
1116 character_body.position(),
1117 Vector::new(0.1, -0.1, 0.1),
1118 |collision| collisions.push(collision),
1119 );
1120 let character_body = bodies.get_mut(handle).unwrap();
1121 let translation = character_body.translation();
1122 assert!(
1123 effective_movement.grounded,
1124 "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1125 i,
1126 translation + effective_movement.translation
1127 );
1128 character_body.set_next_kinematic_translation(
1129 translation + effective_movement.translation,
1130 );
1131 };
1132
1133 update_character_controller(character_controller_no_snap, character_handle_no_snap);
1134 update_character_controller(character_controller_snap, character_handle_snap);
1135 }
1136
1137 let character_body = bodies.get_mut(character_handle_no_snap).unwrap();
1138 let translation = character_body.translation();
1139
1140 assert!(
1143 translation.x >= 997.0,
1144 "actual translation.x:{}",
1145 translation.x
1146 );
1147 assert!(
1148 translation.z >= 997.0,
1149 "actual translation.z:{}",
1150 translation.z
1151 );
1152
1153 let character_body = bodies.get_mut(character_handle_snap).unwrap();
1154 let translation = character_body.translation();
1155 assert!(
1156 translation.x >= 997.0,
1157 "actual translation.x:{}",
1158 translation.x
1159 );
1160 assert!(
1161 translation.z >= 997.0,
1162 "actual translation.z:{}",
1163 translation.z
1164 );
1165 }
1166}