Skip to content

Commit

Permalink
Add new fluent builders and iterators for InputMap (#513)
Browse files Browse the repository at this point in the history
* Add new fluent builders and iterators for `InputMap`

* Improve tests and RELEASES.md

* Fix unused import

* Typo
  • Loading branch information
Shute052 authored May 3, 2024
1 parent 41bfa80 commit bc2375a
Show file tree
Hide file tree
Showing 17 changed files with 391 additions and 335 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 61 additions & 37 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Processor>`):
- 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<Vec<AxisProcessor>>::from` for `AxisProcessor::Pipeline`.
- `DualAxisProcessor::with_processor` or `From<Vec<DualAxisProcessor>>::from` for `DualAxisProcessor::Pipeline`.
- Inversion: Reverses control (positive becomes negative, etc.)
- `AxisProcessor::Inverted`: Single-axis inversion.
- `DualAxisInverted`: Dual-axis inversion, implemented `Into<DualAxisProcessor>`.
- Sensitivity: Adjusts control responsiveness (doubling, halving, etc.).
- `AxisProcessor::Sensitivity`: Single-axis scaling.
- `DualAxisSensitivity`: Dual-axis scaling, implemented `Into<DualAxisProcessor>`.
- Value Bounds: Define the boundaries for constraining input values.
- `AxisBounds`: Restricts single-axis values to a range, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into<DualAxisProcessor>`.
- `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into<DualAxisProcessor>`.
- Deadzones: Ignores near-zero values, treating them as zero.
- Unscaled versions:
- `AxisExclusion`: Excludes small single-axis values, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into<DualAxisProcessor>`.
- `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into<DualAxisProcessor>`.
- Scaled versions:
- `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into<DualAxisProcessor>`.
- `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into<DualAxisProcessor>`.
- 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<Processor>`):
- 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<AxisProcessor>::from_iter` for `AxisProcessor::Pipeline`.
- `DualAxisProcessor::with_processor` or `FromIterator<DualAxisProcessor>::from_iter` for `DualAxisProcessor::Pipeline`.
- Inversion: Reverses control (positive becomes negative, etc.)
- `AxisProcessor::Inverted`: Single-axis inversion.
- `DualAxisInverted`: Dual-axis inversion, implemented `Into<DualAxisProcessor>`.
- Sensitivity: Adjusts control responsiveness (doubling, halving, etc.).
- `AxisProcessor::Sensitivity`: Single-axis scaling.
- `DualAxisSensitivity`: Dual-axis scaling, implemented `Into<DualAxisProcessor>`.
- Value Bounds: Define the boundaries for constraining input values.
- `AxisBounds`: Restricts single-axis values to a range, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into<DualAxisProcessor>`.
- `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into<DualAxisProcessor>`.
- Deadzones: Ignores near-zero values, treating them as zero.
- Unscaled versions:
- `AxisExclusion`: Excludes small single-axis values, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into<DualAxisProcessor>`.
- `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into<DualAxisProcessor>`.
- Scaled versions:
- `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into<AxisProcessor>` and `Into<DualAxisProcessor>`.
- `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into<DualAxisProcessor>`.
- `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into<DualAxisProcessor>`.

### Usability

#### InputMap

Introduce new fluent builders for creating a new `InputMap<A>` with short configurations:

- `fn with(mut self, action: A, input: impl Into<Item = UserInput>)`.
- `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator<Item = UserInput>)`.
- `fn with_multiple(mut self, bindings: impl IntoIterator<Item = (A, UserInput)>) -> Self`.
- `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`.

Introduce new iterators over `InputMap<A>`:

- `bindings(&self) -> impl Iterator<Item = (&A, &UserInput)>` for iterating over all registered action-input bindings.
- `actions(&self) -> impl Iterator<Item = &A>` for iterating over all registered actions.

### Bugs

Expand Down
23 changes: 11 additions & 12 deletions benches/input_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,16 @@ fn construct_input_map_from_iter() -> InputMap<TestAction> {
fn construct_input_map_from_chained_calls() -> InputMap<TestAction> {
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),
)
}

Expand All @@ -63,7 +62,7 @@ fn which_pressed(
clash_strategy: ClashStrategy,
) -> HashMap<TestAction, ActionData> {
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) {
Expand Down
8 changes: 2 additions & 6 deletions examples/action_state_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlayerAction> {
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())
}
}

Expand Down
5 changes: 2 additions & 3 deletions examples/arpg_indirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 5 additions & 6 deletions examples/axis_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions examples/clash_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
9 changes: 4 additions & 5 deletions examples/input_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -35,15 +34,15 @@ 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.
.replace_processor(CircleDeadZone::default())
// 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([
Expand Down
14 changes: 7 additions & 7 deletions examples/mouse_wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ fn main() {
#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
enum CameraMovement {
Zoom,
Pan,
PanLeft,
PanRight,
}

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));
Expand Down
6 changes: 2 additions & 4 deletions examples/multiplayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions examples/register_gamepads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
3 changes: 1 addition & 2 deletions examples/send_actions_over_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 1 addition & 10 deletions examples/virtual_dpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit bc2375a

Please sign in to comment.