use rapier3d::control::{CharacterAutostep, CharacterCollision, CharacterLength, KinematicCharacterController};
use rapier3d::prelude as rp;
use serde::{Deserialize, Serialize};
use super::{CollisionGroups, LinkKind, Physics3D, from_rp, to_rp};
use crate::{Entity, Vec3, World};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Character3D {
pub height: f32,
pub radius: f32,
pub max_slope: f32,
pub step_height: f32,
pub snap_distance: f32,
pub push_mass: f32,
pub gravity_scale: f32,
pub groups: CollisionGroups,
pub velocity: Vec3,
pub grounded: bool,
}
impl Default for Character3D {
fn default() -> Self {
Self::new(1.8, 0.3)
}
}
impl Character3D {
#[must_use]
pub fn new(height: f32, radius: f32) -> Self {
Self {
height,
radius,
max_slope: 45f32.to_radians(),
step_height: 0.35,
snap_distance: 0.3,
push_mass: 80.0,
gravity_scale: 1.0,
groups: CollisionGroups::ALL,
velocity: Vec3::ZERO,
grounded: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Capsule {
radius: f32,
half_height: f32,
groups: CollisionGroups,
}
const SKIN: f32 = 0.01;
impl Capsule {
pub(super) fn of(c: &Character3D) -> Self {
let radius = c.radius.max(0.01);
Self {
radius,
half_height: (c.height * 0.5 - radius).max(0.0),
groups: c.groups,
}
}
fn center(self) -> f32 {
self.half_height + self.radius
}
fn shape(self) -> rp::Capsule {
rp::Capsule::new_y(self.half_height, self.radius)
}
pub(super) fn collider(self, e: Entity) -> rp::Collider {
rp::ColliderBuilder::new(rp::SharedShape::new(self.shape()))
.translation(rp::Vector::new(0.0, self.center(), 0.0))
.collision_groups(self.groups.to_rp())
.active_events(rp::ActiveEvents::COLLISION_EVENTS)
.user_data(u128::from(e.to_bits()))
.build()
}
}
#[derive(Default)]
pub(super) struct Scratch {
plans: Vec<Option<Plan>>,
characters: Vec<(Entity, Character3D)>,
}
pub(super) struct Plan {
body: rp::RigidBodyHandle,
controller: KinematicCharacterController,
shape: rp::Capsule,
filter_groups: rp::InteractionGroups,
feet: Vec3,
velocity: Vec3,
desired: Vec3,
moved: rapier3d::control::EffectiveCharacterMovement,
hits: Vec<CharacterCollision>,
}
const PARALLEL_FROM: usize = 8;
fn plan(physics: &Physics3D, e: Entity, c: &Character3D, dt: f32) -> Option<Plan> {
let gravity = physics.gravity;
let up = Vec3::Y;
let link = physics
.links
.get(e.index() as usize)
.and_then(Option::as_ref)
.filter(|l| l.entity == e)?;
let LinkKind::Character(capsule) = link.kind else {
return None;
};
let body = link.body;
let feet = physics.world.bodies.get(body).map(|b| from_rp(b.translation()))?;
let controller = KinematicCharacterController {
up: to_rp(up),
offset: CharacterLength::Absolute(SKIN),
slide: true,
autostep: (c.step_height > 0.0).then_some(CharacterAutostep {
max_height: CharacterLength::Absolute(c.step_height),
min_width: CharacterLength::Absolute(capsule.radius),
include_dynamic_bodies: false,
}),
max_slope_climb_angle: c.max_slope,
min_slope_slide_angle: c.max_slope,
snap_to_ground: (c.snap_distance > 0.0).then_some(CharacterLength::Absolute(c.snap_distance)),
..KinematicCharacterController::default()
};
let mut velocity = c.velocity;
if !velocity.is_finite() {
velocity = Vec3::ZERO;
}
if c.grounded && velocity.dot(up) <= 0.0 {
velocity -= up * velocity.dot(up);
} else {
velocity += gravity * c.gravity_scale * dt;
}
let desired = velocity * dt;
let shape = capsule.shape();
let center = rp::Pose::from_translation(to_rp(feet + up * capsule.center()));
let filter_groups = capsule.groups.to_rp();
let filter = rp::QueryFilter::default()
.exclude_rigid_body(body)
.exclude_sensors()
.groups(filter_groups);
let mut hits = Vec::new();
let moved = {
let queries = physics.world.query_pipeline_with_filter(filter);
controller.move_shape(dt, &queries, &shape, ¢er, to_rp(desired), |hit| hits.push(hit))
};
if c.push_mass > 0.0 {
let bodies = &physics.world.bodies;
let colliders = &physics.world.colliders;
hits.retain(|h| {
colliders
.get(h.handle)
.and_then(|co| co.parent())
.and_then(|b| bodies.get(b))
.is_some_and(|b| b.is_dynamic())
});
} else {
hits.clear();
}
Some(Plan {
body,
controller,
shape,
filter_groups,
feet,
velocity,
desired,
moved,
hits,
})
}
pub(super) fn move_characters(physics: &mut Physics3D, world: &mut World, dt: f32) {
if dt <= 0.0 {
return;
}
let up = Vec3::Y;
let mut scratch = std::mem::take(&mut physics.character);
scratch.characters.clear();
world
.query::<(Entity, &Character3D)>()
.for_each(|(e, c)| scratch.characters.push((e, *c)));
scratch.plans.clear();
{
let physics = &*physics;
let characters = &scratch.characters;
if characters.len() >= PARALLEL_FROM {
use rayon::prelude::*;
characters
.par_iter()
.map(|(e, c)| plan(physics, *e, c, dt))
.collect_into_vec(&mut scratch.plans);
} else {
scratch
.plans
.extend(characters.iter().map(|(e, c)| plan(physics, *e, c, dt)));
}
}
for (&(e, c), plan) in scratch.characters.iter().zip(scratch.plans.drain(..)) {
let Some(plan) = plan else {
continue;
};
if !plan.hits.is_empty() {
let filter = rp::QueryFilter::default()
.exclude_rigid_body(plan.body)
.exclude_sensors()
.groups(plan.filter_groups);
let w = &mut physics.world;
let mut queries = w.broad_phase.as_query_pipeline_mut(
w.narrow_phase.query_dispatcher(),
&mut w.bodies,
&mut w.colliders,
filter,
);
plan.controller.solve_character_collision_impulses(
dt,
&mut queries,
&plan.shape,
c.push_mass,
plan.hits.iter(),
);
}
let moved_by = from_rp(plan.moved.translation);
let mut velocity = plan.velocity;
let vertical = velocity.dot(up);
if plan.moved.grounded && vertical < 0.0 {
velocity -= up * vertical;
} else if vertical > 0.0 && moved_by.dot(up) < plan.desired.dot(up) * 0.5 {
velocity -= up * vertical;
}
if let Some(character) = world.get_mut::<Character3D>(e) {
character.velocity = velocity;
character.grounded = plan.moved.grounded;
}
if let Some(b) = physics.world.bodies.get_mut(plan.body) {
b.set_next_kinematic_translation(to_rp(plan.feet + moved_by));
}
}
physics.character = scratch;
}