bevy_tnua/util/
mod.rs

1mod command_impl_helpers;
2mod velocity_boundary;
3
4use bevy::prelude::*;
5use bevy_tnua_physics_integration_layer::{
6    data_for_backends::TnuaVelChange,
7    math::{AdjustPrecision, Float, Quaternion, Vector2, Vector3},
8};
9pub use command_impl_helpers::MotionHelper;
10pub use velocity_boundary::VelocityBoundary;
11
12/// Calculate the kinetic energy required to jump to a certain height when different gravity is
13/// applied in different segments of the jump.
14///
15/// **MOTIVATION**: Ballistically accurate jumps where the gravity is constant don't feel good in
16/// games. To improve the player experience, Tnua applies higher gravity in different segments of
17/// the jump (e.g. to get faster takeoff or to reduce the airtime at the tip of the jump). Being
18/// able to control the height of the jump is still vital though, and needs to be done by setting
19/// the initial upward velocity of the jump. `SegmentedJumpInitialVelocityCalculator` is a tool for
20/// calculating the latter from the former.
21///
22/// ```
23/// # use bevy_tnua::util::SegmentedJumpInitialVelocityCalculator;
24/// # use bevy_tnua::math::Float;
25/// # let jump_height = 2.0;
26/// # const GRAVITY: Float = 9.81;
27/// let takeoff_upward_velocity = SegmentedJumpInitialVelocityCalculator::new(jump_height)
28///     // When upward velocity is below 1.0, use an extra gravity of 20.0
29///     .add_segment(GRAVITY + 20.0, 1.0)
30///     // When upward velocity is between 1.0 and 2.0, use regular gravity
31///     .add_segment(GRAVITY, 2.0)
32///     // When upward velocity is higher than 2.0, use an extra gravity of 30.0
33///     .add_final_segment(GRAVITY + 30.0)
34///     // After adding all the segments, get the velocity required to make such a jump
35///     .required_initial_velocity()
36///     .expect("`add_final_segment` should have covered remaining height");
37/// ```
38///
39/// Note that:
40///
41/// * Only the part of the jump where the character goes up is relevant here. The part after the
42///   peak where the character goes down may have its own varying gravity, but since that gravity
43///   can not affect the height of the jump `SegmentedJumpInitialVelocityCalculator` does not need
44///   to care about it.
45/// * Segments are calculated from top to bottom. The very top - the peak of the jump - has, by
46///   definition, zero upward velocity, so the `velocity_threshold` passed to it is the one at the
47///   bottom. The last segment should have `INFINITY` as its velocity.
48/// * The internal representation and calculation is with kinetic energy for a rigid body with a
49///   mass of 1.0 rather than with velocities.
50pub struct SegmentedJumpInitialVelocityCalculator {
51    height: Float,
52    kinetic_energy: Float,
53}
54
55/// Thrown when attempting to retrieve the result of [`SegmentedJumpInitialVelocityCalculator`]
56/// without converting all the height to kinetic energy.
57#[derive(thiserror::Error, Debug)]
58#[error("Engergy or velocity retrived while not all height was coverted")]
59pub struct LeftoverHeight;
60
61impl SegmentedJumpInitialVelocityCalculator {
62    /// Create a `SegmentedJumpInitialVelocityCalculator` ready to calculate the velocity required
63    /// for a jump of the specified height.
64    pub fn new(total_height: Float) -> Self {
65        Self {
66            height: total_height,
67            kinetic_energy: 0.0,
68        }
69    }
70
71    /// Convert height to kinetic energy for segment under the given gravity.
72    ///
73    /// The segment is specified by velocity. The bottom determined by the `velocity_threshold`
74    /// argument and the top is the bottom of the previous call to `add_segment` - or the peak of
75    /// the jump, if this is the first call.
76    ///
77    /// If there is no height left to convert, nothing will be changed.
78    pub fn add_segment(&mut self, gravity: Float, velocity_threshold: Float) -> &mut Self {
79        if self.height <= 0.0 {
80            // No more height to jump
81            return self;
82        }
83
84        let kinetic_energy_at_velocity_threshold = 0.5 * velocity_threshold.powi(2);
85
86        let transferred_energy = kinetic_energy_at_velocity_threshold - self.kinetic_energy;
87        if transferred_energy <= 0.0 {
88            // Already faster than that velocity
89            return self;
90        }
91
92        let segment_height = transferred_energy / gravity;
93        if self.height < segment_height {
94            // This segment will be the last
95            self.add_final_segment(gravity);
96        } else {
97            self.kinetic_energy += transferred_energy;
98            self.height -= segment_height;
99        }
100
101        self
102    }
103
104    /// Convert the remaining height to kinetic energy under the given gravity.
105    pub fn add_final_segment(&mut self, gravity: Float) -> &mut Self {
106        self.kinetic_energy += self.height * gravity;
107        self.height = 0.0;
108        self
109    }
110
111    /// The kinetic energy required to make the jump.
112    ///
113    /// This should only be called after _all_ the height was converted - otherwise it'll return a
114    /// [`LeftoverHeight`] error.
115    pub fn kinetic_energy(&self) -> Result<Float, LeftoverHeight> {
116        if 0.0 < self.height {
117            Err(LeftoverHeight)
118        } else {
119            Ok(self.kinetic_energy)
120        }
121    }
122
123    /// Convert kinetic energy to velocity for a rigid body with a mass of 1.0.
124    pub fn kinetic_energy_to_velocity(kinetic_energy: Float) -> Float {
125        (2.0 * kinetic_energy).sqrt()
126    }
127
128    /// The initial upward velocity required to make the jump.
129    ///
130    /// This should only be called after _all_ the height was converted - otherwise it'll return a
131    /// [`LeftoverHeight`] error.
132    pub fn required_initial_velocity(&self) -> Result<Float, LeftoverHeight> {
133        Ok(Self::kinetic_energy_to_velocity(self.kinetic_energy()?))
134    }
135}
136
137pub struct SegmentedJumpDurationCalculator {
138    velocity: Float,
139    duration: Float,
140}
141
142impl SegmentedJumpDurationCalculator {
143    pub fn new(initial_velocity: Float) -> Self {
144        Self {
145            velocity: initial_velocity,
146            duration: 0.0,
147        }
148    }
149
150    pub fn add_segment(&mut self, gravity: Float, velocity_threshold: Float) -> &mut Self {
151        if velocity_threshold < self.velocity {
152            let lost_velocity = self.velocity - velocity_threshold;
153            self.velocity = velocity_threshold;
154            self.duration += lost_velocity / gravity;
155        }
156        self
157    }
158
159    pub fn duration(&self) -> Float {
160        self.duration
161    }
162}
163
164/// Calculate the rotation around `around_axis` required to rotate the character from
165/// `current_forward` to `desired_forward`.
166pub fn rotation_arc_around_axis(
167    around_axis: Dir3,
168    current_forward: Vector3,
169    desired_forward: Vector3,
170) -> Option<Float> {
171    let around_axis: Vector3 = around_axis.adjust_precision();
172    let rotation_plane_x = current_forward.reject_from(around_axis).try_normalize()?;
173    let rotation_plane_y = around_axis.cross(rotation_plane_x);
174    let desired_forward_in_plane_coords = Vector2::new(
175        rotation_plane_x.dot(desired_forward),
176        rotation_plane_y.dot(desired_forward),
177    )
178    .try_normalize()?;
179    let rotation_to_set_forward =
180        Quaternion::from_rotation_arc_2d(Vector2::X, desired_forward_in_plane_coords);
181    Some(rotation_to_set_forward.xyz().z)
182}
183
184/// Temporary until we get an official release of the physics integration layer crate with
185/// `calc_boost` in it.
186pub(crate) fn calc_boost(
187    vel_change: &bevy_tnua_physics_integration_layer::data_for_backends::TnuaVelChange,
188    frame_duration: Float,
189) -> Vector3 {
190    vel_change.acceleration * frame_duration + vel_change.boost
191}
192
193pub fn calc_angular_velchange_to_force_forward(
194    force_forward: Dir3,
195    current_rotation: Quaternion,
196    current_angvel: Vector3,
197    up_direction: Dir3,
198    frame_duration: Float,
199) -> TnuaVelChange {
200    let current_forward = current_rotation.mul_vec3(Vector3::NEG_Z);
201    let rotation_along_up_axis = rotation_arc_around_axis(
202        up_direction,
203        current_forward,
204        force_forward.adjust_precision(),
205    )
206    .unwrap_or(0.0);
207    let desired_angvel = rotation_along_up_axis / frame_duration;
208
209    let existing_angvel = current_angvel.dot(up_direction.adjust_precision());
210
211    let torque_to_turn = desired_angvel - existing_angvel;
212
213    TnuaVelChange::boost(torque_to_turn * up_direction.adjust_precision())
214}