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