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