Skip to content

Commit

Permalink
Swap to a trait-based architecture for UserInput (#534)
Browse files Browse the repository at this point in the history
* Reimplement keyboard inputs

* Update document

* Migrate to new input processors and refactor the trait

* Implement mouse inputs

* Prefer `FromIter<*AxisProcessor>` over `From<Vec<*AxisProcessor>>`

* Implement `GamepadButtonType` and `GamepadAxisType`

* Add a `pipeline` method to create input processing pipelines

* Typo

* Replace old `UserInput` and `InputKind` with the new ones

* RELEASES.md

* Fix clashing inputs

* Typo

* Docs

* Rename

* Docs

* Docs

* RELEASES.md

* Fix wrong gamepad button value

* Rename `InputChord::from_multiple` to `new`

* Fix ci

* Simple compiler fixes

* Fix modifier import

* Fix bevy_egui dep

* Ignore and fix failing tests

#538

* Reuse try_apply logic

---------

Co-authored-by: Shute052 <sinoctis@163.com>
  • Loading branch information
alice-i-cecile and Shute052 committed Jun 10, 2024
1 parent a513ad0 commit 6e56c2b
Show file tree
Hide file tree
Showing 33 changed files with 4,583 additions and 2,163 deletions.
6 changes: 1 addition & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ bevy = { version = "0.14.0-rc.2", default-features = false, features = [
"serialize",
] }
bevy_ecs = { version = "0.14.0-rc.2", features = ["serde"] }
bevy_egui = { version = "0.27", optional = true }
bevy_egui = { git = "https://github.com/Friz64/bevy_egui", branch = "bevy-0.14", optional = true }

derive_more = { version = "0.99", default-features = false, features = [
"display",
Expand Down Expand Up @@ -75,7 +75,6 @@ bevy = { version = "0.14.0-rc.2", default-features = false, features = [
"zstd",
"bevy_pbr",
] }
bevy_egui = "0.27"
serde_test = "1.0"
criterion = "0.5"

Expand All @@ -96,6 +95,3 @@ path = "src/lib.rs"
name = "press_duration"
path = "examples/press_duration.rs"
required-features = ["timing"]

[patch.crates-io]
bevy_egui = { git = "https://github.com/Friz64/bevy_egui.git", branch = "bevy-0.14" }
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ and a single input can result in multiple actions being triggered, which can be
- Ergonomic insertion API that seamlessly blends multiple input types for you
- Can't decide between `input_map.insert(Action::Jump, KeyCode::Space)` and `input_map.insert(Action::Jump, GamepadButtonType::South)`? Have both!
- Full support for arbitrary button combinations: chord your heart out.
- `input_map.insert_chord(Action::Console, [KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC])`
- `input_map.insert(Action::Console, InputChord::new([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))`
- Sophisticated input disambiguation with the `ClashStrategy` enum: stop triggering individual buttons when you meant to press a chord!
- Create an arbitrary number of strongly typed disjoint action sets by adding multiple copies of this plugin: decouple your camera and player state
- Local multiplayer support: freely bind keys to distinct entities, rather than worrying about singular global state
Expand Down
81 changes: 71 additions & 10 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@

### Breaking Changes

- removed `UserInput` and `InputKind` enums in favor of the new `UserInput` trait and its impls (see 'Enhancements: New Inputs' for details).
- renamed `Modifier` enum to `ModifierKey`.
- by default, all input events are unprocessed now, using `With*ProcessingPipelineExt` methods to configure your preferred processing steps.
- applied clashing check to continuous mouse inputs, for example:
- `MouseScrollAxis::Y` will clash with `MouseScrollDirection::UP` and `MouseScrollDirection::DOWN`.
- `MouseMove` will clash with all the two axes and the four directions.
- refactored the method signatures of `InputMap` to fit the new input types.
- removed `InputMap::insert_chord` and `InputMap::insert_modified` due to their limited applicability within the type system.
- the new `InputChord` contructors and builders allow you to define chords with guaranteed type safety.
- the new `ModifierKey::with` method simplifies the creation of input chords that include the modifier and your desired input.
- the `timing` field of the `ActionData` is now disabled by default. Timing information will only be collected
if the `timing` feature is enabled. It is disabled by default because most games don't require timing information.
(how long a button was pressed for)
- removed `ToggleActions` resource in favor of new methods on `ActionState`: `disable_all`, `disable(action)`, `enable_all`, `enable(action)`, and `disabled(action)`.
- 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.
- replaced axis-like input handling with new input processors (see 'Enhancements: Input Processors' for details).
- removed `DeadZoneShape` in favor of new dead zone processors.
- removed `DeadZoneShape` in favor of new dead zone processors (see 'Enhancements: Input Processors' for details).
- refactored the fields and methods of `RawInputs` to fit the new input types.
- removed `Direction` type in favor of `bevy::math::primitives::Direction2d`.
- removed the hacky `value` field and `from_value` method from `SingleAxis` and `DualAxis`, in favor of new input mocking.
- removed `MockInput::send_input` methods, in favor of new input mocking APIs (see 'Usability: MockInput' for details).
- made the dependency on bevy's `bevy_gilrs` feature optional.
- it is still enabled by leafwing-input-manager's default features.
Expand All @@ -24,6 +33,58 @@
- Inputs are now handled correctly in the `FixedUpdate` schedule! Previously, the `ActionState`s were only updated in the `PreUpdate` schedule, so you could have situations where an action was
marked as `just_pressed` multiple times in a row (if the `FixedUpdate` schedule ran multiple times in a frame) or was missed entirely (if the `FixedUpdate` schedule ran 0 times in a frame).

#### New Inputs

- added `UserInput` trait.
- added `UserInput` impls for gamepad input events:
- implemented `UserInput` for Bevy’s `GamepadAxisType`-related inputs.
- `GamepadStick`: Continuous or discrete movement events of the left or right gamepad stick along both X and Y axes.
- `GamepadControlAxis`: Continuous or discrete movement events of a `GamepadAxisType`.
- `GamepadControlDirection`: Discrete movement direction events of a `GamepadAxisType`, treated as a button press.
- implemented `UserInput` for Bevy’s `GamepadButtonType` directly.
- added `GamepadVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `GamepadButtonType`s.
- added `GamepadVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `GamepadButtonType`s.
- added `UserInput` impls for keyboard inputs:
- implemented `UserInput` for Bevy’s `KeyCode` directly.
- implemented `UserInput` for `ModifierKey`.
- added `KeyboardVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `KeyCode`s.
- added `KeyboardVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `KeyCode`s.
- added `UserInput` impls for mouse inputs:
- implemented `UserInput` for movement-related inputs.
- `MouseMove`: Continuous or discrete movement events of the mouse both X and Y axes.
- `MouseMoveAxis`: Continuous or discrete movement events of the mouse on an axis, similar to the old `SingleAxis::mouse_motion_*`.
- `MouseMoveDirection`: Discrete movement direction events of the mouse on an axis, similar to the old `MouseMotionDirection`.
- implemented `UserInput` for wheel-related inputs.
- `MouseScroll`: Continuous or discrete movement events of the mouse wheel both X and Y axes.
- `MouseScrollAxis`: Continuous or discrete movement events of the mouse wheel on an axis, similar to the old `SingleAxis::mouse_wheel_*`.
- `MouseScrollDirection`: Discrete movement direction events of the mouse wheel on an axis, similar to the old `MouseWheelDirection`.
- added `InputChord` for combining multiple inputs, similar to the old `UserInput::Chord`.

##### Migration Guide

- the old `SingleAxis` is now:
- `GamepadControlAxis` for gamepad axes.
- `MouseMoveAxis::X` and `MouseMoveAxis::Y` for continuous mouse movement.
- `MouseScrollAxis::X` and `MouseScrollAxis::Y` for continuous mouse wheel movement.
- the old `DualAxis` is now:
- `GamepadStick` for gamepad sticks.
- `MouseMove::default()` for continuous mouse movement.
- `MouseScroll::default()` for continuous mouse wheel movement.
- the old `Modifier` is now `ModifierKey`.
- the old `MouseMotionDirection` is now `MouseMoveDirection`.
- the old `MouseWheelDirection` is now `MouseScrollDirection`.
- the old `UserInput::Chord` is now `InputChord`.
- the old `UserInput::VirtualAxis` is now:
- `GamepadVirtualAxis` for four gamepad buttons.
- `KeyboardVirtualAxis` for four keys.
- `MouseMoveAxis::X.digital()` and `MouseMoveAxis::Y.digital()` for discrete mouse movement.
- `MouseScrollAxis::X.digital()` and `MouseScrollAxis::Y.digital()` for discrete mouse wheel movement.
- the old `UserInput::VirtualDPad` is now:
- `GamepadVirtualDPad` for four gamepad buttons.
- `KeyboardVirtualDPad` for four keys.
- `MouseMove::default().digital()` for discrete mouse movement.
- `MouseScroll::default().digital()` for discrete mouse wheel movement.

#### Input Processors

Input processors allow you to create custom logic for axis-like input manipulation.
Expand Down Expand Up @@ -68,27 +129,27 @@ Input processors allow you to create custom logic for axis-like input manipulati
#### InputMap

- added new fluent builders for creating a new `InputMap<A>` with short configurations:
- `fn with(mut self, action: A, input: impl Into<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(mut self, action: A, input: impl UserInput)`.
- `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator<Item = impl UserInput>)`.
- `fn with_multiple(mut self, bindings: impl IntoIterator<Item = (A, impl UserInput)>) -> Self`.
- `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`.

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

### MockInput

- added new methods for the `MockInput` trait.
- `fn press_input(&self, input: impl Into<UserInput>)` for simulating button and key presses.
- `fn send_axis_values(&self, input: impl Into<UserInput>, values: impl IntoIterator<Item = f32>)` for sending value changed events to each axis represented by the input.
- `fn press_input(&self, input: impl UserInput)` for simulating button and key presses.
- `fn send_axis_values(&self, input: impl UserInput, values: impl IntoIterator<Item = f32>)` for sending value changed events to each axis represented by the input.
- as well as methods for a specific gamepad.
- implemented the methods for `MutableInputStreams`, `World`, and `App`.

### QueryInput

- added new methods for the `QueryInput` trait.
- `fn read_axis_values(&self, input: impl Into<UserInput>) -> Vec<f32>` to read the values on all axes represented by an input.
- `fn read_axis_values(&self, input: impl UserInput) -> Vec<f32>` to read the values on all axes represented by an input.
- as well as methods for a specific gamepad.
- implemented the methods for `InputStreams`, `World`, and `App`.

Expand Down
4 changes: 2 additions & 2 deletions examples/action_state_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub enum PlayerAction {

// Exhaustively match `PlayerAction` and define the default bindings to the input
impl PlayerAction {
fn mkb_input_map() -> InputMap<PlayerAction> {
InputMap::new([(Self::Jump, KeyCode::Space)]).with(Self::Move, VirtualDPad::wasd())
fn mkb_input_map() -> InputMap<Self> {
InputMap::new([(Self::Jump, KeyCode::Space)]).with(Self::Move, KeyboardVirtualDPad::WASD)
}
}

Expand Down
8 changes: 4 additions & 4 deletions examples/axis_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ 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 control
.with(Action::Move, DualAxis::left_stick())
// Let's bind the right gamepad trigger to the throttle action
// Let's bind the left stick for the move action
.with(Action::Move, GamepadStick::LEFT)
// And then bind the right gamepad trigger to the throttle action
.with(Action::Throttle, GamepadButtonType::RightTrigger2)
// And we'll use the right stick's x-axis as a rudder control
.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_deadzone_symmetric(0.1),
GamepadControlAxis::RIGHT_X.with_deadzone_symmetric(0.1),
);
commands
.spawn(InputManagerBundle::with_map(input_map))
Expand Down
8 changes: 4 additions & 4 deletions examples/clash_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ fn spawn_input_map(mut commands: Commands) {
// Setting up input mappings in the obvious way
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]);
input_map.insert_chord(TwoAndThree, [Digit2, Digit3]);
input_map.insert(OneAndTwo, InputChord::new([Digit1, Digit2]));
input_map.insert(OneAndThree, InputChord::new([Digit1, Digit3]));
input_map.insert(TwoAndThree, InputChord::new([Digit2, Digit3]));

input_map.insert_chord(OneAndTwoAndThree, [Digit1, Digit2, Digit3]);
input_map.insert(OneAndTwoAndThree, InputChord::new([Digit1, Digit2, Digit3]));

commands.spawn(InputManagerBundle::with_map(input_map));
}
Expand Down
4 changes: 2 additions & 2 deletions examples/default_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ impl PlayerAction {
let mut input_map = InputMap::default();

// Default gamepad input bindings
input_map.insert(Self::Run, DualAxis::left_stick());
input_map.insert(Self::Run, GamepadStick::LEFT);
input_map.insert(Self::Jump, GamepadButtonType::South);
input_map.insert(Self::UseItem, GamepadButtonType::RightTrigger2);

// Default kbm input bindings
input_map.insert(Self::Run, VirtualDPad::wasd());
input_map.insert(Self::Run, KeyboardVirtualDPad::WASD);
input_map.insert(Self::Jump, KeyCode::Space);
input_map.insert(Self::UseItem, MouseButton::Left);

Expand Down
4 changes: 2 additions & 2 deletions examples/input_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn spawn_player(mut commands: Commands) {
let input_map = InputMap::default()
.with(
Action::Move,
VirtualDPad::wasd()
KeyboardVirtualDPad::WASD
// You can configure a processing pipeline to handle axis-like user inputs.
//
// This step adds a circular deadzone that normalizes input values
Expand All @@ -39,7 +39,7 @@ fn spawn_player(mut commands: Commands) {
.with(
Action::LookAround,
// You can also use a sequence of processors as the processing pipeline.
DualAxis::mouse_motion().replace_processing_pipeline([
MouseMove::default().replace_processing_pipeline([
// The first processor is a circular deadzone.
CircleDeadZone::new(0.1).into(),
// The next processor doubles inputs normalized by the deadzone.
Expand Down
4 changes: 2 additions & 2 deletions examples/mouse_motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ fn setup(mut commands: Commands) {
let input_map = InputMap::new([
// This will capture the total continuous value, for direct use.
// Note that you can also use discrete gesture-like motion,
// via the `MouseMotionDirection` enum.
(CameraMovement::Pan, DualAxis::mouse_motion()),
// via the `MouseMoveDirection` enum.
(CameraMovement::Pan, MouseMove::default()),
]);
commands
.spawn(Camera2dBundle::default())
Expand Down
17 changes: 8 additions & 9 deletions examples/mouse_wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ enum CameraMovement {
fn setup(mut commands: Commands) {
let input_map = InputMap::default()
// This will capture the total continuous value, for direct use.
.with(CameraMovement::Zoom, SingleAxis::mouse_wheel_y())
.with(CameraMovement::Zoom, MouseScrollAxis::Y)
// This will return a binary button-like output.
.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
.with(CameraMovement::Pan, VirtualDPad::mouse_wheel())
// Or even a continuous `DualAxis`!
.with(CameraMovement::Pan, DualAxis::mouse_wheel());
.with(CameraMovement::PanLeft, MouseScrollDirection::LEFT)
.with(CameraMovement::PanRight, MouseScrollDirection::RIGHT)
// Alternatively, you could model them as a continuous dual-axis input
.with(CameraMovement::Pan, MouseScroll::default())
// Or even a digital dual-axis input!
.with(CameraMovement::Pan, MouseScroll::default().digital());
commands
.spawn(Camera2dBundle::default())
.insert(InputManagerBundle::with_map(input_map));
Expand Down Expand Up @@ -62,7 +61,7 @@ fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), W

let (mut camera_transform, action_state) = query.single_mut();

// When using the `MouseWheelDirection` type, mouse wheel inputs can be treated like simple buttons
// When using the `MouseScrollDirection` type, mouse wheel inputs can be treated like simple buttons
if action_state.pressed(&CameraMovement::PanLeft) {
camera_transform.translation.x -= CAMERA_PAN_RATE;
}
Expand Down
8 changes: 4 additions & 4 deletions examples/twin_stick_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ impl PlayerAction {
let mut input_map = InputMap::default();

// Default gamepad input bindings
input_map.insert(Self::Move, DualAxis::left_stick());
input_map.insert(Self::Look, DualAxis::right_stick());
input_map.insert(Self::Move, GamepadStick::LEFT);
input_map.insert(Self::Look, GamepadStick::RIGHT);
input_map.insert(Self::Shoot, GamepadButtonType::RightTrigger);

// Default kbm input bindings
input_map.insert(Self::Move, VirtualDPad::wasd());
input_map.insert(Self::Look, VirtualDPad::arrow_keys());
input_map.insert(Self::Move, KeyboardVirtualDPad::WASD);
input_map.insert(Self::Look, KeyboardVirtualDPad::ARROW_KEYS);
input_map.insert(Self::Shoot, MouseButton::Left);

input_map
Expand Down
8 changes: 6 additions & 2 deletions examples/virtual_dpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ 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::wasd())]);
let input_map = InputMap::new([(
Action::Move,
// Define a virtual D-pad using four arbitrary keys.
// You can also use GamepadVirtualDPad to create similar ones using gamepad buttons.
KeyboardVirtualDPad::new(KeyCode::KeyW, KeyCode::KeyS, KeyCode::KeyA, KeyCode::KeyD),
)]);
commands
.spawn(InputManagerBundle::with_map(input_map))
.insert(Player);
Expand Down
Loading

0 comments on commit 6e56c2b

Please sign in to comment.