idanarye / bevy-tnua

A floating character controller for Bevy
https://crates.io/crates/bevy-tnua
Apache License 2.0
214 stars 14 forks source link

Expose or add certain methods for ease of use outside of a TnuaAction implementation #31

Closed Microwonk closed 9 months ago

Microwonk commented 9 months ago

For example wanting to see if a player is grounded in this example:

if dash {
            if keyboard.pressed(KeyConfig::DOWN) {
                direction -= Vec3::Y;
            }
            if keyboard.pressed(KeyConfig::UP) {
                direction += Vec3::Y;
            }

            let mut needs_inf = direction.y < 0.;
             // want to check if the player is on ground during the dash here
            needs_inf |= !controller.context.is_airborne(); // as an example

            controller.action(TnuaBuiltinDash {
                displacement: direction.normalize_or_zero() * config.dash_distance,
                allow_in_air: air_actions_counter.air_count_for(TnuaBuiltinDash::NAME)
                    <= config.actions_in_air,
                brake_to_speed: if needs_inf {
                    // needs to be set to infinity, so there is no braking force being applied when gravity is active or dash is on ground
                    f32::INFINITY
                } else {
                    config.dash.brake_to_speed
                },
                ..config.dash.clone()
            });
}

Correct me if im wrong or if there is a better way that is already available to check for this.

idanarye commented 9 months ago

The decision of whether the character is airborne is made by the basis. Right now TnuaBuiltinWalk is the only basis, but in the future I'll want to add more (climb? swim?) which will make their own airbornness decisions.

You can get that information out of the basis by using a dynamic handle to the basis:

let is_airborne = controller.dynamic_basis().unwrap().is_airborne();

I've noticed that for some reason DynamicBasis is not exposed. It's pub, but not reachable from outside the crate so it does not appear in the docs. I'll fix that.

Note that dynamic_basis() will return None if you call it before setting the basis for the first time. If this is a problem, you should decide on a default value for this.

Also note that if the character is in the air but during coyote time, is_airborne will return false. This is usually the desired behavior - this is what coyote time is for! - but if for whatever reason it isn't you'll have to figure out if the character is airborne via some other means - typically by looking at the proximity sensor's output, but you may also want to check the active action and figure if you want to consider the character airborne when this action is active. Maybe even look at the action's state.

idanarye commented 9 months ago

Would it be better if I added controller.is_airborne()? It'll return Option<bool> (in case the basis is empty), but it'll be easier to set a default that way (controller.is_airborne().unwrap_or(true))

Microwonk commented 9 months ago

I think that would be a great addition. I looked into the source code and did eventually stumble upon the way you described to do it. The controller.is_airborne() would definitely be a nice quality of life improvement!

Microwonk commented 9 months ago

The abstraction would probably make sense as instead of writing this:

let airborne = match controller.dynamic_basis() {
    Some(basis) => basis.is_airborne(),
    None => true,
};

it would be a bit simpler. Not necessary at all but a QOL improvement.