bevy_tnua/util/
velocity_boundary.rs

1use std::time::Duration;
2
3use crate::math::{AdjustPrecision, AsF32, Float, Vector3};
4use bevy::prelude::*;
5
6/// An indication that a character was knocked back and "struggles" to get back to its original
7/// velocity.
8#[derive(Clone, Debug)]
9#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
10pub struct VelocityBoundary {
11    base: Float,
12    original_frontier: Float,
13    frontier: Float,
14    pub direction: Dir3,
15    no_push_timer: Timer,
16}
17
18impl VelocityBoundary {
19    pub fn new(
20        disruption_from: Vector3,
21        disruption_to: Vector3,
22        no_push_timeout: f32,
23    ) -> Option<Self> {
24        let Ok(disruption_direction) = Dir3::new((disruption_to - disruption_from).f32()) else {
25            return None;
26        };
27        let frontier = disruption_to.dot(disruption_direction.adjust_precision());
28        Some(Self {
29            base: disruption_from.dot(disruption_direction.adjust_precision()),
30            original_frontier: frontier,
31            frontier,
32            direction: disruption_direction,
33            no_push_timer: Timer::from_seconds(no_push_timeout, TimerMode::Once),
34        })
35    }
36
37    /// Call this every frame to update the velocity boundary.
38    ///
39    /// This methos takes care of "clearing" the boundary when it gets "pushed" (the character's
40    /// actual velocity goes past the boundary).
41    ///
42    /// This method does not detect when the boundary is cleared - use
43    /// [`is_cleared`](Self::is_cleared) for that purpose
44    ///
45    /// This method does not apply the boundary - it only updates it. To apply the boundary, use
46    /// [`calc_boost_part_on_boundary_axis_after_limit`](Self::calc_boost_part_on_boundary_axis_after_limit)
47    /// to determine how to alter the acceleration.
48    ///
49    /// # Arguments:
50    ///
51    /// * `velocity` - the velocity as reported by the physics backend. This is the data tracked in
52    ///   the [`TnuaRigidBodyTracker`](crate::TnuaRigidBodyTracker), so a typical basis or action
53    ///   will get it from [`TnuaBasisContext::tracker`](crate::TnuaBasisContext::tracker).
54    /// * `frame_duration` - the duration of the current frame, in seconds.
55    pub fn update(&mut self, velocity: Vector3, frame_duration: Duration) {
56        let new_frontier = velocity.dot(self.direction.adjust_precision());
57        if new_frontier < self.frontier {
58            self.frontier = new_frontier;
59            self.no_push_timer.reset();
60        } else {
61            self.no_push_timer.tick(frame_duration);
62        }
63    }
64
65    pub fn is_cleared(&self) -> bool {
66        self.no_push_timer.is_finished() || self.frontier <= self.base
67    }
68
69    /// Calculate how a boost needs to be adjusted according to the boundary.
70    ///
71    /// Note that the returned value is the boost limit only on the axis of the returned direction.
72    /// The other axes should remain the same (unless the caller has a good reason to modify them).
73    /// The reason why this method doesn't simply return the final boost is that the caller may be
74    /// using [`TnuaVelChange`](crate::TnuaVelChange) which combines acceleration and impulse, and
75    /// if so then it is the caller's responsibility to amend the result of this method to match
76    /// that scheme.
77    ///
78    /// # Arguments:
79    ///
80    /// * `current_velocity` - the velocity of the character **before the boost**.
81    /// * `regular_boost` - the boost that the caller would have applied to the character before
82    ///   taking the boundary into account.
83    /// * `boost_limit_inside_barrier` - the maximum boost allowed inside a fully strength barrier,
84    ///   assuming it goes directly against the direction of the boundary.
85    /// * `barrier_strength_diminishing` - an exponent describing how the boundary strength
86    ///   diminishes when the barrier gets cleared. For best results, set it to values larger than
87    ///   1.0.
88    pub fn calc_boost_part_on_boundary_axis_after_limit(
89        &self,
90        current_velocity: Vector3,
91        regular_boost: Vector3,
92        boost_limit_inside_barrier: Float,
93        barrier_strength_diminishing: Float,
94    ) -> Option<(Dir3, Float)> {
95        let boost = regular_boost.dot(self.direction.adjust_precision());
96        if 0.0 <= boost {
97            // Not pushing the barrier
98            return None;
99        }
100        let current = current_velocity.dot(self.direction.adjust_precision());
101        let after_boost = current + boost;
102        if self.frontier <= after_boost {
103            return None;
104        }
105        let boost_before_barrier = (current - self.frontier).max(0.0);
106        let fraction_before_frontier = boost_before_barrier / -boost;
107        let fraction_after_frontier = 1.0 - fraction_before_frontier;
108        let push_inside_barrier = fraction_after_frontier * boost_limit_inside_barrier;
109        let barrier_depth = self.frontier - self.base;
110        if barrier_depth <= 0.0 {
111            return None;
112        }
113        let fraction_inside_barrier = if push_inside_barrier <= barrier_depth {
114            fraction_after_frontier
115        } else {
116            barrier_depth / boost_limit_inside_barrier
117        }
118        .clamp(0.0, 1.0);
119
120        let boost_outside_barrier = (1.0 - fraction_inside_barrier) * boost;
121        // Make it negative here, because this is the one that pushes against the barrier
122        let boost_inside_barrier = fraction_inside_barrier * -boost_limit_inside_barrier;
123
124        let total_boost = boost_outside_barrier + boost_inside_barrier;
125
126        let barrier_strength = self.percentage_left().powf(barrier_strength_diminishing);
127        let total_boost = (1.0 - barrier_strength) * boost + barrier_strength * total_boost;
128
129        Some((-self.direction, -total_boost))
130    }
131
132    fn percentage_left(&self) -> Float {
133        let current_depth = self.frontier - self.base;
134        let original_depth = self.original_frontier - self.base;
135        current_depth / original_depth
136    }
137}