bevy_tnua/builtins/
climb.rs

1use bevy::prelude::*;
2use bevy_tnua_physics_integration_layer::math::{AdjustPrecision, AsF32, Float};
3
4use crate::util::MotionHelper;
5use crate::TnuaActionContext;
6use crate::{
7    math::Vector3, TnuaAction, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
8    TnuaActionLifecycleStatus, TnuaMotor,
9};
10
11/// An [action](TnuaAction) for climbing on things.
12#[derive(Clone)]
13pub struct TnuaBuiltinClimb {
14    /// The entity being climbed on.
15    pub climbable_entity: Option<Entity>,
16
17    /// A point on the climbed entity where the character touches it.
18    ///
19    /// Note that this does not actually have to be on any actual collider. It can be a point
20    /// in the middle of the air, and the action will cause the character to pretend there is something there and climb on it.
21    pub anchor: Vector3,
22
23    /// The position of the [`anchor`](Self::anchor) compared to the character.
24    ///
25    /// The action will try to maintain this horizontal relative position.
26    pub desired_vec_to_anchor: Vector3,
27
28    /// Speed for maintaining [`desired_vec_to_anchor`](Self::desired_vec_to_anchor).
29    pub anchor_speed: Float,
30
31    /// Acceleration for maintaining [`desired_vec_to_anchor`](Self::desired_vec_to_anchor).
32    pub anchor_acceleration: Float,
33
34    /// The velocity to climb at (move up/down the entity)
35    pub desired_climb_velocity: Vector3,
36
37    /// The acceleration to climb at.
38    pub climb_acceleration: Float,
39
40    /// The time, in seconds, the character can still jump after letting go.
41    pub coyote_time: Float,
42
43    /// Force the character to face in a particular direction.
44    pub desired_forward: Option<Dir3>,
45
46    /// Prevent the character from climbing above this point.
47    ///
48    /// Tip: use
49    /// [`probe_extent_from_closest_point`](crate::radar_lens::TnuaRadarBlipLens::probe_extent_from_closest_point)
50    /// to find this point.
51    pub hard_stop_up: Option<Vector3>,
52
53    /// Prevent the character from climbing below this point.
54    ///
55    /// Tip: use
56    /// [`probe_extent_from_closest_point`](crate::radar_lens::TnuaRadarBlipLens::probe_extent_from_closest_point)
57    /// to find this point.
58    pub hard_stop_down: Option<Vector3>,
59
60    /// The direction used to initiate the climb.
61    ///
62    /// This field is not used by the action itself. It's purpose is to help user controller
63    /// systems determine if the player input is a continuation of the motion used to initiate the
64    /// climb, or if it's a motion for breaking from the climb.
65    pub initiation_direction: Vector3,
66}
67
68impl Default for TnuaBuiltinClimb {
69    fn default() -> Self {
70        Self {
71            climbable_entity: None,
72            anchor: Vector3::NAN,
73            desired_vec_to_anchor: Vector3::ZERO,
74            anchor_speed: 150.0,
75            anchor_acceleration: 500.0,
76            desired_climb_velocity: Vector3::ZERO,
77            climb_acceleration: 30.0,
78            coyote_time: 0.15,
79            desired_forward: None,
80            hard_stop_up: None,
81            hard_stop_down: None,
82            initiation_direction: Vector3::ZERO,
83        }
84    }
85}
86
87impl TnuaAction for TnuaBuiltinClimb {
88    const NAME: &'static str = "TnuaBuiltinClimb";
89
90    type State = TnuaBuiltinClimbState;
91
92    const VIOLATES_COYOTE_TIME: bool = true;
93
94    fn apply(
95        &self,
96        state: &mut Self::State,
97        ctx: TnuaActionContext,
98        lifecycle_status: TnuaActionLifecycleStatus,
99        motor: &mut TnuaMotor,
100    ) -> TnuaActionLifecycleDirective {
101        // TODO: Once `std::mem::variant_count` gets stabilized, use that instead. The idea is to
102        // allow jumping through multiple states but failing if we get into loop.
103        for _ in 0..2 {
104            return match state {
105                TnuaBuiltinClimbState::Climbing { climbing_velocity } => {
106                    if matches!(lifecycle_status, TnuaActionLifecycleStatus::NoLongerFed) {
107                        *state = TnuaBuiltinClimbState::Coyote(Timer::from_seconds(
108                            self.coyote_time.f32(),
109                            TimerMode::Once,
110                        ));
111                        continue;
112                    }
113
114                    // TODO: maybe this should try to predict the next-frame velocity? Is there a
115                    // point?
116                    *climbing_velocity = ctx
117                        .tracker
118                        .velocity
119                        .project_onto(ctx.up_direction.adjust_precision());
120
121                    motor
122                        .lin
123                        .cancel_on_axis(ctx.up_direction.adjust_precision());
124                    motor.lin += ctx.negate_gravity();
125                    motor.lin += ctx.adjust_vertical_velocity(
126                        self.desired_climb_velocity
127                            .dot(ctx.up_direction.adjust_precision()),
128                        self.climb_acceleration,
129                    );
130
131                    if let Some(stop_at) = self.hard_stop_up {
132                        motor.lin += ctx.hard_stop(ctx.up_direction, stop_at, &motor.lin);
133                    }
134                    if let Some(stop_at) = self.hard_stop_down {
135                        motor.lin += ctx.hard_stop(-ctx.up_direction, stop_at, &motor.lin);
136                    }
137
138                    let vec_to_anchor = (self.anchor - ctx.tracker.translation)
139                        .reject_from(ctx.up_direction().adjust_precision());
140                    let horizontal_displacement = self.desired_vec_to_anchor - vec_to_anchor;
141
142                    let desired_horizontal_velocity = -horizontal_displacement / ctx.frame_duration;
143
144                    motor.lin += ctx.adjust_horizontal_velocity(
145                        desired_horizontal_velocity.clamp_length_max(self.anchor_speed),
146                        self.anchor_acceleration,
147                    );
148
149                    if let Some(desired_forward) = self.desired_forward {
150                        motor
151                            .ang
152                            .cancel_on_axis(ctx.up_direction.adjust_precision());
153                        motor.ang += ctx.turn_to_direction(desired_forward, ctx.up_direction);
154                    }
155
156                    lifecycle_status.directive_simple()
157                }
158                TnuaBuiltinClimbState::Coyote(timer) => {
159                    if timer.tick(ctx.frame_duration_as_duration()).finished() {
160                        TnuaActionLifecycleDirective::Finished
161                    } else {
162                        lifecycle_status.directive_linger()
163                    }
164                }
165            };
166        }
167        error!("Tnua could not decide on climb state");
168        TnuaActionLifecycleDirective::Finished
169    }
170
171    fn initiation_decision(
172        &self,
173        _ctx: TnuaActionContext,
174        _being_fed_for: &bevy::time::Stopwatch,
175    ) -> TnuaActionInitiationDirective {
176        TnuaActionInitiationDirective::Allow
177    }
178
179    fn target_entity(&self, _state: &Self::State) -> Option<Entity> {
180        self.climbable_entity
181    }
182}
183
184#[derive(Debug)]
185pub enum TnuaBuiltinClimbState {
186    Climbing { climbing_velocity: Vector3 },
187    Coyote(Timer),
188}
189
190impl Default for TnuaBuiltinClimbState {
191    fn default() -> Self {
192        Self::Climbing {
193            climbing_velocity: Vector3::ZERO,
194        }
195    }
196}