diff --git a/README.md b/README.md index 5dcea207..f4be1a0b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ and a single input can result in multiple actions being triggered, which can be - Full keyboard, mouse and joystick support for button-like and axis inputs - Dual axis support for analog inputs from gamepads and joysticks -- Bind arbitrary button inputs into virtual DPads +- Bind arbitrary button inputs into virtual D-Pads - Effortlessly wire UI buttons to game state with one simple component! - When clicked, your button will press the appropriate action on the corresponding entity - Store all your input mappings in a single `InputMap` component diff --git a/RELEASES.md b/RELEASES.md index 35e3a0da..615076ad 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,47 +5,71 @@ ### Breaking Changes - removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. -- added input processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad` to refine input values: - - added processor enums: - - `AxisProcessor`: Handles single-axis values. - - `DualAxisProcessor`: Handles dual-axis values. - - added processor traits for defining custom processors: - - `CustomAxisProcessor`: Handles single-axis values. - - `CustomDualAxisProcessor`: Handles dual-axis values. - - added built-in processor variants (no variant versions implemented `Into`): - - Pipelines: Handle input values sequentially through a sequence of processors. - - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. - - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. - - you can also create them by these methods: - - `AxisProcessor::with_processor` or `From>::from` for `AxisProcessor::Pipeline`. - - `DualAxisProcessor::with_processor` or `From>::from` for `DualAxisProcessor::Pipeline`. - - Inversion: Reverses control (positive becomes negative, etc.) - - `AxisProcessor::Inverted`: Single-axis inversion. - - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. - - Sensitivity: Adjusts control responsiveness (doubling, halving, etc.). - - `AxisProcessor::Sensitivity`: Single-axis scaling. - - `DualAxisSensitivity`: Dual-axis scaling, implemented `Into`. - - Value Bounds: Define the boundaries for constraining input values. - - `AxisBounds`: Restricts single-axis values to a range, implemented `Into` and `Into`. - - `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into`. - - `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into`. - - Deadzones: Ignores near-zero values, treating them as zero. - - Unscaled versions: - - `AxisExclusion`: Excludes small single-axis values, implemented `Into` and `Into`. - - `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into`. - - `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into`. - - Scaled versions: - - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. - - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. - - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. - - removed `DeadZoneShape`. +- replaced axis-like input handling with new input processors (see 'Enhancements: Input Processors' for details). - removed functions for inverting, adjusting sensitivity, and creating deadzones from `SingleAxis` and `DualAxis`. - - added `with_processor`, `replace_processor`, and `no_processor` to manage processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad`. - - added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. - - added `serde_typetag` procedural macro attribute for processor type tagging. + - removed `DeadZoneShape`. - made the dependency on bevy's `bevy_gilrs` feature optional. - it is still enabled by leafwing-input-manager's default features. - if you're using leafwing-input-manager with `default_features = false`, you can readd it by adding `bevy/bevy_gilrs` as a dependency. +- removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). +- renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. + +### Enhancements + +#### Input Processors + +Input processors allow you to create custom logic for axis-like input manipulation. + +- added `with_processor`, `replace_processor`, and `no_processor` to manage processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad`. +- added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. +- added processor enums: + - `AxisProcessor`: Handles single-axis values. + - `DualAxisProcessor`: Handles dual-axis values. +- added processor traits for defining custom processors: + - `CustomAxisProcessor`: Handles single-axis values. + - `CustomDualAxisProcessor`: Handles dual-axis values. +- added built-in processor variants (no variant versions implemented `Into`): + - Pipelines: Handle input values sequentially through a sequence of processors. + - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. + - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. + - you can also create them by these methods: + - `AxisProcessor::with_processor` or `FromIterator::from_iter` for `AxisProcessor::Pipeline`. + - `DualAxisProcessor::with_processor` or `FromIterator::from_iter` for `DualAxisProcessor::Pipeline`. + - Inversion: Reverses control (positive becomes negative, etc.) + - `AxisProcessor::Inverted`: Single-axis inversion. + - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. + - Sensitivity: Adjusts control responsiveness (doubling, halving, etc.). + - `AxisProcessor::Sensitivity`: Single-axis scaling. + - `DualAxisSensitivity`: Dual-axis scaling, implemented `Into`. + - Value Bounds: Define the boundaries for constraining input values. + - `AxisBounds`: Restricts single-axis values to a range, implemented `Into` and `Into`. + - `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into`. + - `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into`. + - Deadzones: Ignores near-zero values, treating them as zero. + - Unscaled versions: + - `AxisExclusion`: Excludes small single-axis values, implemented `Into` and `Into`. + - `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into`. + - `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into`. + - Scaled versions: + - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. + - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. + - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. + +### Usability + +#### InputMap + +Introduce new fluent builders for creating a new `InputMap` with short configurations: + +- `fn with(mut self, action: A, input: impl Into)`. +- `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. +- `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. +- `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`. + +Introduce new iterators over `InputMap`: + +- `bindings(&self) -> impl Iterator` for iterating over all registered action-input bindings. +- `actions(&self) -> impl Iterator` for iterating over all registered actions. ### Bugs diff --git a/benches/input_map.rs b/benches/input_map.rs index 2960822c..249e9855 100644 --- a/benches/input_map.rs +++ b/benches/input_map.rs @@ -44,17 +44,16 @@ fn construct_input_map_from_iter() -> InputMap { fn construct_input_map_from_chained_calls() -> InputMap { black_box( InputMap::default() - .insert(TestAction::A, KeyCode::KeyA) - .insert(TestAction::B, KeyCode::KeyB) - .insert(TestAction::C, KeyCode::KeyC) - .insert(TestAction::D, KeyCode::KeyD) - .insert(TestAction::E, KeyCode::KeyE) - .insert(TestAction::F, KeyCode::KeyF) - .insert(TestAction::G, KeyCode::KeyG) - .insert(TestAction::H, KeyCode::KeyH) - .insert(TestAction::I, KeyCode::KeyI) - .insert(TestAction::J, KeyCode::KeyJ) - .build(), + .with(TestAction::A, KeyCode::KeyA) + .with(TestAction::B, KeyCode::KeyB) + .with(TestAction::C, KeyCode::KeyC) + .with(TestAction::D, KeyCode::KeyD) + .with(TestAction::E, KeyCode::KeyE) + .with(TestAction::F, KeyCode::KeyF) + .with(TestAction::G, KeyCode::KeyG) + .with(TestAction::H, KeyCode::KeyH) + .with(TestAction::I, KeyCode::KeyI) + .with(TestAction::J, KeyCode::KeyJ), ) } @@ -63,7 +62,7 @@ fn which_pressed( clash_strategy: ClashStrategy, ) -> HashMap { let input_map = construct_input_map_from_iter(); - input_map.which_pressed(input_streams, clash_strategy) + input_map.process_actions(input_streams, clash_strategy) } pub fn criterion_benchmark(c: &mut Criterion) { diff --git a/examples/action_state_resource.rs b/examples/action_state_resource.rs index 3f4c5633..310cf750 100644 --- a/examples/action_state_resource.rs +++ b/examples/action_state_resource.rs @@ -25,14 +25,10 @@ pub enum PlayerAction { Jump, } -// Exhaustively match `PlayerAction` and define the default binding to the input +// Exhaustively match `PlayerAction` and define the default bindings to the input impl PlayerAction { fn mkb_input_map() -> InputMap { - use KeyCode::*; - InputMap::new([ - (Self::Jump, UserInput::Single(InputKind::PhysicalKey(Space))), - (Self::Move, UserInput::VirtualDPad(VirtualDPad::wasd())), - ]) + InputMap::new([(Self::Jump, KeyCode::Space)]).with(Self::Move, VirtualDPad::wasd()) } } diff --git a/examples/arpg_indirection.rs b/examples/arpg_indirection.rs index 8651275c..1fec7912 100644 --- a/examples/arpg_indirection.rs +++ b/examples/arpg_indirection.rs @@ -101,9 +101,8 @@ fn spawn_player(mut commands: Commands) { (Slot::Ability3, KeyE), (Slot::Ability4, KeyR), ]) - .insert(Slot::Primary, MouseButton::Left) - .insert(Slot::Secondary, MouseButton::Right) - .build(), + .with(Slot::Primary, MouseButton::Left) + .with(Slot::Secondary, MouseButton::Right), slot_action_state: ActionState::default(), ability_action_state: ActionState::default(), ability_slot_map, diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs index c175b0dd..a5d35ff7 100644 --- a/examples/axis_inputs.rs +++ b/examples/axis_inputs.rs @@ -27,19 +27,18 @@ struct Player; fn spawn_player(mut commands: Commands) { // Describes how to convert from player inputs into those actions let input_map = InputMap::default() - // Configure the left stick as a dual-axis - .insert(Action::Move, DualAxis::left_stick()) + // Configure the left stick as a dual-axis control + .with(Action::Move, DualAxis::left_stick()) // Let's bind the right gamepad trigger to the throttle action - .insert(Action::Throttle, GamepadButtonType::RightTrigger2) + .with(Action::Throttle, GamepadButtonType::RightTrigger2) // And we'll use the right stick's x-axis as a rudder control - .insert( + .with( // Add an AxisDeadzone to process horizontal values of the right stick. // This will trigger if the axis is moved 10% or more in either direction. Action::Rudder, SingleAxis::new(GamepadAxisType::RightStickX) .with_processor(AxisDeadZone::magnitude(0.1)), - ) - .build(); + ); commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player); diff --git a/examples/clash_handling.rs b/examples/clash_handling.rs index 58b10649..52f25b59 100644 --- a/examples/clash_handling.rs +++ b/examples/clash_handling.rs @@ -32,10 +32,8 @@ fn spawn_input_map(mut commands: Commands) { use KeyCode::*; use TestAction::*; - let mut input_map = InputMap::default(); - // Setting up input mappings in the obvious way - input_map.insert_multiple([(One, Digit1), (Two, Digit2), (Three, Digit3)]); + let mut input_map = InputMap::new([(One, Digit1), (Two, Digit2), (Three, Digit3)]); input_map.insert_chord(OneAndTwo, [Digit1, Digit2]); input_map.insert_chord(OneAndThree, [Digit1, Digit3]); diff --git a/examples/input_processing.rs b/examples/input_processing.rs index e4ba17d0..11e7cee6 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -20,9 +20,8 @@ enum Action { struct Player; fn spawn_player(mut commands: Commands) { - let mut input_map = InputMap::default(); - input_map - .insert( + let input_map = InputMap::default() + .with( Action::Move, VirtualDPad::wasd() // You can add a processor to handle axis-like user inputs by using the `with_processor`. @@ -35,7 +34,7 @@ fn spawn_player(mut commands: Commands) { // Followed by appending Y-axis inversion for the next processing step. .with_processor(DualAxisInverted::ONLY_Y), ) - .insert( + .with( Action::Move, DualAxis::left_stick() // You can replace the currently used processor with another processor. @@ -43,7 +42,7 @@ fn spawn_player(mut commands: Commands) { // Or remove the processor directly, leaving no processor applied. .no_processor(), ) - .insert( + .with( Action::LookAround, // You can also use a sequence of processors as the processing pipeline. DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from_iter([ diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index 7d6b12f5..26cd8aff 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -14,6 +14,7 @@ fn main() { #[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)] enum CameraMovement { Zoom, + Pan, PanLeft, PanRight, } @@ -21,16 +22,15 @@ enum CameraMovement { fn setup(mut commands: Commands) { let input_map = InputMap::default() // This will capture the total continuous value, for direct use. - .insert(CameraMovement::Zoom, SingleAxis::mouse_wheel_y()) + .with(CameraMovement::Zoom, SingleAxis::mouse_wheel_y()) // This will return a binary button-like output. - .insert(CameraMovement::PanLeft, MouseWheelDirection::Left) - .insert(CameraMovement::PanRight, MouseWheelDirection::Right) - // Alternatively, you could model this as a virtual Dpad. + .with(CameraMovement::PanLeft, MouseWheelDirection::Left) + .with(CameraMovement::PanRight, MouseWheelDirection::Right) + // Alternatively, you could model this as a virtual D-pad. // It's extremely useful for modeling 4-directional button-like inputs with the mouse wheel - // .insert(VirtualDpad::mouse_wheel(), Pan) + .with(CameraMovement::Pan, VirtualDPad::mouse_wheel()) // Or even a continuous `DualAxis`! - // .insert(DualAxis::mouse_wheel(), Pan) - .build(); + .with(CameraMovement::Pan, DualAxis::mouse_wheel()); commands .spawn(Camera2dBundle::default()) .insert(InputManagerBundle::with_map(input_map)); diff --git a/examples/multiplayer.rs b/examples/multiplayer.rs index 406c402f..1bda0f52 100644 --- a/examples/multiplayer.rs +++ b/examples/multiplayer.rs @@ -42,15 +42,13 @@ impl PlayerBundle { // and gracefully handle disconnects // Note that this step is not required: // if it is skipped, all input maps will read from all connected gamepads - .set_gamepad(Gamepad { id: 0 }) - .build(), + .with_gamepad(Gamepad { id: 0 }), Player::Two => InputMap::new([ (Action::Left, KeyCode::ArrowLeft), (Action::Right, KeyCode::ArrowRight), (Action::Jump, KeyCode::ArrowUp), ]) - .set_gamepad(Gamepad { id: 1 }) - .build(), + .with_gamepad(Gamepad { id: 1 }), }; // Each player will use the same gamepad controls, but on separate gamepads. diff --git a/examples/register_gamepads.rs b/examples/register_gamepads.rs index 38aaea9b..677e865b 100644 --- a/examples/register_gamepads.rs +++ b/examples/register_gamepads.rs @@ -49,8 +49,7 @@ fn join( (Action::Disconnect, GamepadButtonType::Select), ]) // Make sure to set the gamepad or all gamepads will be used! - .set_gamepad(gamepad) - .build(); + .with_gamepad(gamepad); let player = commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player { gamepad }) diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs index 4763a00e..97acb1d5 100644 --- a/examples/send_actions_over_network.rs +++ b/examples/send_actions_over_network.rs @@ -122,8 +122,7 @@ fn spawn_player(mut commands: Commands) { use KeyCode::*; let input_map = InputMap::new([(MoveLeft, KeyW), (MoveRight, KeyD), (Jump, Space)]) - .insert(Shoot, MouseButton::Left) - .build(); + .with(Shoot, MouseButton::Left); commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player); diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs index d30f9d20..0f4f5921 100644 --- a/examples/virtual_dpad.rs +++ b/examples/virtual_dpad.rs @@ -25,16 +25,7 @@ struct Player; fn spawn_player(mut commands: Commands) { // Stores "which actions are currently activated" // Map some arbitrary keys into a virtual direction pad that triggers our move action - let input_map = InputMap::new([( - Action::Move, - VirtualDPad { - up: KeyCode::KeyW.into(), - down: KeyCode::KeyS.into(), - left: KeyCode::KeyA.into(), - right: KeyCode::KeyD.into(), - processor: DualAxisProcessor::None, - }, - )]); + let input_map = InputMap::new([(Action::Move, VirtualDPad::wasd())]); commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player); diff --git a/src/action_state.rs b/src/action_state.rs index e07153ab..c6f299ae 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -647,7 +647,7 @@ mod tests { // Starting state let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -660,7 +660,7 @@ mod tests { app.update(); let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(action_state.pressed(&Action::Run)); assert!(action_state.just_pressed(&Action::Run)); @@ -669,7 +669,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -681,7 +681,7 @@ mod tests { app.update(); let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -690,7 +690,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -723,7 +723,7 @@ mod tests { // Starting state let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.released(&Action::One)); assert!(action_state.released(&Action::Two)); assert!(action_state.released(&Action::OneAndTwo)); @@ -734,7 +734,7 @@ mod tests { let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.pressed(&Action::One)); assert!(action_state.released(&Action::Two)); @@ -743,7 +743,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.pressed(&Action::One)); assert!(action_state.released(&Action::Two)); @@ -755,7 +755,7 @@ mod tests { let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); // Now only the longest OneAndTwo has been pressed, // while both One and Two have been released @@ -766,7 +766,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.released(&Action::One)); assert!(action_state.released(&Action::Two)); diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index b92cdfc1..37da9398 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -25,7 +25,7 @@ use std::cmp::Ordering; /// - `ControlLeft + S`, `AltLeft + S` and `ControlLeft + AltLeft + S`: clashes /// /// This strategy is only used when assessing the actions and input holistically, -/// in [`InputMap::which_pressed`], using [`InputMap::handle_clashes`]. +/// in [`InputMap::process_actions`], using [`InputMap::handle_clashes`]. #[non_exhaustive] #[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)] pub enum ClashStrategy { @@ -580,7 +580,7 @@ mod tests { app.send_input(ControlLeft); app.update(); - let action_data = input_map.which_pressed( + let action_data = input_map.process_actions( &InputStreams::from_world(&app.world, None), ClashStrategy::PrioritizeLongest, ); diff --git a/src/input_map.rs b/src/input_map.rs index 9b8aeb4e..1f814c98 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -1,82 +1,94 @@ //! This module contains [`InputMap`] and its supporting methods and impls. -use crate::action_state::ActionData; -use crate::buttonlike::ButtonState; -use crate::clashing_inputs::ClashStrategy; -use crate::input_streams::InputStreams; -use crate::user_input::{InputKind, Modifier, UserInput}; -use crate::Actionlike; +use std::fmt::Debug; #[cfg(feature = "asset")] use bevy::asset::Asset; -use bevy::ecs::component::Component; -use bevy::ecs::system::Resource; -use bevy::input::gamepad::Gamepad; -use bevy::reflect::Reflect; +use bevy::prelude::{Component, Gamepad, Reflect, Resource}; use bevy::utils::HashMap; +use itertools::Itertools; use serde::{Deserialize, Serialize}; -use core::fmt::Debug; - -/** -Maps from raw inputs to an input-method agnostic representation - -Multiple inputs can be mapped to the same action, -and each input can be mapped to multiple actions. - -The provided input types must be able to be converted into a [`UserInput`]. - -By default, if two actions are triggered by a combination of buttons, -and one combination is a strict subset of the other, only the larger input is registered. -For example, pressing both `S` and `Ctrl + S` in your text editor app would save your file, -but not enter the letters `s`. -Set the [`ClashStrategy`] resource -to configure this behavior. - -# Example -```rust -use bevy::prelude::*; -use leafwing_input_manager::prelude::*; -use leafwing_input_manager::user_input::InputKind; - -// You can Run! -// But you can't Hide :( -#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] -enum Action { - Run, - Hide, -} - -// Construction -let mut input_map = InputMap::new([ - // Note that the type of your iterators must be homogeneous; - // you can use `InputKind` or `UserInput` if needed - // as unifying types - (Action::Run, GamepadButtonType::South), - (Action::Hide, GamepadButtonType::LeftTrigger), - (Action::Hide, GamepadButtonType::RightTrigger), -]); +use crate::action_state::ActionData; +use crate::buttonlike::ButtonState; +use crate::clashing_inputs::ClashStrategy; +use crate::input_streams::InputStreams; +use crate::user_input::{InputKind, Modifier, UserInput}; +use crate::Actionlike; -// Insertion -input_map.insert(Action::Run, MouseButton::Left) -.insert(Action::Run, KeyCode::ShiftLeft) -// Chords -.insert_modified(Action::Run, Modifier::Control, KeyCode::KeyR) -.insert_chord(Action::Run, - [InputKind::PhysicalKey(KeyCode::KeyH), - InputKind::GamepadButton(GamepadButtonType::South), - InputKind::Mouse(MouseButton::Middle)], - ); - -// Removal -input_map.clear_action(&Action::Hide); -``` - **/ +/// A Multi-Map that allows you to map actions to multiple [`UserInput`]s. +/// +/// # Many-to-One Mapping +/// +/// You can associate multiple [`UserInput`]s (e.g., keyboard keys, mouse buttons, gamepad buttons) +/// with a single action, simplifying handling complex input combinations for the same action. +/// Duplicate associations are ignored. +/// +/// # One-to-Many Mapping +/// +/// A single [`UserInput`] can be mapped to multiple actions simultaneously. +/// This allows flexibility in defining alternative ways to trigger an action. +/// +/// # Clash Resolution +/// +/// By default, the [`InputMap`] prioritizes larger [`UserInput`] combinations to trigger actions. +/// This means if two actions share some inputs, and one action requires all the inputs +/// of the other plus additional ones; only the larger combination will be registered. +/// +/// This avoids unintended actions from being triggered by more specific input combinations. +/// For example, pressing both `S` and `Ctrl + S` in your text editor app +/// would only save your file (the larger combination), and not enter the letter `s`. +/// +/// This behavior can be customized using the [`ClashStrategy`] resource. +/// +/// # Examples +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// use leafwing_input_manager::user_input::InputKind; +/// +/// // Define your actions. +/// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +/// enum Action { +/// Move, +/// Run, +/// Jump, +/// } +/// +/// // Create an InputMap from an iterable, +/// // allowing for multiple input types per action. +/// let mut input_map = InputMap::new([ +/// // Multiple inputs can be bound to the same action. +/// // Note that the type of your iterators must be homogeneous. +/// (Action::Run, KeyCode::ShiftLeft), +/// (Action::Run, KeyCode::ShiftRight), +/// // Note that duplicate associations are ignored. +/// (Action::Run, KeyCode::ShiftRight), +/// (Action::Jump, KeyCode::Space), +/// ]) +/// // Associate actions with other input types. +/// .with(Action::Move, VirtualDPad::wasd()) +/// .with(Action::Move, DualAxis::left_stick()) +/// // Associate an action with multiple inputs at once. +/// .with_one_to_many(Action::Jump, [KeyCode::KeyJ, KeyCode::KeyU]); +/// +/// // You can also use methods like a normal MultiMap. +/// input_map.insert(Action::Jump, KeyCode::KeyM); +/// +/// // Remove all bindings to a specific action. +/// input_map.clear_action(&Action::Jump); +/// +/// // Remove all bindings. +/// input_map.clear(); +/// ``` #[derive(Resource, Component, Debug, Clone, PartialEq, Eq, Reflect, Serialize, Deserialize)] #[cfg_attr(feature = "asset", derive(Asset))] pub struct InputMap { - /// The usize stored here is the index of the input in the Actionlike iterator + /// The underlying map that stores action-input mappings. map: HashMap>, + + /// The specified [`Gamepad`] from which this map exclusively accepts input. associated_gamepad: Option, } @@ -91,111 +103,116 @@ impl Default for InputMap { // Constructors impl InputMap { - /// Creates a new [`InputMap`] from an iterator of `(user_input, action)` pairs - /// - /// To create an empty input map, use the [`Default::default`] method instead. - /// - /// # Example - /// ```rust - /// use leafwing_input_manager::input_map::InputMap; - /// use leafwing_input_manager::Actionlike; - /// use bevy::input::keyboard::KeyCode; - /// use bevy::prelude::Reflect; - /// - /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] - /// enum Action { - /// Run, - /// Jump, - /// } - /// - /// let input_map = InputMap::new([ - /// (Action::Run, KeyCode::ShiftLeft), - /// (Action::Jump, KeyCode::Space), - /// ]); + /// Creates an [`InputMap`] from an iterator over action-input bindings. + /// Note that the type of your iterators must be homogeneous. /// - /// assert_eq!(input_map.len(), 2); - /// ``` - #[must_use] + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] pub fn new(bindings: impl IntoIterator)>) -> Self { - let mut input_map = InputMap::default(); - input_map.insert_multiple(bindings); - - input_map + bindings + .into_iter() + .fold(Self::default(), |map, (action, input)| { + map.with(action, input) + }) } - /// Constructs a new [`InputMap`] from a `&mut InputMap`, allowing you to insert or otherwise use it + /// Associates an `action` with a specific `input`. + /// Multiple inputs can be bound to the same action. /// - /// This is helpful when constructing input maps using the "builder pattern": - /// 1. Create a new [`InputMap`] struct using [`InputMap::default`] or [`InputMap::new`]. - /// 2. Add bindings and configure the struct using a chain of method calls directly on this struct. - /// 3. Finish building your struct by calling `.build()`, receiving a concrete struct you can insert as a component. - /// - /// Note that this is not the *original* input map, as we do not have ownership of the struct. - /// Under the hood, this is just a more-readable call to `.clone()`. - /// - /// # Example - /// ```rust - /// use leafwing_input_manager::prelude::*; + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with(mut self, action: A, input: impl Into) -> Self { + self.insert(action, input); + self + } - /// use bevy::input::keyboard::KeyCode; - /// use bevy::prelude::Reflect; + /// Associates an `action` with multiple `inputs`. /// - /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] - /// enum Action { - /// Run, - /// Jump, - /// } + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with_one_to_many( + mut self, + action: A, + inputs: impl IntoIterator>, + ) -> Self { + self.insert_one_to_many(action, inputs); + self + } + + /// Adds multiple action-input bindings with the same input type. /// - /// let input_map: InputMap = InputMap::default() - /// .insert(Action::Jump, KeyCode::Space).build(); - /// ``` - #[inline] - #[must_use] - pub fn build(&mut self) -> Self { - self.clone() + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with_multiple( + mut self, + bindings: impl IntoIterator)>, + ) -> Self { + self.insert_multiple(bindings); + self } } // Insertion impl InputMap { - /// Insert a mapping between `input` and `action` + /// Inserts a binding between an `action` and a specific `input`. + /// Multiple inputs can be bound to the same action. + /// + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] pub fn insert(&mut self, action: A, input: impl Into) -> &mut Self { let input = input.into(); // Check for existing copies of the input: insertion should be idempotent - if !matches!(self.map.get(&action), Some(vec) if vec.contains(&input)) { + let inputs = self.map.get(&action); + if !inputs.is_some_and(|inputs| inputs.contains(&input)) { self.map.entry(action).or_default().push(input); } self } - /// Insert a mapping between many `input`'s and one `action` + /// Inserts bindings between the same `action` and multiple [`UserInput`]s. + /// Note that the type of your iterators must be homogeneous. + /// + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. #[inline(always)] pub fn insert_one_to_many( &mut self, action: A, - input: impl IntoIterator>, + inputs: impl IntoIterator>, ) -> &mut Self { - for input in input { - self.insert(action.clone(), input); + let inputs = inputs.into_iter().map(|input| input.into()); + if let Some(bindings) = self.map.get_mut(&action) { + for input in inputs { + if !bindings.contains(&input) { + bindings.push(input); + } + } + } else { + self.map.insert(action, inputs.unique().collect()); } self } - /// Insert a mapping between the provided `input_action_pairs` + /// Inserts multiple action-input bindings. + /// Note that the type of your iterators must be homogeneous. /// - /// This method creates multiple distinct bindings. - /// If you want to require multiple buttons to be pressed at once, use [`insert_chord`](Self::insert_chord). - /// Any iterator convertible into a [`UserInput`] can be supplied. + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] pub fn insert_multiple( &mut self, - input_action_pairs: impl IntoIterator)>, + bindings: impl IntoIterator)>, ) -> &mut Self { - for (action, input) in input_action_pairs { + for (action, input) in bindings.into_iter() { self.insert(action, input); } - self } @@ -228,20 +245,18 @@ impl InputMap { self } - /// Merges the provided [`InputMap`] into the [`InputMap`] this method was called on - /// - /// This adds both of their bindings to the resulting [`InputMap`]. - /// Like usual, any duplicate bindings are ignored. + /// Merges the provided [`InputMap`] into this `map`, combining their bindings, + /// avoiding duplicates. /// - /// If the associated gamepads do not match, the resulting associated gamepad will be set to `None`. + /// If the associated gamepads do not match, the association will be removed. pub fn merge(&mut self, other: &InputMap) -> &mut Self { if self.associated_gamepad != other.associated_gamepad { - self.associated_gamepad = None; + self.clear_gamepad(); } - for other_action in other.map.iter() { - for input in other_action.1.iter() { - self.insert(other_action.0.clone(), input.clone()); + for (other_action, other_inputs) in other.map.iter() { + for other_input in other_inputs.iter().cloned() { + self.insert(other_action.clone(), other_input); } } @@ -251,18 +266,36 @@ impl InputMap { // Configuration impl InputMap { - /// Fetches the [Gamepad] associated with the entity controlled by this entity map + /// Fetches the [`Gamepad`] associated with the entity controlled by this input map. /// /// If this is [`None`], input from any connected gamepad will be used. #[must_use] - pub fn gamepad(&self) -> Option { + #[inline] + pub const fn gamepad(&self) -> Option { self.associated_gamepad } - /// Assigns a particular [`Gamepad`] to the entity controlled by this input map + /// Assigns a particular [`Gamepad`] to the entity controlled by this input map. + /// + /// Use this when an [`InputMap`] should exclusively accept input + /// from a particular gamepad. + /// + /// If this is not called, input from any connected gamepad will be used. + /// The first matching non-zero input will be accepted, + /// as determined by gamepad registration order. + /// + /// Because of this robust fallback behavior, + /// this method can typically be ignored when writing single-player games. + #[inline] + pub fn with_gamepad(mut self, gamepad: Gamepad) -> Self { + self.set_gamepad(gamepad); + self + } + + /// Assigns a particular [`Gamepad`] to the entity controlled by this input map. /// - /// Use this when an [`InputMap`] should exclusively accept input from a - /// particular gamepad. + /// Use this when an [`InputMap`] should exclusively accept input + /// from a particular gamepad. /// /// If this is not called, input from any connected gamepad will be used. /// The first matching non-zero input will be accepted, @@ -270,24 +303,25 @@ impl InputMap { /// /// Because of this robust fallback behavior, /// this method can typically be ignored when writing single-player games. + #[inline] pub fn set_gamepad(&mut self, gamepad: Gamepad) -> &mut Self { self.associated_gamepad = Some(gamepad); self } - /// Clears any [Gamepad] associated with the entity controlled by this input map + /// Clears any [`Gamepad`] associated with the entity controlled by this input map. + #[inline] pub fn clear_gamepad(&mut self) -> &mut Self { self.associated_gamepad = None; self } } -// Check whether buttons are pressed +// Check whether actions are pressed impl InputMap { - /// Is at least one of the corresponding inputs for `action` found in the provided `input` streams? + /// Checks if the `action` are currently pressed by any of the associated [`UserInput`]s. /// - /// Accounts for clashing inputs according to the [`ClashStrategy`]. - /// If you need to inspect many inputs at once, prefer [`InputMap::which_pressed`] instead. + /// Accounts for clashing inputs according to the [`ClashStrategy`] and remove conflicting actions. #[must_use] pub fn pressed( &self, @@ -295,17 +329,17 @@ impl InputMap { input_streams: &InputStreams, clash_strategy: ClashStrategy, ) -> bool { - self.which_pressed(input_streams, clash_strategy) + self.process_actions(input_streams, clash_strategy) .get(action) .map(|datum| datum.state.pressed()) .unwrap_or_default() } - /// Returns the actions that are currently pressed, and the responsible [`UserInput`] for each action + /// Processes [`UserInput`] bindings for each action and generates corresponding [`ActionData`]. /// - /// Accounts for clashing inputs according to the [`ClashStrategy`]. + /// Accounts for clashing inputs according to the [`ClashStrategy`] and remove conflicting actions. #[must_use] - pub fn which_pressed( + pub fn process_actions( &self, input_streams: &InputStreams, clash_strategy: ClashStrategy, @@ -313,10 +347,10 @@ impl InputMap { let mut action_data = HashMap::new(); // Generate the raw action presses - for (action, input_vec) in self.iter() { + for (action, input_bindings) in self.iter() { let mut action_datum = ActionData::default(); - for input in input_vec { + for input in input_bindings { // Merge the axis pair into action datum if let Some(axis_pair) = input_streams.input_axis_pair(input) { action_datum.axis_pair = action_datum @@ -344,15 +378,24 @@ impl InputMap { // Utilities impl InputMap { - /// Returns an iterator over actions with their inputs + /// Returns an iterator over all registered actions with their input bindings. pub fn iter(&self) -> impl Iterator)> { self.map.iter() } - /// Returns an iterator over actions - pub(crate) fn actions(&self) -> impl Iterator { + + /// Returns an iterator over all registered action-input bindings. + pub fn bindings(&self) -> impl Iterator { + self.map + .iter() + .flat_map(|(action, inputs)| inputs.iter().map(move |input| (action, input))) + } + + /// Returns an iterator over all registered actions. + pub fn actions(&self) -> impl Iterator { self.map.keys() } - /// Returns a reference to the inputs mapped to `action` + + /// Returns a reference to the inputs associated with the given `action`. #[must_use] pub fn get(&self, action: &A) -> Option<&Vec> { self.map.get(action) @@ -364,20 +407,20 @@ impl InputMap { self.map.get_mut(action) } - /// How many input bindings are registered total? + /// Count the total number of registered input bindings. #[must_use] pub fn len(&self) -> usize { self.map.values().map(|inputs| inputs.len()).sum() } - /// Are any input bindings registered at all? + /// Returns `true` if the map contains no action-input bindings. #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Clears the map, removing all action-inputs pairs. + /// Clears the map, removing all action-input bindings. /// /// Keeps the allocated memory for reuse. pub fn clear(&mut self) { @@ -387,67 +430,73 @@ impl InputMap { // Removing impl InputMap { - /// Clears all inputs registered for the `action` + /// Clears all input bindings associated with the `action`. pub fn clear_action(&mut self, action: &A) { self.map.remove(action); } - /// Removes the input for the `action` at the provided index + /// Removes the input for the `action` at the provided index. /// /// Returns `Some(input)` if found. pub fn remove_at(&mut self, action: &A, index: usize) -> Option { - let input_vec = self.map.get_mut(action)?; - (input_vec.len() > index).then(|| input_vec.remove(index)) + let input_bindings = self.map.get_mut(action)?; + (input_bindings.len() > index).then(|| input_bindings.remove(index)) } /// Removes the input for the `action` if it exists /// /// Returns [`Some`] with index if the input was found, or [`None`] if no matching input was found. pub fn remove(&mut self, action: &A, input: impl Into) -> Option { - let input_vec = self.map.get_mut(action)?; + let bindings = self.map.get_mut(action)?; let user_input = input.into(); - let index = input_vec.iter().position(|i| i == &user_input)?; - input_vec.remove(index); + let index = bindings.iter().position(|input| input == &user_input)?; + bindings.remove(index); Some(index) } } impl From>> for InputMap { - /// Create `InputMap` from `HashMap>` + /// Converts a [`HashMap`] mapping actions to multiple [`UserInput`]s into an [`InputMap`]. /// - /// # Example - /// ```rust - /// use leafwing_input_manager::input_map::InputMap; - /// use leafwing_input_manager::user_input::UserInput; - /// use leafwing_input_manager::Actionlike; - /// use bevy::input::keyboard::KeyCode; - /// use bevy::reflect::Reflect; + /// # Examples /// + /// ```rust + /// use bevy::prelude::*; /// use bevy::utils::HashMap; + /// use leafwing_input_manager::prelude::*; /// /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] /// enum Action { /// Run, /// Jump, /// } + /// + /// // Create an InputMap from a HashMap mapping actions to their input bindings. /// let mut map: HashMap> = HashMap::default(); + /// + /// // Bind the "run" action to either the left or right shift keys to trigger the action. /// map.insert( /// Action::Run, /// vec![KeyCode::ShiftLeft.into(), KeyCode::ShiftRight.into()], /// ); - /// let input_map = InputMap::::from(map); + /// + /// let input_map = InputMap::from(map); /// ``` - fn from(map: HashMap>) -> Self { - map.iter() - .flat_map(|(action, inputs)| inputs.iter().map(|input| (action.clone(), input.clone()))) - .collect() + fn from(raw_map: HashMap>) -> Self { + let mut input_map = Self::default(); + for (action, inputs) in raw_map.into_iter() { + input_map.insert_one_to_many(action, inputs); + } + input_map } } impl FromIterator<(A, UserInput)> for InputMap { - /// Create `InputMap` from iterator with the item type `(A, UserInput)` fn from_iter>(iter: T) -> Self { - InputMap::new(iter) + iter.into_iter() + .fold(Self::default(), |map, (action, input)| { + map.with(action, input) + }) } } @@ -455,6 +504,7 @@ mod tests { use bevy::prelude::Reflect; use serde::{Deserialize, Serialize}; + use super::*; use crate as leafwing_input_manager; use crate::prelude::*; @@ -479,64 +529,69 @@ mod tests { } #[test] - fn insertion_idempotency() { + fn creation() { use bevy::input::keyboard::KeyCode; - let mut input_map = InputMap::::default(); - input_map.insert(Action::Run, KeyCode::Space); - - assert_eq!( - input_map.get(&Action::Run), - Some(&vec![KeyCode::Space.into()]) - ); - - // Duplicate insertions should not change anything - input_map.insert(Action::Run, KeyCode::Space); - assert_eq!( - input_map.get(&Action::Run), - Some(&vec![KeyCode::Space.into()]) - ); + let input_map = InputMap::default() + .with(Action::Run, KeyCode::KeyW) + .with(Action::Run, KeyCode::ShiftLeft) + // Duplicate associations should be ignored + .with(Action::Run, KeyCode::ShiftLeft) + .with_one_to_many(Action::Run, [KeyCode::KeyR, KeyCode::ShiftRight]) + .with_multiple([ + (Action::Jump, KeyCode::Space), + (Action::Hide, KeyCode::ControlLeft), + (Action::Hide, KeyCode::ControlRight), + ]); + + let expected_bindings: HashMap = HashMap::from([ + (KeyCode::KeyW.into(), Action::Run), + (KeyCode::ShiftLeft.into(), Action::Run), + (KeyCode::KeyR.into(), Action::Run), + (KeyCode::ShiftRight.into(), Action::Run), + (KeyCode::Space.into(), Action::Jump), + (KeyCode::ControlLeft.into(), Action::Hide), + (KeyCode::ControlRight.into(), Action::Hide), + ]); + + for (action, input) in input_map.bindings() { + let expected_action = expected_bindings.get(input).unwrap(); + assert_eq!(expected_action, action); + } } #[test] - fn multiple_insertion() { + fn insertion_idempotency() { use bevy::input::keyboard::KeyCode; - let mut input_map_1 = InputMap::::default(); - input_map_1.insert(Action::Run, KeyCode::Space); - input_map_1.insert(Action::Run, KeyCode::Enter); - - assert_eq!( - input_map_1.get(&Action::Run), - Some(&vec![KeyCode::Space.into(), KeyCode::Enter.into()]) - ); + let mut input_map = InputMap::default(); + input_map.insert(Action::Run, KeyCode::Space); - let input_map_2 = - InputMap::::new([(Action::Run, KeyCode::Space), (Action::Run, KeyCode::Enter)]); + let expected: Vec = vec![KeyCode::Space.into()]; + assert_eq!(input_map.get(&Action::Run), Some(&expected)); - assert_eq!(input_map_1, input_map_2); + // Duplicate insertions should not change anything + input_map.insert(Action::Run, KeyCode::Space); + assert_eq!(input_map.get(&Action::Run), Some(&expected)); } #[test] - fn chord_singleton_coercion() { - use crate::input_map::UserInput; + fn multiple_insertion() { use bevy::input::keyboard::KeyCode; - // Single items in a chord should be coerced to a singleton - let mut input_map_1 = InputMap::::default(); - input_map_1.insert(Action::Run, KeyCode::Space); - - let mut input_map_2 = InputMap::::default(); - input_map_2.insert(Action::Run, UserInput::chord([KeyCode::Space])); + let mut input_map = InputMap::default(); + input_map.insert(Action::Run, KeyCode::Space); + input_map.insert(Action::Run, KeyCode::Enter); - assert_eq!(input_map_1, input_map_2); + let expected: Vec = vec![KeyCode::Space.into(), KeyCode::Enter.into()]; + assert_eq!(input_map.get(&Action::Run), Some(&expected)); } #[test] fn input_clearing() { use bevy::input::keyboard::KeyCode; - let mut input_map = InputMap::::default(); + let mut input_map = InputMap::default(); input_map.insert(Action::Run, KeyCode::Space); // Clearing action diff --git a/src/systems.rs b/src/systems.rs index 1823b9bb..f77a7071 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -136,7 +136,7 @@ pub fn update_action_state( associated_gamepad: input_map.gamepad(), }; - action_state.update(input_map.which_pressed(&input_streams, *clash_strategy)); + action_state.update(input_map.process_actions(&input_streams, *clash_strategy)); } }