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}