Skip to content

Commit

Permalink
Refactor UserInput into Buttonlike, Axislike and DualAxislike (
Browse files Browse the repository at this point in the history
…#548)

* Initial trait split

* Migrate simple forms of user input

* First try for moving over chords

* Fix ModifierKey::with

* Fix missing import

* Rename InputMap::map

* Split apart fancy reflection and serde code

* Add more reflect and serde impls for split traits

* Simple compile error

* Don't bother updating ActionData with axislike values

* Migrate input mocking to new API

* Add more serde trait registries

* Missing import

* Rename InputChord to ButtonlikeChord

* Add chords for axislike types

* Clarify InputMap docs for [`Buttonlike`] limitations

* Clear actions regardless of their binding

* Add Actionlike::input_control_kind

* Add InputMap::insert_axis and friends

* Validate InputControlKind in InputMap construction

* Remove complex and unhelpful test

* Fix test

* Migrate and simplify user input tests

* More input test cleanup

* Fix UserInput::decompose for fancy chords

* Try to parse the fields to determine correct input kind

* Revert "Try to parse the fields to determine correct input kind"

This reverts commit 91bf83b.

* Just manually implement Actionlike for non-trivial cases

* Update release notes

* Move InputControlKind to crate root

* Add InputControlKind to the prelude

* Make tests and examples compile

* ActionData -> ButtonData

* Add AxisDatum and DualAxisDatum

* DualAxisData yeet

* Yeet silly Datum naming

* Unused imports

* Remove value and axis_pair fields from ButtonData

* Newtype the InputMap -> ActionData interchange

* with_dualaxis -> with_dual_axis

* Fix logic for ActionDiff::apply_diff

* `ActionDiff::ValueChanged` is now `ActionDiff::AxisChanged`

* Start refactoring ActionDiff

* Finish rewriting action diffing

* Improve docs for BasicInputs

* Improve clashing input docs

* Split apart methods on `InputMap`

* Fix incorrect link

* Progress towards making examples and benchmarks compile

* Stub out `InputMap::process_actions`

* Use simple data in UpdatedActions struct

* Trivial fixes

* Actual update the action values based on the input map

* Make ActionState::axis_pair non-optional

* Make tests and examples compile

* Cargo fmt

* Clippy autofix

* Cargo fmt

* Fix broken doc links

* Split apart action data for code organization

* Clarify code for ActionState::pressed and friends

* Fix default behavior of ActionState buttons

* More doc links

* Add tests for the length of input types

* Refactor BasicInputs to be much simpler

* Add ButtonlikeChord::modified

* Fix length computation for buttonlike chords

* Make clash detection test more useful

* Improve clashing inputs logic slightly

* Revert "Refactor BasicInputs to be much simpler"

This reverts commit 70fe3fb.

* BasicInputs::Group -> BasicInputs::Chord

* Better docs for BasicInputs::len

* Store Buttonlikes in `BasicInputs`

* Clean up docs

* Add more debugging to dpad + chord clash test

* InputMap::get -> InputMap::get_buttonlike

* Upgrade warnings to errors for wrong input kind

* More debugging for tests and debug_assert for misconfigured Actionlike

* Add InputMap::decomposed

* Add UserInputWrapper and UserInput::get

* Add debug asserts to UserInputWrapper::kind

* Ignore failing tests for now

* Fix doc tests

* Clean up migration guide

* Cargo fmt

* Disable ActionDiff tests

* Remove test checking that an axis is pressed

* Remove meaningless assertions that axes are released

* Remove assertions that axes are pressed

* Remove asserrtions calling .value on a dual_axis action

* Add debug asserts for UserInputKind to ActionState

* Stop calling .value on DualAxis actions in tests

* Fix Actionlike impl for AxislikeTestAction

* Clean up mouse integration tests in the same ways

* Remove nonsensical test
  • Loading branch information
alice-i-cecile authored Jul 18, 2024
1 parent 1b75a86 commit 5e90a7d
Show file tree
Hide file tree
Showing 39 changed files with 3,992 additions and 2,924 deletions.
161 changes: 87 additions & 74 deletions RELEASES.md

Large diffs are not rendered by default.

34 changes: 10 additions & 24 deletions benches/action_state.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use bevy::{prelude::Reflect, utils::HashMap};
use criterion::{criterion_group, criterion_main, Criterion};
#[cfg(feature = "timing")]
use leafwing_input_manager::timing::Timing;
use leafwing_input_manager::{
action_state::ActionData, buttonlike::ButtonState, prelude::ActionState, Actionlike,
};
use leafwing_input_manager::{input_map::UpdatedActions, prelude::ActionState, Actionlike};

#[derive(Actionlike, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
enum TestAction {
Expand Down Expand Up @@ -43,7 +39,7 @@ fn just_released(action_state: &ActionState<TestAction>) -> bool {
action_state.just_released(&TestAction::A)
}

fn update(mut action_state: ActionState<TestAction>, action_data: HashMap<TestAction, ActionData>) {
fn update(mut action_state: ActionState<TestAction>, action_data: UpdatedActions<TestAction>) {
action_state.update(action_data);
}

Expand All @@ -58,27 +54,17 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("released", |b| b.iter(|| released(&action_state)));
c.bench_function("just_released", |b| b.iter(|| just_released(&action_state)));

let action_data: HashMap<TestAction, ActionData> = TestAction::variants()
.map(|action| {
(
action,
ActionData {
state: ButtonState::JustPressed,
update_state: ButtonState::Released,
fixed_update_state: ButtonState::Released,
value: 0.0,
axis_pair: None,
#[cfg(feature = "timing")]
timing: Timing::default(),
consumed: false,
disabled: false,
},
)
})
let button_actions: HashMap<TestAction, bool> = TestAction::variants()
.map(|action| (action, true))
.collect();

let updated_actions = UpdatedActions {
button_actions,
..Default::default()
};

c.bench_function("update", |b| {
b.iter(|| update(action_state.clone(), action_data.clone()))
b.iter(|| update(action_state.clone(), updated_actions.clone()))
});
}

Expand Down
5 changes: 2 additions & 3 deletions benches/input_map.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use bevy::prelude::Reflect;
use bevy::utils::HashMap;
use bevy::{
input::InputPlugin,
prelude::{App, KeyCode},
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use leafwing_input_manager::input_map::UpdatedActions;
use leafwing_input_manager::{
action_state::ActionData,
input_streams::InputStreams,
prelude::{ClashStrategy, InputMap, MockInput},
Actionlike,
Expand Down Expand Up @@ -60,7 +59,7 @@ fn construct_input_map_from_chained_calls() -> InputMap<TestAction> {
fn which_pressed(
input_streams: &InputStreams,
clash_strategy: ClashStrategy,
) -> HashMap<TestAction, ActionData> {
) -> UpdatedActions<TestAction> {
let input_map = construct_input_map_from_iter();
input_map.process_actions(input_streams, clash_strategy)
}
Expand Down
7 changes: 4 additions & 3 deletions examples/action_state_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub enum PlayerAction {
// Exhaustively match `PlayerAction` and define the default bindings to the input
impl PlayerAction {
fn mkb_input_map() -> InputMap<Self> {
InputMap::new([(Self::Jump, KeyCode::Space)]).with(Self::Move, KeyboardVirtualDPad::WASD)
InputMap::new([(Self::Jump, KeyCode::Space)])
.with_dual_axis(Self::Move, KeyboardVirtualDPad::WASD)
}
}

Expand All @@ -38,8 +39,8 @@ fn move_player(
) {
if action_state.pressed(&PlayerAction::Move) {
// We're working with gamepads, so we want to defensively ensure that we're using the clamped values
let axis_pair = action_state.clamped_axis_pair(&PlayerAction::Move).unwrap();
println!("Move: ({}, {})", axis_pair.x(), axis_pair.y());
let axis_pair = action_state.clamped_axis_pair(&PlayerAction::Move);
println!("Move: ({}, {})", axis_pair.x, axis_pair.y);
}

if action_state.pressed(&PlayerAction::Jump) {
Expand Down
4 changes: 2 additions & 2 deletions examples/arpg_indirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ fn copy_action_state(
if let Some(matching_ability) = ability_slot_map.get(&slot) {
// This copies the `ActionData` between the ActionStates,
// including information about how long the buttons have been pressed or released
ability_state.set_action_data(
ability_state.set_button_data(
*matching_ability,
slot_state.action_data_mut_or_default(&slot).clone(),
slot_state.button_data_mut_or_default(&slot).clone(),
);
}
}
Expand Down
22 changes: 16 additions & 6 deletions examples/axis_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,35 @@ fn main() {
.run();
}

#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
enum Action {
Move,
Throttle,
Rudder,
}

impl Actionlike for Action {
fn input_control_kind(&self) -> InputControlKind {
match self {
Action::Move => InputControlKind::DualAxis,
Action::Throttle => InputControlKind::Button,
Action::Rudder => InputControlKind::Axis,
}
}
}

#[derive(Component)]
struct Player;

fn spawn_player(mut commands: Commands) {
// Describes how to convert from player inputs into those actions
let input_map = InputMap::default()
// Let's bind the left stick for the move action
.with(Action::Move, GamepadStick::LEFT)
.with_dual_axis(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(
.with_axis(
// 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,
Expand All @@ -50,11 +60,11 @@ fn move_player(query: Query<&ActionState<Action>, With<Player>>) {
// Each action has a button-like state of its own that you can check
if action_state.pressed(&Action::Move) {
// We're working with gamepads, so we want to defensively ensure that we're using the clamped values
let axis_pair = action_state.clamped_axis_pair(&Action::Move).unwrap();
let axis_pair = action_state.clamped_axis_pair(&Action::Move);
println!("Move:");
println!(" distance: {}", axis_pair.length());
println!(" x: {}", axis_pair.x());
println!(" y: {}", axis_pair.y());
println!(" x: {}", axis_pair.x);
println!(" y: {}", axis_pair.y);
}

if action_state.pressed(&Action::Throttle) {
Expand Down
13 changes: 8 additions & 5 deletions examples/clash_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ 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(OneAndTwo, InputChord::new([Digit1, Digit2]));
input_map.insert(OneAndThree, InputChord::new([Digit1, Digit3]));
input_map.insert(TwoAndThree, InputChord::new([Digit2, Digit3]));

input_map.insert(OneAndTwoAndThree, InputChord::new([Digit1, Digit2, Digit3]));
input_map.insert(OneAndTwo, ButtonlikeChord::new([Digit1, Digit2]));
input_map.insert(OneAndThree, ButtonlikeChord::new([Digit1, Digit3]));
input_map.insert(TwoAndThree, ButtonlikeChord::new([Digit2, Digit3]));

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

commands.spawn(InputManagerBundle::with_map(input_map));
}
Expand Down
20 changes: 13 additions & 7 deletions examples/default_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,34 @@ fn main() {
.run();
}

#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
enum PlayerAction {
Run,
Jump,
UseItem,
}

impl Actionlike for PlayerAction {
fn input_control_kind(&self) -> InputControlKind {
match self {
PlayerAction::Run => InputControlKind::DualAxis,
_ => InputControlKind::Button,
}
}
}

impl PlayerAction {
/// Define the default bindings to the input
fn default_input_map() -> InputMap<Self> {
let mut input_map = InputMap::default();

// Default gamepad input bindings
input_map.insert(Self::Run, GamepadStick::LEFT);
input_map.insert_dual_axis(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, KeyboardVirtualDPad::WASD);
input_map.insert_dual_axis(Self::Run, KeyboardVirtualDPad::WASD);
input_map.insert(Self::Jump, KeyCode::Space);
input_map.insert(Self::UseItem, MouseButton::Left);

Expand All @@ -57,10 +66,7 @@ fn use_actions(query: Query<&ActionState<PlayerAction>, With<Player>>) {
if action_state.pressed(&PlayerAction::Run) {
println!(
"Moving in direction {}",
action_state
.clamped_axis_pair(&PlayerAction::Run)
.unwrap()
.xy()
action_state.clamped_axis_pair(&PlayerAction::Run).xy()
);
}

Expand Down
15 changes: 12 additions & 3 deletions examples/input_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,27 @@ fn main() {
.run();
}

#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
enum Action {
Move,
LookAround,
}

impl Actionlike for Action {
fn input_control_kind(&self) -> InputControlKind {
match self {
Action::Move => InputControlKind::DualAxis,
Action::LookAround => InputControlKind::DualAxis,
}
}
}

#[derive(Component)]
struct Player;

fn spawn_player(mut commands: Commands) {
let input_map = InputMap::default()
.with(
.with_dual_axis(
Action::Move,
KeyboardVirtualDPad::WASD
// You can configure a processing pipeline to handle axis-like user inputs.
Expand All @@ -36,7 +45,7 @@ fn spawn_player(mut commands: Commands) {
// Or reset the pipeline, leaving no any processing applied.
.reset_processing_pipeline(),
)
.with(
.with_dual_axis(
Action::LookAround,
// You can also use a sequence of processors as the processing pipeline.
MouseMove::default().replace_processing_pipeline([
Expand Down
24 changes: 14 additions & 10 deletions examples/mouse_motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ fn main() {
.run();
}

#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
enum CameraMovement {
Pan,
}

impl Actionlike for CameraMovement {
fn input_control_kind(&self) -> InputControlKind {
InputControlKind::DualAxis
}
}

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 `MouseMoveDirection` enum.
(CameraMovement::Pan, MouseMove::default()),
]);
// This will capture the total continuous value, for direct use.
// Note that you can also use discrete gesture-like motion,
// via the `MouseMoveDirection` enum.
let input_map = InputMap::default().with_dual_axis(CameraMovement::Pan, MouseMove::default());
commands
.spawn(Camera2dBundle::default())
.insert(InputManagerBundle::with_map(input_map));
Expand All @@ -37,10 +41,10 @@ fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), W

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

let camera_pan_vector = action_state.axis_pair(&CameraMovement::Pan).unwrap();
let camera_pan_vector = action_state.axis_pair(&CameraMovement::Pan);

// Because we're moving the camera, not the object, we want to pan in the opposite direction.
// However, UI coordinates are inverted on the y-axis, so we need to flip y a second time.
camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x();
camera_transform.translation.y += CAMERA_PAN_RATE * camera_pan_vector.y();
camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x;
camera_transform.translation.y += CAMERA_PAN_RATE * camera_pan_vector.y;
}
16 changes: 8 additions & 8 deletions examples/mouse_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ fn pan_camera(
let (camera, camera_transform) = camera.single();

// Note: Nothing is stopping us from doing this in the action update system instead!
if let Some(box_pan_vector) = action_state
.axis_pair(&BoxMovement::MousePosition)
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor.xy()))
.map(|ray| ray.origin.truncate())
{
box_transform.translation.x = box_pan_vector.x;
box_transform.translation.y = box_pan_vector.y;
}
let cursor_movement = action_state.axis_pair(&BoxMovement::MousePosition);
let ray = camera
.viewport_to_world(camera_transform, cursor_movement.xy())
.unwrap();
let box_pan_vector = ray.origin.truncate();

box_transform.translation.x = box_pan_vector.x;
box_transform.translation.y = box_pan_vector.y;
}
18 changes: 14 additions & 4 deletions examples/mouse_wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,35 @@ fn main() {
.run();
}

#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
enum CameraMovement {
Zoom,
Pan,
PanLeft,
PanRight,
}

impl Actionlike for CameraMovement {
fn input_control_kind(&self) -> InputControlKind {
match self {
CameraMovement::Zoom => InputControlKind::Axis,
CameraMovement::Pan => InputControlKind::DualAxis,
CameraMovement::PanLeft | CameraMovement::PanRight => InputControlKind::Button,
}
}
}

fn setup(mut commands: Commands) {
let input_map = InputMap::default()
// This will capture the total continuous value, for direct use.
.with(CameraMovement::Zoom, MouseScrollAxis::Y)
.with_axis(CameraMovement::Zoom, MouseScrollAxis::Y)
// This will return a binary button-like output.
.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())
.with_dual_axis(CameraMovement::Pan, MouseScroll::default())
// Or even a digital dual-axis input!
.with(CameraMovement::Pan, MouseScroll::default().digital());
.with_dual_axis(CameraMovement::Pan, MouseScroll::default().digital());
commands
.spawn(Camera2dBundle::default())
.insert(InputManagerBundle::with_map(input_map));
Expand Down
Loading

0 comments on commit 5e90a7d

Please sign in to comment.