use avian3d::{
math::{Vector, *},
prelude::*,
};
use bevy::prelude::*;
use crate::system_sets::IchunSystemSet;
const DISABLE_PLATFORM_SYNC_FRAMES: u8 = 4;
pub struct IchunKccPlugin;
impl Plugin for IchunKccPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_grounded_sys,
apply_gravity_sys,
handle_moving_platforms_sys,
count_jump_frames_sys,
)
.chain()
.in_set(IchunSystemSet::KccPhysicsSet),
)
.add_systems(
Update,
apply_velocity_with_damping_sys.in_set(IchunSystemSet::VelocityModifiersSet),
)
.add_systems(
PostProcessCollisions,
kinematic_controller_collisions_sys,
);
}
}
#[derive(Component)]
#[require(
RigidBody(|| RigidBody::Kinematic),
Collider(|| Collider::capsule(0.4, 1.0)),
ShapeCaster(|| ShapeCaster::new(
{
let mut collider = Collider::capsule(0.4, 1.0);
collider.set_scale(Vector::ONE * 0.99, 10);
collider
},
Vector::ZERO,
Quaternion::default(),
Dir3::NEG_Y).with_max_distance(0.2)
), KccVelocity)]
pub struct Kcc {
pub controller_gravity: Vector,
pub max_fall_speed: Scalar,
pub max_slope_angle: Option<Scalar>,
pub is_grounded: bool,
pub movement_damping_factor: Scalar,
pub external_force: Vector,
pub external_force_damping_factor: Scalar,
pub max_jumped_frames: u8,
}
impl Default for Kcc {
fn default() -> Self {
Self {
controller_gravity: Vector::NEG_Y * 9.81 * 2.0,
max_fall_speed: 30.0,
max_slope_angle: Some((30.0 as Scalar).to_radians()),
is_grounded: false,
movement_damping_factor: 0.025,
external_force: Vector::ZERO,
external_force_damping_factor: 0.9,
max_jumped_frames: 4,
}
}
}
#[derive(Clone, Copy, Component, Debug, Deref, DerefMut, PartialEq)]
pub struct KccVelocity(pub Vector);
impl Default for KccVelocity {
fn default() -> Self {
Self(Vector::ZERO)
}
}
#[derive(Component, Clone, Copy)]
pub struct JumpedRecently {
frame_counter: u8,
}
impl Default for JumpedRecently {
fn default() -> Self {
Self { frame_counter: 0 }
}
}
impl JumpedRecently {
pub fn get_frame_counter(self) -> u8 {
self.frame_counter
}
}
fn update_grounded_sys(mut query: Query<(&ShapeHits, &Rotation, &mut Kcc)>) {
for (hits, rotation, mut kcc) in &mut query {
let is_grounded = hits.iter().any(|hit| {
if let Some(angle) = kcc.max_slope_angle {
(rotation * -hit.normal2).angle_between(Vector::Y).abs() <= angle
} else {
true
}
});
if is_grounded {
kcc.is_grounded = true;
} else {
kcc.is_grounded = false;
}
}
}
fn apply_gravity_sys(time: Res<Time>, mut controllers: Query<(&Kcc, &mut KccVelocity)>) {
let delta_time = time.delta_secs_f64().adjust_precision();
for (kcc, mut kcc_vel) in &mut controllers {
if kcc.is_grounded {
continue;
}
if kcc_vel.0.dot(kcc.controller_gravity.normalize_or_zero()) < kcc.max_fall_speed {
let gravity = kcc.controller_gravity * delta_time;
kcc_vel.0 += gravity;
}
}
}
fn handle_moving_platforms_sys(
time: Res<Time>,
mut kcc_qry: Query<(
Entity,
&ShapeHits,
&Rotation,
&mut Kcc,
&mut KccVelocity,
Has<JumpedRecently>,
)>,
jumped_recently_qry: Query<(Entity, &JumpedRecently)>,
hit_qry: Query<(Entity, &LinearVelocity), Without<Kcc>>,
) {
let delta_time = time.delta_secs_f64().adjust_precision();
for (entity, hits, rotation, mut kcc, mut kcc_vel, has_jumped_recently) in kcc_qry.iter_mut() {
if has_jumped_recently {
if let Ok((_, jumped_recently)) = jumped_recently_qry.get(entity) {
if jumped_recently.get_frame_counter() <= DISABLE_PLATFORM_SYNC_FRAMES {
continue;
}
}
}
if !kcc.is_grounded {
continue;
}
let mut platform_velocity = Vec3::ZERO;
for hit in hits.iter() {
let grounded_hit = match kcc.max_slope_angle {
Some(angle) => {
if (rotation * -hit.normal2).angle_between(Vector::Y).abs() > angle {
continue;
}
hit
}
None => hit,
};
let Some((_, ground_velocity)) = hit_qry
.iter()
.find(|ground| ground.0 == grounded_hit.entity)
else {
continue;
};
platform_velocity += ground_velocity.0;
}
if platform_velocity != kcc.external_force {
kcc.external_force = platform_velocity;
}
if kcc.is_grounded {
if kcc.external_force.y < 0.0 {
kcc_vel.0.y = -kcc.max_fall_speed * delta_time;
}
}
}
}
fn count_jump_frames_sys(
mut cmd: Commands,
mut kcc_qry: Query<(Entity, &Kcc, &mut JumpedRecently)>,
) {
for (entity, kcc, mut jumped_recently) in &mut kcc_qry {
if jumped_recently.get_frame_counter() < kcc.max_jumped_frames {
jumped_recently.frame_counter += 1;
} else {
cmd.entity(entity).remove::<JumpedRecently>();
}
}
}
#[allow(clippy::type_complexity)]
fn kinematic_controller_collisions_sys(
collisions: Res<Collisions>,
bodies_qry: Query<&RigidBody>,
collider_parents_qry: Query<&ColliderParent, Without<Sensor>>,
mut character_controllers_qry: Query<
(&mut Position, &Rotation, &mut KccVelocity, &Kcc),
With<RigidBody>,
>,
time: Res<Time>,
) {
for contacts in collisions.iter() {
let Ok([collider_parent1, collider_parent2]) =
collider_parents_qry.get_many([contacts.entity1, contacts.entity2])
else {
continue;
};
let is_first: bool;
let character_rb: RigidBody;
let is_other_dynamic: bool;
let (mut position, rotation, mut kcc_vel, kcc) = if let Ok(character) =
character_controllers_qry.get_mut(collider_parent1.get())
{
is_first = true;
character_rb = *bodies_qry.get(collider_parent1.get()).unwrap();
is_other_dynamic = bodies_qry
.get(collider_parent2.get())
.is_ok_and(|rb| rb.is_dynamic());
character
} else if let Ok(character) = character_controllers_qry.get_mut(collider_parent2.get()) {
is_first = false;
character_rb = *bodies_qry.get(collider_parent2.get()).unwrap();
is_other_dynamic = bodies_qry
.get(collider_parent1.get())
.is_ok_and(|rb| rb.is_dynamic());
character
} else {
continue;
};
if !character_rb.is_kinematic() {
continue;
}
for manifold in contacts.manifolds.iter() {
let normal = if is_first {
-manifold.global_normal1(rotation)
} else {
-manifold.global_normal2(rotation)
};
let mut deepest_penetration: Scalar = Scalar::MIN;
for contact in manifold.contacts.iter() {
deepest_penetration = deepest_penetration.max(contact.penetration);
}
if deepest_penetration > 0.0 {
position.0 += normal * deepest_penetration;
}
if is_other_dynamic {
continue;
}
let slope_angle = normal.angle_between(Vector::Y);
let climbable = kcc
.max_slope_angle
.is_some_and(|angle| slope_angle.abs() <= angle);
let total_velocity = kcc_vel.0 + kcc.external_force;
if deepest_penetration > 0.0 {
if climbable {
let normal_direction_xz =
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
let kcc_vel_xz = total_velocity.dot(normal_direction_xz);
let max_y_speed = -kcc_vel_xz * slope_angle.tan();
kcc_vel.y = kcc_vel.y.max(max_y_speed);
} else {
if total_velocity.dot(normal) > 0.0 {
continue;
}
let impulse = total_velocity.reject_from_normalized(normal);
kcc_vel.0 = impulse;
}
} else {
let normal_speed = total_velocity.dot(normal);
if normal_speed > 0.0 {
continue;
}
let impulse_magnitude =
normal_speed - (deepest_penetration / time.delta_secs_f64().adjust_precision());
let mut impulse = impulse_magnitude * normal;
if climbable {
kcc_vel.y -= impulse.y.min(0.0);
} else {
impulse.y = impulse.y.max(0.0);
kcc_vel.0 -= impulse;
}
}
}
}
}
fn apply_velocity_with_damping_sys(
time: Res<Time>,
mut controllers_qry: Query<(&mut Kcc, &mut KccVelocity, &mut LinearVelocity)>,
) {
let delta_time = time.delta_secs_f64().adjust_precision();
for (mut kcc, mut kcc_vel, mut linear_velocity) in &mut controllers_qry {
kcc_vel.x *= f32::powf(kcc.movement_damping_factor, delta_time);
kcc_vel.z *= f32::powf(kcc.movement_damping_factor, delta_time);
if !kcc.is_grounded {
let damping = kcc.external_force_damping_factor;
kcc.external_force *= f32::powf(damping, delta_time);
}
linear_velocity.0 = kcc_vel.0 + kcc.external_force;
}
}