Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new fluent builders and iterators for InputMap #513

Merged
merged 4 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

@Shute052 Shute052 May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clippy (1.79.0-nightly (aed2187d5 2024-04-27) reports doc_markdown here

- 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 'Enhancements: InputMap' for details).
- renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity.

### Enhancements

- added `serde_typetag` procedural macro attribute for trait object type tagging.

#### 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.

#### 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>`.

### 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