Skip to content

Jump fall Through Platforms

Idan Arye edited this page Jun 5, 2023 · 12 revisions

Jump/fall through platforms are Rapier colliders that:

  1. Have their solver groups (not collision groups) set to not resolve the contact with the character entity.
  2. Have the TnuaGhostPlatform component.

By default, the player character will treat these platforms as thin air. To be able to stand on them, special intervention is required.

Preparation

The jump/fall through platforms will need to have TnuaGhostPlatform and SolverGroups configured as mentioned above:

cmd.insert(SolverGroups {
    // Or pick some other configuration - as long as it excludes the character
    // that needs to jump or fall through this platform.
    memberships: Group::empty(),
    filters: Group::empty(),
});
cmd.insert(TnuaGhostPlatform);

The character entity needs, in addition to its TnuaProximitySensor, to also have TnuaGhostSensor. This sensor will contain a list of all the ghost sensor.

The user system that operates it needs to run inside the TnuaUserControlsSystemSet set:

app.add_system(apply_tnua_fall_through_controls.in_set(TnuaUserControlsSystemSet));

Just jump-through

Manually check if any ghost platforms were detected, and move the first one to override the proximity sensor's output:

fn apply_tnua_fall_through_controls(
    mut query: Query<
        &mut TnuaProximitySensor,
        &TnuaGhostSensor,
    >,
) { 
    for (mut proximity_sensor, ghost_sensor) in query.iter_mut() {
        for ghost_platform in ghost_sensor.iter() {
            if MIN_PROXIMITY <= ghost_platform.proximity {
                proximity_sensor.output = Some(ghost_platform.clone());
                break;
            }
        }
    }
}

The proximity sensor terminates once it detects a non-ghost object, so all the ghost platforms are guaranteed to be higher (assuming the sensor faces down) and even if the is an original input - it's okay to overwrite it. The list is in order of proximity, so the first item will be the closest.

We are checking MIN_PROXIMITY <= ghost_platform.proximity because by default the sensor starts detecting entities from the origin of the character entity. Since the character phases through the platform when jumping through it, we don't want to consider that character as standing on the platform when it is only halfway through it. Typically we'd want MAX_PROXIMITY to be the distance from the character's origin to the bottom of its collider, so that we get the same climbing behavior we get with fully solid platforms.

Peek.2023-06-05.19-44.mp4

Simplistic fall-through

Since standing on a ghost platform requires actively overriding the proximity sensor's output, falling through can be as simple as not overriding it:

if fall_through_input_invoked() {
    for ghost_platform in ghost_sensor.iter() {
        if MIN_PROXIMITY <= ghost_platform.proximity {
            proximity_sensor.output = Some(ghost_platform.clone());
            break;
        }
    }
}

fall_through_input_invoked() is placeholder for actual user input the system needs to check.

The drawback of this approach is that if the player leaves the button too soon, the fall through will be cancelled and visibly "reverted":

Peek.2023-06-05.19-46.mp4

This may not be an issue if the character floats very low above the ground (in which case many advantages of the floating character model are lost). In cases it is an issue, TnuaSimpleFallThroughPlatformsHelper can be used to solve it.

TnuaSimpleFallThroughPlatformsHelper

TnuaSimpleFallThroughPlatformsHelper is a component that needs to be added to the character entity:

cmd.insert(TnuaPlatformerBundle {
    config: TnuaPlatformerConfig {
        // ...
    },
    ..Default::default()
});
cmd.insert(TnuaGhostSensor::default()); // we already needed this one for the previous methods
cmd.insert(TnuaSimpleFallThroughPlatformsHelper::default());

In the control system, instead of operating over the proximity and ghost sensors directly, we pass them to the helper (which we need to get mutably):

let mut handle = fall_through_helper.with(proximity_sensor.as_mut(), ghost_sensor, MIN_PROXIMITY);

We can then use the handle to control the fall through:

Fall through one layer at a time

if fall_through_input_invoked() {
    handle.try_falling(that_input_was_just_pressed())
} else {
    handle.dont_fall();
}

Again - fall_through_input_invoked() and that_input_was_just_pressed() are placeholders for actual user input the system needs to check.

This will cause the character to fall through a single layer of ghost platforms. When the just_pressed argument is passed as true to try_falling, the helper will make a list of all the ghost platforms it currently detects. Typically that would be everything up to cling_distance below the ground. In the next frames, where just_pressed is false, the character will only fall through these platforms.

When the player releases that button, and dont_fall is getting called each frame instead, the platforms remain in that list until the character finishes falling through them. This ensures the fall is not visibly reverted.

Peek.2023-06-05.19-48.mp4

Fall through all the layers at once

By always passing true to just_pressed, we can update the list of ghost platforms to fall through on each and every frame, making the character fall down until the player releases the button or until the character reaches a solid object.

if fall_through_input_invoked() {
    handle.try_falling(that_input_was_just_pressed())
} else {
    handle.dont_fall();
}
Peek.2023-06-05.19-49.mp4