bevy-tnua

repository·main·Indexed 19 days ago

https://github.com/idanarye/bevy-tnua

A floating character controller for the Bevy game engine (v0.32.0) that simplifies complex movement such as platforming, slope handling, and coyote time using a floating capsule model. It supports multiple physics backends including Rapier (2D/3D) and Avian (2D/3D), and provides features for jumping, crouching, dashing, wall sliding, and animation state determination.

Tokens
24.8K
Snippets
61
Records
100
Agent score
66%

What's inside bevy-tnua

  1. Overview of Tnua features

    main

    Tnua is a "floating" character controller for Bevy. Instead of the character constantly touching the ground, it floats above it, simplifying motion control.

    Key features include:

    • Movement: Running, jumping (including variable height), crouching, and dashing.
    • Platforming Mechanics: Coyote time, jump buffering, running up/down slopes/stairs, tilt correction, and moving/rotating platforms.
    • Advanced Actions: Jump/fall through platforms, air actions, and obstacle actions (wall sliding, wall jumping, and climbing).
    • Animation Support: Provides facilities to help determine which animation state to play based on motion data.
  2. How to feed actions and basis in Tnua 0.27+

    main

    Tnua 0.27+ requires explicit management of the action feeding lifecycle.

    Action Feeding Lifecycle

    Before feeding any actions in a frame, you must call controller.initiate_action_feeding(). If you do not call this, the action() method will panic. This method informs Tnua that an action was not fed so it can properly finish ongoing actions.

    Feeding Methods

    Depending on the desired behavior, use one of these methods:

    • action(): Standard method for feeding actions (requires initiate_action_feeding first).
    • action_trigger(): For actions that don't care about duration (e.g., a dash).
    • action_interrupt(): For forced actions that override everything else (e.g., TnuaBuiltinKnockback).
    • action_start() and action_end(): For manual press/release handling.

    Feeding Basis

    Instead of dynamic feeding, set the pub field basis directly on the controller.

  3. Working with Sensors in Tnua 0.27+

    main

    Sensors are no longer located on the same entity as the TnuaController.

    • Accessing Sensors: Use the entities field on the controller to get the entity representing the sensor. The structure of these entities depends on the basis definition.
    • Ghost Sensors: TnuaGhostSensor must be placed on the sensor entity, not the controller entity. To automate this, add TnuaGhostOverwrites to the controller entity; it will automatically propagate TnuaGhostSensor to the relevant sensors.
    • Overwriting Output: Do not attempt to overwrite the output field of TnuaProximitySensor. Instead, use the set method of TnuaGhostOverwrite.
  4. Accessing Basis and Action data in Tnua 0.27+

    main

    Tnua 0.27+ changed how internal state is accessed. The term State now refers to the triplet of (input, config, and memory).

    Key Terminology Changes

    • Memory: Formerly State. Used for internal data like animation triggers.
    • Input: The configuration/input part of an action.
    • Config: The static configuration part of an action.

    Querying Data

    • Basis: Access via controller.basis (input), controller.basis_config (config), and controller.basis_memory (memory).
    • Actions: Access via controller.current_action. This is an enum of type [YourSchemeName]ActionState generated by the TnuaScheme macro.
    • Discriminants: Use controller.action_discriminant() to query the current action type.
  5. Define Control Schemes in Tnua 0.27

    main

    Starting with Tnua 0.27, you must statically define a control scheme using the TnuaScheme macro. This scheme defines the available basis and actions for the controller.

    Both TnuaController and TnuaControllerPlugin are now parameterized by this scheme. The macro automatically generates a configuration struct named [YourSchemeName]Config. You can load this configuration from an asset or inject it directly into Assets.

    #[derive(TnuaScheme)]
    #[scheme(basis = TnuaBuiltinWalk)]
    enum ControlScheme {
        Jump(TnuaBuiltinJump),
        Crouch(TnuaBuiltinCrouch),
        DifferentKindOfJump(TnuaBuiltinJump),
    }
    
    // Injecting configuration manually:
    cmd.insert(TnuaController::<ControlScheme>::new(
        control_scheme_configs.add(ControlSchemeConfig {
            basis: TnuaBuiltinWalkConfig { ..Default::default() },
            jump: TnuaBuiltinJumpConfig { ..Default::default() },
            crouch: TnuaBuiltinCrouchConfig { ..Default::default() },
            different_kind_of_jump: TnuaBuiltinJumpConfig { ..Default::default() },
        })
    ));
  6. Migrate Air Action Counting to Tnua 0.30

    main

    In Tnua 0.30, TnuaSimpleAirActionsCounter is deprecated. It has been replaced by TnuaActionsCounter, which uses a slot-based system. Instead of one counter for all air actions, you define a slots struct where each slot can track specific actions separately.

    To implement this:

    1. Define a slots struct using #[derive(TnuaActionSlots)] and #[slots(scheme = YourScheme)].
    2. Assign specific actions to slots using #[slots(ActionName)].
    3. Register the actions using TnuaAirActionsPlugin::<YourSlotsStruct>::new(Schedule).

    This plugin automatically adds TnuaActionsCounter<YourSlotsStruct> as a dependency to your TnuaController and handles the update loop automatically in the specified schedule.

    #[derive(TnuaActionSlots)]
    #[slots(scheme = ControlScheme)]
    struct AirActionSlots {
        #[slots(Jump)]
        jump: usize,
        #[slots(Dash)]
        dash: usize,
    }
    
    // In your app setup:
    app.add_plugins(TnuaAirActionsPlugin::<AirActionSlots>::new(FixedUpdate));
  7. Install Tnua with a physics backend

    main

    Tnua requires both the main bevy-tnua crate and a specific integration crate for your chosen physics backend. You must add the main plugin from both crates to your Bevy app.

    Choose the integration crate based on your physics engine and dimensionality:

    • Rapier 2D: Add bevy-tnua-rapier2d
    • Rapier 3D: Add bevy-tnua-rapier3d
    • Avian 2D: Add bevy-tnua-avian2d
    • Avian 3D: Add bevy-tnua-avian3d

    Important Notes:

    • Double Precision: If using a physics backend with double precision (e.g., Avian with the f64 flag), you must also add the f64 flag to all Tnua crates. This ensures data consistency for physics-defined values, though Bevy will still use single precision for position and rotation.
    • Third-party Integrations: If you are building a new integration crate, it should depend on bevy-tnua-physics-integration-layer rather than the main bevy-tnua crate.
  8. Retrieve animation data from TnuaController

    main

    In Tnua 0.30, TnuaPlatformerAnimatingOutput is removed. Animation data must be retrieved directly from the TnuaController using concrete_basis or concrete_action.

    Accessing Basis State

    To get the current state of a basis (e.g., for calculating running velocity), use controller.concrete_basis::<T>(). This returns a tuple of (basis_input, basis_state).

    Identifying Current Basis or Action

    If your game uses multiple bases (e.g., walking vs. swimming) or actions, you can use controller.basis_name() or controller.action_name() to get a string identifier. For matching, it is best practice to use the NAME constant provided by the basis/action type rather than string literals.

    // Get basis state for animation
    let Some((_basis_input, basis_state)) = controller.concrete_basis::<TnuaBuiltinWalk>()
    else {
        continue;
    };
    let speed = basis_state.running_velocity.length();
    
    // Match on action name using NAME constants
    match controller.action_name() {
        Some(TnuaBuiltinJump::NAME) => { /* ... */ }
        Some(TnuaBuiltinCrouch::NAME) => { /* ... */ }
        Some(other) => panic!("Unknown action {other}"),
        None => { /* ... */ }
    }
  9. Run Tnua demos locally

    main

    You can run the provided demos using cargo run. You must specify the demo binary name and the corresponding physics backend feature. Ensure the backend dimensionality (2D or 3D) matches the demo.

    Example: To run the 3D platformer demo with the Avian 3D backend, use:

    $ cargo run --bin platformer_3d --features avian3d
    $ cargo run --bin <demo-name> --features <physics-backend>
  10. Store character motion configuration in ECS

    main

    Since TnuaPlatformerConfig has been removed, the recommended way to store character movement configuration in the ECS is to create a custom component that holds the basis and action templates. You can then clone these templates into the TnuaController during your control system execution.

    Because TnuaBuiltinWalk::desired_velocity is a vector (direction + speed), it is often better to store a scalar speed in your config component and multiply it by a direction vector at runtime.

    #[derive(Component)]
    pub struct CharacterMotionConfigForPlatformerDemo {
        pub speed: f32,
        pub walk: TnuaBuiltinWalk,
        pub jump: TnuaBuiltinJump,
        pub crouch: TnuaBuiltinCrouch,
    }
    
    // In your control system:
    controller.basis(TnuaBuiltinWalk {
        desired_velocity: direction * config.speed,
        ..config.walk.clone()
    });
    
    if crouch {
        controller.action(config.crouch.clone());
    }
    
    if jump {
        controller.action(config.jump.clone());
    }
  11. Migrate character controls to Tnua 0.30

    main

    In Tnua 0.30, character controls have moved from the TnuaPlatformerControls component to the TnuaController component. Controls should be passed every frame, preferably within the TnuaUserControlsSystemSet system set.

    Base Movement

    Instead of modifying TnuaPlatformerControls::desired_velocity, you must now pass a basis (such as TnuaBuiltinWalk) to the TnuaController. Note that TnuaBuiltinWalk::desired_velocity now represents both speed and direction, and float_height must be explicitly provided.

    Jumping

    Use the TnuaBuiltinJump action via the controller. The jump height is controlled by TnuaBuiltinJump::height. The jump is active as long as the action is being fed; once you stop passing the action, the jump stops automatically (no manual nullification required).

    Crouching

    Use the TnuaBuiltinCrouch action via the controller. Set the float_offset within the action. For enforcing crouching behavior under obstacles, use TnuaCrouchEnforcer instead of the deprecated TnuaKeepCrouchingBelowObstacles.

    // Base movement
    controller.basis(TnuaBuiltinWalk {
        desired_velocity: the_desired_velocity,
        float_height: 2.0, // must be passed
        ..Default::default()
    });
    
    // Jumping
    if should_jump {
        controller.action(TnuaBuiltinJump {
            height: 4.0,
            ..Default::default()
        });
    }
    
    // Crouching
    if should_crouch {
        controller.action(TnuaBuiltinCrouch {
            float_offset: -0.9,
            ..Default::default()
        });
    }
  12. Migrate TnuaController Configuration to Tnua 0.28

    main

    In Tnua 0.28, configuration is no longer passed via TnuaController::new. Instead, you must insert TnuaConfig as a separate component alongside TnuaController::default().

    Additionally, the sensors_entities field has moved to a new component called TnuaSensorsEntities. This component is added automatically via Bevy's required components, but if you need to access it in a system, you must explicitly request it in your query.

    // Old way (0.27 and below)
    cmd.insert(
        TnuaController::<ControlScheme>::new(asset_server.load("configuration.ron")),
    );
    
    // New way (0.28+)
    cmd.insert((
        TnuaController::<ControlScheme>::default(),
        TnuaConfig::<ControlScheme>(asset_server.load("configuration.ron")),
    ));