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