Skip to main content

rapier3d/control/
character_controller.rs

1use crate::alloc_prelude::*;
2use crate::geometry::{ColliderHandle, ContactManifold, Shape, ShapeCastHit};
3use crate::math::{Pose, Real, Vector};
4use crate::pipeline::{QueryFilterFlags, QueryPipeline, QueryPipelineMut};
5use crate::utils;
6use na::{RealField, Vector2};
7use parry::bounding_volume::BoundingVolume;
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)]
13/// A length measure used for various options of a character controller.
14pub enum CharacterLength {
15    /// The length is specified relative to some of the character shape’s size.
16    ///
17    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
18    /// for a shape with a height equal to 20.0 will result in a maximum step height
19    /// of `0.1 * 20.0 = 2.0`.
20    Relative(Real),
21    /// The length is specified as an absolute value, independent from the character shape’s size.
22    ///
23    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
24    /// for a shape with a height equal to 20.0 will result in a maximum step height
25    /// of `0.1` (the shape height is ignored in for this value).
26    Absolute(Real),
27}
28
29impl CharacterLength {
30    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Absolute`
31    /// variant.
32    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    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Relative`
41    /// variant.
42    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/// Configuration for the auto-stepping character controller feature.
66#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct CharacterAutostep {
69    /// The maximum step height a character can automatically step over.
70    pub max_height: CharacterLength,
71    /// The minimum width of free space that must be available after stepping on a stair.
72    pub min_width: CharacterLength,
73    /// Can the character automatically step over dynamic bodies too?
74    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,
90    horizontal_tangent: Vector,
91    vertical_tangent: Vector,
92    // NOTE: we don’t store the penetration part since we don’t really need it
93    //       for anything.
94}
95
96impl HitDecomposition {
97    pub fn unconstrained_slide_part(&self) -> Vector {
98        self.normal_part + self.horizontal_tangent + self.vertical_tangent
99    }
100}
101
102/// A collision between the character and its environment during its movement.
103#[derive(Copy, Clone, Debug)]
104pub struct CharacterCollision {
105    /// The collider hit by the character.
106    pub handle: ColliderHandle,
107    /// The position of the character when the collider was hit.
108    pub character_pos: Pose,
109    /// The translation that was already applied to the character when the hit happens.
110    pub translation_applied: Vector,
111    /// The translations that was still waiting to be applied to the character when the hit happens.
112    pub translation_remaining: Vector,
113    /// Geometric information about the hit.
114    pub hit: ShapeCastHit,
115}
116
117/// A kinematic character controller for player/NPC movement (walking, climbing, sliding).
118///
119/// This provides classic game character movement: walking on floors, sliding on slopes,
120/// climbing stairs, and snapping to ground. It's kinematic (not physics-based), meaning
121/// you control movement directly rather than applying forces.
122///
123/// **Not suitable for:** Ragdolls, vehicles, or physics-driven movement (use dynamic bodies instead).
124///
125/// # How it works
126///
127/// 1. You provide desired movement (e.g., "move forward 5 units")
128/// 2. Controller casts the character shape through the world
129/// 3. It handles collisions: sliding along walls, stepping up stairs, snapping to ground
130/// 4. Returns the final movement to apply
131///
132/// # Example
133///
134/// ```
135/// # use rapier3d::prelude::*;
136/// # use rapier3d::control::{CharacterAutostep, KinematicCharacterController};
137/// # let mut bodies = RigidBodySet::new();
138/// # let mut colliders = ColliderSet::new();
139/// # let broad_phase = BroadPhaseBvh::new();
140/// # let narrow_phase = NarrowPhase::new();
141/// # let dt = 1.0 / 60.0;
142/// # let speed = 5.0;
143/// # let (input_x, input_z) = (1.0, 0.0);
144/// # let character_shape = Ball::new(0.5);
145/// # let mut character_pos = Pose::IDENTITY;
146/// # let query_pipeline = broad_phase.as_query_pipeline(
147/// #     narrow_phase.query_dispatcher(),
148/// #     &bodies,
149/// #     &colliders,
150/// #     QueryFilter::default(),
151/// # );
152/// let controller = KinematicCharacterController {
153///     slide: true,  // Slide along walls instead of stopping
154///     autostep: Some(CharacterAutostep::default()),  // Auto-climb stairs
155///     max_slope_climb_angle: 45.0_f32.to_radians(),  // Max climbable slope
156///     ..Default::default()
157/// };
158///
159/// // In your game loop:
160/// let desired_movement = Vector::new(input_x, 0.0, input_z) * speed * dt;
161/// let movement = controller.move_shape(
162///     dt,
163///     &query_pipeline,
164///     &character_shape,
165///     &character_pos,
166///     desired_movement,
167///     |_| {}  // Collision event callback
168/// );
169/// character_pos.translation += movement.translation;
170/// ```
171#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
172#[derive(Copy, Clone, Debug)]
173pub struct KinematicCharacterController {
174    /// The direction that goes "up". Used to determine where the floor is, and the floor’s angle.
175    pub up: Vector,
176    /// A small gap to preserve between the character and its surroundings.
177    ///
178    /// This value should not be too large to avoid visual artifacts, but shouldn’t be too small
179    /// (must not be zero) to improve numerical stability of the character controller.
180    pub offset: CharacterLength,
181    /// Should the character try to slide against the floor if it hits it?
182    pub slide: bool,
183    /// Should the character automatically step over small obstacles? (disabled by default)
184    ///
185    /// Note that autostepping is currently a very computationally expensive feature, so it
186    /// is disabled by default.
187    pub autostep: Option<CharacterAutostep>,
188    /// The maximum angle (radians) between the floor’s normal and the `up` vector that the
189    /// character is able to climb.
190    pub max_slope_climb_angle: Real,
191    /// The minimum angle (radians) between the floor’s normal and the `up` vector before the
192    /// character starts to slide down automatically.
193    pub min_slope_slide_angle: Real,
194    /// Should the character be automatically snapped to the ground if the distance between
195    /// the ground and its feed are smaller than the specified threshold?
196    pub snap_to_ground: Option<CharacterLength>,
197    /// Increase this number if your character appears to get stuck when sliding against surfaces.
198    ///
199    /// This is a small distance applied to the movement toward the contact normals of shapes hit
200    /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating
201    /// state during the sliding calculation.
202    ///
203    /// This value should remain fairly small since it can introduce artificial "bumps" when sliding
204    /// along a flat surface.
205    pub normal_nudge_factor: Real,
206}
207
208impl Default for KinematicCharacterController {
209    fn default() -> Self {
210        Self {
211            up: Vector::Y,
212            offset: CharacterLength::Relative(0.01),
213            slide: true,
214            autostep: None,
215            max_slope_climb_angle: Real::frac_pi_4(),
216            min_slope_slide_angle: Real::frac_pi_4(),
217            snap_to_ground: Some(CharacterLength::Relative(0.2)),
218            normal_nudge_factor: 1.0e-4,
219        }
220    }
221}
222
223/// The effective movement computed by the character controller.
224#[derive(Debug)]
225pub struct EffectiveCharacterMovement {
226    /// The movement to apply.
227    pub translation: Vector,
228    /// Is the character touching the ground after applying `EffectiveKineamticMovement::translation`?
229    pub grounded: bool,
230    /// Is the character sliding down a slope due to slope angle being larger than `min_slope_slide_angle`?
231    pub is_sliding_down_slope: bool,
232}
233
234impl KinematicCharacterController {
235    /// Pushes the character out of any non-sensor collider it is overlapping with, which is
236    /// what stops an obstacle pushed into a non-moving character from tunneling through it
237    /// ([`Self::move_shape`]'s shape-casts never run on a zero desired translation).
238    ///
239    /// The total correction per call is capped relative to the character’s height.
240    fn check_and_fix_penetrations(
241        &self,
242        queries: &QueryPipeline,
243        character_shape: &dyn Shape,
244        character_pos: &Pose,
245        dims: Vector2<Real>,
246        result: &mut EffectiveCharacterMovement,
247    ) {
248        let offset = self.offset.eval(dims.y);
249        let max_correction = dims.y * 0.25;
250        let mut applied = 0.0;
251
252        // Run a few passes so that getting pushed out of one collider doesn’t leave the
253        // character stuck inside another one.
254        for _ in 0..4 {
255            let character_aabb = character_shape
256                .compute_aabb(&(Pose::from_translation(result.translation) * *character_pos))
257                .loosened(offset);
258            let mut corrected = false;
259
260            for (_, collider) in queries.intersect_aabb_conservative(character_aabb) {
261                if collider.is_sensor() {
262                    continue;
263                }
264
265                let character_pos = Pose::from_translation(result.translation) * *character_pos;
266                let pos12 = character_pos.inv_mul(collider.position());
267
268                if let Ok(Some(contact)) =
269                    queries
270                        .dispatcher
271                        .contact(&pos12, character_shape, collider.shape(), 0.0)
272                {
273                    if contact.dist < -1.0e-5 {
274                        // Push out until the usual `offset` gap is restored.
275                        let push = (offset - contact.dist).min(max_correction - applied);
276                        if push <= 0.0 {
277                            return; // The per-call correction budget is exhausted.
278                        }
279
280                        // `normal1` (expressed in the character’s local frame) points towards
281                        // the obstacle: move backwards along it to resolve the overlap.
282                        result.translation -= (character_pos.rotation * contact.normal1) * push;
283                        applied += push;
284                        corrected = true;
285                    }
286                }
287            }
288
289            if !corrected {
290                break;
291            }
292        }
293    }
294
295    /// Computes the possible movement for a shape.
296    #[profiling::function]
297    pub fn move_shape(
298        &self,
299        dt: Real,
300        queries: &QueryPipeline,
301        character_shape: &dyn Shape,
302        character_pos: &Pose,
303        desired_translation: Vector,
304        mut events: impl FnMut(CharacterCollision),
305    ) -> EffectiveCharacterMovement {
306        let mut result = EffectiveCharacterMovement {
307            translation: Vector::ZERO,
308            grounded: false,
309            is_sliding_down_slope: false,
310        };
311        let dims = self.compute_dims(character_shape);
312
313        // 1. Depenetrate, but only when there is no desired movement: the shape-casting
314        //    loop below never runs then, so obstacles pushed into the character would
315        //    tunnel through it (#485). When it moves, the shape-casts handle them instead.
316        if utils::try_normalize_and_get_length(desired_translation, 1.0e-5).is_none() {
317            self.check_and_fix_penetrations(
318                queries,
319                character_shape,
320                character_pos,
321                dims,
322                &mut result,
323            );
324        }
325
326        let mut translation_remaining = desired_translation;
327
328        let grounded_at_starting_pos = self.detect_grounded_status_and_apply_friction(
329            dt,
330            queries,
331            character_shape,
332            &(Pose::from_translation(result.translation) * *character_pos),
333            dims,
334            None,
335            None,
336        );
337
338        let mut max_iters = 20;
339        let mut kinematic_friction_translation = Vector::ZERO;
340        let offset = self.offset.eval(dims.y);
341        let mut is_moving = false;
342
343        while let Some((translation_dir, translation_dist)) =
344            utils::try_normalize_and_get_length(translation_remaining, 1.0e-5)
345        {
346            if max_iters == 0 {
347                break;
348            } else {
349                max_iters -= 1;
350            }
351            is_moving = true;
352
353            // 2. Cast towards the movement direction.
354            if let Some((handle, hit)) = queries.cast_shape(
355                &(Pose::from_translation(result.translation) * *character_pos),
356                translation_dir,
357                character_shape,
358                ShapeCastOptions {
359                    target_distance: offset,
360                    stop_at_penetration: false,
361                    max_time_of_impact: translation_dist,
362                    compute_impact_geometry_on_penetration: true,
363                },
364            ) {
365                // We hit something, compute and apply the allowed interference-free translation.
366                let allowed_dist = hit.time_of_impact;
367                let allowed_translation = translation_dir * allowed_dist;
368                result.translation += allowed_translation;
369                translation_remaining -= allowed_translation;
370
371                events(CharacterCollision {
372                    handle,
373                    character_pos: Pose::from_translation(result.translation) * *character_pos,
374                    translation_applied: result.translation,
375                    translation_remaining,
376                    hit,
377                });
378
379                let hit_info = self.compute_hit_info(hit);
380
381                // Try to go upstairs.
382                if !self.handle_stairs(
383                    *queries,
384                    character_shape,
385                    &(Pose::from_translation(result.translation) * *character_pos),
386                    dims,
387                    handle,
388                    &hit_info,
389                    &mut translation_remaining,
390                    &mut result,
391                ) {
392                    // No stairs, try to move along slopes.
393                    translation_remaining = self.handle_slopes(
394                        &hit_info,
395                        desired_translation,
396                        translation_remaining,
397                        self.normal_nudge_factor,
398                        &mut result,
399                    );
400                }
401            } else {
402                // No interference along the path.
403                result.translation += translation_remaining;
404                result.grounded = self.detect_grounded_status_and_apply_friction(
405                    dt,
406                    queries,
407                    character_shape,
408                    &(Pose::from_translation(result.translation) * *character_pos),
409                    dims,
410                    None,
411                    None,
412                );
413                break;
414            }
415            result.grounded = self.detect_grounded_status_and_apply_friction(
416                dt,
417                queries,
418                character_shape,
419                &(Pose::from_translation(result.translation) * *character_pos),
420                dims,
421                Some(&mut kinematic_friction_translation),
422                Some(&mut translation_remaining),
423            );
424
425            if !self.slide {
426                break;
427            }
428        }
429        // When not moving, `detect_grounded_status_and_apply_friction` is not reached
430        // so we call it explicitly here.
431        if !is_moving {
432            result.grounded = self.detect_grounded_status_and_apply_friction(
433                dt,
434                queries,
435                character_shape,
436                &(Pose::from_translation(result.translation) * *character_pos),
437                dims,
438                None,
439                None,
440            );
441        }
442        // If needed, and if we are not already grounded, snap to the ground.
443        if grounded_at_starting_pos {
444            self.snap_to_ground(
445                queries,
446                character_shape,
447                &(Pose::from_translation(result.translation) * *character_pos),
448                dims,
449                &mut result,
450            );
451        }
452
453        // Return the result.
454        result
455    }
456
457    fn snap_to_ground(
458        &self,
459        queries: &QueryPipeline,
460        character_shape: &dyn Shape,
461        character_pos: &Pose,
462        dims: Vector2<Real>,
463        result: &mut EffectiveCharacterMovement,
464    ) -> Option<(ColliderHandle, ShapeCastHit)> {
465        if let Some(snap_distance) = self.snap_to_ground {
466            if result.translation.dot(self.up) <= 0.0 {
467                let snap_distance = snap_distance.eval(dims.y);
468                let offset = self.offset.eval(dims.y);
469                if let Some((hit_handle, hit)) = queries.cast_shape(
470                    character_pos,
471                    -self.up,
472                    character_shape,
473                    ShapeCastOptions {
474                        target_distance: offset,
475                        stop_at_penetration: false,
476                        max_time_of_impact: snap_distance,
477                        compute_impact_geometry_on_penetration: true,
478                    },
479                ) {
480                    // Apply the snap.
481                    result.translation -= self.up * hit.time_of_impact;
482                    result.grounded = true;
483                    return Some((hit_handle, hit));
484                }
485            }
486        }
487
488        None
489    }
490
491    fn predict_ground(&self, up_extends: Real) -> Real {
492        self.offset.eval(up_extends) + 0.05
493    }
494
495    #[profiling::function]
496    fn detect_grounded_status_and_apply_friction(
497        &self,
498        dt: Real,
499        queries: &QueryPipeline,
500        character_shape: &dyn Shape,
501        character_pos: &Pose,
502        dims: Vector2<Real>,
503        mut kinematic_friction_translation: Option<&mut Vector>,
504        mut translation_remaining: Option<&mut Vector>,
505    ) -> bool {
506        let prediction = self.predict_ground(dims.y);
507
508        // TODO: allow custom dispatchers.
509        let dispatcher = DefaultQueryDispatcher;
510
511        let mut manifolds: Vec<ContactManifold> = vec![];
512        let character_aabb = character_shape
513            .compute_aabb(character_pos)
514            .loosened(prediction);
515
516        let mut grounded = false;
517
518        'outer: for (_, collider) in queries.intersect_aabb_conservative(character_aabb) {
519            manifolds.clear();
520            let pos12 = character_pos.inv_mul(collider.position());
521            let _ = dispatcher.contact_manifolds(
522                &pos12,
523                character_shape,
524                collider.shape(),
525                prediction,
526                &mut manifolds,
527                &mut None,
528            );
529
530            if let (Some(kinematic_friction_translation), Some(translation_remaining)) = (
531                kinematic_friction_translation.as_deref_mut(),
532                translation_remaining.as_deref_mut(),
533            ) {
534                let init_kinematic_friction_translation = *kinematic_friction_translation;
535                let kinematic_parent = collider
536                    .parent
537                    .and_then(|p| queries.bodies.get(p.handle))
538                    .filter(|rb| rb.is_kinematic());
539
540                for m in &manifolds {
541                    if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
542                        grounded = true;
543                    }
544
545                    if let Some(kinematic_parent) = kinematic_parent {
546                        let mut num_active_contacts = 0;
547                        let mut manifold_center = Vector::ZERO;
548                        let normal = -(character_pos.rotation * m.local_n1);
549
550                        for contact in &m.points {
551                            if contact.dist <= prediction {
552                                num_active_contacts += 1;
553                                let contact_point = collider.position() * contact.local_p2;
554                                let target_vel = kinematic_parent.velocity_at_point(contact_point);
555
556                                let normal_target_mvt = target_vel.dot(normal) * dt;
557                                let normal_current_mvt = translation_remaining.dot(normal);
558
559                                manifold_center += contact_point;
560                                *translation_remaining +=
561                                    normal * (normal_target_mvt - normal_current_mvt);
562                            }
563                        }
564
565                        if num_active_contacts > 0 {
566                            let target_vel = kinematic_parent
567                                .velocity_at_point(manifold_center / num_active_contacts as Real);
568                            let tangent_platform_mvt =
569                                (target_vel - normal * target_vel.dot(normal)) * dt;
570                            // Apply larger-absolute-value-wins component-wise
571                            if tangent_platform_mvt.x.abs() > kinematic_friction_translation.x.abs()
572                            {
573                                kinematic_friction_translation.x = tangent_platform_mvt.x;
574                            }
575                            if tangent_platform_mvt.y.abs() > kinematic_friction_translation.y.abs()
576                            {
577                                kinematic_friction_translation.y = tangent_platform_mvt.y;
578                            }
579                            #[cfg(feature = "dim3")]
580                            if tangent_platform_mvt.z.abs() > kinematic_friction_translation.z.abs()
581                            {
582                                kinematic_friction_translation.z = tangent_platform_mvt.z;
583                            }
584                        }
585                    }
586                }
587
588                *translation_remaining +=
589                    *kinematic_friction_translation - init_kinematic_friction_translation;
590            } else {
591                for m in &manifolds {
592                    if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
593                        grounded = true;
594                        break 'outer; // We can stop the search early.
595                    }
596                }
597            }
598        }
599        grounded
600    }
601
602    fn is_grounded_at_contact_manifold(
603        &self,
604        manifold: &ContactManifold,
605        character_pos: &Pose,
606        dims: Vector2<Real>,
607    ) -> bool {
608        let normal = -(character_pos.rotation * manifold.local_n1);
609
610        // For the controller to be grounded, the angle between the contact normal and the up vector
611        // has to be smaller than acos(1.0e-3) = 89.94 degrees.
612        if normal.dot(self.up) >= 1.0e-3 {
613            let prediction = self.predict_ground(dims.y);
614            for contact in &manifold.points {
615                if contact.dist <= prediction {
616                    return true;
617                }
618            }
619        }
620        false
621    }
622
623    fn handle_slopes(
624        &self,
625        hit: &HitInfo,
626        movement_input: Vector,
627        translation_remaining: Vector,
628        normal_nudge_factor: Real,
629        result: &mut EffectiveCharacterMovement,
630    ) -> Vector {
631        let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
632        let horiz_input_decomp = self.decompose_hit(horizontal_input, &hit.toi);
633        let decomp = self.decompose_hit(translation_remaining, &hit.toi);
634
635        // An object is trying to slip if the tangential movement induced by its vertical movement
636        // points downward.
637        let slipping_intent = self.up.dot(horiz_input_decomp.vertical_tangent) < 0.0;
638        // An object is slipping if its vertical movement points downward.
639        let slipping = self.up.dot(decomp.vertical_tangent) < 0.0;
640
641        // An object is trying to climb if its vertical input motion points upward.
642        let climbing_intent = self.up.dot(_vertical_input) > 0.0;
643        // An object is climbing if the tangential movement induced by its vertical movement points upward.
644        let climbing = self.up.dot(decomp.vertical_tangent) > 0.0;
645
646        let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
647            // Can’t climb the slope, remove the vertical tangent motion induced by the forward motion.
648            decomp.horizontal_tangent + decomp.normal_part
649        } else if hit.is_nonslip_slope && slipping && !slipping_intent {
650            // Prevent the vertical movement from sliding down.
651            decomp.horizontal_tangent + decomp.normal_part
652        } else {
653            // Let it slide (including climbing the slope).
654            result.is_sliding_down_slope = true;
655            decomp.unconstrained_slide_part()
656        };
657
658        allowed_movement + hit.toi.normal1 * normal_nudge_factor
659    }
660
661    fn split_into_components(&self, translation: Vector) -> [Vector; 2] {
662        let vertical_translation = self.up * (self.up.dot(translation));
663        let horizontal_translation = translation - vertical_translation;
664        [vertical_translation, horizontal_translation]
665    }
666
667    fn compute_hit_info(&self, toi: ShapeCastHit) -> HitInfo {
668        #[cfg(feature = "dim2")]
669        let angle_with_floor = self.up.angle_to(toi.normal1);
670        #[cfg(feature = "dim3")]
671        let angle_with_floor = self.up.angle_between(toi.normal1);
672        let is_ceiling = self.up.dot(toi.normal1) < 0.0;
673        let is_wall = angle_with_floor >= self.max_slope_climb_angle && !is_ceiling;
674        let is_nonslip_slope = angle_with_floor <= self.min_slope_slide_angle;
675
676        HitInfo {
677            toi,
678            is_wall,
679            is_nonslip_slope,
680        }
681    }
682
683    fn decompose_hit(&self, translation: Vector, hit: &ShapeCastHit) -> HitDecomposition {
684        let dist_to_surface = translation.dot(hit.normal1);
685        let normal_part;
686        let penetration_part;
687
688        if dist_to_surface < 0.0 {
689            normal_part = Vector::ZERO;
690            penetration_part = dist_to_surface * hit.normal1;
691        } else {
692            penetration_part = Vector::ZERO;
693            normal_part = dist_to_surface * hit.normal1;
694        }
695
696        let tangent = translation - normal_part - penetration_part;
697        #[cfg(feature = "dim3")]
698        let horizontal_tangent_dir = hit.normal1.cross(self.up);
699        #[cfg(feature = "dim2")]
700        let horizontal_tangent_dir = Vector::ZERO;
701
702        let horizontal_tangent_dir = horizontal_tangent_dir.try_normalize().unwrap_or_default();
703        let horizontal_tangent = tangent.dot(horizontal_tangent_dir) * horizontal_tangent_dir;
704        let vertical_tangent = tangent - horizontal_tangent;
705
706        HitDecomposition {
707            normal_part,
708            horizontal_tangent,
709            vertical_tangent,
710        }
711    }
712
713    fn compute_dims(&self, character_shape: &dyn Shape) -> Vector2<Real> {
714        let extents = character_shape.compute_local_aabb().extents();
715        let up_extent = extents.dot(self.up.abs());
716        let side_extent = (extents - (self.up).abs() * up_extent).length();
717        Vector2::new(side_extent, up_extent)
718    }
719
720    #[profiling::function]
721    fn handle_stairs(
722        &self,
723        mut queries: QueryPipeline,
724        character_shape: &dyn Shape,
725        character_pos: &Pose,
726        dims: Vector2<Real>,
727        stair_handle: ColliderHandle,
728        hit: &HitInfo,
729        translation_remaining: &mut Vector,
730        result: &mut EffectiveCharacterMovement,
731    ) -> bool {
732        let Some(autostep) = self.autostep else {
733            return false;
734        };
735
736        // Only try to autostep on walls.
737        if !hit.is_wall {
738            return false;
739        }
740
741        let offset = self.offset.eval(dims.y);
742        let min_width = autostep.min_width.eval(dims.x) + offset;
743        let max_height = autostep.max_height.eval(dims.y) + offset;
744
745        if !autostep.include_dynamic_bodies {
746            if queries
747                .colliders
748                .get(stair_handle)
749                .and_then(|co| co.parent)
750                .and_then(|p| queries.bodies.get(p.handle))
751                .map(|b| b.is_dynamic())
752                == Some(true)
753            {
754                // The "stair" is a dynamic body, which the user wants to ignore.
755                return false;
756            }
757
758            queries.filter.flags |= QueryFilterFlags::EXCLUDE_DYNAMIC;
759        }
760
761        let shifted_character_pos = Pose::from_parts(
762            character_pos.translation + self.up * max_height,
763            character_pos.rotation,
764        );
765
766        let Some(horizontal_dir) =
767            (*translation_remaining - self.up * translation_remaining.dot(self.up)).try_normalize()
768        else {
769            return false;
770        };
771
772        if queries
773            .cast_shape(
774                character_pos,
775                self.up,
776                character_shape,
777                ShapeCastOptions {
778                    target_distance: offset,
779                    stop_at_penetration: false,
780                    max_time_of_impact: max_height,
781                    compute_impact_geometry_on_penetration: true,
782                },
783            )
784            .is_some()
785        {
786            // We can’t go up.
787            return false;
788        }
789
790        if queries
791            .cast_shape(
792                &shifted_character_pos,
793                horizontal_dir,
794                character_shape,
795                ShapeCastOptions {
796                    target_distance: offset,
797                    stop_at_penetration: false,
798                    max_time_of_impact: min_width,
799                    compute_impact_geometry_on_penetration: true,
800                },
801            )
802            .is_some()
803        {
804            // We don’t have enough room on the stair to stay on it.
805            return false;
806        }
807
808        // Check that we are not getting into a ramp that is too steep
809        // after stepping.
810        if let Some((_, hit)) = queries.cast_shape(
811            &Pose::from_parts(
812                shifted_character_pos.translation + horizontal_dir * min_width,
813                shifted_character_pos.rotation,
814            ),
815            -self.up,
816            character_shape,
817            ShapeCastOptions {
818                target_distance: offset,
819                stop_at_penetration: false,
820                max_time_of_impact: max_height,
821                compute_impact_geometry_on_penetration: true,
822            },
823        ) {
824            let [vertical_slope_translation, horizontal_slope_translation] = self
825                .split_into_components(*translation_remaining)
826                .map(|remaining| subtract_hit(remaining, &hit));
827
828            let slope_translation = horizontal_slope_translation + vertical_slope_translation;
829
830            #[cfg(feature = "dim2")]
831            let angle_with_floor = self.up.angle_to(hit.normal1);
832            #[cfg(feature = "dim3")]
833            let angle_with_floor = self.up.angle_between(hit.normal1);
834            let climbing = self.up.dot(slope_translation) >= 0.0;
835
836            if climbing && angle_with_floor > self.max_slope_climb_angle {
837                return false; // The target ramp is too steep.
838            }
839        }
840
841        // We can step, we need to find the actual step height.
842        let step_height = max_height
843            - queries
844                .cast_shape(
845                    &Pose::from_parts(
846                        shifted_character_pos.translation + horizontal_dir * min_width,
847                        shifted_character_pos.rotation,
848                    ),
849                    -self.up,
850                    character_shape,
851                    ShapeCastOptions {
852                        target_distance: offset,
853                        stop_at_penetration: false,
854                        max_time_of_impact: max_height,
855                        compute_impact_geometry_on_penetration: true,
856                    },
857                )
858                .map(|hit| hit.1.time_of_impact)
859                .unwrap_or(max_height);
860
861        // Remove the step height from the vertical part of the self.
862        let step = self.up * step_height;
863        *translation_remaining -= step;
864
865        // Advance the collider on the step horizontally, to make sure further
866        // movement won’t just get stuck on its edge.
867        let horizontal_nudge =
868            horizontal_dir * horizontal_dir.dot(*translation_remaining).min(min_width);
869        *translation_remaining -= horizontal_nudge;
870
871        result.translation += step + horizontal_nudge;
872        true
873    }
874
875    /// For the given collisions between a character and its environment, this method will apply
876    /// impulses to the rigid-bodies surrounding the character shape at the time of the collisions.
877    /// Note that the impulse calculation is only approximate as it is not based on a global
878    /// constraints resolution scheme.
879    #[profiling::function]
880    pub fn solve_character_collision_impulses<'a>(
881        &self,
882        dt: Real,
883        queries: &mut QueryPipelineMut,
884        character_shape: &dyn Shape,
885        character_mass: Real,
886        collisions: impl IntoIterator<Item = &'a CharacterCollision>,
887    ) {
888        for collision in collisions {
889            self.solve_single_character_collision_impulse(
890                dt,
891                queries,
892                character_shape,
893                character_mass,
894                collision,
895            );
896        }
897    }
898
899    /// For the given collision between a character and its environment, this method will apply
900    /// impulses to the rigid-bodies surrounding the character shape at the time of the collision.
901    /// Note that the impulse calculation is only approximate as it is not based on a global
902    /// constraints resolution scheme.
903    #[profiling::function]
904    fn solve_single_character_collision_impulse(
905        &self,
906        dt: Real,
907        queries: &mut QueryPipelineMut,
908        character_shape: &dyn Shape,
909        character_mass: Real,
910        collision: &CharacterCollision,
911    ) {
912        let extents = character_shape.compute_local_aabb().extents();
913        let up_extent = extents.dot(self.up.abs());
914        let movement_to_transfer =
915            collision.hit.normal1 * collision.translation_remaining.dot(collision.hit.normal1);
916        let prediction = self.predict_ground(up_extent);
917
918        // TODO: allow custom dispatchers.
919        let dispatcher = DefaultQueryDispatcher;
920
921        let mut manifolds: Vec<ContactManifold> = vec![];
922        // World pose of the collider each manifold was computed against: the `local_p2`
923        // points are in the collider’s frame, which differs from its body’s when offset.
924        let mut manifold_collider_poses: Vec<Pose> = vec![];
925        // output vec for contact manifolds returned by parry
926        let mut pair_manifolds: Vec<ContactManifold> = vec![];
927        let character_aabb = character_shape
928            .compute_aabb(&collision.character_pos)
929            .loosened(prediction);
930
931        for (_, collider) in queries.as_ref().intersect_aabb_conservative(character_aabb) {
932            if let Some(parent) = collider.parent {
933                if let Some(body) = queries.bodies.get(parent.handle) {
934                    if body.is_dynamic() {
935                        let pos12 = collision.character_pos.inv_mul(collider.position());
936                        pair_manifolds.clear();
937                        let _ = dispatcher.contact_manifolds(
938                            &pos12,
939                            character_shape,
940                            collider.shape(),
941                            prediction,
942                            &mut pair_manifolds,
943                            &mut None,
944                        );
945
946                        for mut m in pair_manifolds.drain(..) {
947                            m.data.rigid_body2 = Some(parent.handle);
948                            m.data.normal = collision.character_pos.rotation * m.local_n1;
949                            manifolds.push(m);
950                            manifold_collider_poses.push(*collider.position());
951                        }
952                    }
953                }
954            }
955        }
956
957        let velocity_to_transfer = movement_to_transfer * utils::inv(dt);
958
959        for (manifold, collider_pos) in manifolds.iter().zip(manifold_collider_poses.iter()) {
960            let body_handle = manifold.data.rigid_body2.unwrap();
961            let body = &mut queries.bodies[body_handle];
962
963            for pt in &manifold.points {
964                if pt.dist <= prediction {
965                    let body_mass = body.mass();
966                    let contact_point = collider_pos * pt.local_p2;
967                    let delta_vel_per_contact = (velocity_to_transfer
968                        - body.velocity_at_point(contact_point))
969                    .dot(manifold.data.normal);
970                    let mass_ratio = body_mass * character_mass / (body_mass + character_mass);
971
972                    body.apply_impulse_at_point(
973                        manifold.data.normal * delta_vel_per_contact.max(0.0) * mass_ratio,
974                        contact_point,
975                        true,
976                    );
977                }
978            }
979        }
980    }
981}
982
983fn subtract_hit(translation: Vector, hit: &ShapeCastHit) -> Vector {
984    let surface_correction = (-translation).dot(hit.normal1).max(0.0);
985    // This fixes some instances of moving through walls
986    let surface_correction = surface_correction * (1.0 + 1.0e-5);
987    translation + hit.normal1 * surface_correction
988}
989
990#[cfg(all(feature = "dim3", feature = "f32"))]
991#[cfg(test)]
992mod test {
993    #[allow(unused_imports)]
994    use crate::alloc_prelude::*;
995    use crate::{
996        control::{CharacterLength, KinematicCharacterController},
997        prelude::*,
998    };
999    use std::dbg;
1000
1001    #[test]
1002    fn character_controller_climb_test() {
1003        let mut colliders = ColliderSet::new();
1004        let mut impulse_joints = ImpulseJointSet::new();
1005        let mut multibody_joints = MultibodyJointSet::new();
1006        let mut soft_bodies = SoftBodySet::new();
1007        let mut pipeline = PhysicsPipeline::new();
1008        let mut bf = BroadPhaseBvh::new();
1009        let mut nf = NarrowPhase::new();
1010        let mut islands = IslandManager::new();
1011
1012        let mut bodies = RigidBodySet::new();
1013
1014        let gravity = Vector::Y * -9.81;
1015
1016        let ground_size = 100.0;
1017        let ground_height = 0.1;
1018        /*
1019         * Create a flat ground
1020         */
1021        let rigid_body =
1022            RigidBodyBuilder::fixed().translation(Vector::new(0.0, -ground_height, 0.0));
1023        let floor_handle = bodies.insert(rigid_body);
1024        let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1025        colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1026
1027        /*
1028         * Create a slope we can climb.
1029         */
1030        let slope_angle = 0.2;
1031        let slope_size = 2.0;
1032        let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
1033            .translation(Vector::new(0.1 + slope_size, -ground_height + 0.4, 0.0))
1034            .rotation(Vector::Z * slope_angle);
1035        colliders.insert(collider);
1036
1037        /*
1038         * Create a slope we can't climb.
1039         */
1040        let impossible_slope_angle = 0.6;
1041        let impossible_slope_size = 2.0;
1042        // The slope is long enough that the character never reaches its crest within the
1043        // iteration budget: wrapping around the crest is a knife-edge configuration and a
1044        // known corner-robustness limitation (#809), not what this test is about.
1045        let collider = ColliderBuilder::cuboid(slope_size * 2.0, ground_height, ground_size)
1046            .translation(Vector::new(
1047                0.1 + slope_size * 2.0 + impossible_slope_size - 0.9 + slope_size,
1048                -ground_height + 1.7 + slope_size * impossible_slope_angle.sin(),
1049                0.0,
1050            ))
1051            .rotation(Vector::Z * impossible_slope_angle);
1052        colliders.insert(collider);
1053
1054        let integration_parameters = IntegrationParameters::default();
1055
1056        // Initialize character which can climb
1057        let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
1058            .additional_mass(1.0)
1059            .build();
1060        character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1061        let character_handle_can_climb = bodies.insert(character_body_can_climb);
1062
1063        let collider = ColliderBuilder::ball(0.5).build();
1064        colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
1065
1066        // Initialize character which cannot climb
1067        let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
1068            .additional_mass(1.0)
1069            .build();
1070        character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1071        let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
1072
1073        let collider = ColliderBuilder::ball(0.5).build();
1074        let character_shape = collider.shape();
1075        colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
1076
1077        let mut airborne_can_climb = (0u32, 0u32);
1078        let mut airborne_cannot_climb = (0u32, 0u32);
1079        for i in 0..200 {
1080            // Step once
1081            pipeline.step(
1082                gravity,
1083                &integration_parameters,
1084                &mut islands,
1085                &mut bf,
1086                &mut nf,
1087                &mut bodies,
1088                &mut colliders,
1089                &mut impulse_joints,
1090                &mut multibody_joints,
1091                &mut soft_bodies,
1092                &mut CCDSolver::new(),
1093                &(),
1094                &(),
1095            );
1096
1097            let mut update_character_controller =
1098                |controller: KinematicCharacterController,
1099                 handle: RigidBodyHandle,
1100                 airborne: &mut (u32, u32)| {
1101                    let character_body = bodies.get(handle).unwrap();
1102                    // Use a closure to handle or collect the collisions while
1103                    // the character is being moved.
1104                    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.0),
1118                        |collision| collisions.push(collision),
1119                    );
1120                    let character_body = bodies.get_mut(handle).unwrap();
1121                    let translation = character_body.translation();
1122                    // Grounded-ness must hold except for rare isolated frames: at slope
1123                    // transitions the contact sits right on the detection threshold, so a
1124                    // 1-ulp change flips one frame. Consecutive ones are a real regression.
1125                    let (total_airborne, consecutive_airborne) = airborne;
1126                    if effective_movement.grounded {
1127                        *consecutive_airborne = 0;
1128                    } else {
1129                        *total_airborne += 1;
1130                        *consecutive_airborne += 1;
1131                        assert!(
1132                            *consecutive_airborne <= 1 && *total_airborne <= 3,
1133                            "movement should be grounded at all times (up to isolated \
1134                             threshold flickers) for current setup (iter: {}), pos: {}.",
1135                            i,
1136                            translation + effective_movement.translation
1137                        );
1138                    }
1139                    character_body.set_next_kinematic_translation(
1140                        translation + effective_movement.translation,
1141                    );
1142                };
1143
1144            let character_controller_cannot_climb = KinematicCharacterController {
1145                max_slope_climb_angle: impossible_slope_angle - 0.001,
1146                ..Default::default()
1147            };
1148            let character_controller_can_climb = KinematicCharacterController {
1149                max_slope_climb_angle: impossible_slope_angle + 0.001,
1150                ..Default::default()
1151            };
1152            update_character_controller(
1153                character_controller_cannot_climb,
1154                character_handle_cannot_climb,
1155                &mut airborne_cannot_climb,
1156            );
1157            update_character_controller(
1158                character_controller_can_climb,
1159                character_handle_can_climb,
1160                &mut airborne_can_climb,
1161            );
1162        }
1163        let character_body = bodies.get(character_handle_can_climb).unwrap();
1164        assert!(character_body.translation().x > 6.0);
1165        assert!(character_body.translation().y > 3.0);
1166        let character_body = bodies.get(character_handle_cannot_climb).unwrap();
1167        // The blocked character rests against the (extended) steep slope's base.
1168        assert!(character_body.translation().x < 4.5);
1169        assert!(dbg!(character_body.translation().y) < 2.0);
1170    }
1171
1172    #[test]
1173    fn character_controller_ground_detection() {
1174        let mut colliders = ColliderSet::new();
1175        let mut impulse_joints = ImpulseJointSet::new();
1176        let mut multibody_joints = MultibodyJointSet::new();
1177        let mut soft_bodies = SoftBodySet::new();
1178        let mut pipeline = PhysicsPipeline::new();
1179        let mut bf = BroadPhaseBvh::new();
1180        let mut nf = NarrowPhase::new();
1181        let mut islands = IslandManager::new();
1182
1183        let mut bodies = RigidBodySet::new();
1184
1185        let gravity = Vector::Y * -9.81;
1186
1187        let ground_size = 1001.0;
1188        let ground_height = 1.0;
1189        /*
1190         * Create a flat ground
1191         */
1192        let rigid_body =
1193            RigidBodyBuilder::fixed().translation(Vector::new(0.0, -ground_height / 2.0, 0.0));
1194        let floor_handle = bodies.insert(rigid_body);
1195        let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1196        colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1197
1198        let integration_parameters = IntegrationParameters::default();
1199
1200        // Initialize character with snap to ground
1201        let character_controller_snap = KinematicCharacterController {
1202            snap_to_ground: Some(CharacterLength::Relative(0.2)),
1203            ..Default::default()
1204        };
1205        let mut character_body_snap = RigidBodyBuilder::kinematic_position_based()
1206            .additional_mass(1.0)
1207            .build();
1208        character_body_snap.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1209        let character_handle_snap = bodies.insert(character_body_snap);
1210
1211        let collider = ColliderBuilder::ball(0.5).build();
1212        colliders.insert_with_parent(collider.clone(), character_handle_snap, &mut bodies);
1213
1214        // Initialize character without snap to ground
1215        let character_controller_no_snap = KinematicCharacterController {
1216            snap_to_ground: None,
1217            ..Default::default()
1218        };
1219        let mut character_body_no_snap = RigidBodyBuilder::kinematic_position_based()
1220            .additional_mass(1.0)
1221            .build();
1222        character_body_no_snap.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1223        let character_handle_no_snap = bodies.insert(character_body_no_snap);
1224
1225        let collider = ColliderBuilder::ball(0.5).build();
1226        let character_shape = collider.shape();
1227        colliders.insert_with_parent(collider.clone(), character_handle_no_snap, &mut bodies);
1228
1229        for i in 0..10000 {
1230            // Step once
1231            pipeline.step(
1232                gravity,
1233                &integration_parameters,
1234                &mut islands,
1235                &mut bf,
1236                &mut nf,
1237                &mut bodies,
1238                &mut colliders,
1239                &mut impulse_joints,
1240                &mut multibody_joints,
1241                &mut soft_bodies,
1242                &mut CCDSolver::new(),
1243                &(),
1244                &(),
1245            );
1246
1247            let mut update_character_controller =
1248                |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1249                    let character_body = bodies.get(handle).unwrap();
1250                    // Use a closure to handle or collect the collisions while
1251                    // the character is being moved.
1252                    let mut collisions = vec![];
1253                    let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1254                    let query_pipeline = bf.as_query_pipeline(
1255                        nf.query_dispatcher(),
1256                        &bodies,
1257                        &colliders,
1258                        filter_character_controller,
1259                    );
1260                    let effective_movement = controller.move_shape(
1261                        integration_parameters.dt,
1262                        &query_pipeline,
1263                        character_shape,
1264                        character_body.position(),
1265                        Vector::new(0.1, -0.1, 0.1),
1266                        |collision| collisions.push(collision),
1267                    );
1268                    let character_body = bodies.get_mut(handle).unwrap();
1269                    let translation = character_body.translation();
1270                    assert!(
1271                        effective_movement.grounded,
1272                        "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1273                        i,
1274                        translation + effective_movement.translation
1275                    );
1276                    character_body.set_next_kinematic_translation(
1277                        translation + effective_movement.translation,
1278                    );
1279                };
1280
1281            update_character_controller(character_controller_no_snap, character_handle_no_snap);
1282            update_character_controller(character_controller_snap, character_handle_snap);
1283        }
1284
1285        let character_body = bodies.get_mut(character_handle_no_snap).unwrap();
1286        let translation = character_body.translation();
1287
1288        // Accumulated numerical error makes the character fall short of the ideal distance.
1289        // The bound was 940 until parry 0.30.2 tightened the shape-cast TOI at GJK stagnation
1290        // (parry#429), which costs a little more progress per step.
1291        assert!(
1292            translation.x >= 930.0,
1293            "actual translation.x:{}",
1294            translation.x
1295        );
1296        assert!(
1297            translation.z >= 930.0,
1298            "actual translation.z:{}",
1299            translation.z
1300        );
1301
1302        let character_body = bodies.get_mut(character_handle_snap).unwrap();
1303        let translation = character_body.translation();
1304        assert!(
1305            translation.x >= 960.0,
1306            "actual translation.x:{}",
1307            translation.x
1308        );
1309        assert!(
1310            translation.z >= 960.0,
1311            "actual translation.z:{}",
1312            translation.z
1313        );
1314    }
1315
1316    #[test]
1317    fn character_controller_negative_up_axis() {
1318        // Regression test for issue #492: a `KinematicCharacterController` whose `up` vector
1319        // points along negative Y must not panic in `move_shape`, and must still detect the
1320        // ground.
1321        let mut colliders = ColliderSet::new();
1322        let mut impulse_joints = ImpulseJointSet::new();
1323        let mut multibody_joints = MultibodyJointSet::new();
1324        let mut soft_bodies = SoftBodySet::new();
1325        let mut pipeline = PhysicsPipeline::new();
1326        let mut bf = BroadPhaseBvh::new();
1327        let mut nf = NarrowPhase::new();
1328        let mut islands = IslandManager::new();
1329        let mut bodies = RigidBodySet::new();
1330
1331        // In a world whose "up" is -Y, the floor is *above* the character (higher Y).
1332        // Floor half-height 0.1 centered at y = 0.6 => its lower face is at y = 0.5.
1333        let ground = RigidBodyBuilder::fixed().translation(Vector::new(0.0, 0.6, 0.0));
1334        let ground_handle = bodies.insert(ground);
1335        colliders.insert_with_parent(
1336            ColliderBuilder::cuboid(10.0, 0.1, 10.0),
1337            ground_handle,
1338            &mut bodies,
1339        );
1340
1341        // Character ball of radius 0.5 at the origin => its top is at y = 0.5, i.e. touching
1342        // the floor's lower face.
1343        let character_handle = bodies.insert(RigidBodyBuilder::kinematic_position_based());
1344        let character_collider = ColliderBuilder::ball(0.5).build();
1345        colliders.insert_with_parent(character_collider.clone(), character_handle, &mut bodies);
1346
1347        let integration_parameters = IntegrationParameters::default();
1348        pipeline.step(
1349            Vector::ZERO,
1350            &integration_parameters,
1351            &mut islands,
1352            &mut bf,
1353            &mut nf,
1354            &mut bodies,
1355            &mut colliders,
1356            &mut impulse_joints,
1357            &mut multibody_joints,
1358            &mut soft_bodies,
1359            &mut CCDSolver::new(),
1360            &(),
1361            &(),
1362        );
1363
1364        let controller = KinematicCharacterController {
1365            up: -Vector::Y,
1366            ..Default::default()
1367        };
1368
1369        let filter = QueryFilter::new().exclude_rigid_body(character_handle);
1370        let query_pipeline =
1371            bf.as_query_pipeline(nf.query_dispatcher(), &bodies, &colliders, filter);
1372
1373        // This used to panic with "The loosening margin must be positive." (issue #492).
1374        let movement = controller.move_shape(
1375            integration_parameters.dt,
1376            &query_pipeline,
1377            character_collider.shape(),
1378            bodies[character_handle].position(),
1379            Vector::new(0.1, 0.0, 0.0),
1380            |_| {},
1381        );
1382
1383        // The floor lies in the +Y direction, i.e. along `-up`, so the character is grounded.
1384        assert!(movement.grounded);
1385    }
1386    #[test]
1387    fn character_controller_impulses_with_multiple_dynamic_bodies() {
1388        // `solve_character_collision_impulses` used to gather every nearby body's manifolds
1389        // into one vec and slice it at the previous length, assuming `contact_manifolds`
1390        // appends. Parry's composite-shape paths rebuild the vec for the current pair instead,
1391        // so with a compound character and two dynamic bodies in range the second pair either
1392        // indexed past the end or left its manifolds without a body handle, panicking either way.
1393        let mut colliders = ColliderSet::new();
1394        let mut impulse_joints = ImpulseJointSet::new();
1395        let mut multibody_joints = MultibodyJointSet::new();
1396        let mut soft_bodies = SoftBodySet::new();
1397        let mut pipeline = PhysicsPipeline::new();
1398        let mut bf = BroadPhaseBvh::new();
1399        let mut nf = NarrowPhase::new();
1400        let mut islands = IslandManager::new();
1401        let mut bodies = RigidBodySet::new();
1402
1403        let ground = RigidBodyBuilder::fixed().translation(Vector::new(0.0, -0.6, 0.0));
1404        let ground_handle = bodies.insert(ground);
1405        colliders.insert_with_parent(
1406            ColliderBuilder::cuboid(10.0, 0.1, 10.0),
1407            ground_handle,
1408            &mut bodies,
1409        );
1410
1411        // A 1x1x1 character made of two stacked half-cubes, so its shape is a compound.
1412        let character_handle = bodies.insert(RigidBodyBuilder::kinematic_position_based());
1413        let character_collider = ColliderBuilder::compound(vec![
1414            (
1415                Pose::from_translation(Vector::new(0.0, -0.25, 0.0)),
1416                SharedShape::cuboid(0.5, 0.25, 0.5),
1417            ),
1418            (
1419                Pose::from_translation(Vector::new(0.0, 0.25, 0.0)),
1420                SharedShape::cuboid(0.5, 0.25, 0.5),
1421            ),
1422        ])
1423        .build();
1424        colliders.insert_with_parent(character_collider.clone(), character_handle, &mut bodies);
1425
1426        // Two dynamic unit cubes touching the character: one ahead (+X), in the movement
1427        // path, and one beside it (+Z), close enough to be gathered by the impulse solver but
1428        // not in the way.
1429        let mut insert_box = |translation: Vector| {
1430            let handle = bodies.insert(RigidBodyBuilder::dynamic().translation(translation));
1431            colliders.insert_with_parent(
1432                ColliderBuilder::cuboid(0.5, 0.5, 0.5),
1433                handle,
1434                &mut bodies,
1435            );
1436            handle
1437        };
1438        let box_ahead = insert_box(Vector::new(1.0, 0.0, 0.0));
1439        let box_beside = insert_box(Vector::new(0.0, 0.0, 1.0));
1440
1441        let integration_parameters = IntegrationParameters::default();
1442        pipeline.step(
1443            Vector::ZERO,
1444            &integration_parameters,
1445            &mut islands,
1446            &mut bf,
1447            &mut nf,
1448            &mut bodies,
1449            &mut colliders,
1450            &mut impulse_joints,
1451            &mut multibody_joints,
1452            &mut soft_bodies,
1453            &mut CCDSolver::new(),
1454            &(),
1455            &(),
1456        );
1457
1458        let controller = KinematicCharacterController::default();
1459        let filter = QueryFilter::new().exclude_rigid_body(character_handle);
1460
1461        let mut collisions = vec![];
1462        let query_pipeline =
1463            bf.as_query_pipeline(nf.query_dispatcher(), &bodies, &colliders, filter);
1464        controller.move_shape(
1465            integration_parameters.dt,
1466            &query_pipeline,
1467            character_collider.shape(),
1468            bodies[character_handle].position(),
1469            Vector::new(0.1, 0.0, 0.0),
1470            |c| collisions.push(c),
1471        );
1472        assert!(
1473            collisions
1474                .iter()
1475                .any(|c| colliders[c.handle].parent() == Some(box_ahead)),
1476            "the character should have run into the box ahead of it"
1477        );
1478
1479        let mut query_pipeline_mut =
1480            bf.as_query_pipeline_mut(nf.query_dispatcher(), &mut bodies, &mut colliders, filter);
1481        // This used to panic with "range start index N out of range for slice of length 0"
1482        // or on `rigid_body2.unwrap()`.
1483        controller.solve_character_collision_impulses(
1484            integration_parameters.dt,
1485            &mut query_pipeline_mut,
1486            character_collider.shape(),
1487            1.0,
1488            &collisions,
1489        );
1490
1491        // The box ahead was pushed forward; the box beside lies across the motion and is
1492        // left alone.
1493        assert!(bodies[box_ahead].linvel().x > 0.0);
1494        assert_eq!(bodies[box_beside].linvel(), Vector::ZERO);
1495    }
1496}