bevy_tnua/control_helpers/
simple_fall_through_platforms.rs

1use bevy::platform::collections::HashSet;
2use bevy::prelude::*;
3use bevy_tnua_physics_integration_layer::math::Float;
4
5use crate::TnuaGhostSensor;
6use crate::ghost_overrides::TnuaGhostOverwrite;
7
8/// Helper component for implementing fall-through platforms.
9///
10/// See <https://github.com/idanarye/bevy-tnua/wiki/Jump-fall-Through-Platforms>
11///
12/// Place this component on the character entity (the one that has the
13/// [`TnuaController`](crate::TnuaController), use [`with`](Self::with), and call one of the
14/// methods of [the returned handle object](TnuaHandleForSimpleFallThroughPlatformsHelper). See the
15/// description of these methods to determine which one to call.
16#[derive(Component, Default)]
17pub struct TnuaSimpleFallThroughPlatformsHelper {
18    currently_falling_through: HashSet<Entity>,
19}
20
21impl TnuaSimpleFallThroughPlatformsHelper {
22    /// Get an handle for operating the helper.
23    ///
24    /// The `min_proximity` argument is the minimal distance from the origin of the cast ray/shape
25    /// (usually the center of the character) to the platform. If the distance to the platform is
26    /// below that, the helper will assume that the character only jumped halfway through it, not
27    /// high enough to stand on it.
28    pub fn with<'a>(
29        &'a mut self,
30        ghost_overwrite: &'a mut TnuaGhostOverwrite,
31        ghost_sensor: &'a TnuaGhostSensor,
32        min_proximity: Float,
33    ) -> TnuaHandleForSimpleFallThroughPlatformsHelper<'a> {
34        TnuaHandleForSimpleFallThroughPlatformsHelper {
35            parent: self,
36            ghost_overwrite,
37            ghost_sensor,
38            min_proximity,
39        }
40    }
41}
42
43/// Handle for working with [`TnuaSimpleFallThroughPlatformsHelper`].
44///
45/// This object should be created each frame, and one of its methods should be called depending on
46/// whether the character wants to keep standing on the platform or fall through it.
47pub struct TnuaHandleForSimpleFallThroughPlatformsHelper<'a> {
48    parent: &'a mut TnuaSimpleFallThroughPlatformsHelper,
49    ghost_overwrite: &'a mut TnuaGhostOverwrite,
50    ghost_sensor: &'a TnuaGhostSensor,
51    min_proximity: Float,
52}
53
54impl TnuaHandleForSimpleFallThroughPlatformsHelper<'_> {
55    /// Call this method to make the character stand on the platform (if there is any)
56    pub fn dont_fall(&mut self) {
57        let mut already_falling_through_not_yet_seen =
58            self.parent.currently_falling_through.clone();
59        self.ghost_overwrite
60            .set(self.ghost_sensor.iter().find(|ghost_platform| {
61                self.min_proximity <= ghost_platform.proximity
62                    && !already_falling_through_not_yet_seen.remove(&ghost_platform.entity)
63            }));
64        self.parent
65            .currently_falling_through
66            .retain(|entity| !already_falling_through_not_yet_seen.contains(entity));
67    }
68
69    /// Call this method to make the character drop through the platform.
70    ///
71    /// The character will fall through the first layer of ghost platforms detected since the last
72    /// time it was called with `just_pressed` being `true`. This means that:
73    ///
74    /// * To let the player fall through all the platforms by simply holding the button, call this
75    ///   with `just_pressed = true` as long as the button is held.
76    /// * To let the player fall through one layer of platforms at a time, forcing them to release
77    ///   and press again for each layer, pass `just_pressed = true` only when the button really is
78    ///   just pressed.
79    ///
80    /// Returns `true` if actually dropping through a platform, to help determining if the
81    /// character should be crouching (since these buttons are usually the same)
82    pub fn try_falling(&mut self, just_pressed: bool) -> bool {
83        if !just_pressed && !self.parent.currently_falling_through.is_empty() {
84            self.ghost_overwrite
85                .set(self.ghost_sensor.iter().find(|ghost_platform| {
86                    self.min_proximity <= ghost_platform.proximity
87                        && !self
88                            .parent
89                            .currently_falling_through
90                            .contains(&ghost_platform.entity)
91                }));
92            true
93        } else {
94            self.ghost_overwrite.set(None);
95            self.parent.currently_falling_through.clear();
96            for ghost_platform in self.ghost_sensor.iter() {
97                if self.min_proximity <= ghost_platform.proximity {
98                    self.parent
99                        .currently_falling_through
100                        .insert(ghost_platform.entity);
101                }
102            }
103            !self.parent.currently_falling_through.is_empty()
104        }
105    }
106}