From 8a49bc98a8be81d38cfd11b88e269797048256e4 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Thu, 3 Sep 2026 20:51:01 -0700 Subject: character interactions with entities --- src/sim/rb_manager/character_impulses.rs | 136 +++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/sim/rb_manager/character_impulses.rs (limited to 'src/sim/rb_manager/character_impulses.rs') diff --git a/src/sim/rb_manager/character_impulses.rs b/src/sim/rb_manager/character_impulses.rs new file mode 100644 index 0000000..7016956 --- /dev/null +++ b/src/sim/rb_manager/character_impulses.rs @@ -0,0 +1,136 @@ +//! Local port of `KinematicCharacterController::solve_character_collision_impulses`. +//! +//! Rapier's version (0.35.2, still present on master) accumulates every nearby collider's +//! contact manifolds into one shared `Vec` and then slices it with `manifolds[prev_len..]`. +//! That assumes `contact_manifolds` appends, but parry's composite-shape paths (compound, +//! polyline, trimesh, ...) `mem::take` the output vec and rebuild it for the current pair +//! only. With two or more dynamic bodies inside the character's AABB the second call empties +//! the vec and the slice panics with "range start index 1 out of range for slice of length 0". +//! The character's collider here is a convex decomposition (a compound), so we hit it. +//! +//! This port uses a fresh scratch vec per collider, which is what rapier's own code intended. + +use rapier2d::{ + control::{CharacterCollision, CharacterLength, KinematicCharacterController}, + geometry::{ContactManifold, Shape}, + math::{Pose, Real}, + parry::{ + bounding_volume::BoundingVolume, + query::{DefaultQueryDispatcher, PersistentQueryDispatcher}, + }, + pipeline::QueryPipelineMut, +}; + +/// Apply approximate impulses to the dynamic bodies a kinematic character ran into. +pub fn solve_character_collision_impulses<'a>( + controller: &KinematicCharacterController, + dt: Real, + queries: &mut QueryPipelineMut, + character_shape: &dyn Shape, + character_mass: Real, + collisions: impl IntoIterator, +) { + for collision in collisions { + solve_single_character_collision_impulse( + controller, + dt, + queries, + character_shape, + character_mass, + collision, + ); + } +} + +fn eval_length(length: CharacterLength, value: Real) -> Real { + match length { + CharacterLength::Relative(x) => value * x, + CharacterLength::Absolute(x) => x, + } +} + +fn solve_single_character_collision_impulse( + controller: &KinematicCharacterController, + dt: Real, + queries: &mut QueryPipelineMut, + character_shape: &dyn Shape, + character_mass: Real, + collision: &CharacterCollision, +) { + let extents = character_shape.compute_local_aabb().extents(); + let up_extent = extents.dot(controller.up.abs()); + let movement_to_transfer = + collision.hit.normal1 * collision.translation_remaining.dot(collision.hit.normal1); + // `KinematicCharacterController::predict_ground` is private; this is its body. + let prediction = eval_length(controller.offset, up_extent) + 0.05; + + let dispatcher = DefaultQueryDispatcher; + + let mut manifolds: Vec = Vec::new(); + // World pose of the collider each manifold was computed against: the `local_p2` + // points are in the collider's frame, which differs from its body's when offset. + let mut manifold_collider_poses: Vec = Vec::new(); + let character_aabb = character_shape + .compute_aabb(&collision.character_pos) + .loosened(prediction); + + for (_, collider) in queries.as_ref().intersect_aabb_conservative(character_aabb) { + let Some(parent) = collider.parent() else { + continue; + }; + let Some(body) = queries.bodies.get(parent) else { + continue; + }; + if !body.is_dynamic() { + continue; + } + + let pos12 = collision.character_pos.inv_mul(collider.position()); + // Fresh vec per pair: parry may take/replace it rather than append. + let mut pair_manifolds: Vec = Vec::new(); + let _ = dispatcher.contact_manifolds( + &pos12, + character_shape, + collider.shape(), + prediction, + &mut pair_manifolds, + &mut None, + ); + + for mut m in pair_manifolds { + m.data.rigid_body2 = Some(parent); + m.data.normal = collision.character_pos.rotation * m.local_n1; + manifolds.push(m); + manifold_collider_poses.push(*collider.position()); + } + } + + let inv_dt = if dt != 0.0 { 1.0 / dt } else { 0.0 }; + let velocity_to_transfer = movement_to_transfer * inv_dt; + + for (manifold, collider_pos) in manifolds.iter().zip(manifold_collider_poses.iter()) { + let Some(body_handle) = manifold.data.rigid_body2 else { + continue; + }; + let Some(body) = queries.bodies.get_mut(body_handle) else { + continue; + }; + + for pt in &manifold.points { + if pt.dist <= prediction { + let body_mass = body.mass(); + let contact_point = *collider_pos * pt.local_p2; + let delta_vel_per_contact = (velocity_to_transfer + - body.velocity_at_point(contact_point)) + .dot(manifold.data.normal); + let mass_ratio = body_mass * character_mass / (body_mass + character_mass); + + body.apply_impulse_at_point( + manifold.data.normal * delta_vel_per_contact.max(0.0) * mass_ratio, + contact_point, + true, + ); + } + } + } +} -- cgit v1.3.1