leafwing-input-manager

repository·main·Indexed 21 days ago

https://github.com/leafwing-studios/leafwing-input-manager

A flexible input-action management system for the Bevy game engine (v0.21.0) that decouples raw hardware inputs (keyboard, mouse, gamepad) from logical game actions. It uses an Actionlike trait for action definitions, InputMap for keybindings, and ActionState to aggregate and query input states such as button presses and axis values.

Tokens
19.5K
Snippets
53
Records
68
Agent score
75%

What's inside leafwing-input-manager

  1. How leafwing-input-manager works

    main

    The leafwing-input-manager is an input-action manager for Bevy that decouples raw hardware inputs (keyboard, mouse, gamepad) from logical game actions.

    Core Workflow

    1. Define Actions: Create an enum representing your game's logical actions (e.g., Jump, Move) and derive the Actionlike trait.
    2. Map Inputs: Use an InputMap<A> component on an entity to define which hardware inputs (like KeyCode::Space or GamepadButton::South) trigger which actions.
    3. Collect State: The plugin automatically populates an ActionState<A> component on the same entity, which aggregates all input sources into a single, easy-to-query state.
    4. Consume Actions: In your game systems, query for &ActionState<A> to check for button presses, releases, or axis values.

    Key Abstractions

    • Actionlike: A trait derived by your action enum to allow it to be used within the manager.
    • InputMap<A>: A component that stores the many-to-many mappings between inputs and actions. It supports chords (combinations of keys) and multiple input types for the same action.
    • ActionState<A>: A component that holds the current state of all actions for an entity, providing methods like .just_pressed(), .pressed(), and axis-related queries.
    // 1. Define actions
    #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
    enum Action {
        Run,
        Jump,
    }
    
    // 2. Map inputs on an entity
    let input_map = InputMap::new([(Action::Jump, KeyCode::Space)]);
    commands.spawn(input_map).insert(Player);
    
    // 3. Read state in a system
    fn jump(query: Query<&ActionState<Action>, With<Player>>) {
        let action_state = query.single();
        if action_state.just_pressed(&Action::Jump) {
            println!("I'm jumping!");
        }
    }
  2. Getting started with leafwing-input-manager

    main

    To integrate leafwing-input-manager into your Bevy project, follow these steps:

    1. Add the dependency to your Cargo.toml:
      leafwing-input-manager = "0.21"
    2. Define your actions by creating an enum and deriving Actionlike:
      #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
      enum Action {
          Run,
          Jump,
      }
    3. Initialize the plugin in your Bevy App:
      app.add_plugins(InputManagerPlugin::<Action>::default());
    4. Spawn an entity with an InputMap to define bindings:
      let input_map = InputMap::new([(Action::Jump, KeyCode::Space)]);
      commands.spawn(input_map);
    5. Query ActionState<Action> in your systems to handle gameplay logic.
    use bevy::prelude::*;
    use leafwing_input_manager::prelude::*;
    
    #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
    enum Action {
        Run,
        Jump,
    }
    
    #[derive(Component)]
    struct Player;
    
    fn main() {
        App::new()
            .add_plugins(DefaultPlugins)
            .add_plugins(InputManagerPlugin::<Action>::default())
            .add_systems(Startup, spawn_player)
            .add_systems(Update, jump)
            .run();
    }
    
    fn spawn_player(mut commands: Commands) {
        let input_map = InputMap::new([(Action::Jump, KeyCode::Space)]);
        commands.spawn(input_map).insert(Player);
    }
    
    fn jump(query: Query<&ActionState<Action>, With<Player>>) {
        let action_state = query.single();
        if let Ok(action_state) = action_state {
            if action_state.just_pressed(&Action::Jump) {
                println!("I'm jumping!");
            }
        }
    }
  3. What is an AxisProcessor?

    main

    An AxisProcessor is a component used to transform single-axis input values (of type f32). It accepts an input value and produces a transformed output value. Processors can be chained together to create a processing pipeline, allowing you to perform multiple operations like inverting, scaling, or applying deadzones in sequence.

    Common built-in processors include:

    • Digital: Converts input to discrete values (1.0, 0.0, or -1.0).
    • Inverted: Flips the sign of the input.
    • Sensitivity(f32): Scales the input by a multiplier.
    • ValueBounds: Clamps the input within a specific range.
    • DeadZone: Implements a scaled deadzone (normalizing values outside the deadzone to the [0.0, 1.0] or [-1.0, 0.0] range).
    • Exclusion: Implements an unscaled deadzone (treating values within the range as 0.0 without rescaling the remaining values).
    use leafwing_input_manager::prelude::AxisProcessor;
    
    // Example of manual processing
    let processor = AxisProcessor::Sensitivity(2.0);
    let output = processor.process(0.5); // returns 1.0
  4. What is ActionState and how to use it

    main

    An ActionState<A> is a Bevy Component that stores the canonical, input-method-agnostic representation of inputs for a set of actions defined by the type A (which must implement Actionlike).

    Usage Patterns

    • Direct Control: Spawn an entity with ActionState<A> to control it directly from player input.
    • Global Input: To model a single global input, spawn one dedicated entity holding an InputMap (which automatically adds an ActionState via required components) and read/write to it using Single<&ActionState>.

    Disabling Actions

    Actions can be disabled with varying granularity. Note that more general disabling mechanisms override specific ones (e.g., disabling the entire ActionState makes individual action disabling/enabling ineffective).

    1. Global Disable: Use a run condition on InputManagerSystem::Update to stop all updates.
    2. Type-specific Disable: Use a run condition on TickActionStateSystem::<A> to disable all actions of type A.
    3. State Disable: Use ActionState::disable to disable the entire component.
    4. Action Disable: Use ActionState::disable_action to disable a specific action.

    Note: Disabled actions report as released (not just released) and their values are zero. Their underlying values are still updated to prevent surprises when re-enabled, but standard methods like ActionState::pressed will not report them. To see the raw values, access ActionData directly.

    use bevy::reflect::Reflect;
    use leafwing_input_manager::prelude::*;
    use bevy::platform::time::Instant;
    
    #[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
    enum Action {
        Left,
        Right,
        Jump,
    }
    
    let mut action_state = ActionState::<Action>::default();
    
    // Typically, this is done automatically by the `InputManagerPlugin` from user inputs
    // using the `ActionState::update` method
    action_state.press(&Action::Jump);
    
    assert!(action_state.pressed(&Action::Jump));
    assert!(action_state.just_pressed(&Action::Jump));
    assert!(action_state.released(&Action::Left));
    
    // Resets just_pressed and just_released
    let t0 = Instant::now();
    let t1 = Instant::now();
    
    action_state.tick(t1, t0);
    assert!(action_state.pressed(&Action::Jump));
    assert!(!action_state.just_pressed(&Action::Jump));
    
    action_state.release(&Action::Jump);
    assert!(!action_state.pressed(&Action::Jump));
    assert!(action_state.released(&Action::Jump));
    assert!(action_state.just_released(&Action::Jump));
    
    let t2 = Instant::now();
    action_state.tick(t2, t1);
    assert!(action_state.released(&Action::Jump));
    assert!(!action_state.just_released(&Action::Jump));
  5. Use DualAxisDeadZone to normalize input

    main

    A DualAxisDeadZone is a scaled version of DualAxisExclusion. It excludes values within a specified range and then normalizes the remaining "live zone" so that the input values are mapped smoothly from the edge of the dead zone to the full range (typically 1.0). This prevents sudden jumps in input magnitude when moving out of a dead zone.

    Note: This processor increases the magnitude of diagonal values because each axis is processed individually.

    use bevy::prelude::*;
    use leafwing_input_manager::prelude::*;
    
    // Create a deadzone that excludes X in [-0.2, 0.3] and Y in [-0.1, 0.4]
    let deadzone = DualAxisDeadZone::new((-0.2, 0.3), (-0.1, 0.4));
    
    // Normalize an input value
    let input = Vec2::new(0.5, 0.5);
    let normalized = deadzone.normalize(input);
  6. Understand BasicInputs and input length

    main

    The BasicInputs enum represents a decomposed view of user inputs used for clash detection. It categorizes inputs into several types:

    • None: No button-like inputs (e.g., an axis).
    • Simple: A single fundamental input (e.g., a single key).
    • Composite: Multiple inputs that represent a single logical input (e.g., a virtual D-Pad where any of several keys can trigger it).
    • Chord: A group of multiple keys that must be pressed together (e.g., Ctrl + S).

    Important: When checking for clashes or determining the complexity of an input, use BasicInputs::len() rather than the length of the vector returned by inputs(). len() correctly identifies a Composite input as having a length of 1, whereas inputs() returns the full list of underlying buttons.

    use leafwing_input_manager::BasicInputs;
    
    // A chord of 3 keys has a length of 3
    let chord = BasicInputs::Chord(vec![...]);
    assert_eq!(chord.len(), 3);
    
    // A composite input (like a D-Pad) has a length of 1
    let composite = BasicInputs::Composite(vec![...]);
    assert_eq!(composite.len(), 1);
  7. Create a triple-axis input with VirtualDPad3D

    main

    A VirtualDPad3D is a three-dimensional directional pad constructed from six Buttonlike inputs: up, down, left, right, forward, and backward. It allows for movement on X, Y, and Z axes, including complex diagonals.

    Raw Value Logic

    • X-axis: right - left
    • Y-axis: up - down
    • Z-axis: backward - forward

    Values range from -1.0 to 1.0 on each axis based on the button states.

    use bevy::prelude::*;
    use leafwing_input_manager::prelude::*;
    
    // Manually constructing a 3D D-Pad
    let xyz = VirtualDPad3D::new(
        KeyCode::ArrowUp,      // up
        KeyCode::ArrowDown,    // down
        KeyCode::ArrowLeft,    // left
        KeyCode::ArrowRight,   // right
        KeyCode::KeyF,        // forward
        KeyCode::KeyB,        // backward
    );
  8. Configure dual-axis value bounds

    main

    Use DualAxisBounds to restrict input values to a specific rectangular area on a 2D plane. This is useful for clamping joystick or mouse input to a specific range. You can create bounds that are symmetric, only apply to one axis, or define specific minimum and maximum values for both X and Y.

    Common ways to create DualAxisBounds include:

    • DualAxisBounds::all(min, max): Sets the same bounds for both axes.
    • DualAxisBounds::only_x(min, max): Sets bounds for X and uses the full range for Y.
    • DualAxisBounds::only_y(min, max): Sets bounds for Y and uses the full range for X.
    • DualAxisBounds::symmetric(val1, val2): Creates symmetric bounds around zero.
    • DualAxisBounds::at_least(min_x, min_y): Sets a minimum threshold.
    • DualAxisBounds::at_most(max_x, max_y): Sets a maximum threshold.
    // Example: Restricting input to a specific rectangle
    let bounds = DualAxisBounds::new((-2.0, 2.5), (-1.0, 1.5));
    
    // Example: Symmetric bounds on both axes
    let symmetric = DualAxisBounds::symmetric_all(2.5);
    
    // Example: Converting to a processor
    let processor = DualAxisProcessor::from(bounds);
  9. Use GamepadControlDirection for axis-to-button mapping

    main

    A GamepadControlDirection allows you to treat a specific direction on a GamepadAxis as a virtual button. This is useful for mapping stick directions (like LEFT_UP) to button actions.

    Key Features

    • Directional Mapping: Use constants like LEFT_UP, RIGHT_DOWN, etc., or create custom ones using .positive(axis) or .negative(axis).
    • Thresholding: You can set a threshold (must be $\ge 0.0$) that must be exceeded in the specified direction to trigger the 'pressed' state.
    • Default Behavior: By default, it monitors any connected gamepad. To target a specific one, use InputMap::set_gamepad.
    // Positive Y-axis movement on left stick
    let input = GamepadControlDirection::LEFT_UP;
    
    // Movement in the opposite direction doesn't activate the input
    GamepadControlAxis::LEFT_Y.set_value(app.world_mut(), -1.0);
    app.update();
    assert!(!app.read_pressed(input));
    
    // Movement in the chosen direction activates the input
    GamepadControlAxis::LEFT_Y.set_value(app.world_mut(), 1.0);
    app.update();
    assert!(app.read_pressed(input));
  10. Create a dual-axis input with VirtualDPad

    main

    A VirtualDPad is a dual-axis control (X and Y) constructed from four Buttonlike inputs (up, down, left, right). It supports intermediate diagonals when multiple buttons are pressed simultaneously.

    Raw Value Logic

    • X-axis: -1.0 if only left is pressed; 1.0 if only right is pressed.
    • Y-axis: -1.0 if only down is pressed; 1.0 if only up is pressed.
    • Diagonals: Combinations of buttons result in intermediate Vec2 values (e.g., Vec2::new(1.0, 1.0) for Up + Right).

    Customizing Values

    Use .sensitivity_x(f32), .sensitivity_y(f32), or the dual-axis processing pipeline via .with_processor(processor) to modify the Vec2 output.

    use bevy::prelude::*;
    use leafwing_input_manager::prelude::*;
    
    // Define a virtual D-pad using the WASD keys
    let input = VirtualDPad::wasd();
    
    // You can configure a processing pipeline (e.g., doubling the Y value)
    let doubled = VirtualDPad::wasd().sensitivity_y(2.0);