Bevy Enhanced Input
repository·master·Indexed 43 days ago
https://github.com/simgine/bevy_enhanced_inputAn observer-based input management system for the Bevy engine, inspired by Unreal Engine's Enhanced Input. It allows developers to bind physical inputs to abstract in-game actions, decoupling hardware from game logic. Version 0.26.0 supports Bevy 0.19.0.
What's inside Bevy Enhanced Input
- Bevy Enhanced Input is an observer-based input manager for the Bevy engine. It allows developers to bind physical inputs (like keys) to abstract in-game actions, a design pattern inspired by Unreal Engine's Enhanced Input plugin. This abstraction decouples input hardware from game logic, making it easier to manage complex input mappings and rebindable controls.
What are input bindings and how to use them
masterInput bindings define which physical inputs (keyboard, mouse, gamepad, etc.) map to a specific action.
To use them, you create a
Bindingand relate it to an action entity using theBindingOfrelationship. This is typically done using thebindings!macro, which should be spawned as a child of the action entity.Key concepts:
- Modifiers: Use input modifiers to alter the captured input value (e.g., adding
Shiftto a key press). - Conditions: Use input conditions to change when an action is triggered.
- Conversion: If the action's dimension (e.g.,
f32) differs from the input's dimension (e.g.,Axis2D), the value is converted usingActionValue::convert.
actions!(PlayerCam[ ( Action::<Zoom>::new(), bindings![(Binding::mouse_wheel(), SwizzleAxis::YXZ)], ) ]);- Modifiers: Use input modifiers to alter the captured input value (e.g., adding
Use AccumulateBy to sum input values based on another action
masterThe
AccumulateBymodifier allows an action to continuously add its input values together as long as a secondary 'trigger' action is in theTriggerState::Firedstate.Behavior:
- When the trigger action is
Fired: The current action's input values are added to a running total (self.value). - When the trigger action is inactive: The accumulation resets, and the action's value becomes equal to the current frame's input value.
This is useful for mechanics like movement acceleration, where holding a modifier key (like Shift) causes input (like WASD) to accumulate or scale over time.
use bevy::{ecs::spawn::SpawnWith, prelude::*}; use bevy_enhanced_input::prelude::*; Actions::<TestContext>::spawn(SpawnWith(|context: &mut ActionSpawner<_>| { // 1. Define the trigger action (e.g., a modifier key) let accelerate = context .spawn(( Action::<Accelerate>::new(), bindings![KeyCode::ShiftLeft, KeyCode::ShiftRight], )) .id(); // 2. Define the primary action that accumulates values based on the trigger context.spawn(( Action::<Movement>::new(), AccumulateBy::new(accelerate), Bindings::spawn(Cardinal::wasd_keys().with(DeadZone::default())), )); })); #[derive(Component)] struct TestContext; #[derive(InputAction)] #[action_output(bool)] struct Accelerate; #[derive(InputAction)] #[action_output(f32)] struct Movement;- When the trigger action is
How Actions, Bindings, and Contexts work together
masterBevy Enhanced Input uses three core abstractions to manage player input:
- Actions: Represent high-level gameplay behaviors (e.g., "Jump", "Move") independent of specific keys. They are defined by implementing the
InputActiontrait and are stored as entities with theAction<A>component. - Bindings: Connect Actions to physical input sources (e.g.,
KeyCode::Space,MouseButton::Left). Bindings are entities with theBindingcomponent associated with an action via theBindingOfrelationship. - Contexts: Define the active input state (e.g., "On Foot", "In Vehicle"). Contexts are regular components registered as input contexts. They associate actions with specific entities and control when those actions are evaluated.
Workflow Summary:
- Register a component as a context using
add_input_context. - Define an
InputActionstruct. - Spawn an entity with the context component and use the
actions!macro to spawnActionentities and theirBindingentities in one go.
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; // In your App setup: app.add_plugins(EnhancedInputPlugin) .add_input_context::<Player>() .finish(); // Spawning the player with actions and bindings: app.world_mut().spawn(( Player, actions!(Player[ ( Action::<Jump>::new(), bindings![KeyCode::Space, GamepadButton::South], ), ( Action::<Fire>::new(), bindings![MouseButton::Left, GamepadButton::RightTrigger2], ), ]) ));- Actions: Represent high-level gameplay behaviors (e.g., "Jump", "Move") independent of specific keys. They are defined by implementing the
How ConditionKind affects TriggerState combination
masterWhen multiple conditions are present, the
ConditionKinddetermines how their individualTriggerStateresults are combined to produce the final state for the action:ConditionKind::Explicit: The most significantTriggerStatefrom all explicit conditions will be the resulting state.ConditionKind::Implicit: Similar toExplicit, butTriggerState::Firedis only set if all implicit conditions return it. Otherwise, the most significant state is capped atTriggerState::Ongoing.ConditionKind::Blocker: Any blocking condition that returnsTriggerState::Nonewill override the state withTriggerState::None. A blocker does not contribute to the state on its own; it only serves to potentially cancel it.
Use SwizzleAxis to reorder or swap input axes
masterThe
SwizzleAxiscomponent is an input modifier used to reorder or swap the axes of an input value. This is particularly useful for mapping 1D inputs (like a single button or trigger) onto specific axes of a higher-dimensional action (like the Y axis of a 2D movement stick).Behavioral Rules:
- It attempts to preserve the original dimension where possible.
- If an axis from the original input is promoted to a higher dimension, the
ActionValuetype will change (e.g., aBoolorAxis1Dmight become anAxis2DorAxis3D). - Missing axes in the target configuration are replaced with zero.
Common Use Cases:
- Mapping a 1D input to the Y axis of a 2D action using
SwizzleAxis::YXZ. - Reordering 3D axes for specific coordinate system requirements.
use bevy::prelude::*; use crate::prelude::*; // Example of applying a swizzle (conceptual usage via component attachment) // entity.insert(SwizzleAxis::YXZ);Mock input actions for testing or automation
masterThe
ActionMockcomponent allows you to simulate input actions without physical device interaction. This is useful for automated testing, AI-driven inputs, cutscenes, or network-driven input.When an action is mocked:
- Input reading, conditions, and modifiers are skipped.
- The action reports the provided
TriggerStateandActionValue. - State transition events (e.g.,
Start<A>,Fire<A>) are still triggered. - Mocking takes effect during the next context evaluation.
Once the specified
MockSpanexpires,ActionMock::enabledis set tofalse, and the action resumes regular evaluation. The component is not automatically removed, allowing for reuse.// Example: Spawn and move up for 2 seconds world.spawn(( Player, actions!(Player[ ( Action::<Movement>::new(), ActionMock::new(TriggerState::Fired, Vec2::Y, Duration::from_secs(2)), Bindings::spawn(Cardinal::wasd_keys()), ), ]), ));Understand the relationship between Actions and Bindings
masterIn
bevy_enhanced_input, actions and their bindings are linked using Bevy's relationship system via two components:BindingOf(Entity): This component is attached to a binding entity. It points to the action entity it belongs to. It uses a relationship target ofBindings.Bindings(Vec<Entity>): This component is attached to the action entity. It holds a list of all entities that represent bindings for that specific action.
This structure allows you to efficiently query all bindings associated with a specific action or find the parent action from a binding entity.
Configure DeadZone behavior using DeadZoneKind
masterThe
DeadZoneKindenum defines how the dead zone logic is applied to multi-axis inputs (Axis2DandAxis3D):Radial(Default): Applies dead zone logic to all axes simultaneously. This provides smooth, circular or spherical input coverage, which is generally preferred for thumbsticks.Axial: Applies the dead zone to each axis individually. This results in input being "chamfered" at the corners (the input area becomes a square/cube rather than a circle/sphere).
// Radial (smooth circular movement) let radial = DeadZone::new(DeadZoneKind::Radial); // Axial (square/chamfered movement) let axial = DeadZone::new(DeadZoneKind::Axial);How input conditions work in Bevy Enhanced Input
masterInput conditions are components that implement the
InputConditiontrait. They define the logic for when an action is triggered (e.g., "hold for 1 second" or "tap").Key Behaviors:
- Attachment: Conditions can be attached to both actions and bindings.
- Evaluation: They are evaluated during
EnhancedInputSystems::Updateafter modifiers, in their insertion order. They update theTriggerStateon the associated action entity. - Default Behavior: If no conditions are attached to an action, it defaults to behaving like a
Downcondition with a zero actuation threshold (triggering on any non-zero input value). - Custom Conditions: If you implement a custom condition, you must register it using
InputConditionAppExt::add_input_condition.
use bevy::prelude::*; use bevy_enhanced_input::prelude::*; #[derive(InputAction)] #[action_output(bool)] struct Jump; // Example of attaching a condition to an action world.spawn(actions!(( Action::<Jump>::new(), Hold::new(1.0), bindings![KeyCode::Space], )));Understand ActionEvents and state transitions
masterThe
ActionEventsbitset tracks events caused by transitions in an action'sTriggerState. DuringEnhancedInputSystems::Trigger, these bitflags are set based on the transition from the previous state to the new state. You can useActionEventsas a component in systems or react to them via observers.Last state New state Events TriggerState::NoneTriggerState::NoneNo events TriggerState::NoneTriggerState::OngoingSTART+ONGOINGTriggerState::NoneTriggerState::FiredSTART+FIRETriggerState::OngoingTriggerState::NoneCANCELTriggerState::OngoingTriggerState::OngoingONGOINGTriggerState::OngoingTriggerState::FiredFIRETriggerState::FiredTriggerState::FiredFIRETriggerState::FiredTriggerState::OngoingONGOINGTriggerState::FiredTriggerState::NoneCOMPLETE// ActionEvents bitflags // START, ONGOING, FIRE, CANCEL, COMPLETEUse ActionMock to simulate actions
masterYou can mock an action by inserting an
ActionMockcomponent onto the specific action entity, or by using the convenience extension traits onEntityCommandsorEntityWorldMut.Using EntityCommands (Commands API)
Use
mock,try_mock,mock_once, ortry_mock_onceon an entity to find the correct action within itsActions<C>container and apply the mock.mock: Emits a warning if the entity or action is not found.try_mock: Silences warnings if the entity or action is not found.mock_once: Mocks the action for a single update (shorthand forMockSpan::once()).try_mock_once: Silences warnings and mocks for a single update.
Using EntityWorldMut (Direct World Access)
Use
mock,mock_onceonEntityWorldMutfor immediate application during system execution.// Mock a 2 second long press using EntityCommands fn mock_fire(mut commands: Commands, player: Single<Entity, With<Player>>) { commands .entity(player.into_inner()) .mock::<Player, PrimaryFire>(TriggerState::Fired, true, Duration::from_secs(2)); } // Mock a jump action for just the next frame fn mock_jump(mut commands: Commands, jump: Single<Entity, With<Action<Jump>>>) { commands.entity(*jump).insert(ActionMock::once(TriggerState::Fired, true)); }