use std::time::Duration;
use bevy::prelude::*;
use bevy_tnua_physics_integration_layer::data_for_backends::{
TnuaGhostSensor, TnuaProximitySensor,
};
use serde::{Deserialize, Serialize};
use crate::TnuaBasis;
use crate::basis_action_traits::TnuaBasisAccess;
use crate::basis_capabilities::{
TnuaBasisWithDisplacement, TnuaBasisWithFloating, TnuaBasisWithFrameOfReferenceSurface,
TnuaBasisWithGround, TnuaBasisWithHeadroom, TnuaBasisWithSpring,
};
use crate::ghost_overrides::TnuaGhostOverwrite;
use crate::math::*;
use crate::sensor_sets::{ProximitySensorPreparationHelper, TnuaSensors};
use crate::util::rotation_arc_around_axis;
use crate::{TnuaBasisContext, TnuaMotor, TnuaVelChange};
use super::walk_sensors::TnuaBuiltinWalkSensors;
#[derive(Default)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct TnuaBuiltinWalk {
pub desired_motion: Vector3,
pub desired_forward: Option<Dir3>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TnuaBuiltinWalkConfig {
pub speed: Float,
pub float_height: Float,
pub headroom: Option<TnuaBuiltinWalkHeadroom>,
pub cling_distance: Float,
pub spring_strength: Float,
pub spring_dampening: Float,
pub acceleration: Float,
pub air_acceleration: Float,
pub coyote_time: Float,
pub free_fall_extra_gravity: Float,
pub tilt_offset_angvel: Float,
pub tilt_offset_angacl: Float,
pub turning_angvel: Float,
pub max_slope: Float,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TnuaBuiltinWalkHeadroom {
pub distance_to_collider_top: Float,
pub sensor_extra_distance: Float,
}
impl Default for TnuaBuiltinWalkHeadroom {
fn default() -> Self {
Self {
distance_to_collider_top: 0.0,
sensor_extra_distance: 0.1,
}
}
}
impl Default for TnuaBuiltinWalkConfig {
fn default() -> Self {
Self {
speed: 20.0,
float_height: 0.0,
headroom: None,
cling_distance: 1.0,
spring_strength: 400.0,
spring_dampening: 1.2,
acceleration: 60.0,
air_acceleration: 20.0,
coyote_time: 30.15,
free_fall_extra_gravity: 60.0,
tilt_offset_angvel: 5.0,
tilt_offset_angacl: 500.0,
turning_angvel: 10.0,
max_slope: float_consts::FRAC_PI_2,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
struct StandingOnState {
entity: Entity,
entity_linvel: Vector3,
}
#[derive(Default, Debug)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct TnuaBuiltinWalkMemory {
airborne_timer: Option<Timer>,
pub standing_offset: Vector3,
standing_on: Option<StandingOnState>,
effective_velocity: Vector3,
vertical_velocity: Float,
pub running_velocity: Vector3,
extra_headroom: Float,
}
impl TnuaBuiltinWalkMemory {
pub fn standing_on_entity(&self) -> Option<Entity> {
Some(self.standing_on.as_ref()?.entity)
}
}
impl TnuaBasis for TnuaBuiltinWalk {
type Config = TnuaBuiltinWalkConfig;
type Memory = TnuaBuiltinWalkMemory;
type Sensors<'a> = TnuaBuiltinWalkSensors<'a>;
fn apply(
&self,
config: &Self::Config,
memory: &mut Self::Memory,
sensors: &Self::Sensors<'_>,
ctx: TnuaBasisContext,
motor: &mut TnuaMotor,
) {
if let Some(stopwatch) = &mut memory.airborne_timer {
#[allow(clippy::unnecessary_cast)]
stopwatch.tick(Duration::from_secs_f64(ctx.frame_duration as f64));
}
memory.extra_headroom = 0.0;
let climb_vectors: Option<ClimbVectors>;
let considered_in_air: bool;
let impulse_to_offset: Vector3;
let slipping_vector: Option<Vector3>;
if let Some(sensor_output) = &sensors.ground.output {
memory.effective_velocity = ctx.tracker.velocity - sensor_output.entity_linvel;
let sideways_unnormalized = sensor_output
.normal
.cross(*ctx.up_direction)
.adjust_precision();
if sideways_unnormalized == Vector3::ZERO {
climb_vectors = None;
} else {
climb_vectors = Some(ClimbVectors {
direction: sideways_unnormalized
.cross(sensor_output.normal.adjust_precision())
.normalize_or_zero()
.adjust_precision(),
sideways: sideways_unnormalized.normalize_or_zero().adjust_precision(),
});
}
slipping_vector = {
let angle_with_floor = sensor_output
.normal
.angle_between(*ctx.up_direction)
.adjust_precision();
if angle_with_floor <= config.max_slope {
None
} else {
Some(
sensor_output
.normal
.reject_from(*ctx.up_direction)
.adjust_precision(),
)
}
};
if memory.airborne_timer.is_some() {
considered_in_air = true;
impulse_to_offset = Vector3::ZERO;
memory.standing_on = None;
} else {
if let Some(standing_on_state) = &memory.standing_on {
if standing_on_state.entity != sensor_output.entity {
impulse_to_offset = Vector3::ZERO;
} else {
impulse_to_offset =
sensor_output.entity_linvel - standing_on_state.entity_linvel;
}
} else {
impulse_to_offset = Vector3::ZERO;
}
if slipping_vector.is_none() {
considered_in_air = false;
memory.standing_on = Some(StandingOnState {
entity: sensor_output.entity,
entity_linvel: sensor_output.entity_linvel,
});
} else {
considered_in_air = true;
memory.standing_on = None;
}
}
} else {
memory.effective_velocity = ctx.tracker.velocity;
climb_vectors = None;
considered_in_air = true;
impulse_to_offset = Vector3::ZERO;
slipping_vector = None;
memory.standing_on = None;
}
memory.effective_velocity += impulse_to_offset;
let velocity_on_plane = memory
.effective_velocity
.reject_from(ctx.up_direction.adjust_precision());
let desired_velocity = self.desired_motion * config.speed;
let desired_boost = desired_velocity - velocity_on_plane;
let safe_direction_coefficient = desired_velocity
.normalize_or_zero()
.dot(velocity_on_plane.normalize_or_zero());
let direction_change_factor = 1.5 - 0.5 * safe_direction_coefficient;
let relevant_acceleration_limit = if considered_in_air {
config.air_acceleration
} else {
config.acceleration
};
let max_acceleration = direction_change_factor * relevant_acceleration_limit;
memory.vertical_velocity = if let Some(climb_vectors) = &climb_vectors {
memory.effective_velocity.dot(climb_vectors.direction)
* climb_vectors
.direction
.dot(ctx.up_direction.adjust_precision())
} else {
0.0
};
let walk_vel_change = if desired_velocity == Vector3::ZERO && slipping_vector.is_none() {
let walk_boost = desired_boost.clamp_length_max(ctx.frame_duration * max_acceleration);
let walk_boost = if let Some(climb_vectors) = &climb_vectors {
climb_vectors.project(walk_boost)
} else {
walk_boost
};
TnuaVelChange::boost(walk_boost)
} else {
let walk_acceleration =
(desired_boost / ctx.frame_duration).clamp_length_max(max_acceleration);
let walk_acceleration =
if let (Some(climb_vectors), None) = (&climb_vectors, slipping_vector) {
climb_vectors.project(walk_acceleration)
} else {
walk_acceleration
};
let slipping_boost = 'slipping_boost: {
let Some(slipping_vector) = slipping_vector else {
break 'slipping_boost Vector3::ZERO;
};
let vertical_velocity = if 0.0 <= memory.vertical_velocity {
ctx.tracker.gravity.dot(ctx.up_direction.adjust_precision())
* ctx.frame_duration
} else {
memory.vertical_velocity
};
let Ok((slipping_direction, slipping_per_vertical_unit)) =
Dir3::new_and_length(slipping_vector.f32())
else {
break 'slipping_boost Vector3::ZERO;
};
let required_veloicty_in_slipping_direction =
slipping_per_vertical_unit.adjust_precision() * -vertical_velocity;
let expected_velocity = velocity_on_plane + walk_acceleration * ctx.frame_duration;
let expected_velocity_in_slipping_direction =
expected_velocity.dot(slipping_direction.adjust_precision());
let diff = required_veloicty_in_slipping_direction
- expected_velocity_in_slipping_direction;
if diff <= 0.0 {
break 'slipping_boost Vector3::ZERO;
}
slipping_direction.adjust_precision() * diff
};
TnuaVelChange {
acceleration: walk_acceleration,
boost: slipping_boost,
}
};
let upward_impulse: TnuaVelChange = 'upward_impulse: {
let should_disable_due_to_slipping =
slipping_vector.is_some() && memory.vertical_velocity <= 0.0;
for _ in 0..2 {
#[allow(clippy::unnecessary_cast)]
match &mut memory.airborne_timer {
None => {
if let (false, Some(sensor_output)) =
(should_disable_due_to_slipping, &sensors.ground.output)
{
let spring_offset =
config.float_height - sensor_output.proximity.adjust_precision();
memory.standing_offset =
-spring_offset * ctx.up_direction.adjust_precision();
break 'upward_impulse Self::spring_force(
&TnuaBasisAccess {
input: self,
config,
memory,
},
&ctx,
spring_offset,
);
} else {
memory.airborne_timer = Some(Timer::from_seconds(
config.coyote_time as f32,
TimerMode::Once,
));
continue;
}
}
Some(_) => {
if let (false, Some(sensor_output)) =
(should_disable_due_to_slipping, &sensors.ground.output)
&& sensor_output.proximity.adjust_precision() <= config.float_height
{
memory.airborne_timer = None;
continue;
}
if memory.vertical_velocity <= 0.0 {
break 'upward_impulse TnuaVelChange::acceleration(
-config.free_fall_extra_gravity
* ctx.up_direction.adjust_precision(),
);
} else {
break 'upward_impulse TnuaVelChange::ZERO;
}
}
}
}
error!("Tnua could not decide on jump state");
TnuaVelChange::ZERO
};
motor.lin = walk_vel_change + TnuaVelChange::boost(impulse_to_offset) + upward_impulse;
let new_velocity = memory.effective_velocity
+ motor.lin.boost
+ ctx.frame_duration * motor.lin.acceleration
- impulse_to_offset;
memory.running_velocity = new_velocity.reject_from(ctx.up_direction.adjust_precision());
let torque_to_fix_tilt = {
let tilted_up = ctx.tracker.rotation.mul_vec3(Vector3::Y);
let rotation_required_to_fix_tilt =
Quaternion::from_rotation_arc(tilted_up, ctx.up_direction.adjust_precision());
let desired_angvel = (rotation_required_to_fix_tilt.xyz() / ctx.frame_duration)
.clamp_length_max(config.tilt_offset_angvel);
let angular_velocity_diff = desired_angvel - ctx.tracker.angvel;
angular_velocity_diff.clamp_length_max(ctx.frame_duration * config.tilt_offset_angacl)
};
let desired_angvel = if let Some(desired_forward) = self.desired_forward {
let current_forward = ctx.tracker.rotation.mul_vec3(Vector3::NEG_Z);
let rotation_along_up_axis = rotation_arc_around_axis(
ctx.up_direction,
current_forward,
desired_forward.adjust_precision(),
)
.unwrap_or(0.0);
(rotation_along_up_axis / ctx.frame_duration)
.clamp(-config.turning_angvel, config.turning_angvel)
} else {
0.0
};
let existing_angvel = ctx.tracker.angvel.dot(ctx.up_direction.adjust_precision());
let torque_to_turn = desired_angvel - existing_angvel;
let existing_turn_torque = torque_to_fix_tilt.dot(ctx.up_direction.adjust_precision());
let torque_to_turn = torque_to_turn - existing_turn_torque;
motor.ang = TnuaVelChange::boost(
torque_to_fix_tilt + torque_to_turn * ctx.up_direction.adjust_precision(),
);
}
fn get_or_create_sensors<'a: 'b, 'b>(
up_direction: Dir3,
config: &'a Self::Config,
memory: &Self::Memory,
entities: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
proximity_sensors_query: &'b Query<(&TnuaProximitySensor, Has<TnuaGhostSensor>)>,
controller_entity: Entity,
commands: &mut Commands,
has_ghost_overwrites: bool,
) -> Option<Self::Sensors<'b>> {
let ground = ProximitySensorPreparationHelper {
cast_direction: -up_direction,
cast_range: config.float_height + config.cling_distance,
ghost_sensor: has_ghost_overwrites,
..Default::default()
}
.prepare_for(
&mut entities.ground,
proximity_sensors_query,
controller_entity,
commands,
);
let headroom = if let Some(headroom) = config.headroom.as_ref() {
ProximitySensorPreparationHelper {
cast_direction: up_direction,
cast_range: headroom.distance_to_collider_top
+ headroom.sensor_extra_distance
+ memory.extra_headroom,
..Default::default()
}
.prepare_for(
&mut entities.headroom,
proximity_sensors_query,
controller_entity,
commands,
)
} else {
ProximitySensorPreparationHelper::ensure_not_existing(
&mut entities.headroom,
proximity_sensors_query,
commands,
)
};
Some(Self::Sensors {
ground: ground?,
headroom,
})
}
fn ghost_sensor_overwrites<'a>(
ghost_overwrites: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::GhostOverwrites,
entities: &<Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
) -> impl Iterator<Item = (&'a mut TnuaGhostOverwrite, Entity)> {
[(&mut ghost_overwrites.ground, entities.ground)]
.into_iter()
.flat_map(|(o, e)| Some((o, e?)))
}
}
impl TnuaBasisWithFrameOfReferenceSurface for TnuaBuiltinWalk {
fn effective_velocity(access: &TnuaBasisAccess<Self>) -> Vector3 {
access.memory.effective_velocity
}
fn vertical_velocity(access: &TnuaBasisAccess<Self>) -> Float {
access.memory.vertical_velocity
}
}
impl TnuaBasisWithDisplacement for TnuaBuiltinWalk {
fn displacement(access: &TnuaBasisAccess<Self>) -> Option<Vector3> {
match access.memory.airborne_timer {
None => Some(access.memory.standing_offset),
Some(_) => None,
}
}
}
impl TnuaBasisWithGround for TnuaBuiltinWalk {
fn is_airborne(access: &TnuaBasisAccess<Self>) -> bool {
access
.memory
.airborne_timer
.as_ref()
.is_some_and(|timer| timer.is_finished())
}
fn violate_coyote_time(memory: &mut Self::Memory) {
if let Some(timer) = &mut memory.airborne_timer {
timer.set_duration(Duration::ZERO);
}
}
fn ground_sensor<'a>(sensors: &Self::Sensors<'a>) -> &'a TnuaProximitySensor {
sensors.ground
}
}
impl TnuaBasisWithHeadroom for TnuaBuiltinWalk {
fn headroom_intrusion<'a>(
access: &TnuaBasisAccess<Self>,
sensors: &Self::Sensors<'a>,
) -> Option<std::ops::Range<Float>> {
let headroom_config = access.config.headroom.as_ref()?;
let headroom_sensor_output = sensors.headroom?.output.as_ref()?;
Some(headroom_config.distance_to_collider_top..headroom_sensor_output.proximity)
}
fn set_extra_headroom(memory: &mut Self::Memory, extra_headroom: Float) {
memory.extra_headroom = extra_headroom.max(0.0);
}
}
impl TnuaBasisWithFloating for TnuaBuiltinWalk {
fn float_height(access: &TnuaBasisAccess<Self>) -> Float {
access.config.float_height
}
}
impl TnuaBasisWithSpring for TnuaBuiltinWalk {
fn spring_force(
access: &TnuaBasisAccess<Self>,
ctx: &TnuaBasisContext,
spring_offset: Float,
) -> TnuaVelChange {
let spring_force: Float = spring_offset * access.config.spring_strength;
let relative_velocity = access
.memory
.effective_velocity
.dot(ctx.up_direction.adjust_precision())
- access.memory.vertical_velocity;
let gravity_compensation = -ctx.tracker.gravity;
let dampening_boost = relative_velocity * access.config.spring_dampening;
TnuaVelChange {
acceleration: ctx.up_direction.adjust_precision() * spring_force + gravity_compensation,
boost: ctx.up_direction.adjust_precision() * -dampening_boost,
}
}
}
#[derive(Debug, Clone)]
struct ClimbVectors {
direction: Vector3,
sideways: Vector3,
}
impl ClimbVectors {
fn project(&self, vector: Vector3) -> Vector3 {
let axis_direction = vector.dot(self.direction) * self.direction;
let axis_sideways = vector.dot(self.sideways) * self.sideways;
axis_direction + axis_sideways
}
}