bevy_tnua/builtins/
climb.rs

1use bevy::prelude::*;
2use bevy_tnua_physics_integration_layer::math::{AdjustPrecision, AsF32, Float};
3use serde::{Deserialize, Serialize};
4
5use crate::basis_capabilities::TnuaBasisWithGround;
6use crate::util::MotionHelper;
7use crate::{
8    TnuaAction, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
9    TnuaActionLifecycleStatus, TnuaMotor, math::Vector3,
10};
11use crate::{TnuaActionContext, TnuaBasis};
12
13/// An [action](TnuaAction) for climbing on things.
14#[derive(Clone, Default)]
15#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
16pub struct TnuaBuiltinClimb {
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    /// The direction (in the world space) and speed to climb at (move up/down the entity)
29    pub desired_climb_motion: Vector3,
30
31    /// Force the character to face in a particular direction.
32    pub desired_forward: Option<Dir3>,
33
34    /// Prevent the character from climbing above this point.
35    ///
36    /// Tip: use
37    /// [`probe_extent_from_closest_point`](crate::radar_lens::TnuaRadarBlipLens::probe_extent_from_closest_point)
38    /// to find this point.
39    pub hard_stop_up: Option<Vector3>,
40
41    /// Prevent the character from climbing below this point.
42    ///
43    /// Tip: use
44    /// [`probe_extent_from_closest_point`](crate::radar_lens::TnuaRadarBlipLens::probe_extent_from_closest_point)
45    /// to find this point.
46    pub hard_stop_down: Option<Vector3>,
47}
48
49#[derive(Clone, Serialize, Deserialize)]
50pub struct TnuaBuiltinClimbConfig {
51    /// Speed for maintaining [`desired_vec_to_anchor`](TnuaBuiltinClimb::desired_vec_to_anchor).
52    pub anchor_speed: Float,
53
54    /// Acceleration for maintaining
55    /// [`desired_vec_to_anchor`](TnuaBuiltinClimb::desired_vec_to_anchor).
56    pub anchor_acceleration: Float,
57
58    // How fast the character will climb.
59    //
60    // Note that this will be the speed when
61    // [`desired_climb_motion`](TnuaBuiltinClimb::desired_climb_motion) is a unit vector - meaning
62    // that its length is 1.0. If its not 1.0, the speed will be a multiply of that length.
63    pub climb_speed: Float,
64
65    /// The acceleration to climb at.
66    pub climb_acceleration: Float,
67
68    /// The time, in seconds, the character can still jump after letting go.
69    pub coyote_time: Float,
70}
71
72impl Default for TnuaBuiltinClimbConfig {
73    fn default() -> Self {
74        Self {
75            anchor_speed: 150.0,
76            anchor_acceleration: 500.0,
77            climb_speed: 10.0,
78            climb_acceleration: 30.0,
79            coyote_time: 0.15,
80        }
81    }
82}
83
84impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinClimb
85where
86    B: TnuaBasisWithGround,
87{
88    type Config = TnuaBuiltinClimbConfig;
89    type Memory = TnuaBuiltinClimbMemory;
90
91    // const VIOLATES_COYOTE_TIME: bool = true;
92
93    fn initiation_decision(
94        &self,
95        _config: &Self::Config,
96        _sensors: &B::Sensors<'_>,
97        _ctx: TnuaActionContext<B>,
98        _being_fed_for: &bevy::time::Stopwatch,
99    ) -> TnuaActionInitiationDirective {
100        TnuaActionInitiationDirective::Allow
101    }
102
103    fn apply(
104        &self,
105        config: &Self::Config,
106        memory: &mut Self::Memory,
107        _sensors: &B::Sensors<'_>,
108        ctx: TnuaActionContext<B>,
109        lifecycle_status: TnuaActionLifecycleStatus,
110        motor: &mut TnuaMotor,
111    ) -> TnuaActionLifecycleDirective {
112        // TODO: Once `std::mem::variant_count` gets stabilized, use that instead. The idea is to
113        // allow jumping through multiple states but failing if we get into loop.
114        for _ in 0..2 {
115            return match memory {
116                TnuaBuiltinClimbMemory::Climbing { climbing_velocity } => {
117                    if matches!(lifecycle_status, TnuaActionLifecycleStatus::NoLongerFed) {
118                        *memory = TnuaBuiltinClimbMemory::Coyote(Timer::from_seconds(
119                            config.coyote_time.f32(),
120                            TimerMode::Once,
121                        ));
122                        continue;
123                    }
124
125                    // TODO: maybe this should try to predict the next-frame velocity? Is there a
126                    // point?
127                    *climbing_velocity = ctx
128                        .tracker
129                        .velocity
130                        .project_onto(ctx.up_direction.adjust_precision());
131
132                    motor
133                        .lin
134                        .cancel_on_axis(ctx.up_direction.adjust_precision());
135                    motor.lin += ctx.negate_gravity();
136                    motor.lin += ctx.adjust_vertical_velocity(
137                        config.climb_speed
138                            * self
139                                .desired_climb_motion
140                                .dot(ctx.up_direction.adjust_precision()),
141                        config.climb_acceleration,
142                    );
143
144                    if let Some(stop_at) = self.hard_stop_up {
145                        motor.lin += ctx.hard_stop(ctx.up_direction, stop_at, &motor.lin);
146                    }
147                    if let Some(stop_at) = self.hard_stop_down {
148                        motor.lin += ctx.hard_stop(-ctx.up_direction, stop_at, &motor.lin);
149                    }
150
151                    let vec_to_anchor = (self.anchor - ctx.tracker.translation)
152                        .reject_from(ctx.up_direction().adjust_precision());
153                    let horizontal_displacement = self.desired_vec_to_anchor - vec_to_anchor;
154
155                    let desired_horizontal_velocity = -horizontal_displacement / ctx.frame_duration;
156
157                    motor.lin += ctx.adjust_horizontal_velocity(
158                        desired_horizontal_velocity.clamp_length_max(config.anchor_speed),
159                        config.anchor_acceleration,
160                    );
161
162                    if let Some(desired_forward) = self.desired_forward {
163                        motor
164                            .ang
165                            .cancel_on_axis(ctx.up_direction.adjust_precision());
166                        motor.ang += ctx.turn_to_direction(desired_forward, ctx.up_direction);
167                    }
168
169                    lifecycle_status.directive_simple()
170                }
171                TnuaBuiltinClimbMemory::Coyote(timer) => {
172                    if timer.tick(ctx.frame_duration_as_duration()).is_finished() {
173                        TnuaActionLifecycleDirective::Finished
174                    } else {
175                        lifecycle_status.directive_linger()
176                    }
177                }
178            };
179        }
180        error!("Tnua could not decide on climb state");
181        TnuaActionLifecycleDirective::Finished
182    }
183
184    fn influence_basis(
185        &self,
186        _config: &Self::Config,
187        _memory: &Self::Memory,
188        _ctx: crate::TnuaBasisContext,
189        _basis_input: &B,
190        _basis_config: &<B as TnuaBasis>::Config,
191        basis_memory: &mut <B as TnuaBasis>::Memory,
192    ) {
193        B::violate_coyote_time(basis_memory);
194    }
195}
196
197#[derive(Debug)]
198#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
199pub enum TnuaBuiltinClimbMemory {
200    Climbing { climbing_velocity: Vector3 },
201    Coyote(Timer),
202}
203
204impl Default for TnuaBuiltinClimbMemory {
205    fn default() -> Self {
206        Self::Climbing {
207            climbing_velocity: Vector3::ZERO,
208        }
209    }
210}