Crate bevy_enhanced_input

Crate bevy_enhanced_input 

Source
Expand description

A powerful observer-based input manager for Bevy.

The design of this crate is heavily inspired by Unreal Engine Enhanced Input, but adapted to Bevy’s ECS architecture and idioms.

§Core Concepts

This crate introduces three main concepts:

  • Actions represent something a player can do, like “Jump”, “Movement”, or “Open Menu”. They are not tied to specific input.
  • Bindings connect those actions to real input sources such as keyboard keys, mouse buttons, gamepad axes, etc.
  • Contexts represent a certain input state the player can be in, such as “On foot” or “In car”. They associate actions with entities and define when those actions are evaluated.

In short, actions are mapped to inputs via bindings, and contexts control which actions are active.

§Actions

Each action represents a different in-game behavior. To create a new action, you will need to define a new struct and implement the InputAction trait for it, typically using the provided InputAction derive macro.

The action’s output type is defined by the InputAction::Output associated type, which can be one of bool, f32, Vec2, or Vec3. This type determines the kind of value the action will produce when triggered. For example, a “Jump” action might produce a bool indicating whether the jump button is pressed, while a “Movement” action might produce a Vec2 representing the direction and magnitude of movement input.

Actions are stored as entities with the Action<A> component, where A is your InputAction type. These are associated to contexts via the ActionOf<C> relationship, where C is your context type, and can be quickly bound to them using the actions! macro.

By default, when actions are triggered, they “consume” the input values from their bindings. This means that if multiple actions are bound to the same input source (e.g., the same key), the action that is evaluated first will take precedence, and the others will not receive the input value. This behavior (and other action-specific configuration) can be further customized using the ActionSettings component.

§Bindings

Bindings define how actions are triggered by input sources (e.g. mouse movement or keyboard buttons) that your player might press, like keyboard keys or gamepad buttons. We provide support for a variety of input sources out of the box: see the Binding enum for a full list.

Bindings are represented by entities with the Binding component. Bindings associated with actions via BindingOf relationship. Similar to actions!, we provide the bindings! macro to spawn related bindings.

By default, input is read from all connected gamepads. You can customize this by adding the GamepadDevice component to the context entity.

§Contexts

Contexts define when actions are evaluated. They are associated with action entities via the Actions<C> relationship mentioned earlier. Depending on your type of game, you may have a single global context or multiple contexts for different gameplay states. For games with multiple entities driven by a single context it’s common to create a “controller” entity which applies the input to the desired entity.

Contexts are stored using regular components, commonly on an entity for which the input is associated (player, button, dialog, etc.).

Contexts can be activated or deactivated using the ContextActivity component. By default, contexts are active when the component is present. When active, all actions associated with the context are evaluated.

By default contexts evaluated in their spawn order, but you can also control this with ContextPriority. To register a component as an input context, you need to call InputContextAppExt::add_input_context. By default, contexts are evaluated during PreUpdate, but you can customize this by using InputContextAppExt::add_input_context_to instead.

§Putting it all together

Let’s summarize how contexts, actions, and bindings relate to each other in the ECS world.

You start with an entity that has a context component, such as Player with OnFoot context. This context is an ordinary component, registered as an input context using App::add_input_context.

Then, for each of the actions that you might want the player to be able to take while “on foot”, you define a new action type A that implements the InputAction trait. These actions are represented as entities with the Action<A> component, and are associated with the context entity via the ActionOf<C> relationship, spawned using the actions! macro.

Finally, for each action, you define one or more bindings that specify which input sources will trigger the action. These bindings are represented as entities with the Binding component, and are associated with the action entity via the BindingOf relationship, spawned using the bindings! macro.

Here’s a complete example that demonstrates these concepts in action.

use bevy::prelude::*;
use bevy_enhanced_input::prelude::*;

#[derive(Component)]
struct Player;

#[derive(InputAction)]
#[action_output(bool)]
struct Jump;

#[derive(InputAction)]
#[action_output(bool)]
struct Fire;

let mut app = App::new();
app.add_plugins(EnhancedInputPlugin)
    .add_input_context::<Player>()
    .finish();

app.world_mut().spawn((
    Player,
    actions!(Player[
        (
            Action::<Jump>::new(),
            bindings![KeyCode::Space, GamepadButton::South],
        ),
        (
            Action::<Fire>::new(),
            bindings![MouseButton::Left, GamepadButton::RightTrigger2],
        ),
    ])
));

If we wanted to add a new context for when the player is in a vehicle, we would create a new context component InVehicle and register it as an input context. We would then define new actions specific to vehicle control, such as Accelerate and Brake, and associate them with the InVehicle context entity.

If we wanted to add another action to the OnFoot context, such as Crouch, we would define a new action type Crouch and associate it with the OnFoot context for our player entity.

And if we wanted to add a new key binding for the Jump action, such as the “J” key, we would simply add a new binding to the existing Jump action on our player entity.

These patterns make it easy to manage complex input schemes in a structured but flexible way, and support complex scenarios like multiple players, different gameplay states, customizable controls, and computer-controlled entities that take the same actions as players.

§More sophisticated input patterns

While we can bind actions directly to input sources like buttons, further customization or preprocessing is often needed. Not all inputs are “buttonlike”: we may want to only trigger an action when a button is held for a certain duration, or when a joystick is moved beyond a certain threshold.

There are two main ways to achieve this: using input conditions and input modifiers:

  • Input conditions define when an action is considered to be triggered based on the state of its bindings.
    • For example, the DeadZone modifier can be used to ignore small movements of a joystick.
    • When no input conditions are attached to an action or its bindings, the action behaves as if it has a Down condition with a zero actuation threshold.
  • Input modifiers transform the raw input values from bindings before they are processed by the action.
    • For example, the Hold condition can be used to trigger an action only when a button is held down for a specified duration.
    • When no input modifiers are attached, the raw input value is passed through unchanged.

See the module docs for input conditions and input modifiers for more details.

These complex input patterns can be tedious to set up manually, especially for common use cases like character movement. To simplify this, we provide a number of presets that bundle common bindings and modifiers together.

§Reacting to actions

Up to this point, we’ve explained how to define actions and link them to users inputs, but haven’t explained how you might actually react to those actions in your game logic.

We provide two flavors of API for this: a push-style API based on observers and a pull-style API based on querying action components.

Most users find the push-style API more ergonomic and easier to reason about, but the pull-style API can allow for more complex checks and interactions between the state of multiple actions.

Ultimately, the choice between these two approaches depends on your specific use case and preferences, with performance playing a relatively minor role unless you have a very large number of acting entities or if you have a complex logic for your action reaction.

§Push-style: responding to action events

When an action is triggered, we can notify your game logic using Bevy’s Event system. These triggers are driven by changes (including transitions from a state to itself) in the action’s ActionState, updated during EnhancedInputSystems::Apply.

There are a number of different action events, but the most commonly used are:

  • Start<A>: The action has started triggering (e.g. button pressed).
  • Fire<A>: The action is currently triggering (e.g. button held).
  • Complete<A>: The action has stopped triggering (e.g. button released after being held).

The exact meaning of each action event depends on the attached input conditions. For example, with the Down condition, Fire<A> triggers when the user holds the button. But with HoldAndRelease it will trigger when user releases the button after holding it for the specified amount of time.

These events are targeted at the entity with the context component, and will include information about the input values based on the InputAction::Output, as well as additional metadata such as timing information. See the documentation for each event type for more details.

Each of these events can be observed using the On<E> system parameter in a Bevy observer, responding to changes as they occur during command flushes.

use bevy::prelude::*;
use bevy_enhanced_input::prelude::*;

let mut app = App::new();
app.add_observer(apply_movement);

#[derive(InputAction)]
#[action_output(Vec2)]
struct Movement;

/// Apply movement when `Movement` action considered fired.
fn apply_movement(movement: On<Fire<Movement>>, mut players: Query<&mut Transform>) {
    // Read transform from the context entity.
    let mut transform = players.get_mut(movement.context).unwrap();

    // We defined the output of `Movement` as `Vec2`,
    // but since translation expects `Vec3`, we extend it to 3 axes.
    transform.translation += movement.value.extend(0.0);
}

The event system is highly flexible. For example, you can use the Hold condition for an attack action, triggering strong attacks on Complete events and regular attacks on Cancel events.

This approach can be mixed with the pull-style API if you need to access values of other actions in your observer.

§Pull-style: polling action state

Sometimes you may want to access multiple actions at the same time, or check an action state during other gameplay logic. For cases like you can use pull-style API.

Since actions just entities, you can query Action<C> in a system to get the action value in a strongly typed form. Alternatively, you can query ActionValue in its dynamically typed form.

To access the action state, use the ActionState component. State transitions from the last action evaluation are recorded in the ActionEvents component, which lets you detect when an action has just started or stopped triggering.

Timing information provided via ActionTime component.

You can also use Bevy’s change detection - these components marked as changed only if their values actually change.

For single-player games you can use Single for convenient access:

fn apply_input(
    jump_events: Single<&ActionEvents, With<Action<Jump>>>,
    movement: Single<&Action<Movement>>,
    mut player_transform: Single<&mut Transform, With<Player>>,
) {
    // Jumped this frame
    if jump_events.contains(ActionEvents::STARTED) {
        // User logic...
    }

    // We defined the output of `Movement` as `Vec2`,
    // but since translation expects `Vec3`, we extend it to 3 axes.
    player_transform.translation = movement.extend(0.0);
}

For games with multiple contexts you can query for specific action or iterate over action contexts.

fn apply_input(
    jumps: Query<&ActionEvents, With<Action<Jump>>>,
    movements: Query<&Action<Movement>>,
    mut players: Query<(&mut Transform, &Actions<Player>)>,
) {
    for (mut transform, actions) in &mut players {
        let Some(jump_events) = jumps.iter_many(actions).next() else {
            continue;
        };
        let Some(movement) = movements.iter_many(actions).next() else {
            continue;
        };

        // Jumped this frame
        if jump_events.contains(ActionEvents::STARTED) {
            // User logic...
        }

        // We defined the output of `Movement` as `Vec2`,
        // but since translation expects `Vec3`, we extend it to 3 axes.
        transform.translation = movement.extend(0.0);
    }
}

§Next steps

While this is enough to allow you to understand the examples and get started, there are a number of other useful features to learn about. Each of these is complex to deserve their own section:

  • input modifiers for combining and transforming input values (e.g. applying dead zones or sensitivity or creating chords)
  • input conditions for defining when actions are triggered (e.g. on press, release, hold, tap, etc.)
  • presets for common bindings and modifiers (e.g. WASD keys and gamepad sticks for movement)
  • mocking for simulating input in tests, cutscenes or as part of replicated network state
  • the details of working with contexts (e.g. managing multiple players or gameplay states)

§Input and UI

Currently, we don’t integrate bevy_input_focus directly. But we provide ActionSources resource that could be used to prevents actions from triggering during UI interactions. See its docs for details.

§Troubleshooting

If you face any issue, try to enable logging to see what is going on. To enable logging, you can temporarily set RUST_LOG environment variable to bevy_enhanced_input=debug (or bevy_enhanced_input=trace for more noisy output) like this:

RUST_LOG=bevy_enhanced_input=debug cargo run

The exact method depends on the OS shell.

Alternatively you can configure LogPlugin to make it permanent.

Modules§

action
Actions represent high-level user intents, such as “Jump” or “Move Forward”.
binding
Input bindings define which physical inputs map to an action.
condition
Instead of hardcoded states like “pressed” or “released”, all actions use an abstract ActionState component (which is a required component of Action<C>). Its meaning depends on the assigned input conditions, which determine when the action is triggered. This allows you to define flexible behaviors, such as “hold for 1 second”.
context
Contexts are a way to group actions and manage their evaluation order. They allow you to define when actions are active and which inputs they respond to.
modifier
Action values are stored in two forms:
prelude
preset
Preset bindings for common input binding patterns.

Macros§

actions
Returns a SpawnRelatedBundle that will insert the Actions<C> component and spawn a SpawnableList of entities with given bundles that relate to the context entity via the ActionOf<C> component.
bindings
Returns a SpawnRelatedBundle that will insert the Bindings component and spawn a SpawnableList of entities with given bundles that relate to the context entity via the BindingOf component.

Structs§

EnhancedInputPlugin
Initializes contexts and feeds inputs to them.

Enums§

EnhancedInputSystems
Label for the system that updates input context instances.