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}