You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
KCC: panic in solve_single_character_collision_impulse when pushing a collider whose parent body isn't registered yet (0.35) #1009
KinematicCharacterController panics inside solve_single_character_collision_impulse (rapier3d 0.35.0-glamx0.2,
via bevy_rapier3d 0.36) when the character moves into a dynamic collider
during the first frames after world spawn — before the collider's parent
rigid body is fully registered.
Two distinct panics from the same code path, depending on timing:
rapier3d-0.35.0-glamx0.2/src/control/character_controller.rs:957:
called Option::unwrap() on a None value
rapier3d-0.35.0-glamx0.2/src/control/character_controller.rs:944:
range start index 1 out of range for slice of length 0
Line 957 is manifold.data.rigid_body2.unwrap() — None when the
manifold's collider has no parent body attached (yet).
Reproduction (bevy_rapier context)
Spawn a level containing RigidBody::Dynamic props near the player
spawn point (pushable crates/barrels).
Spawn a KinematicCharacterController and feed it a translation on the first frames (i.e. the player moves immediately).
bevy_rapier registers colliders and bodies in separate systems, so for
a few frames a dynamic prop's collider exists in the pipeline while its
body attachment doesn't. If the KCC contacts it in that window and
enters the push-dynamic-bodies path → panic.
100% reproducible by moving immediately after spawn; never reproduces
if you wait ~1 s (registration completes).
A pure-rapier repro should reduce to: call solve_character_collision_impulses with a manifold against a collider
whose parent() is None (standalone collider on the character's path).
Expected
Manifolds whose collider has no parent body should be skipped (nothing
to apply an impulse to) instead of unwrap()-ing. rapier 0.32's
character controller handled this case gracefully — the panic is a
regression of the 0.35 KCC rework.
Workaround
Pin to the previous core (bevy_rapier 0.35 / rapier 0.32) — no panic.
Linux x86_64, occurs in both debug and with default KCC settings
(slide: true, snap_to_ground, autostep; the rest default)
Happy to submit a PR — the fix looks like skipping manifolds whose collider has no parent body instead of unwrapping. Let me know if that approach sounds right.
Both panics here reproduce in plain rapier without an unregistered body, and #1004 (released in 0.36.0) fixes both.
Why an unregistered body is not the cause. In 0.35.0 through 0.35.3, solve_single_character_collision_impulse skips any collider whose parent is not a registered dynamic body before it computes contacts (character_controller.rs:930-932). So a collider without a body should not reach :944 or :957.
What reaches those lines. They are reached through one manifolds Vec shared by every dynamic collider in range. The loop assumes contact_manifolds only appends to it, and parry does not.
Plain convex shapes: parry's default dispatcher pushes a manifold only when the Vec is empty; otherwise it computes into manifolds[0] (parry 0.30.2, default_query_dispatcher.rs:740-751). A second plain dynamic collider therefore silently overwrites the first one's manifold, which stays tagged with the first body. There is no panic, but that body is pushed at the wrong points. That is the "incorrect computation" case Fix character controller manifold collection #1004 mentions.
Composite shapes: parry replaces the Vec's contents. This was measured with a compound. For heightfield (contact_manifolds_heightfield_shape.rs:119, mem::take) and trimesh (contact_manifolds_trimesh_shape.rs:125, mem::swap) it is from the source.
When a compound collider is visited after another dynamic collider:
if none of its parts is within the prediction distance, the Vec comes back shorter than before, and manifolds[prev_manifolds_len..] panics at :944;
if its parts are in range, its manifolds land below prev_manifolds_len and are never tagged with a body, so rigid_body2.unwrap() panics at :957.
Repro. Tested with rapier3d-f64 0.35.3 (default features) and a compound. The reported version, 0.35.0-glamx0.2, was not checked, but its panic lines, 944 and 957, match 0.35.3. The scene is a dynamic box in front of a character, with a dynamic compound whose two parts sit gap from the character's sides:
use rapier3d_f64::control::{CharacterLength,KinematicCharacterController};use rapier3d_f64::prelude::*;/// A dynamic box in front of the character, then a dynamic compound whose two parts sit/// `gap` from the character's sides.fnpush(gap:f64){let dt = 1.0 / 64.0;letmut world = PhysicsWorld::new();
world.gravity = Vector::ZERO;
world.insert(RigidBodyBuilder::dynamic(),ColliderBuilder::cuboid(0.12,0.2,0.12));let z = 0.25 + gap + 0.12;
world.insert(RigidBodyBuilder::dynamic().translation(Vector::new(-0.379,0.0,0.0)),ColliderBuilder::compound(vec![(Pose::from_translation(Vector::new(0.0,0.0, z)),SharedShape::cuboid(0.12,0.1,0.12)),(Pose::from_translation(Vector::new(0.0,0.0, -z)),SharedShape::cuboid(0.12,0.1,0.12)),]),);
world.step();// fills the broad phase; nothing moveslet controller = KinematicCharacterController{offset:CharacterLength::Absolute(0.01),snap_to_ground:None,
..Default::default()};let shape = SharedShape::cuboid(0.25,0.25,0.25);let pos = Pose::from_translation(Vector::new(-0.379,0.0,0.0));letmut collisions = vec![];let query = world.broad_phase.as_query_pipeline(
world.narrow_phase.query_dispatcher(),&world.bodies,&world.colliders,QueryFilter::new(),);
controller.move_shape(dt,&query,&*shape,&pos,Vector::new(dt,0.0,0.0), |c| collisions.push(c));letPhysicsWorld{ broad_phase, narrow_phase, bodies, colliders, .. } = &mut world;letmut query =
broad_phase.as_query_pipeline_mut(narrow_phase.query_dispatcher(), bodies, colliders,QueryFilter::new());
controller.solve_character_collision_impulses(dt,&mut query,&*shape,1.0,&collisions);println!("gap {gap}: no panic");}fnmain(){let gap:f64 = std::env::args().nth(1).and_then(|a| a.parse().ok()).unwrap_or(0.1);push(gap);}
With gap 0.1, where the parts are outside the 0.06 prediction distance but the compound's AABB spans the character (path shortened):
thread 'main' (23488) panicked at .../rapier3d-f64-0.35.3/src/control/character_controller.rs:944:48:
range start index 1 out of range for slice of length 0
With gap 0.01 (path shortened):
thread 'main' (35452) panicked at .../rapier3d-f64-0.35.3/src/control/character_controller.rs:957:57:
called `Option::unwrap()` on a `None` value
Results.
On 0.36.0 both runs print no panic.
The panic depends on which collider is visited first: with the compound inserted before the box, the gap 0.1 case does not panic on 0.35.3.
A collider with no parent body next to the character does not panic on either version.
What this means for this issue. If the pushed props include a compound, trimesh or heightfield collider, 0.36.0 should fix this.
Activity
mcp-tool-shop commentedon Sep 26, 2026
Both panics here reproduce in plain rapier without an unregistered body, and #1004 (released in 0.36.0) fixes both.
Why an unregistered body is not the cause. In 0.35.0 through 0.35.3,
solve_single_character_collision_impulseskips any collider whose parent is not a registered dynamic body before it computes contacts (character_controller.rs:930-932). So a collider without a body should not reach:944or:957.What reaches those lines. They are reached through one
manifoldsVec shared by every dynamic collider in range. The loop assumescontact_manifoldsonly appends to it, and parry does not.manifolds[0](parry 0.30.2,default_query_dispatcher.rs:740-751). A second plain dynamic collider therefore silently overwrites the first one's manifold, which stays tagged with the first body. There is no panic, but that body is pushed at the wrong points. That is the "incorrect computation" case Fix character controller manifold collection #1004 mentions.contact_manifolds_heightfield_shape.rs:119,mem::take) and trimesh (contact_manifolds_trimesh_shape.rs:125,mem::swap) it is from the source.When a compound collider is visited after another dynamic collider:
manifolds[prev_manifolds_len..]panics at:944;prev_manifolds_lenand are never tagged with a body, sorigid_body2.unwrap()panics at:957.Repro. Tested with rapier3d-f64 0.35.3 (default features) and a compound. The reported version, 0.35.0-glamx0.2, was not checked, but its panic lines, 944 and 957, match 0.35.3. The scene is a dynamic box in front of a character, with a dynamic compound whose two parts sit
gapfrom the character's sides:With
gap0.1, where the parts are outside the 0.06 prediction distance but the compound's AABB spans the character (path shortened):With
gap0.01 (path shortened):Results.
no panic.gap0.1 case does not panic on 0.35.3.What this means for this issue. If the pushed props include a compound, trimesh or heightfield collider, 0.36.0 should fix this.