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}