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}