From 3e224e08ce4e67dccd80c2dd26adc121ef2d1c1b Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 1 Aug 2022 20:19:39 -0400 Subject: [PATCH] Release 0.5 (#164) * Start 0.3 process * Swap back to a by-value API * Primitives for working with Axislike inputs * Split primitives into a dedicated file * Improve conversion strategy for primitives * Direction partitionings * Algebraic operations for Rotation * Fixed broken tests * InputAxis -> AxisPair * AxisPair::new() * Swap to usize-based hashing to support value-ful actions * Noted addition of geometric rotation primitives * Simplify set_state logic * Correctly initialize ActionState again * Fix PartialOrd trait for Timing * Store and set action values * Use Vec storage internally Fixes #69. * Experimentation with action value API * Revert "Experimentation with action value API" This reverts commit 76b9d1583e69336c2d740a2fed60221ae8580a72. * Don't store action values * Run examples in CI Borrowed from https://github.com/bevyengine/bevy/blob/main/.github/workflows/ci.yml * Remove bevy-specific logic in examples CI * Don't try to run examples; way too hard * Use underscores for crate name * mdlint rules * Move geometric primitives into leafwing_2d * Remove dead code in macro * Underscores in crate name * Local development * Relative path for macros crate * Fix outdated example * Fix underscores in crate name * Polish README * Fix broken doc links * Simplify CI: no reason to try to run on all platforms * Refactor `which_pressed` to return a `Vec` This is not only cleaner than returning a `HashSet` of indicies, but also opens up the way to add `UserInput` information * Update `get_clashes` to use a slice instead of a full `Vec` Clippy recommended this for _performance purposes_ * Run `cargo fmt` Oops * Add API method to return which `UserInput` triggered an action * Move bevy_ui dependency under a feature * Vendor leafwing_2d orientation code Should not block a release * replace_at and clear_at can be used in a device-agnostic way * Use usize for container sizes * Fix missing dependencies * Remove per_mode_cap * Simplify InputMap API * Fix docs formatting * Remove extra pub specifier * Remove UserInput::Null * Remove empty_chords test Doesn't make sense after `UserInput::Null` removal. * Update RELEASES.md * Add missing change to RELEASES.md * Add methods to conveniently iterate over mappings * Use type alias * Add InputMap::remove * Store ClashStrategy in a resource * Remove clashing input caching * Enable default for serde * Fix formatting * Changed `ActionState.button_states` inserts into direct assignments * Added helper function for setting `action` as "held" * Iterate over actions with mapping by default In previous PR I added a method to iterate over inputs. But I realized that such iteration should be with action like in normal map. So I renamed the submitted `iter()` method into `iter_inputs()` and added `iter()` that iterates over actions with inputs. I also have to delete `IntoIter` currently because we can't use opaque type in traits without nightly compiler. If you know how to implement it properly - I would appreciate your advice. But for now I removed it. * Add binding_menu example * Fix tests (#99) * Fix which_pressed test * Remove references to leafwing_2d * Fix failing doc tests * Depend on bevy subcrates (#100) * Fix doc strings * Use bevy subcrates for faster compiles and easier feature creation * Replace run_in_state with DisableInput resource (#106) * Replace run_in_state with DisableInput resource * Update release notes * Fix formatting * Improve system label description * Apply suggestions * Systems refactor (#107) * Refactor plugin systems logic Insert systems right away instead of putting them into input_manager_systems system set. * Rename `InputManagerSystem::Reset` into `InputManagerSystem::Tick` * Chain add_system_to_stage calls * Allow use of InputMap and ActionState as resources * Adding InputResource and supporting system modifications * Second Attempt * Cleaning up formatting. * Adding RELEASES.md notice. * Update RELEASES.md New working as provided by alice-i-cecile Co-authored-by: Alice Cecile * Addressing Shatur's naming Concerns. * Adding lint exception * adding clippy:: to too_many_arguments Co-authored-by: Alice Cecile * Naming nits for systems (#111) * Tests and refactoring for reasons pressed (#110) * Move VirtualButtonState and Timing into a more appropriate location * Use named fields in VirtualButtonState * Removed poorly motivated ButtonThresholds struct * reasons_pressed methods * press and release methods on VirtualButtonState * VirtualButtonState::tick * Doc tests for reasons_pressed * Shorten module names * Refactor UserInput into its own file * Fix timing test * Fix broken doc tests * ActionState::reset API (#112) * ActionState::make_held -> ActionState::reset * Fix rename in doc test * Fixed misleading merge conflict in RELEASES * Clean up release notes * Fix import in doc tests * More release note polish... * Fix doc test * Added example demonstrating how to use reset in system * Refactor VirtualButtonState (#113) * Rename VirtualButtonState to ButtonState * Radically simplify ButtonState * Re-add reasons_pressed functionality * Fetch ActionData, not ButtonState * Re-add Timing functionality * Consolidate tests * Updated release notes * Assorted cleanup * Fix tests * Provide functionality to release inputs during mocking (#114) * Note that mocked inputs will not be automatically released * Add input releasing to input mocking tools * Add UserInput::raw_inputs * Add ActionState::freeze (#115) * ActionState::freeze and ActionState::unfreeze * Yeet DisableInput resource * Fix stray doc test (#116) * Replace freeze API with improved DisableInput resource (#117) * Replace freeze API with DisableInput improved resource ActionState::freeze was introduced to consume actions. But it turns out not very ergonomic. Also freeze require to spawn an entity or init the resource should which is not always the case. In this iteration I used run criteria to make the code nicer. Also I extended disable_input test to check if global and entity input is released on disable. * Use field to control if actions are enabled * Update RELEASES.md * Add ActionState::consume (#118) * Update to Bevy 0.7 (#119) * Bump dependencies * New ergonomics! * Fix typo * Bump bevy_egui dev-dependency * Finalize release notes (#120) * Reduce Bevy dependencies (#126) * Properly document ToggleActions (#134) * Fix dead links to ToggleActions * Add plugin-level documentation to describe how to pause and resume * Improve plugin-level docs (#137) * Document the need to specify system ordering * Document the use of multiple actionlike enums * Expanded ActionStateDriver docs (#138) * Don't run CI twice for repo-owned branches (#140) * Bump LWIM dependency * Add code snippet to README (#139) * Remove missed boilerplate from README (#142) * Update crate name in README (#143) * Add Bevy Gilrs to Dev Dependency Features (#150) This way you can run the examples with gamepad support. * Fix broken duration pressed functionality (#129) * Improve press_duration example description * Added integration test for durations * Test cleanup * Create seemingly useless test * Isolate source of problems * Don't flip the timing unless a state change occurs * Add bug fix to release notes * Made duration integration test stricter * Handle time not having a last update gracefully (#146) * Identify source(s) of problems * Improve tricky duration pressed integration test * Do not advance timing info for consumed actions * Remove reset_inputs from tests * Move trivial do_nothing test to top of integration test file * Remove double updates from duration test * Improve strategy for computing elapsed time * Don't sleep in unit tests * Simplify duration tests * Start timer at the correct time * Current duration should be measured from the last frame * Add missing release note * Fix CI * Bump to 0.4 because breaking change needed Co-authored-by: Aceeri * Reverse order of `InputMap` from `(action, input)` to `(input, action)` (#152) * Swap order of InputMap methods from (Action, Input) to (Input, Action) * Update release notes * Don't duplicate CI checks for Leafwing-Studios PRs (#153) * Add Support For Gamepad Axes (#151) * Add Support For Gamepad Axes * Update Doc Comment Co-authored-by: Nathan Stocks * Demonstrate Button Value in the Analog Stick Demo * Add Helper Method ActionState::get_value() * Add Dual Axis Support * Implement Virtual DPad * Implement Values and Axis Pairs for Chords * Address Code Review Co-authored-by: Alice Cecile * Update README.md and RELEASES.md * Cargo Format * Non-Inclusive Comparisons For positive_low/high * Split Dual Axis Deadzone Into Component Parts * Apply Updates Suggested in Review * Apply More Suggestions * Move VirtualDPad To Its Own Struct * Apply Updates From Code Review Co-authored-by: Nathan Stocks * Fix Compile Errors * Fix DPad Clash Detection * Cargo Format Co-authored-by: Nathan Stocks Co-authored-by: Alice Cecile * Merge changes from main into dev branch (#163) * Release 0.4 (#141) * Start 0.3 process * Swap back to a by-value API * Primitives for working with Axislike inputs * Split primitives into a dedicated file * Improve conversion strategy for primitives * Direction partitionings * Algebraic operations for Rotation * Fixed broken tests * InputAxis -> AxisPair * AxisPair::new() * Swap to usize-based hashing to support value-ful actions * Noted addition of geometric rotation primitives * Simplify set_state logic * Correctly initialize ActionState again * Fix PartialOrd trait for Timing * Store and set action values * Use Vec storage internally Fixes #69. * Experimentation with action value API * Revert "Experimentation with action value API" This reverts commit 76b9d1583e69336c2d740a2fed60221ae8580a72. * Don't store action values * Run examples in CI Borrowed from https://github.com/bevyengine/bevy/blob/main/.github/workflows/ci.yml * Remove bevy-specific logic in examples CI * Don't try to run examples; way too hard * Use underscores for crate name * mdlint rules * Move geometric primitives into leafwing_2d * Remove dead code in macro * Underscores in crate name * Local development * Relative path for macros crate * Fix outdated example * Fix underscores in crate name * Polish README * Fix broken doc links * Simplify CI: no reason to try to run on all platforms * Refactor `which_pressed` to return a `Vec` This is not only cleaner than returning a `HashSet` of indicies, but also opens up the way to add `UserInput` information * Update `get_clashes` to use a slice instead of a full `Vec` Clippy recommended this for _performance purposes_ * Run `cargo fmt` Oops * Add API method to return which `UserInput` triggered an action * Move bevy_ui dependency under a feature * Vendor leafwing_2d orientation code Should not block a release * replace_at and clear_at can be used in a device-agnostic way * Use usize for container sizes * Fix missing dependencies * Remove per_mode_cap * Simplify InputMap API * Fix docs formatting * Remove extra pub specifier * Remove UserInput::Null * Remove empty_chords test Doesn't make sense after `UserInput::Null` removal. * Update RELEASES.md * Add missing change to RELEASES.md * Add methods to conveniently iterate over mappings * Use type alias * Add InputMap::remove * Store ClashStrategy in a resource * Remove clashing input caching * Enable default for serde * Fix formatting * Changed `ActionState.button_states` inserts into direct assignments * Added helper function for setting `action` as "held" * Iterate over actions with mapping by default In previous PR I added a method to iterate over inputs. But I realized that such iteration should be with action like in normal map. So I renamed the submitted `iter()` method into `iter_inputs()` and added `iter()` that iterates over actions with inputs. I also have to delete `IntoIter` currently because we can't use opaque type in traits without nightly compiler. If you know how to implement it properly - I would appreciate your advice. But for now I removed it. * Add binding_menu example * Fix tests (#99) * Fix which_pressed test * Remove references to leafwing_2d * Fix failing doc tests * Depend on bevy subcrates (#100) * Fix doc strings * Use bevy subcrates for faster compiles and easier feature creation * Replace run_in_state with DisableInput resource (#106) * Replace run_in_state with DisableInput resource * Update release notes * Fix formatting * Improve system label description * Apply suggestions * Systems refactor (#107) * Refactor plugin systems logic Insert systems right away instead of putting them into input_manager_systems system set. * Rename `InputManagerSystem::Reset` into `InputManagerSystem::Tick` * Chain add_system_to_stage calls * Allow use of InputMap and ActionState as resources * Adding InputResource and supporting system modifications * Second Attempt * Cleaning up formatting. * Adding RELEASES.md notice. * Update RELEASES.md New working as provided by alice-i-cecile Co-authored-by: Alice Cecile * Addressing Shatur's naming Concerns. * Adding lint exception * adding clippy:: to too_many_arguments Co-authored-by: Alice Cecile * Naming nits for systems (#111) * Tests and refactoring for reasons pressed (#110) * Move VirtualButtonState and Timing into a more appropriate location * Use named fields in VirtualButtonState * Removed poorly motivated ButtonThresholds struct * reasons_pressed methods * press and release methods on VirtualButtonState * VirtualButtonState::tick * Doc tests for reasons_pressed * Shorten module names * Refactor UserInput into its own file * Fix timing test * Fix broken doc tests * ActionState::reset API (#112) * ActionState::make_held -> ActionState::reset * Fix rename in doc test * Fixed misleading merge conflict in RELEASES * Clean up release notes * Fix import in doc tests * More release note polish... * Fix doc test * Added example demonstrating how to use reset in system * Refactor VirtualButtonState (#113) * Rename VirtualButtonState to ButtonState * Radically simplify ButtonState * Re-add reasons_pressed functionality * Fetch ActionData, not ButtonState * Re-add Timing functionality * Consolidate tests * Updated release notes * Assorted cleanup * Fix tests * Provide functionality to release inputs during mocking (#114) * Note that mocked inputs will not be automatically released * Add input releasing to input mocking tools * Add UserInput::raw_inputs * Add ActionState::freeze (#115) * ActionState::freeze and ActionState::unfreeze * Yeet DisableInput resource * Fix stray doc test (#116) * Replace freeze API with improved DisableInput resource (#117) * Replace freeze API with DisableInput improved resource ActionState::freeze was introduced to consume actions. But it turns out not very ergonomic. Also freeze require to spawn an entity or init the resource should which is not always the case. In this iteration I used run criteria to make the code nicer. Also I extended disable_input test to check if global and entity input is released on disable. * Use field to control if actions are enabled * Update RELEASES.md * Add ActionState::consume (#118) * Update to Bevy 0.7 (#119) * Bump dependencies * New ergonomics! * Fix typo * Bump bevy_egui dev-dependency * Finalize release notes (#120) * Reduce Bevy dependencies (#126) * Properly document ToggleActions (#134) * Fix dead links to ToggleActions * Add plugin-level documentation to describe how to pause and resume * Improve plugin-level docs (#137) * Document the need to specify system ordering * Document the use of multiple actionlike enums * Expanded ActionStateDriver docs (#138) * Don't run CI twice for repo-owned branches (#140) * Bump LWIM dependency * Add code snippet to README (#139) * Remove missed boilerplate from README (#142) * Update crate name in README (#143) * Add Bevy Gilrs to Dev Dependency Features (#150) This way you can run the examples with gamepad support. * Fix broken duration pressed functionality (#129) * Improve press_duration example description * Added integration test for durations * Test cleanup * Create seemingly useless test * Isolate source of problems * Don't flip the timing unless a state change occurs * Add bug fix to release notes * Made duration integration test stricter * Handle time not having a last update gracefully (#146) * Identify source(s) of problems * Improve tricky duration pressed integration test * Do not advance timing info for consumed actions * Remove reset_inputs from tests * Move trivial do_nothing test to top of integration test file * Remove double updates from duration test * Improve strategy for computing elapsed time * Don't sleep in unit tests * Simplify duration tests * Start timer at the correct time * Current duration should be measured from the last frame * Add missing release note * Fix CI * Bump to 0.4 because breaking change needed Co-authored-by: Aceeri * Reverse order of `InputMap` from `(action, input)` to `(input, action)` (#152) * Swap order of InputMap methods from (Action, Input) to (Input, Action) * Update release notes Co-authored-by: Rose Peck Co-authored-by: Hennadii Chernyshchyk Co-authored-by: Elfein Landers Co-authored-by: Hans W. Uhlig Co-authored-by: Christopher Biscardi Co-authored-by: Zicklag Co-authored-by: Aceeri * Hyphens in crate name * sync up macros dependency with macros crate name (#161) * sync up macros dependency with macros crate name * version bump and release notes for 0.4.1 * Update hyphenation of parent crate in docs * Fix conflict between macro crate name and version (#162) * Revert "sync up macros dependency with macros crate name (#161)" This reverts commit ba4e704dac4cdde79f4c270a73e0f1282b764dc2. * Swap to underscores in macro crate name only This matches the published crate name. * Add release notes * Bump version of macros crate * Bump version of main crate Co-authored-by: Shea Newton * Bump version numbers * Fixed release notes * Specify minor versions Co-authored-by: Rose Peck Co-authored-by: Hennadii Chernyshchyk Co-authored-by: Elfein Landers Co-authored-by: Hans W. Uhlig Co-authored-by: Christopher Biscardi Co-authored-by: Zicklag Co-authored-by: Aceeri Co-authored-by: Shea Newton * Add builder methods for new axislike input types (#165) * Move VirtualDpad to axislike.rs file * Work around RA macro import resolution issue * Add simple builder methods for VirtualDPads * Move SingleGamepadAxis and DualGamepadAxis to axislike.rs * Simple builder methods for Single and DualGamepadAxis * Add builder methods for gamepad analogue sticks * Update example to use new builders * Don't store DualGamepadAxis in VirtualDPad * Silence spurious clippy warning * Remove reasons_pressed API. (#167) * Scan all gamepads for inputs if none is registered (#168) * Remove reasons_pressed API. * Scan the list of all gamepads if no particular gamepad is registered * Update examples * Use same strategies for InputMocking::pressed * Record FIXME notes * Search all gamepads if no gamepad is registered * Remove incorrect Chord logic for axis_pair and input_value * Use button value in get_input_value as a fallback, rather than 0.0 * Explain weird design choice * Update docs * Clippy * Update to Bevy 0.8-dev (#170) * Update dependencies to 0.8-dev branch of Bevy * Fix compile errors * Use new const Vec2 methods * Make egui example pass CI * Align variable naming with bevy_input conventions * Set axis values when mocking user input (#171) * Remove resolved FIXME comments * Allow users to send analog input values * Clarity rename * Update doc string to reflect current design * Fix broken imports (#172) Thanks merge conflicts :( * Cleanup old TODO and FIXME comments (#176) * Remove completed TODO / FIXME comments * Add Direction::try_new * Measure rotation counterclockwise from x (#177) * switch from 'clockwise from midnight' to 'counterclockwise from the positive x axis' for Rotation and Direction * fix usage * stop using .normalize(), which can panic * simplify logic * remove unused import * Doc nit * re-enable all assertions * add release notes * clarify why we changed rotation direction Co-authored-by: Alice Cecile * use Result consistently * Tweak release notes * Remove Direction::try_new Co-authored-by: Nathan Stocks * Support mouse wheel inputs (#173) * Rename SingleGamepadAxis to SingleAxis * Do not clamp lengths of axis pairs * Move InputStreams into a dedicated file UserInput was getting excessively long and InputStreams code is well isolated. * Add helper methods to construct InputStream types from the World * Add mouse wheel events to InputStreams * Generalize GamepadAxisType to AxisType * Update release notes * Rename for generality * Rename AxisPair to DualAxisData * Store two SingleAxis inside of DualAxis * Use more descriptive variable names in user_input.rs * Account for mouse wheel inputs in InputMode conversion * Yeet InputMode enum * Variable renaming for generality * Mock mouse wheel inputs * Refactor input_value methods on InputStreams * Take a pair of floats in DualAxisData::new * Sum mouse wheel events to get input_value * Add buttonlike MouseWheelDirection InputKind * Rederive Debug Fixes #175. * clippy * Builder methods for working with mouse wheel inputs * Add new types to prelude * Add missing From impls * Add mouse_wheel example * Make mouse wheel direction summing less clever * Actually add code for panning right to example * Add note about dumb Bevy bug * Scale 2D cameras correctly :upside_down: * Fix stated ordering in InputMap docs * Add tests for working with mouse wheel inputs * Add DualAxis::from_value helpers * Add test for raw events * get_input_value -> input_value * Add clamped helper methods * Shorten axis_pair and value method names * Make CI pass * Support Single Axis Inputs in Virtual DPads (#180) * Temporarily Comment Out Bevy Egui * Make Axis Input Example Slightly More Readable * Fix Bug in Symmetric Axis Constructor Axis would always trigger because negative_low was positive. * Return Axis Pair Results When Action Isn't Pressed This makes sure that axis_pair() returns `Some` even if the value is zero because the axis isn't moved. * Set Dual Axis Value Zero if Not in Trigger Range * Make Single-Axis Inputs Work in Virtual DPad This makes sure that SingleAxis inputs will report a value of 0.0 if the axis value is not withing the triggering zone. This makes it possible to create a virtual gamepad made up of multiple single-axis inputs on the same stick. * Cargo Format * Add mouse motion support (#186) * Add MouseMotion events to input streams * Add discretized MouseMotionDirection buttonlike input support * Continuous-valued mouse motion inputs * Doc fixes for AxisData * Builder methods for Axis types * Add simple mouse_motion example * Depend on bevy directly (#187) This is a) much cleaner and easier to maintain b) makes it easier for others to patch this crate to a specific version and c) slightly more robust to bevy shuffling code around * Add bevy_asset as a feature for the examples (#190) * Bump to Bevy 0.8 (#192) * Bump dependencies * Re-enable binding_menu example * Make input stream fields non-optional (#193) * Add better module-level docs * Make input streams non-optional * InputStreams::from_world does not need &mut World * Satisfy borrow checker in tests * Fix lifetime of MutableInputStreams -> InputStreams method * Fix integration test * Update release notes * Add mockable_world builder function * Clean up input mocking code (#194) * Documentation improvements to input mocking * (Mostly) implement InputMocking trait for MutableInputStreams * Use InputStreams::guess_gamepad internally * Allows conversion by reference from MutableInputStreams * Add tests for decomposing `UserInput` into raw inputs (#197) * Documentation improvements to input mocking * (Mostly) implement InputMocking trait for MutableInputStreams * Use InputStreams::guess_gamepad internally * Allows conversion by reference from MutableInputStreams * Correctly release input * Swap to a RawInputs struct * Update docs for UserInput::chord * Add tests for RawInputs * Fix Compile Without UI Feature (#198) * Fix axislike input mocking (kind of) (#200) * Re-enable tests * Actually add axislike inputmanagerplugin XD * Move actionstate init to test setup * Improve axis-like input mocking tests * Reorganize MutableInputStreams::send_input * Actually send discrete mouse motion events * Add more tests for axislike inputs * Improve gamepad status handling for axislike gamepad inputs * Ignore failing tests * Actually register a gamepad for tests * Disable tests that are still failing * Mocking inputs should send raw events (#207) * Add more fields to mutable input streams * Rename InputStreams fields for clarity * Send raw events when doing input mocking * Don't guess gamepad in send_input_to_gamepad; handled upstream * Enable fixed gamepad mocking tests * Always enable bevy_gilrs * Fix failing tests * Fix failing gamepad axis tests * Minor documentation fix, s/pressed/released/ (#206) Co-authored-by: Alice Cecile Co-authored-by: Rose Peck Co-authored-by: Hennadii Chernyshchyk Co-authored-by: Elfein Landers Co-authored-by: Hans W. Uhlig Co-authored-by: Christopher Biscardi Co-authored-by: Zicklag Co-authored-by: Aceeri Co-authored-by: Nathan Stocks Co-authored-by: Shea Newton Co-authored-by: Nolan Darilek --- Cargo.toml | 24 +- README.md | 9 +- RELEASE-CHECKLIST.md | 12 + RELEASES.md | 51 ++ examples/arpg_indirection.rs | 2 +- examples/axis_inputs.rs | 79 ++ examples/binding_menu.rs | 49 +- examples/consuming_actions.rs | 2 +- examples/mouse_motion.rs | 47 ++ examples/mouse_wheel.rs | 76 ++ examples/multiplayer.rs | 6 +- examples/press_duration.rs | 2 +- examples/send_actions_over_network.rs | 5 +- examples/single_player.rs | 5 - examples/ui_driven_actions.rs | 5 +- examples/virtual_dpad.rs | 59 ++ macros/Cargo.toml | 3 +- src/action_state.rs | 167 ++-- src/axislike.rs | 610 ++++++++++++--- src/buttonlike.rs | 30 + src/clashing_inputs.rs | 218 ++++-- src/display_impl.rs | 27 +- src/errors.rs | 2 +- src/input_map.rs | 237 ++---- src/input_mocking.rs | 552 +++++++++----- src/input_streams.rs | 481 ++++++++++++ src/lib.rs | 14 +- src/orientation.rs | 251 +++--- src/plugin.rs | 8 +- src/systems.rs | 67 +- src/user_input.rs | 1017 ++++++++++++++----------- tests/clashes.rs | 73 +- tests/gamepad_axis.rs | 246 ++++++ tests/integration.rs | 21 +- tests/mouse_motion.rs | 289 +++++++ tests/mouse_wheel.rs | 290 +++++++ 36 files changed, 3758 insertions(+), 1278 deletions(-) create mode 100644 RELEASE-CHECKLIST.md create mode 100644 examples/axis_inputs.rs create mode 100644 examples/mouse_motion.rs create mode 100644 examples/mouse_wheel.rs create mode 100644 examples/virtual_dpad.rs create mode 100644 src/input_streams.rs create mode 100644 tests/gamepad_axis.rs create mode 100644 tests/mouse_motion.rs create mode 100644 tests/mouse_wheel.rs diff --git a/Cargo.toml b/Cargo.toml index d22dbfbb..e4b90aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "leafwing-input-manager" +name = "leafwing_input_manager" description = "A powerfully direct stateful input manager for the Bevy game engine." -version = "0.4.1" +version = "0.5.0" authors = ["Leafwing Studios"] homepage = "https://leafwing-studios.com/" repository = "https://github.com/leafwing-studios/leafwing-input-manager" @@ -19,20 +19,12 @@ members = ["./", "tools/ci", "macros"] [features] default = ['ui'] -ui = ['bevy_ui'] +ui = ['bevy/bevy_ui'] [dependencies] -leafwing_input_manager_macros = { path = "macros", version = "0.4.1" } - -bevy_app = {version = "0.7", default-features = false} -bevy_core = {version = "0.7", default-features = false} -bevy_transform = {version = "0.7", default-features = false} -bevy_ecs = {version = "0.7", default-features = false} -bevy_input = {version = "0.7", default-features = false, features = ["serialize"]} -bevy_math = {version = "0.7", default-features = false} -bevy_utils = {version = "0.7", default-features = false} -bevy_ui = {version = "0.7", default-features = false, optional = true} -bevy_window = {version = "0.7", default-features = false} +leafwing_input_manager_macros = { path = "macros", version = "0.5" } + +bevy = {version = "0.8", default-features = false, features = ["serialize", "bevy_gilrs"]} petitset = {version = "0.2.1", features = ["serde_compat"]} derive_more = {version = "0.99", default-features = false, features = ["display", "error"]} @@ -40,8 +32,8 @@ itertools = "0.10" serde = {version = "1.0", features = ["derive"]} [dev-dependencies] -bevy = {version = "0.7", default-features = false, features = ["bevy_gilrs", "bevy_sprite", "bevy_text", "bevy_ui", "bevy_render", "bevy_core_pipeline", "x11"]} -bevy_egui = {version="0.13", default-features = false} +bevy = {version = "0.8", default-features = false, features = ["bevy_asset", "bevy_sprite", "bevy_text", "bevy_ui", "bevy_render", "bevy_core_pipeline", "x11"]} +bevy_egui = "0.15.0" [lib] name = "leafwing_input_manager" diff --git a/README.md b/README.md index d8b2b008..c8029de8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ and a single input can result in multiple actions being triggered, which can be ## Features -- Full keyboard, mouse and joystick support for button-like inputs. +- 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 - 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 @@ -33,11 +35,6 @@ and a single input can result in multiple actions being triggered, which can be ## Limitations -- The `Button` enum only includes `KeyCode`, `MouseButton` and `GamepadButtonType`. - - This is due to object-safety limitations on the types stored in `bevy::input::Input` - - Please file an issue if you would like something more exotic! -- No built-in support for non-button input types (e.g. gestures or analog sticks). - - All methods on `ActionState` are `pub`: it's designed to be hooked into and extended. - Gamepads must be manually assigned to each input map: read from the `Gamepads` resource and use `InputMap::set_gamepad`. ## Instructions diff --git a/RELEASE-CHECKLIST.md b/RELEASE-CHECKLIST.md new file mode 100644 index 00000000..9096f8bc --- /dev/null +++ b/RELEASE-CHECKLIST.md @@ -0,0 +1,12 @@ +# LWIM Release Checklist + +## Adding a new input kind + +1. Ensure that `reset_inputs` for `MutableInputStreams` is resetting all relevant fields. +2. Ensure that `RawInputs` struct has fields that cover all necessary input types. +3. Ensure that `send_input` and `release_input` check all possible fields on `RawInputs`. + +## Before release + +1. Ensure no tests (other than ones in the README) are ignored. +2. Manually verify that all examples work. diff --git a/RELEASES.md b/RELEASES.md index 74a90a94..2f4c788e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,56 @@ # Release Notes +## Version 0.5 + +### Enhancements + +- Added gamepad axis support. + - Use the new `SingleAxis` and `DualAxis` types / variants. +- Added mousewheel and mouse motion support. + - Use the new `SingleAxis` and `DualAxis` types / variants when you care about the continous values. + - Use the new `MouseWheelDirection` enum as an `InputKind`. +- Added `SingleAxis` and `DualAxis` structs that can be supplied to an `InputMap` to trigger on axis inputs. +- Added `VirtualDPad` struct that can be supplied to an `InputMap` to trigger on four direction-representing inputs. +- Added `ActionState::action_axis_pair()` which can return an `AxisPair` containing the analog values of a `SingleAxis`, `DualAxis`, or `VirtualDPad`. +- Added `ActionState::action_value()` which represents the floating point value of any action: + - `1.0` or `0.0` for pressed or unpressed button-like inputs + - a value (typically) in the range `-1.0..=1.0` for a single axis representing its analog input + - or a value (typically) in the range `0.0..=1.0` for a dual axis representing the magnitude (length) of its vector. + +### Usability + +- If no gamepad is registered to a specific `InputMap`, inputs from any gamepad in the `Gamepads` resource will be used. +- Removed the `ActionState::reasons_pressed` API. + - This API was quite complex, not terribly useful and had nontrivial performance overhead. + - This was not needed for axislike inputs in the end. +- Added `Direction::try_new()` to fallibly create a new `Direction` struct (which cannot be created from the zero vector). +- Removed the `InputMode` enum. + - This was poorly motivated and had no internal usages. + - This could not accurately represent more complex compound input types. +- `ButtonKind` was renamed to `InputKind` to reflect the new non-button input types. +- Renamed `AxisPair` to `DualAxisData`. + - `DualAxisData::new` now takes two `f32` values for ergonomic reasons. + - Use `DualAxisData::from_xy` to construct this directly from a `Vec2` as before. +- Rotation is now measured from the positive x axis in a counterclockwise direction. This applies to both `Rotation` and `Direction`. + - This increases consistency with `glam` and makes trigonometry easier. +- Added `Direction::try_from` which never panics; consider using this in place of `Direction::new`. +- Converting from a `Direction` (which uses a `Vec2` of `f32`'s internally) to a `Rotation` (which uses exact decidegrees) now has special cases to ensure all eight cardinal directions result in exact degrees. + - For example, a unit vector pointing to the Northeast now always converts to a `Direction` with exactly 1350 decidegrees. + - Rounding errors may still occur when converting from arbitrary directions to the other 3592 discrete decidegrees. +- `InputStreams` and `MutableInputStreams` no longer store e.g. `Option>>`, and instead simply store `Res>` + - This makes them much easier to work with and dramatically simplifies internal logic. +- `InputStreams::from_world` no longer requires `&mut World`, as it does not require mutable access to any resources. +- Renamed `InputMocking::send_input_to_gamepad` and `InputMocking::release_input_for_gamepad` to `InputMocking::send_input_as_gamepad` and `InputMocking::send_input_as_gamepad`. +- Added the `guess_gamepad` method to `InputStreams` and `MutableInputStreams`, which attempts to find an appropriate gamepad to use. +- `InputMocking::pressed` and `pressed_for_gamepad` no longer require `&mut self`. +- `UserInput::raw_inputs` now returns a `RawInputs` struct, rather than a tuple struct. +- The `mouse` and `keyboard` fields on the two `InputStreams` types are now named `mouse_button` and `keycode` respectively. + +## Bug fixes + +- mocked inputs are now sent at the low-level `Events` form, rather than in their `Input` format. + - this ensures that user code that is reading these events directly can be tested accurately. + ## Version 0.4.1 ### Bug fixes diff --git a/examples/arpg_indirection.rs b/examples/arpg_indirection.rs index 17ad662b..94db6485 100644 --- a/examples/arpg_indirection.rs +++ b/examples/arpg_indirection.rs @@ -6,7 +6,7 @@ //! between two distinct [`ActionState`] components. use bevy::prelude::*; -use bevy_utils::HashMap; +use bevy::utils::HashMap; use leafwing_input_manager::plugin::InputManagerSystem; use leafwing_input_manager::prelude::*; diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs new file mode 100644 index 00000000..4558a324 --- /dev/null +++ b/examples/axis_inputs.rs @@ -0,0 +1,79 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // This plugin maps inputs to an input-type agnostic action-state + // We need to provide it with an enum which stores the possible actions a player could take + .add_plugin(InputManagerPlugin::::default()) + // Spawn an entity with Player, InputMap, and ActionState components + .add_startup_system(spawn_player) + // Read the ActionState in your systems using queries! + .add_system(move_player) + .run(); +} + +#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] +enum Action { + Move, + Throttle, + Rudder, +} + +#[derive(Component)] +struct Player; + +fn spawn_player(mut commands: Commands) { + commands + .spawn() + .insert(Player) + .insert_bundle(InputManagerBundle:: { + // Stores "which actions are currently activated" + action_state: ActionState::default(), + // Describes how to convert from player inputs into those actions + input_map: InputMap::default() + // Configure the left stick as a dual-axis + .insert(DualAxis::left_stick(), Action::Move) + // Let's bind the right gamepad trigger to the throttle action + .insert(GamepadButtonType::RightTrigger2, Action::Throttle) + // And we'll use the right stick's x axis as a rudder control + .insert( + // This will trigger if the axis is moved 10% or more in either direction. + SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), + Action::Rudder, + ) + .build(), + }); +} + +// Query for the `ActionState` component in your game logic systems! +fn move_player(query: Query<&ActionState, With>) { + let action_state = query.single(); + + // 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(); + println!("Move:"); + println!(" distance: {}", axis_pair.length()); + println!(" x: {}", axis_pair.x()); + println!(" y: {}", axis_pair.y()); + } + + if action_state.pressed(Action::Throttle) { + // Note that some gamepad buttons are also tied to axes, so even though we used a + // GamepadbuttonType::RightTrigger2 binding to trigger the throttle action, we can get a + // variable value here if you have a variable right trigger on your gamepad. + // + // If you don't have a variable trigger, this will just return 0.0 when not pressed and 1.0 + // when pressed. + let value = action_state.clamped_value(Action::Throttle); + println!("Throttle: {}", value); + } + + if action_state.pressed(Action::Rudder) { + let value = action_state.clamped_value(Action::Rudder); + println!("Rudder: {}", value); + } +} diff --git a/examples/binding_menu.rs b/examples/binding_menu.rs index 982259a1..0b7cc59d 100644 --- a/examples/binding_menu.rs +++ b/examples/binding_menu.rs @@ -1,6 +1,6 @@ use bevy::{ ecs::system::SystemParam, - input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ElementState}, + input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ButtonState}, prelude::*, }; use bevy_egui::{ @@ -8,10 +8,8 @@ use bevy_egui::{ EguiContext, EguiPlugin, }; use derive_more::Display; -use leafwing_input_manager::{prelude::*, user_input::InputButton}; - +use leafwing_input_manager::{prelude::*, user_input::InputKind}; const UI_MARGIN: f32 = 10.0; - fn main() { App::new() .insert_resource(ControlSettings::default()) @@ -25,7 +23,6 @@ fn main() { .add_system(binding_window_system) .run(); } - fn spawn_player_system(mut commands: Commands, control_settings: Res) { commands.spawn().insert(control_settings.input.clone()); commands.insert_resource(InputMap::::new([( @@ -34,7 +31,6 @@ fn spawn_player_system(mut commands: Commands, control_settings: Res::default()); } - fn controls_window_system( mut commands: Commands, mut egui: ResMut, @@ -43,7 +39,6 @@ fn controls_window_system( ) { let main_window = windows.get_primary().unwrap(); let window_width_margin = egui.ctx_mut().style().spacing.window_margin.left * 2.0; - Window::new("Settings") .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) .collapsible(false) @@ -52,7 +47,6 @@ fn controls_window_system( .show(egui.ctx_mut(), |ui| { const INPUT_VARIANTS: usize = 3; const COLUMNS_COUNT: usize = INPUT_VARIANTS + 1; - Grid::new("Control grid") .num_columns(COLUMNS_COUNT) .striped(true) @@ -63,13 +57,15 @@ fn controls_window_system( let inputs = control_settings.input.get(action); for index in 0..INPUT_VARIANTS { let button_text = match inputs.get_at(index) { - Some(UserInput::Single(InputButton::Gamepad(gamepad_button))) => { + Some(UserInput::Single(InputKind::GamepadButton( + gamepad_button, + ))) => { format!("🎮 {:?}", gamepad_button) } - Some(UserInput::Single(InputButton::Keyboard(keycode))) => { + Some(UserInput::Single(InputKind::Keyboard(keycode))) => { format!("🖮 {:?}", keycode) } - Some(UserInput::Single(InputButton::Mouse(mouse_button))) => { + Some(UserInput::Single(InputKind::Mouse(mouse_button))) => { format!("🖱 {:?}", mouse_button) } _ => "Empty".to_string(), @@ -84,7 +80,6 @@ fn controls_window_system( ui.expand_to_include_rect(ui.available_rect_before_wrap()); }); } - fn buttons_system( mut egui: ResMut, mut control_settings: ResMut, @@ -103,7 +98,6 @@ fn buttons_system( }) }); } - fn binding_window_system( mut commands: Commands, mut egui: ResMut, @@ -116,7 +110,6 @@ fn binding_window_system( Some(active_binding) => active_binding, None => return, }; - Window::new(format!("Binding \"{}\"", active_binding.action)) .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) .collapsible(false) @@ -174,7 +167,6 @@ fn binding_window_system( } }); } - #[derive(Actionlike, PartialEq, Clone, Copy, Display)] pub(crate) enum ControlAction { // Movement @@ -183,7 +175,6 @@ pub(crate) enum ControlAction { Left, Right, Jump, - // Abilities activation BaseAttack, Ability1, @@ -191,16 +182,13 @@ pub(crate) enum ControlAction { Ability3, Ultimate, } - #[derive(Actionlike, PartialEq, Clone, Copy)] pub(crate) enum UiAction { Back, } - struct ControlSettings { input: InputMap, } - impl Default for ControlSettings { fn default() -> Self { let mut input = InputMap::default(); @@ -215,17 +203,14 @@ impl Default for ControlSettings { .insert(KeyCode::E, ControlAction::Ability2) .insert(KeyCode::LShift, ControlAction::Ability3) .insert(KeyCode::R, ControlAction::Ultimate); - Self { input } } } - struct ActiveBinding { action: ControlAction, index: usize, conflict: Option, } - impl ActiveBinding { fn new(action: ControlAction, index: usize) -> Self { Self { @@ -235,12 +220,10 @@ impl ActiveBinding { } } } - struct BindingConflict { action: ControlAction, - input_button: InputButton, + input_button: InputKind, } - /// Helper for collecting input #[derive(SystemParam)] struct InputEvents<'w, 's> { @@ -248,31 +231,31 @@ struct InputEvents<'w, 's> { mouse_buttons: EventReader<'w, 's, MouseButtonInput>, gamepad_events: EventReader<'w, 's, GamepadEvent>, } - impl InputEvents<'_, '_> { - fn input_button(&mut self) -> Option { + fn input_button(&mut self) -> Option { if let Some(keyboard_input) = self.keys.iter().next() { - if keyboard_input.state == ElementState::Released { + if keyboard_input.state == ButtonState::Released { if let Some(key_code) = keyboard_input.key_code { return Some(key_code.into()); } } } - if let Some(mouse_input) = self.mouse_buttons.iter().next() { - if mouse_input.state == ElementState::Released { + if mouse_input.state == ButtonState::Released { return Some(mouse_input.button.into()); } } - - if let Some(GamepadEvent(_, event_type)) = self.gamepad_events.iter().next() { + if let Some(GamepadEvent { + gamepad: _, + event_type, + }) = self.gamepad_events.iter().next() + { if let GamepadEventType::ButtonChanged(button, strength) = event_type.to_owned() { if strength <= 0.5 { return Some(button.into()); } } } - None } } diff --git a/examples/consuming_actions.rs b/examples/consuming_actions.rs index a7a8185f..17a5fa6a 100644 --- a/examples/consuming_actions.rs +++ b/examples/consuming_actions.rs @@ -1,7 +1,7 @@ //! Demonstrates how to "consume" actions, so they can only be responded to by a single system +use bevy::ecs::system::Resource; use bevy::prelude::*; -use bevy_ecs::system::Resource; use leafwing_input_manager::prelude::*; use menu_mocking::*; diff --git a/examples/mouse_motion.rs b/examples/mouse_motion.rs new file mode 100644 index 00000000..3d2bada5 --- /dev/null +++ b/examples/mouse_motion.rs @@ -0,0 +1,47 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(InputManagerPlugin::::default()) + .add_startup_system(setup) + .add_system(pan_camera) + .run() +} + +#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq)] +enum CameraMovement { + Pan, +} + +fn setup(mut commands: Commands) { + commands + .spawn() + .insert_bundle(Camera2dBundle::default()) + .insert_bundle(InputManagerBundle:: { + input_map: InputMap::default() + // This will capture the total continous value, for direct use + // Note that you can also use discrete gesture-like motion, via the `MouseMotionDirection` enum + .insert(DualAxis::mouse_motion(), CameraMovement::Pan) + .build(), + action_state: ActionState::default(), + }); + + commands.spawn().insert_bundle(SpriteBundle { + transform: Transform::from_scale(Vec3::new(100., 100., 1.)), + ..default() + }); +} + +fn pan_camera(mut query: Query<(&mut Transform, &ActionState), With>) { + const CAMERA_PAN_RATE: f32 = 0.5; + + let (mut camera_transform, action_state) = query.single_mut(); + + let camera_pan_vector = action_state.axis_pair(CameraMovement::Pan).unwrap(); + + // Because we're moving the camera, not the object, we want to pan in the opposite direction + camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x(); + camera_transform.translation.y -= CAMERA_PAN_RATE * camera_pan_vector.y(); +} diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs new file mode 100644 index 00000000..c5feb6f2 --- /dev/null +++ b/examples/mouse_wheel.rs @@ -0,0 +1,76 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(InputManagerPlugin::::default()) + .add_startup_system(setup) + .add_system(zoom_camera) + .add_system(pan_camera.after(zoom_camera)) + .run() +} + +#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq)] +enum CameraMovement { + Zoom, + PanLeft, + PanRight, +} + +fn setup(mut commands: Commands) { + commands + .spawn() + .insert_bundle(Camera2dBundle::default()) + .insert_bundle(InputManagerBundle:: { + input_map: InputMap::default() + // This will capture the total continous value, for direct use + .insert(SingleAxis::mouse_wheel_y(), CameraMovement::Zoom) + // This will return a binary button-like output + .insert(MouseWheelDirection::Left, CameraMovement::PanLeft) + .insert(MouseWheelDirection::Right, CameraMovement::PanRight) + // Alternatively, you could model this as a virtual Dpad, + // which is extremely useful when you want to model 4-directional buttonlike inputs using the mouse wheel + // .insert(VirtualDpad::mouse_wheel(), Pan) + // Or even a continous `DualAxis`! + // .insert(DualAxis::mouse_wheel(), Pan) + .build(), + action_state: ActionState::default(), + }); + + commands.spawn().insert_bundle(SpriteBundle { + transform: Transform::from_scale(Vec3::new(100., 100., 1.)), + ..default() + }); +} + +fn zoom_camera( + mut query: Query<(&mut OrthographicProjection, &ActionState), With>, +) { + const CAMERA_ZOOM_RATE: f32 = 0.05; + + let (mut camera_projection, action_state) = query.single_mut(); + // Here, we use the `action_value` method to extract the total net amount that the mouse wheel has travelled + // Up and right axis movements are always positive by default + let zoom_delta = action_state.value(CameraMovement::Zoom); + + // We want to zoom in when we use mouse wheel up + // so we increase the scale proportionally + // Note that the projections scale should always be positive (or our images will flip) + camera_projection.scale *= 1. - zoom_delta * CAMERA_ZOOM_RATE; +} + +fn pan_camera(mut query: Query<(&mut Transform, &ActionState), With>) { + const CAMERA_PAN_RATE: f32 = 10.; + + let (mut camera_transform, action_state) = query.single_mut(); + + // When using the `MouseWheelDirection` type, mouse wheel inputs can be treated like simple buttons + if action_state.pressed(CameraMovement::PanLeft) { + camera_transform.translation.x -= CAMERA_PAN_RATE; + } + + if action_state.pressed(CameraMovement::PanRight) { + camera_transform.translation.x += CAMERA_PAN_RATE; + } +} diff --git a/examples/multiplayer.rs b/examples/multiplayer.rs index 19db799d..e70bf62d 100644 --- a/examples/multiplayer.rs +++ b/examples/multiplayer.rs @@ -41,14 +41,16 @@ impl PlayerBundle { // This is a quick and hacky solution: // you should coordinate with the `Gamepads` resource to determine the correct gamepad for each player // and gracefully handle disconnects - .set_gamepad(Gamepad(0)) + // 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(), Player::Two => InputMap::new([ (KeyCode::Left, Action::Left), (KeyCode::Right, Action::Right), (KeyCode::Up, Action::Jump), ]) - .set_gamepad(Gamepad(1)) + .set_gamepad(Gamepad { id: 1 }) .build(), }; diff --git a/examples/press_duration.rs b/examples/press_duration.rs index 82a4cf0a..cca8c64b 100644 --- a/examples/press_duration.rs +++ b/examples/press_duration.rs @@ -80,7 +80,7 @@ fn spawn_player(mut commands: Commands) { } fn spawn_camera(mut commands: Commands) { - commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + commands.spawn_bundle(Camera2dBundle::default()); } /// The longer you hold, the faster you dash when released! diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs index 0163e31d..19295ae7 100644 --- a/examples/send_actions_over_network.rs +++ b/examples/send_actions_over_network.rs @@ -6,13 +6,12 @@ //! Note that [`ActionState`] can also be serialized and sent directly. //! This approach will be less bandwidth efficient, but involve less complexity and CPU work. +use bevy::ecs::event::{Events, ManualEventReader}; +use bevy::input::InputPlugin; use bevy::prelude::*; -use bevy_ecs::event::{Events, ManualEventReader}; -use bevy_input::InputPlugin; use leafwing_input_manager::action_state::ActionDiff; use leafwing_input_manager::prelude::*; use leafwing_input_manager::systems::{generate_action_diffs, process_action_diffs}; -use leafwing_input_manager::MockInput; use std::fmt::Debug; diff --git a/examples/single_player.rs b/examples/single_player.rs index 456f56e1..db0d8d9a 100644 --- a/examples/single_player.rs +++ b/examples/single_player.rs @@ -75,11 +75,6 @@ impl PlayerBundle { use ArpgAction::*; let mut input_map = InputMap::default(); - // This is a quick and hacky solution: - // you should coordinate with the `Gamepads` resource to determine the correct gamepad for each player - // and gracefully handle disconnects - input_map.set_gamepad(Gamepad(0)); - // Movement input_map.insert(KeyCode::Up, Up); input_map.insert(GamepadButtonType::DPadUp, Up); diff --git a/examples/ui_driven_actions.rs b/examples/ui_driven_actions.rs index 0b552b1e..89b3d1d9 100644 --- a/examples/ui_driven_actions.rs +++ b/examples/ui_driven_actions.rs @@ -1,4 +1,4 @@ -//! Demonstrates how to connect `bevy_ui` buttons to [`ActionState`] components using the [`ActionStateDriver`] component on your button +//! Demonstrates how to connect `bevy::ui` buttons to [`ActionState`] components using the [`ActionStateDriver`] component on your button use bevy::prelude::*; use leafwing_input_manager::prelude::*; @@ -49,8 +49,7 @@ fn spawn_player(mut commands: Commands) { } fn spawn_cameras(mut commands: Commands) { - commands.spawn_bundle(UiCameraBundle::default()); - commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + commands.spawn_bundle(Camera2dBundle::default()); } fn spawn_ui(mut commands: Commands, player_query: Query>) { diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs new file mode 100644 index 00000000..a17bec7b --- /dev/null +++ b/examples/virtual_dpad.rs @@ -0,0 +1,59 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // This plugin maps inputs to an input-type agnostic action-state + // We need to provide it with an enum which stores the possible actions a player could take + .add_plugin(InputManagerPlugin::::default()) + // Spawn an entity with Player, InputMap, and ActionState components + .add_startup_system(spawn_player) + // Read the ActionState in your systems using queries! + .add_system(move_player) + .run(); +} + +#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] +enum Action { + Move, +} + +#[derive(Component)] +struct Player; + +fn spawn_player(mut commands: Commands) { + commands + .spawn() + .insert(Player) + .insert_bundle(InputManagerBundle:: { + // Stores "which actions are currently activated" + action_state: ActionState::default(), + // Map some arbitrary keys into a virtual direction pad that triggers our move action + input_map: InputMap::new([( + VirtualDPad { + up: KeyCode::W.into(), + down: KeyCode::S.into(), + left: KeyCode::A.into(), + right: KeyCode::D.into(), + }, + Action::Move, + )]) + .build(), + }); +} + +// Query for the `ActionState` component in your game logic systems! +fn move_player(query: Query<&ActionState, With>) { + let action_state = query.single(); + // If any button in a virtual direction pad is pressed, then the action state is "pressed" + if action_state.pressed(Action::Move) { + // Virtual direction pads are one of the types which return an AxisPair. The values will be + // represented as `-1.0`, `0.0`, or `1.0` depending on the combination of buttons pressed. + let axis_pair = action_state.axis_pair(Action::Move).unwrap(); + println!("Move:"); + println!(" distance: {}", axis_pair.length()); + println!(" x: {}", axis_pair.x()); + println!(" y: {}", axis_pair.y()); + } +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 071bc559..9f3f9b8e 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "leafwing_input_manager_macros" description = "Macros for the `leafwing-input-manager` crate" -version = "0.4.1" +version = "0.5.0" + license = "MIT OR Apache-2.0" edition = "2021" authors = ["Leafwing Studios"] diff --git a/src/action_state.rs b/src/action_state.rs index 4f24ec00..458a6e24 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -1,11 +1,10 @@ //! This module contains [`ActionState`] and its supporting methods and impls. -use crate::buttonlike::ButtonState; -use crate::user_input::UserInput; use crate::Actionlike; +use crate::{axislike::DualAxisData, buttonlike::ButtonState}; -use bevy_ecs::{component::Component, entity::Entity}; -use bevy_utils::{Duration, Instant}; +use bevy::ecs::{component::Component, entity::Entity}; +use bevy::utils::{Duration, Instant}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -16,8 +15,17 @@ use std::marker::PhantomData; pub struct ActionData { /// Is the action pressed or released? pub state: ButtonState, - /// What inputs were responsible for causing this action to be pressed? - pub reasons_pressed: Vec, + /// The "value" of the binding that triggered the action. + /// + /// See [`ActionState::action_value()`] for more details. + /// + /// **Warning:** this value may not be bounded as you might expect. + /// Consider clamping this to account for multiple triggering inputs. + pub value: f32, + /// The [`AxisPair`] of the binding that triggered the action. + /// + /// See [`ActionState::action_axis_pair()`] for more details. + pub axis_pair: Option, /// When was the button pressed / released, and how long has it been held for? pub timing: Timing, /// Was this action consumed by [`ActionState::consume`]? @@ -34,7 +42,7 @@ pub struct ActionData { /// # Example /// ```rust /// use leafwing_input_manager::prelude::*; -/// use bevy_utils::Instant; +/// use bevy::utils::Instant; /// /// #[derive(Actionlike, PartialEq, Eq, Clone, Copy, Debug)] /// enum Action { @@ -96,7 +104,8 @@ impl ActionState { ButtonState::Released => self.release(action), } - self.action_data[i].reasons_pressed = action_data[i].reasons_pressed.clone(); + self.action_data[i].axis_pair = action_data[i].axis_pair; + self.action_data[i].value = action_data[i].value; } } @@ -111,7 +120,7 @@ impl ActionState { /// ```rust /// use leafwing_input_manager::prelude::*; /// use leafwing_input_manager::buttonlike::ButtonState; - /// use bevy_utils::Instant; + /// use bevy::utils::Instant; /// /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Debug)] /// enum Action { @@ -181,6 +190,65 @@ impl ActionState { self.action_data[action.index()].clone() } + /// Get the value associated with the corresponding `action` + /// + /// Different kinds of bindings have different ways of calculating the value: + /// + /// - Binary buttons will have a value of `0.0` when the button is not pressed, and a value of + /// `1.0` when the button is pressed. + /// - Some axes, such as an analog stick, will have a value in the range `-1.0..=1.0`. + /// - Some axes, such as a variable trigger, will have a value in the range `0.0..=1.0`. + /// - Some buttons will also return a value in the range `0.0..=1.0`, such as analog gamepad + /// triggers which may be tracked as buttons or axes. Examples of these include the Xbox LT/RT + /// triggers and the Playstation L2/R2 triggers. See also the `axis_inputs` example in the + /// repository. + /// - Dual axis inputs will return the magnitude of its [`AxisPair`] and will be in the range + /// `0.0..=1.0`. + /// - Chord inputs will return the value of its first input. + /// + /// If multiple inputs trigger the same game action at the same time, the value of each + /// triggering input will be added together. + /// + /// # Warning + /// + /// This value may not be bounded as you might expect. + /// Consider clamping this to account for multiple triggering inputs, + /// typically using the [`clamped_value`](Self::clamped_value) method instead. + pub fn value(&self, action: A) -> f32 { + self.action_data(action).value + } + + /// Get the value associated with the corresponding `action`, clamped to `[-1.0, 1.0]`. + pub fn clamped_value(&self, action: A) -> f32 { + self.value(action).clamp(-1., 1.) + } + + /// Get the [`DualAxisData`] from the binding that triggered the corresponding `action`. + /// + /// Only certain events such as [`VirtualDPad`][crate::user_input::VirtualDPad] and + /// [`DualAxis`][crate::user_input::DualAxis] provide an [`DualAxisData`], and this + /// will return [`None`] for other events. + /// + /// Chord inputs will return the [`DualAxisData`] of it's first input. + /// + /// If multiple inputs with an axis pair trigger the same game action at the same time, the + /// value of each axis pair will be added together. + /// + /// # Warning + /// + /// These values may not be bounded as you might expect. + /// Consider clamping this to account for multiple triggering inputs, + /// typically using the [`clamped_axis_pair`](Self::clamped_axis_pair) method instead. + pub fn axis_pair(&self, action: A) -> Option { + self.action_data(action).axis_pair + } + + /// Get the [`DualAxisData`] associated with the corresponding `action`, clamped to `[-1.0, 1.0]`. + pub fn clamped_axis_pair(&self, action: A) -> Option { + self.axis_pair(action) + .map(|pair| DualAxisData::new(pair.x().clamp(-1.0, 1.0), pair.y().clamp(-1.0, 1.0))) + } + /// Manually sets the [`ActionData`] of the corresponding `action` /// /// You should almost always use more direct methods, as they are simpler and less error-prone. @@ -250,7 +318,6 @@ impl ActionState { if self.pressed(action) { self.action_data[index].timing.flip(); - self.action_data[index].reasons_pressed = Vec::new(); } self.action_data[index].state.release(); @@ -300,7 +367,6 @@ impl ActionState { // This is the only difference from action_state.release(action) self.action_data[index].consumed = true; self.action_data[index].state.release(); - self.action_data[index].reasons_pressed = Vec::new(); self.action_data[index].timing.flip(); } @@ -334,7 +400,7 @@ impl ActionState { self.action_data[action.index()].state.released() } - /// Was this `action` pressed since the last time [tick](ActionState::tick) was called? + /// Was this `action` released since the last time [tick](ActionState::tick) was called? #[inline] #[must_use] pub fn just_released(&self, action: A) -> bool { @@ -369,46 +435,6 @@ impl ActionState { .collect() } - /// The reasons (in terms of [`UserInput`]) that the button was pressed - /// - /// If the button is currently released, the `Vec returned will be empty - /// - /// # Example - /// - /// ```rust - /// use leafwing_input_manager::prelude::*; - /// use leafwing_input_manager::buttonlike::ButtonState; - /// use leafwing_input_manager::action_state::ActionData; - /// use bevy_input::keyboard::KeyCode; - /// - /// #[derive(Actionlike, Clone)] - /// enum PlatformerAction{ - /// Move, - /// Jump, - /// } - /// - /// let mut action_state = ActionState::::default(); - /// - /// // Usually this will be done automatically for you, via [`ActionState::update`] - /// action_state.set_action_data(PlatformerAction::Jump, - /// ActionData { - /// state: ButtonState::JustPressed, - /// // Manually setting the reason why this action was pressed - /// reasons_pressed: vec![KeyCode::Space.into()], - /// // For the sake of this example, we don't care about any other fields - /// ..Default::default() - /// } - /// ); - /// - /// let reasons_jumped = action_state.reasons_pressed(PlatformerAction::Jump); - /// assert_eq!(reasons_jumped[0], KeyCode::Space.into()); - /// ``` - #[inline] - #[must_use] - pub fn reasons_pressed(&self, action: A) -> Vec { - self.action_data[action.index()].reasons_pressed.clone() - } - /// The [`Instant`] that the action was last pressed or released /// /// If the action was pressed or released since the last time [`ActionState::tick`] was called @@ -447,7 +473,7 @@ impl Default for ActionState { /// # Examples /// /// By default, [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) uses this component -/// in order to connect `bevy_ui` buttons to the corresponding `ActionState`. +/// in order to connect `bevy::ui` buttons to the corresponding `ActionState`. /// /// ```rust /// use bevy::prelude::*; @@ -534,8 +560,6 @@ impl Timing { /// Flips the metaphorical hourglass, storing `current_duration` in `previous_duration` and resetting `instant_started` /// /// This method is called whenever actions are pressed or released - /// - /// FIXME: Ensure that the timing starts on the same frame that the input is flipped. pub fn flip(&mut self) { self.previous_duration = self.current_duration; self.current_duration = Duration::ZERO; @@ -568,9 +592,11 @@ pub enum ActionDiff { }, } +#[cfg(test)] mod tests { use crate as leafwing_input_manager; - use crate::Actionlike; + use crate::input_mocking::MockInput; + use leafwing_input_manager_macros::Actionlike; #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Debug)] enum Action { @@ -584,9 +610,13 @@ mod tests { use crate::action_state::ActionState; use crate::clashing_inputs::ClashStrategy; use crate::input_map::InputMap; - use crate::user_input::InputStreams; + use crate::input_streams::InputStreams; + use bevy::input::InputPlugin; use bevy::prelude::*; - use bevy_utils::{Duration, Instant}; + use bevy::utils::{Duration, Instant}; + + let mut app = App::new(); + app.add_plugin(InputPlugin); // Action state let mut action_state = ActionState::::default(); @@ -595,11 +625,8 @@ mod tests { let mut input_map = InputMap::default(); input_map.insert(KeyCode::R, Action::Run); - // Input streams - let mut keyboard_input_stream = Input::::default(); - let input_streams = InputStreams::from_keyboard(&keyboard_input_stream); - // Starting state + let input_streams = InputStreams::from_world(&app.world, None); action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(Action::Run)); @@ -608,8 +635,10 @@ mod tests { assert!(!action_state.just_released(Action::Run)); // Pressing - keyboard_input_stream.press(KeyCode::R); - let input_streams = InputStreams::from_keyboard(&keyboard_input_stream); + app.send_input(KeyCode::R); + // Process the input events into Input data + app.update(); + let input_streams = InputStreams::from_world(&app.world, None); action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); @@ -628,8 +657,9 @@ mod tests { assert!(!action_state.just_released(Action::Run)); // Releasing - keyboard_input_stream.release(KeyCode::R); - let input_streams = InputStreams::from_keyboard(&keyboard_input_stream); + app.release_input(KeyCode::R); + app.update(); + let input_streams = InputStreams::from_world(&app.world, None); action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); @@ -651,7 +681,7 @@ mod tests { #[test] fn time_tick_ticks_away() { use crate::action_state::ActionState; - use bevy_utils::{Duration, Instant}; + use bevy::utils::{Duration, Instant}; let mut action_state = ActionState::::default(); @@ -672,11 +702,10 @@ mod tests { assert!(!action_state.just_pressed(Action::Jump)); } - // FIXME: these tests are flaky because floats #[test] fn durations() { use crate::action_state::ActionState; - use bevy_utils::{Duration, Instant}; + use bevy::utils::{Duration, Instant}; let mut action_state = ActionState::::default(); diff --git a/src/axislike.rs b/src/axislike.rs index d21b15ee..c9607cf7 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -1,98 +1,512 @@ -//! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equvalents) - -use crate::orientation::{Direction, Rotation}; -use bevy_math::Vec2; - -/// A high-level abstract user input that varies from -1 to 1, inclusive, along two axes -/// -/// The neutral origin is always at 0, 0. -/// When constructed; the magnitude is capped at 1, but direction is preserved. -/// -/// This struct should store the processed form of your raw inputs in a device-agnostic fashion. -/// Any deadzone correction, rescaling or drift-correction should be done at an earlier level. -#[derive(Debug, Clone, PartialEq)] -pub struct AxisPair { - xy: Vec2, -} - -// Constructors -impl AxisPair { - /// Creates a new [`AxisPair`] from the provided (x,y) coordinates - /// - /// The direction is preserved, by the magnitude will be clamped to at most 1. - pub fn new(xy: Vec2) -> AxisPair { - let magnitude = xy.length(); - if magnitude <= 1. { - AxisPair { xy } - } else { - AxisPair { xy: xy / magnitude } - } - } -} - -// Methods -impl AxisPair { - /// The value along the x-axis, ranging from -1 to 1 - #[must_use] - #[inline] - pub fn x(&self) -> f32 { - self.xy.x - } - - /// The value along the y-axis, ranging from -1 to 1 - #[must_use] - #[inline] - pub fn y(&self) -> f32 { - self.xy.y - } - - /// The (x, y) values, each ranging from -1 to 1 - #[must_use] - #[inline] - pub fn xy(&self) -> Vec2 { - self.xy - } - - /// The [`Direction`] that this axis is pointing towards, if any - /// - /// If the axis is neutral (x,y) = (0,0), a (0, 0) `Direction` will be returned - #[must_use] - #[inline] - pub fn direction(&self) -> Direction { - Direction::new(self.xy) - } - - /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any - /// - /// If the axis is neutral (x,y) = (0,0), this will be `None` - #[must_use] - #[inline] - pub fn rotation(&self) -> Option { - match Rotation::from_xy(self.xy) { - Ok(rotation) => Some(rotation), - Err(_) => None, - } - } - - /// How far from the origin is this axis's position? - /// - /// Always bounded between 0 and 1. - /// - /// If you only need to compare relative magnitudes, use `magnitude_squared` instead for faster computation. - #[must_use] - #[inline] - pub fn magnitude(&self) -> f32 { - self.xy.length() - } - - /// The square of the axis' magnitude - /// - /// Always bounded between 0 and 1. - /// - /// This is faster than `magnitude`, as it avoids a square root, but will generally have less natural behavior. - #[must_use] - #[inline] - pub fn magnitude_squared(&self) -> f32 { - self.xy.length_squared() - } -} +//! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equvalents) + +use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::orientation::{Direction, Rotation}; +use crate::user_input::InputKind; +use bevy::input::{ + gamepad::{GamepadAxisType, GamepadButtonType}, + keyboard::KeyCode, +}; +use bevy::math::Vec2; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +/// A single directional axis with a configurable trigger zone. +/// +/// These can be stored in a [`InputKind`] to create a virtual button. +/// +/// # Warning +/// +/// `positive_low` must be greater than or equal to `negative_low` for this type to be validly constructed. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct SingleAxis { + /// The axis that is being checked. + pub axis_type: AxisType, + /// Any axis value higher than this will trigger the input. + pub positive_low: f32, + /// Any axis value lower than this will trigger the input. + pub negative_low: f32, + /// The target value for this input, used for input mocking. + /// + /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) + pub value: Option, +} + +impl SingleAxis { + /// Creates a [`SingleAxis`] with both `positive_low` and `negative_low` set to `threshold`. + #[must_use] + pub fn symmetric(axis_type: impl Into, threshold: f32) -> SingleAxis { + SingleAxis { + axis_type: axis_type.into(), + positive_low: threshold, + negative_low: -threshold, + value: None, + } + } + + /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`. + /// + /// All thresholds are set to 0.0. + /// Primarily useful for [input mocking](crate::MockInput). + #[must_use] + pub fn from_value(axis_type: impl Into, value: f32) -> SingleAxis { + SingleAxis { + axis_type: axis_type.into(), + positive_low: 0.0, + negative_low: 0.0, + value: Some(value), + } + } + + /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseWheel`](bevy::input::mouse::MouseWheel) movement + #[must_use] + pub const fn mouse_wheel_x() -> SingleAxis { + SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + positive_low: 0., + negative_low: 0., + value: None, + } + } + + /// Creates a [`SingleAxis`] corresponding to vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement + #[must_use] + pub const fn mouse_wheel_y() -> SingleAxis { + SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + positive_low: 0., + negative_low: 0., + value: None, + } + } + + /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseMotion`](bevy::input::mouse::MouseMotion) movement + #[must_use] + pub const fn mouse_motion_x() -> SingleAxis { + SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + positive_low: 0., + negative_low: 0., + value: None, + } + } + + /// Creates a [`SingleAxis`] corresponding to vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement + #[must_use] + pub const fn mouse_motion_y() -> SingleAxis { + SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + positive_low: 0., + negative_low: 0., + value: None, + } + } +} + +impl PartialEq for SingleAxis { + fn eq(&self, other: &Self) -> bool { + self.axis_type == other.axis_type + && FloatOrd(self.positive_low) == FloatOrd(other.positive_low) + && FloatOrd(self.negative_low) == FloatOrd(other.negative_low) + } +} +impl Eq for SingleAxis {} +impl std::hash::Hash for SingleAxis { + fn hash(&self, state: &mut H) { + self.axis_type.hash(state); + FloatOrd(self.positive_low).hash(state); + FloatOrd(self.negative_low).hash(state); + } +} + +/// Two directional axes combined as one input. +/// +/// These can be stored in a [`VirtualDPad`], which is itself stored in an [`InputKind`] for consumption. +/// +/// This input will generate [`AxisPair`] can be read with +/// [`ActionState::action_axis_pair()`][crate::ActionState::action_axis_pair()]. +/// +/// # Warning +/// +/// `positive_low` must be greater than or equal to `negative_low` for both `x` and `y` for this type to be validly constructed. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct DualAxis { + /// The axis representing horizontal movement. + pub x: SingleAxis, + /// The axis representing vertical movement. + pub y: SingleAxis, +} + +impl DualAxis { + /// The default size of the deadzone used by constructor methods. + /// + /// This cannot be changed, but the struct can be easily manually constructed. + pub const DEFAULT_DEADZONE: f32 = 0.1; + + /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold`. + #[must_use] + pub fn symmetric( + x_axis_type: impl Into, + y_axis_type: impl Into, + threshold: f32, + ) -> DualAxis { + DualAxis { + x: SingleAxis::symmetric(x_axis_type, threshold), + y: SingleAxis::symmetric(y_axis_type, threshold), + } + } + + /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`. + /// + /// All thresholds are set to 0.0. + /// Primarily useful for [input mocking](crate::MockInput). + #[must_use] + pub fn from_value( + x_axis_type: impl Into, + y_axis_type: impl Into, + x_value: f32, + y_value: f32, + ) -> DualAxis { + DualAxis { + x: SingleAxis::from_value(x_axis_type, x_value), + y: SingleAxis::from_value(y_axis_type, y_value), + } + } + + /// Creates a [`DualAxis`] for the left analogue stick of the gamepad. + #[must_use] + pub fn left_stick() -> DualAxis { + DualAxis::symmetric( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + Self::DEFAULT_DEADZONE, + ) + } + + /// Creates a [`DualAxis`] for the right analogue stick of the gamepad. + #[must_use] + pub fn right_stick() -> DualAxis { + DualAxis::symmetric( + GamepadAxisType::RightStickX, + GamepadAxisType::LeftStickY, + Self::DEFAULT_DEADZONE, + ) + } + + /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement + pub const fn mouse_wheel() -> DualAxis { + DualAxis { + x: SingleAxis::mouse_wheel_x(), + y: SingleAxis::mouse_wheel_y(), + } + } + + /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement + pub const fn mouse_motion() -> DualAxis { + DualAxis { + x: SingleAxis::mouse_motion_x(), + y: SingleAxis::mouse_motion_y(), + } + } +} + +#[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an un-quoted item +/// A virtual DPad that you can get an [`AxisPair`] from +/// +/// Typically, you don't want to store a [`DualAxis`] in this type, +/// even though it can be stored as an [`InputKind`]. +/// +/// Instead, use it directly as [`InputKind::DualAxis`]! +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VirtualDPad { + /// The input that represents the up direction in this virtual DPad + pub up: InputKind, + /// The input that represents the down direction in this virtual DPad + pub down: InputKind, + /// The input that represents the left direction in this virtual DPad + pub left: InputKind, + /// The input that represents the right direction in this virtual DPad + pub right: InputKind, +} + +impl VirtualDPad { + /// Generates a [`VirtualDPad`] corresponding to the arrow keyboard keycodes + pub fn arrow_keys() -> VirtualDPad { + VirtualDPad { + up: InputKind::Keyboard(KeyCode::Up), + down: InputKind::Keyboard(KeyCode::Down), + left: InputKind::Keyboard(KeyCode::Left), + right: InputKind::Keyboard(KeyCode::Right), + } + } + + /// Generates a [`VirtualDPad`] corresponding to the `WASD` keyboard keycodes + pub fn wasd() -> VirtualDPad { + VirtualDPad { + up: InputKind::Keyboard(KeyCode::W), + down: InputKind::Keyboard(KeyCode::S), + left: InputKind::Keyboard(KeyCode::A), + right: InputKind::Keyboard(KeyCode::D), + } + } + + #[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an un-quoted item + /// Generates a [`VirtualDPad`] corresponding to the DPad on a gamepad + pub fn dpad() -> VirtualDPad { + VirtualDPad { + up: InputKind::GamepadButton(GamepadButtonType::DPadUp), + down: InputKind::GamepadButton(GamepadButtonType::DPadDown), + left: InputKind::GamepadButton(GamepadButtonType::DPadLeft), + right: InputKind::GamepadButton(GamepadButtonType::DPadRight), + } + } + + /// Generates a [`VirtualDPad`] corresponding to the face buttons on a gamepad + /// + /// North corresponds to up, west corresponds to left, east corresponds to right, south corresponds to down + pub fn gamepad_face_buttons() -> VirtualDPad { + VirtualDPad { + up: InputKind::GamepadButton(GamepadButtonType::North), + down: InputKind::GamepadButton(GamepadButtonType::South), + left: InputKind::GamepadButton(GamepadButtonType::West), + right: InputKind::GamepadButton(GamepadButtonType::East), + } + } + + /// Generates a [`VirtualDPad`] corresponding to discretized mousewheel movements + pub fn mouse_wheel() -> VirtualDPad { + VirtualDPad { + up: InputKind::MouseWheel(MouseWheelDirection::Up), + down: InputKind::MouseWheel(MouseWheelDirection::Down), + left: InputKind::MouseWheel(MouseWheelDirection::Left), + right: InputKind::MouseWheel(MouseWheelDirection::Right), + } + } + + /// Generates a [`VirtualDPad`] corresponding to discretized mouse motions + pub fn mouse_motion() -> VirtualDPad { + VirtualDPad { + up: InputKind::MouseMotion(MouseMotionDirection::Up), + down: InputKind::MouseMotion(MouseMotionDirection::Down), + left: InputKind::MouseMotion(MouseMotionDirection::Left), + right: InputKind::MouseMotion(MouseMotionDirection::Right), + } + } +} + +/// The type of axis used by a [`UserInput`](crate::user_input::UserInput). +/// +/// This is stored in either a [`SingleAxis`] or [`DualAxis`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AxisType { + /// Input associated with a gamepad, such as the triggers or one axis of an analog stick. + Gamepad(GamepadAxisType), + /// Input associated with a mouse wheel. + MouseWheel(MouseWheelAxisType), + /// Input associated with movement of the mouse + MouseMotion(MouseMotionAxisType), +} + +/// The direction of motion of the mouse wheel. +/// +/// Stored in the [`AxisType`] enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MouseWheelAxisType { + /// Horizontal movement. + /// + /// This is much less common than the `Y` variant, and is only supported on some devices. + X, + /// Vertical movement. + /// + /// This is the standard behavior for a mouse wheel, used to scroll up and down pages. + Y, +} + +/// The direction of motion of the mouse. +/// +/// Stored in the [`AxisType`] enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MouseMotionAxisType { + /// Horizontal movement. + X, + /// Vertical movement. + Y, +} + +impl From for AxisType { + fn from(axis_type: GamepadAxisType) -> Self { + AxisType::Gamepad(axis_type) + } +} + +impl From for AxisType { + fn from(axis_type: MouseWheelAxisType) -> Self { + AxisType::MouseWheel(axis_type) + } +} + +impl From for AxisType { + fn from(axis_type: MouseMotionAxisType) -> Self { + AxisType::MouseMotion(axis_type) + } +} + +impl TryFrom for GamepadAxisType { + type Error = AxisConversionError; + + fn try_from(axis_type: AxisType) -> Result { + match axis_type { + AxisType::Gamepad(inner) => Ok(inner), + _ => Err(AxisConversionError), + } + } +} + +impl TryFrom for MouseWheelAxisType { + type Error = AxisConversionError; + + fn try_from(axis_type: AxisType) -> Result { + match axis_type { + AxisType::MouseWheel(inner) => Ok(inner), + _ => Err(AxisConversionError), + } + } +} + +impl TryFrom for MouseMotionAxisType { + type Error = AxisConversionError; + + fn try_from(axis_type: AxisType) -> Result { + match axis_type { + AxisType::MouseMotion(inner) => Ok(inner), + _ => Err(AxisConversionError), + } + } +} + +/// An [`AxisType`] could not be converted into a more specialized variant +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct AxisConversionError; + +/// A wrapped [`Vec2`] that represents the combaination of two input axes. +/// +/// The neutral origin is always at 0, 0. +/// When working with gamepad axes, both `x` and `y` values are bounded by [-1.0, 1.0]. +/// For other input axes (such as mousewheel data), this may not be true! +/// +/// This struct should store the processed form of your raw inputs in a device-agnostic fashion. +/// Any deadzone correction, rescaling or drift-correction should be done at an earlier level. +#[derive(Debug, Copy, Clone, PartialEq, Default, Deserialize, Serialize)] +pub struct DualAxisData { + xy: Vec2, +} + +// Constructors +impl DualAxisData { + /// Creates a new [`AxisPair`] from the provided (x,y) coordinates + pub fn new(x: f32, y: f32) -> DualAxisData { + DualAxisData { + xy: Vec2::new(x, y), + } + } + + /// Creates a new [`AxisPair`] directly from a [`Vec2`] + pub fn from_xy(xy: Vec2) -> DualAxisData { + DualAxisData { xy } + } + + /// Merge the state of this [`AxisPair`] with another. + /// + /// This is useful if you have multiple sticks bound to the same game action, + /// and you want to get their combined position. + /// + /// # Warning + /// + /// This method can result in values with a greater maximum magnitude than expected! + /// Use [`AxisPair::clamp_length`] to limit the resulting direction. + pub fn merged_with(&self, other: DualAxisData) -> DualAxisData { + DualAxisData::from_xy(self.xy() + other.xy()) + } +} + +// Methods +impl DualAxisData { + /// The value along the x-axis, typically ranging from -1 to 1 + #[must_use] + #[inline] + pub fn x(&self) -> f32 { + self.xy.x + } + + /// The value along the y-axis, typically ranging from -1 to 1 + #[must_use] + #[inline] + pub fn y(&self) -> f32 { + self.xy.y + } + + /// The (x, y) values, each typically ranging from -1 to 1 + #[must_use] + #[inline] + pub fn xy(&self) -> Vec2 { + self.xy + } + + /// The [`Direction`] that this axis is pointing towards, if any + /// + /// If the axis is neutral (x,y) = (0,0), a (0, 0) `None` will be returned + #[must_use] + #[inline] + pub fn direction(&self) -> Option { + // TODO: replace this quick-n-dirty hack once Direction::new no longer panics + if self.xy.length() > 0.00001 { + return Some(Direction::new(self.xy)); + } + None + } + + /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any + /// + /// If the axis is neutral (x,y) = (0,0), this will be `None` + #[must_use] + #[inline] + pub fn rotation(&self) -> Option { + match Rotation::from_xy(self.xy) { + Ok(rotation) => Some(rotation), + Err(_) => None, + } + } + + /// How far from the origin is this axis's position? + /// + /// Typically bounded by 0 and 1. + /// + /// If you only need to compare relative magnitudes, use `magnitude_squared` instead for faster computation. + #[must_use] + #[inline] + pub fn length(&self) -> f32 { + self.xy.length() + } + + /// The square of the axis' magnitude + /// + /// Typically bounded by 0 and 1. + /// + /// This is faster than `magnitude`, as it avoids a square root, but will generally have less natural behavior. + #[must_use] + #[inline] + pub fn length_squared(&self) -> f32 { + self.xy.length_squared() + } + + /// Clamps the magnitude of the axis + pub fn clamp_length(&mut self, max: f32) { + self.xy = self.xy.clamp_length_max(max); + } +} + +impl From for Vec2 { + fn from(data: DualAxisData) -> Vec2 { + data.xy + } +} diff --git a/src/buttonlike.rs b/src/buttonlike.rs index 23eaa5f3..e22173ed 100644 --- a/src/buttonlike.rs +++ b/src/buttonlike.rs @@ -87,3 +87,33 @@ impl Default for ButtonState { ButtonState::Released } } + +/// A buttonlike-input triggered by [`MouseWheel`](bevy::input::mouse::MouseWheel) events +/// +/// These will be considered pressed if non-zero net movement in the correct direction is detected. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MouseWheelDirection { + /// Corresponds to `+y` + Up, + /// Corresponds to `-y` + Down, + /// Corresponds to `+x` + Right, + /// Corresponds to `-x` + Left, +} + +/// A buttonlike-input triggered by [`MouseMotion`](bevy::input::mouse::MouseMotion) events +/// +/// These will be considered pressed if non-zero net movement in the correct direction is detected. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MouseMotionDirection { + /// Corresponds to `+y` + Up, + /// Corresponds to `-y` + Down, + /// Corresponds to `+x` + Right, + /// Corresponds to `-x` + Left, +} diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 93c9af4c..fa47a1f6 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -1,8 +1,10 @@ //! Handles clashing inputs into a [`InputMap`](crate::input_map::InputMap) in a configurable fashion. use crate::action_state::ActionData; +use crate::axislike::VirtualDPad; use crate::input_map::InputMap; -use crate::user_input::{InputButton, InputStreams, UserInput}; +use crate::input_streams::InputStreams; +use crate::user_input::{InputKind, UserInput}; use crate::Actionlike; use itertools::Itertools; @@ -55,11 +57,18 @@ impl UserInput { match self { Single(self_button) => match other { Single(_) => false, - Chord(other_set) => button_chord_clash(self_button, other_set), + Chord(other_chord) => button_chord_clash(self_button, other_chord), + VirtualDPad(other_dpad) => dpad_button_clash(other_dpad, self_button), }, - Chord(self_set) => match other { - Single(other_button) => button_chord_clash(other_button, self_set), - Chord(other_set) => chord_chord_clash(self_set, other_set), + Chord(self_chord) => match other { + Single(other_button) => button_chord_clash(other_button, self_chord), + Chord(other_chord) => chord_chord_clash(self_chord, other_chord), + VirtualDPad(other_dpad) => dpad_chord_clash(other_dpad, self_chord), + }, + VirtualDPad(self_dpad) => match other { + Single(other_button) => dpad_button_clash(self_dpad, other_button), + Chord(other_chord) => dpad_chord_clash(self_dpad, other_chord), + VirtualDPad(other_dpad) => dpad_dpad_clash(self_dpad, other_dpad), }, } } @@ -187,9 +196,9 @@ impl Clash { } } -/// Does the `button` clash with the `chord`? +// Does the `button` clash with the `chord`? #[must_use] -fn button_chord_clash(button: &InputButton, chord: &PetitSet) -> bool { +fn button_chord_clash(button: &InputKind, chord: &PetitSet) -> bool { if chord.len() <= 1 { return false; } @@ -197,12 +206,47 @@ fn button_chord_clash(button: &InputButton, chord: &PetitSet) -> chord.contains(button) } +// Does the `dpad` clash with the `chord`? +#[must_use] +fn dpad_chord_clash(dpad: &VirtualDPad, chord: &PetitSet) -> bool { + if chord.len() <= 1 { + return false; + } + + for button in &[dpad.up, dpad.down, dpad.left, dpad.right] { + if chord.contains(button) { + return true; + } + } + + false +} + +fn dpad_button_clash(dpad: &VirtualDPad, button: &InputKind) -> bool { + for dpad_button in &[dpad.up, dpad.down, dpad.left, dpad.right] { + if button == dpad_button { + return true; + } + } + + false +} + +fn dpad_dpad_clash(dpad1: &VirtualDPad, dpad2: &VirtualDPad) -> bool { + for button1 in &[dpad1.up, dpad1.down, dpad1.left, dpad1.right] { + for button2 in &[dpad2.up, dpad2.down, dpad2.left, dpad2.right] { + if button1 == button2 { + return true; + } + } + } + + false +} + /// Does the `chord_a` clash with `chord_b`? #[must_use] -fn chord_chord_clash( - chord_a: &PetitSet, - chord_b: &PetitSet, -) -> bool { +fn chord_chord_clash(chord_a: &PetitSet, chord_b: &PetitSet) -> bool { if chord_a.len() <= 1 || chord_b.len() <= 1 { return false; } @@ -279,6 +323,8 @@ fn resolve_clash( } } + println!("real clash"); + // There's a real clash; resolve it according to the `clash_strategy` match clash_strategy { // Do nothing @@ -315,8 +361,9 @@ fn resolve_clash( mod tests { use super::*; use crate as leafwing_input_manager; - use crate::Actionlike; - use bevy_input::keyboard::KeyCode::*; + use bevy::app::App; + use bevy::input::keyboard::KeyCode::*; + use leafwing_input_manager_macros::Actionlike; #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug)] enum Action { @@ -328,6 +375,8 @@ mod tests { CtrlOne, AltOne, CtrlAltOne, + MoveDPad, + CtrlUp, } fn test_input_map() -> InputMap { @@ -343,11 +392,27 @@ mod tests { input_map.insert_chord([LControl, Key1], CtrlOne); input_map.insert_chord([LAlt, Key1], AltOne); input_map.insert_chord([LControl, LAlt, Key1], CtrlAltOne); + input_map.insert( + VirtualDPad { + up: Up.into(), + down: Down.into(), + left: Left.into(), + right: Right.into(), + }, + MoveDPad, + ); + input_map.insert_chord([LControl, Up], CtrlUp); input_map } mod basic_functionality { + use crate::axislike::VirtualDPad; + use crate::buttonlike::ButtonState; + use crate::input_mocking::MockInput; + use bevy::input::InputPlugin; + use Action::*; + use super::*; #[test] @@ -358,18 +423,44 @@ mod tests { let ab = UserInput::chord([A, B]); let bc = UserInput::chord([B, C]); let abc = UserInput::chord([A, B, C]); + let axyz_dpad: UserInput = VirtualDPad { + up: A.into(), + down: X.into(), + left: Y.into(), + right: Z.into(), + } + .into(); + let abcd_dpad: UserInput = VirtualDPad { + up: A.into(), + down: B.into(), + left: C.into(), + right: D.into(), + } + .into(); + + let ctrl_up: UserInput = UserInput::chord([Up, LControl]); + let directions_dpad: UserInput = VirtualDPad { + up: Up.into(), + down: Down.into(), + left: Left.into(), + right: Right.into(), + } + .into(); assert!(!a.clashes(&b)); assert!(a.clashes(&ab)); assert!(!c.clashes(&ab)); assert!(!ab.clashes(&bc)); - assert!(ab.clashes(&abc)) + assert!(ab.clashes(&abc)); + assert!(axyz_dpad.clashes(&a)); + assert!(axyz_dpad.clashes(&ab)); + assert!(!axyz_dpad.clashes(&bc)); + assert!(axyz_dpad.clashes(&abcd_dpad)); + assert!(ctrl_up.clashes(&directions_dpad)); } #[test] fn button_chord_clash_construction() { - use Action::*; - let input_map = test_input_map(); let observed_clash = input_map.possible_clash(One, OneAndTwo).unwrap(); @@ -386,8 +477,6 @@ mod tests { #[test] fn chord_chord_clash_construction() { - use Action::*; - let input_map = test_input_map(); let observed_clash = input_map @@ -406,8 +495,6 @@ mod tests { #[test] fn can_clash() { - use Action::*; - let input_map = test_input_map(); assert!(input_map.possible_clash(One, Two).is_none()); @@ -423,29 +510,29 @@ mod tests { fn clash_caching() { let mut input_map = test_input_map(); // Possible clashes are cached upon initialization - assert_eq!(input_map.possible_clashes().len(), 12); + assert_eq!(input_map.possible_clashes().len(), 13); // Possible clashes are cached upon binding insertion input_map.insert(UserInput::chord([LControl, LAlt, Key1]), Action::Two); - assert_eq!(input_map.possible_clashes().len(), 15); + assert_eq!(input_map.possible_clashes().len(), 16); // Possible clashes are cached upon binding removal input_map.clear_action(Action::One); - assert_eq!(input_map.possible_clashes().len(), 9); + assert_eq!(input_map.possible_clashes().len(), 10); } #[test] fn resolve_prioritize_longest() { - use bevy::prelude::*; - use Action::*; + let mut app = App::new(); + app.add_plugin(InputPlugin); let input_map = test_input_map(); let simple_clash = input_map.possible_clash(One, OneAndTwo).unwrap(); - let mut keyboard: Input = Default::default(); - keyboard.press(Key1); - keyboard.press(Key2); + app.send_input(Key1); + app.send_input(Key2); + app.update(); - let input_streams = InputStreams::from_keyboard(&keyboard); + let input_streams = InputStreams::from_world(&app.world, None); assert_eq!( resolve_clash( @@ -469,9 +556,10 @@ mod tests { let chord_clash = input_map .possible_clash(OneAndTwo, OneAndTwoAndThree) .unwrap(); - keyboard.press(Key3); + app.send_input(Key3); + app.update(); - let input_streams = InputStreams::from_keyboard(&keyboard); + let input_streams = InputStreams::from_world(&app.world, None); assert_eq!( resolve_clash( @@ -485,17 +573,17 @@ mod tests { #[test] fn resolve_use_action_order() { - use bevy::prelude::*; - use Action::*; + let mut app = App::new(); + app.add_plugin(InputPlugin); let input_map = test_input_map(); let simple_clash = input_map.possible_clash(One, CtrlOne).unwrap(); let reversed_clash = input_map.possible_clash(CtrlOne, One).unwrap(); - let mut keyboard: Input = Default::default(); - keyboard.press(Key1); - keyboard.press(LControl); + app.send_input(Key1); + app.send_input(LControl); + app.update(); - let input_streams = InputStreams::from_keyboard(&keyboard); + let input_streams = InputStreams::from_world(&app.world, None); assert_eq!( resolve_clash(&simple_clash, ClashStrategy::UseActionOrder, &input_streams,), @@ -514,15 +602,13 @@ mod tests { #[test] fn handle_clashes() { - use crate::buttonlike::ButtonState; - use bevy::prelude::*; - use Action::*; - + let mut app = App::new(); + app.add_plugin(InputPlugin); let input_map = test_input_map(); - let mut keyboard: Input = Default::default(); - keyboard.press(Key1); - keyboard.press(Key2); + app.send_input(Key1); + app.send_input(Key2); + app.update(); let mut action_data = vec![ActionData::default(); Action::N_VARIANTS]; action_data[One.index()].state = ButtonState::JustPressed; @@ -531,7 +617,7 @@ mod tests { input_map.handle_clashes( &mut action_data, - &InputStreams::from_keyboard(&keyboard), + &InputStreams::from_world(&app.world, None), ClashStrategy::PrioritizeLongest, ); @@ -541,20 +627,46 @@ mod tests { assert_eq!(action_data, expected); } + // Checks that a clash between a VirtualDPad and a chord choses the chord #[test] - fn which_pressed() { - use bevy::prelude::*; - use Action::*; + fn handle_clashes_dpad_chord() { + let mut app = App::new(); + app.add_plugin(InputPlugin); + let input_map = test_input_map(); + + app.send_input(LControl); + app.send_input(Up); + app.update(); + + let mut action_data = vec![ActionData::default(); Action::N_VARIANTS]; + action_data[MoveDPad.index()].state = ButtonState::JustPressed; + action_data[CtrlUp.index()].state = ButtonState::JustPressed; + input_map.handle_clashes( + &mut action_data, + &InputStreams::from_world(&app.world, None), + ClashStrategy::PrioritizeLongest, + ); + + let mut expected = vec![ActionData::default(); Action::N_VARIANTS]; + expected[CtrlUp.index()].state = ButtonState::JustPressed; + + assert_eq!(action_data, expected); + } + + #[test] + fn which_pressed() { + let mut app = App::new(); + app.add_plugin(InputPlugin); let input_map = test_input_map(); - let mut keyboard: Input = Default::default(); - keyboard.press(Key1); - keyboard.press(Key2); - keyboard.press(LControl); + app.send_input(Key1); + app.send_input(Key2); + app.send_input(LControl); + app.update(); let action_data = input_map.which_pressed( - &InputStreams::from_keyboard(&keyboard), + &InputStreams::from_world(&app.world, None), ClashStrategy::PrioritizeLongest, ); diff --git a/src/display_impl.rs b/src/display_impl.rs index 5a13554c..b0e70f55 100644 --- a/src/display_impl.rs +++ b/src/display_impl.rs @@ -1,6 +1,7 @@ //! Containment module for boring implmentations of the [`Display`] trait -use crate::user_input::{InputButton, UserInput}; +use crate::axislike::VirtualDPad; +use crate::user_input::{InputKind, UserInput}; use std::fmt::Display; impl Display for UserInput { @@ -17,16 +18,32 @@ impl Display for UserInput { } write!(f, "{string}") } + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + write!( + f, + "VirtualDPad(up: {}, down: {}, left: {}, right: {})", + up, down, left, right + ) + } } } } -impl Display for InputButton { +impl Display for InputKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - InputButton::Gamepad(button) => write!(f, "{button:?}"), - InputButton::Mouse(button) => write!(f, "{button:?}"), - InputButton::Keyboard(button) => write!(f, "{button:?}"), + InputKind::SingleAxis(axis) => write!(f, "{axis:?}"), + InputKind::DualAxis(axis) => write!(f, "{axis:?}"), + InputKind::GamepadButton(button) => write!(f, "{button:?}"), + InputKind::Mouse(button) => write!(f, "{button:?}"), + InputKind::MouseWheel(button) => write!(f, "{button:?}"), + InputKind::MouseMotion(button) => write!(f, "{button:?}"), + InputKind::Keyboard(button) => write!(f, "{button:?}"), } } } diff --git a/src/errors.rs b/src/errors.rs index 62d2380a..2e6b325f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,7 +5,7 @@ use derive_more::{Display, Error}; /// The supplied vector-like struct was too close to zero to be converted into a rotation-like type /// /// This error is produced when attempting to convert into a rotation-like type -/// such as a [`Rotation`] or [`Quat`](bevy_math::Quat) from a vector-like type +/// such as a [`Rotation`] or [`Quat`](bevy::math::Quat) from a vector-like type /// such as a [`Vec2`]. /// /// In almost all cases, the correct way to handle this error is to simply not change the rotation. diff --git a/src/input_map.rs b/src/input_map.rs index acd550c5..67920c1f 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -3,11 +3,12 @@ use crate::action_state::ActionData; use crate::buttonlike::ButtonState; use crate::clashing_inputs::ClashStrategy; -use crate::user_input::{InputButton, InputStreams, UserInput}; +use crate::input_streams::InputStreams; +use crate::user_input::{InputKind, UserInput}; use crate::Actionlike; -use bevy_ecs::component::Component; -use bevy_input::gamepad::Gamepad; +use bevy::ecs::component::Component; +use bevy::input::gamepad::Gamepad; use core::fmt::Debug; use petitset::PetitSet; @@ -19,14 +20,11 @@ use std::marker::PhantomData; /// Multiple inputs can be mapped to the same action, /// and each input can be mapped to multiple actions. /// -/// The provided input types must be one of [`GamepadButtonType`], [`KeyCode`] or [`MouseButton`]. +/// The provided input types must be able to be converted into a [`UserInput`]. /// /// The maximum number of bindings (total) that can be stored for each action is 16. /// Insertions will silently fail if you have reached this cap. /// -/// In addition, you can configure the per-mode cap for each [`InputMode`] using [`InputMap::new`] or [`InputMap::set_per_mode_cap`]. -/// This can be useful if your UI can only display one or two possible keybindings for each input mode. -/// /// By default, if two actions would be 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, @@ -38,7 +36,7 @@ use std::marker::PhantomData; /// ```rust /// use bevy::prelude::*; /// use leafwing_input_manager::prelude::*; -/// use leafwing_input_manager::user_input::InputButton; +/// use leafwing_input_manager::user_input::InputKind; /// /// // You can Run! /// // But you can't Hide :( @@ -51,7 +49,7 @@ use std::marker::PhantomData; /// // Construction /// let mut input_map = InputMap::new([ /// // Note that the type of your iterators must be homogenous; -/// // you can use `InputButton` or `UserInput` if needed +/// // you can use `InputKind` or `UserInput` if needed /// // as unifiying types /// (GamepadButtonType::South, Action::Run), /// (GamepadButtonType::LeftTrigger, Action::Hide), @@ -63,9 +61,13 @@ use std::marker::PhantomData; /// .insert(KeyCode::LShift, Action::Run) /// // Chords /// .insert_chord([KeyCode::LControl, KeyCode::R], Action::Run) -/// .insert_chord([InputButton::Keyboard(KeyCode::H), -/// InputButton::Gamepad(GamepadButtonType::South), -/// InputButton::Mouse(MouseButton::Middle)], +/// .insert_chord([InputKind::Keyboard(KeyCode::H), +/// InputKind::GamepadButton(GamepadButtonType::South), +/// InputKind::Mouse(MouseButton::Middle)], +/// Action::Run) +/// .insert_chord([InputKind::Keyboard(KeyCode::H), +/// InputKind::GamepadButton(GamepadButtonType::South), +/// InputKind::Mouse(MouseButton::Middle)], /// Action::Hide); /// /// // Removal @@ -94,7 +96,7 @@ impl Default for InputMap { // Constructors impl InputMap { - /// Creates a new [`InputMap`] from an iterator of `(action, user_input)` pairs + /// Creates a new [`InputMap`] from an iterator of `(user_input, action)` pairs /// /// To create an empty input map, use the [`Default::default`] method instead. /// @@ -102,7 +104,7 @@ impl InputMap { /// ```rust /// use leafwing_input_manager::input_map::InputMap; /// use leafwing_input_manager::Actionlike; - /// use bevy_input::keyboard::KeyCode; + /// use bevy::input::keyboard::KeyCode; /// /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash)] /// enum Action { @@ -139,7 +141,7 @@ impl InputMap { /// ```rust /// use leafwing_input_manager::prelude::*; - /// use bevy_input::keyboard::KeyCode; + /// use bevy::input::keyboard::KeyCode; /// /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash)] /// enum Action { @@ -159,7 +161,7 @@ impl InputMap { // Insertion impl InputMap { - /// Insert a mapping between `action` and `input` + /// Insert a mapping between `input` and `action` /// /// # Panics /// @@ -172,7 +174,7 @@ impl InputMap { self } - /// Insert a mapping between `action` and `input` at the provided index + /// Insert a mapping between `input` and `action` at the provided index /// /// If a matching input already existed in the set, it will be moved to the supplied index. Any input that was previously there will be moved to the matching input’s original index. /// @@ -187,7 +189,7 @@ impl InputMap { self } - /// Insert a mapping between `action` and the provided `inputs` + /// Insert a mapping between the provided `input_action_pairs` /// /// This method creates multiple distinct bindings. /// If you want to require multiple buttons to be pressed at once, use [`insert_chord`](Self::insert_chord). @@ -198,16 +200,16 @@ impl InputMap { /// Panics if the map is full and any of `inputs` is not a duplicate. pub fn insert_multiple( &mut self, - inputs: impl IntoIterator, A)>, + input_action_pairs: impl IntoIterator, A)>, ) -> &mut Self { - for (action, input) in inputs { + for (action, input) in input_action_pairs { self.insert(action, input); } self } - /// Insert a mapping between `action` and the simultaneous combination of `buttons` provided + /// Insert a mapping between the simultaneous combination of `buttons` and the `action` provided /// /// Any iterator that can be converted into a [`Button`] can be supplied, but will be converted into a [`PetitSet`] for storage and use. /// Chords can also be added with the [insert](Self::insert) method, if the [`UserInput::Chord`] variant is constructed explicitly. @@ -217,7 +219,7 @@ impl InputMap { /// Panics if the map is full and `buttons` is not a duplicate. pub fn insert_chord( &mut self, - buttons: impl IntoIterator>, + buttons: impl IntoIterator>, action: A, ) -> &mut Self { self.insert(UserInput::chord(buttons), action); @@ -260,12 +262,21 @@ impl InputMap { // Configuration impl InputMap { /// Fetches the [Gamepad] associated with the entity controlled by this entity map + /// + /// If this is [`None`], input from any connected gamepad will be used. #[must_use] pub fn gamepad(&self) -> Option { self.associated_gamepad } /// Assigns a particular [`Gamepad`] to the entity controlled by this input map + /// + /// 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. pub fn set_gamepad(&mut self, gamepad: Gamepad) -> &mut Self { self.associated_gamepad = Some(gamepad); self @@ -312,11 +323,22 @@ impl InputMap { let mut inputs = Vec::new(); for input in self.get(action.clone()).iter() { + let action = &mut action_data[action.index()]; + + // Merge axis pair into action data + let axis_pair = input_streams.input_axis_pair(input); + if let Some(axis_pair) = axis_pair { + if let Some(current_axis_pair) = &mut action.axis_pair { + *current_axis_pair = current_axis_pair.merged_with(axis_pair); + } else { + action.axis_pair = Some(axis_pair); + } + } + if input_streams.input_pressed(input) { inputs.push(input.clone()); - action_data[action.index()] - .reasons_pressed - .push(input.clone()); + + action.value += input_streams.input_value(input); } } @@ -406,7 +428,7 @@ mod tests { #[test] fn insertion_idempotency() { - use bevy_input::keyboard::KeyCode; + use bevy::input::keyboard::KeyCode; use petitset::PetitSet; let mut input_map = InputMap::::default(); @@ -428,7 +450,7 @@ mod tests { #[test] fn multiple_insertion() { use crate::user_input::UserInput; - use bevy_input::keyboard::KeyCode; + use bevy::input::keyboard::KeyCode; use petitset::PetitSet; let mut input_map_1 = InputMap::::default(); @@ -451,7 +473,7 @@ mod tests { #[test] fn chord_singleton_coercion() { use crate::input_map::UserInput; - use bevy_input::keyboard::KeyCode; + use bevy::input::keyboard::KeyCode; // Single items in a chord should be coerced to a singleton let mut input_map_1 = InputMap::::default(); @@ -465,7 +487,7 @@ mod tests { #[test] fn input_clearing() { - use bevy_input::keyboard::KeyCode; + use bevy::input::keyboard::KeyCode; let mut input_map = InputMap::::default(); input_map.insert(KeyCode::Space, Action::Run); @@ -491,7 +513,7 @@ mod tests { #[test] fn merging() { - use bevy_input::{gamepad::GamepadButtonType, keyboard::KeyCode}; + use bevy::input::{gamepad::GamepadButtonType, keyboard::KeyCode}; let mut input_map = InputMap::default(); let mut default_keyboard_map = InputMap::default(); @@ -512,164 +534,15 @@ mod tests { #[test] fn gamepad_swapping() { - use bevy_input::gamepad::Gamepad; + use bevy::input::gamepad::Gamepad; let mut input_map = InputMap::::default(); assert_eq!(input_map.gamepad(), None); - input_map.set_gamepad(Gamepad(0)); - assert_eq!(input_map.gamepad(), Some(Gamepad(0))); + input_map.set_gamepad(Gamepad { id: 0 }); + assert_eq!(input_map.gamepad(), Some(Gamepad { id: 0 })); input_map.clear_gamepad(); assert_eq!(input_map.gamepad(), None); } - - #[test] - fn mock_inputs() { - use crate::input_map::InputButton; - use crate::user_input::InputStreams; - use bevy::prelude::*; - - // Setting up the input map - let mut input_map = InputMap::::default(); - input_map.set_gamepad(Gamepad(42)); - - // Gamepad - input_map.insert(GamepadButtonType::South, Action::Run); - input_map.insert_chord( - [GamepadButtonType::North, GamepadButtonType::South], - Action::Jump, - ); - - // Keyboard - input_map.insert(KeyCode::LShift, Action::Run); - input_map.insert(KeyCode::LShift, Action::Hide); - - // Mouse - input_map.insert(MouseButton::Left, Action::Run); - input_map.insert(MouseButton::Other(42), Action::Jump); - - // Cross-device chords - input_map.insert_chord( - [ - InputButton::Keyboard(KeyCode::LControl), - InputButton::Mouse(MouseButton::Left), - ], - Action::Hide, - ); - - // Input streams - let mut gamepad_input_stream = Input::::default(); - let mut keyboard_input_stream = Input::::default(); - let mut mouse_input_stream = Input::::default(); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - // With no inputs, nothing should be detected - for action in Action::variants() { - assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll)); - } - - // Pressing the wrong gamepad - gamepad_input_stream.press(GamepadButton(Gamepad(0), GamepadButtonType::South)); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - for action in Action::variants() { - assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll)); - } - - // Pressing the correct gamepad - gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::South)); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll)); - assert!(!input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll)); - - // Chord - gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::South)); - gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::North)); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll)); - assert!(input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll)); - - // Clearing inputs - gamepad_input_stream = Input::::default(); - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - for action in Action::variants() { - assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll)); - } - - // Keyboard - keyboard_input_stream.press(KeyCode::LShift); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll)); - assert!(input_map.pressed(Action::Hide, &input_streams, ClashStrategy::PressAll)); - - keyboard_input_stream = Input::::default(); - - // Mouse - mouse_input_stream.press(MouseButton::Left); - mouse_input_stream.press(MouseButton::Other(42)); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll)); - assert!(input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll)); - - mouse_input_stream = Input::::default(); - - // Cross-device chording - keyboard_input_stream.press(KeyCode::LControl); - mouse_input_stream.press(MouseButton::Left); - - let input_streams = InputStreams { - gamepad: Some(&gamepad_input_stream), - keyboard: Some(&keyboard_input_stream), - mouse: Some(&mouse_input_stream), - associated_gamepad: Some(Gamepad(42)), - }; - - assert!(input_map.pressed(Action::Hide, &input_streams, ClashStrategy::PressAll)); - } } diff --git a/src/input_mocking.rs b/src/input_mocking.rs index 2d79d32e..54ebd8f4 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -1,22 +1,37 @@ //! Helpful utilities for testing input management by sending mock input events - -use crate::user_input::{InputStreams, MutableInputStreams, UserInput}; -use bevy_app::App; -use bevy_ecs::event::Events; -use bevy_ecs::system::{Res, ResMut, SystemState}; -use bevy_ecs::world::World; +//! +//! The [`MockInput`] trait contains methods with the same API that operate at three levels: +//! [`App`], [`World`] and [`MutableInputStreams`], each passing down the supplied arguments to the next. +//! +//! Inputs are provided in the convenient, high-level [`UserInput`] form. +//! These are then parsed down to their [`UserInput::raw_inputs()`], +//! which are then sent as [`bevy::input`] events of the appropriate types. + +use crate::axislike::{AxisType, MouseMotionAxisType, MouseWheelAxisType}; +use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::input_streams::{InputStreams, MutableInputStreams}; +use crate::user_input::UserInput; + +use bevy::app::App; +use bevy::ecs::event::Events; +use bevy::ecs::system::{ResMut, SystemState}; +use bevy::ecs::world::World; #[cfg(feature = "ui")] -use bevy_ecs::{component::Component, query::With, system::Query}; -use bevy_input::{ - gamepad::{Gamepad, GamepadButton, GamepadEvent, Gamepads}, +use bevy::ecs::{component::Component, query::With, system::Query}; +use bevy::input::gamepad::GamepadEventRaw; +use bevy::input::mouse::MouseScrollUnit; +use bevy::input::ButtonState; +use bevy::input::{ + gamepad::{Gamepad, GamepadButton, GamepadEvent, GamepadEventType}, keyboard::{KeyCode, KeyboardInput}, - mouse::{MouseButton, MouseButtonInput, MouseWheel}, + mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, touch::{TouchInput, Touches}, Input, }; +use bevy::math::Vec2; #[cfg(feature = "ui")] -use bevy_ui::Interaction; -use bevy_window::CursorMoved; +use bevy::ui::Interaction; +use bevy::window::CursorMoved; /// Send fake input events for testing purposes /// @@ -26,19 +41,25 @@ use bevy_window::CursorMoved; /// # Examples /// ```rust /// use bevy::prelude::*; -/// use leafwing_input_manager::MockInput; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::input_mocking::MockInput; /// -/// let mut world = World::new(); +/// // Remember to add InputPlugin so the resources will be there! +/// let mut app = App::new(); +/// app.add_plugin(InputPlugin); /// /// // Pay respects! -/// world.send_input(KeyCode::F); +/// app.send_input(KeyCode::F); +/// app.update(); /// ``` /// /// ```rust /// use bevy::prelude::*; -/// use leafwing_input_manager::{MockInput, user_input::UserInput}; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::{input_mocking::MockInput, user_input::UserInput}; /// /// let mut app = App::new(); +/// app.add_plugin(InputPlugin); /// /// // Send inputs one at a time /// let B_E_V_Y = [KeyCode::B, KeyCode::E, KeyCode::V, KeyCode::Y]; @@ -49,22 +70,32 @@ use bevy_window::CursorMoved; /// /// // Or use chords! /// app.send_input(UserInput::chord(B_E_V_Y)); +/// app.update(); /// ``` pub trait MockInput { /// Send the specified `user_input` directly /// + /// These are sent as the raw input events, and do not set the value of [`Input`] or [`Axis`] directly. /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called. /// + /// To send specific values for axislike inputs, set their `value` field. + /// /// Gamepad input will be sent by the first registed controller found. /// If none are found, gamepad input will be silently skipped. + /// + /// # Warning + /// + /// You *must* call `app.update()` at least once after sending input + /// with `InputPlugin` included in your plugin set + /// for the raw input events to be processed into [`Input`] and [`Axis`] data. fn send_input(&mut self, input: impl Into); /// Send the specified `user_input` directly, using the specified gamepad /// /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called. /// - /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating inputs from - fn send_input_to_gamepad(&mut self, input: impl Into, gamepad: Option); + /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating. + fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option); /// Releases the specified `user_input` directly /// @@ -74,24 +105,20 @@ pub trait MockInput { /// Releases the specified `user_input` directly, using the specified gamepad /// - /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating inputs from - fn release_input_for_gamepad(&mut self, input: impl Into, gamepad: Option); + /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating. + fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option); /// Is the provided `user_input` pressed? /// /// This method is intended as a convenience for testing; check the [`Input`] resource directly, /// or use an [`InputMap`](crate::input_map::InputMap) in real code. - fn pressed(&mut self, input: impl Into) -> bool; + fn pressed(&self, input: impl Into) -> bool; /// Is the provided `user_input` pressed for the provided [`Gamepad`]? /// /// This method is intended as a convenience for testing; check the [`Input`] resource directly, /// or use an [`InputMap`](crate::input_map::InputMap) in real code. - fn pressed_for_gamepad( - &mut self, - input: impl Into, - gamepad: Option, - ) -> bool; + fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool; /// Clears all user input streams, resetting them to their default state /// @@ -102,171 +129,247 @@ pub trait MockInput { /// as well as any [`Interaction`] components and all input [`Events`]. fn reset_inputs(&mut self); - /// Presses all `bevy_ui` buttons with the matching `Marker` component + /// Presses all `bevy::ui` buttons with the matching `Marker` component /// /// Changes their [`Interaction`] component to [`Interaction::Clicked`] #[cfg(feature = "ui")] fn click_button(&mut self); - /// Hovers over all `bevy_ui` buttons with the matching `Marker` component + /// Hovers over all `bevy::ui` buttons with the matching `Marker` component /// /// Changes their [`Interaction`] component to [`Interaction::Clicked`] #[cfg(feature = "ui")] fn hover_button(&mut self); } -impl<'a> MutableInputStreams<'a> { - /// Send the specified `user_input` directly, using the specified gamepad - /// - /// Called by the methods of [`MockInput`]. - pub fn send_user_input(&mut self, input: impl Into) { +impl MockInput for MutableInputStreams<'_> { + fn send_input(&mut self, input: impl Into) { + self.send_input_as_gamepad(input, self.guess_gamepad()); + } + + fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { let input_to_send: UserInput = input.into(); - let (gamepad_buttons, keyboard_buttons, mouse_buttons) = input_to_send.raw_inputs(); + // Extract the raw inputs + let raw_inputs = input_to_send.raw_inputs(); + + // Keyboard buttons + for button in raw_inputs.keycodes { + self.keyboard_events.send(KeyboardInput { + scan_code: u32::MAX, + key_code: Some(button), + state: ButtonState::Pressed, + }); + } - if let Some(ref mut gamepad_input) = self.gamepad { - for button in gamepad_buttons { - if let Some(associated_gamepad) = self.associated_gamepad { - let gamepad_button = GamepadButton(associated_gamepad, button); - gamepad_input.press(gamepad_button); - } - } + // Mouse buttons + for button in raw_inputs.mouse_buttons { + self.mouse_button_events.send(MouseButtonInput { + button, + state: ButtonState::Pressed, + }); } - if let Some(ref mut keyboard_input) = self.keyboard { - for button in keyboard_buttons { - keyboard_input.press(button); + // Discrete mouse wheel events + for mouse_wheel_direction in raw_inputs.mouse_wheel { + match mouse_wheel_direction { + MouseWheelDirection::Left => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: -1.0, + y: 0.0, + }), + MouseWheelDirection::Right => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: 1.0, + y: 0.0, + }), + MouseWheelDirection::Up => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: 0.0, + y: 1.0, + }), + MouseWheelDirection::Down => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: 0.0, + y: -1.0, + }), } } - if let Some(ref mut mouse_input) = self.mouse { - for button in mouse_buttons { - mouse_input.press(button); + // Discrete mouse motion event + for mouse_motion_direction in raw_inputs.mouse_motion { + match mouse_motion_direction { + MouseMotionDirection::Up => self.mouse_motion.send(MouseMotion { + delta: Vec2 { x: 0.0, y: 1.0 }, + }), + MouseMotionDirection::Down => self.mouse_motion.send(MouseMotion { + delta: Vec2 { x: 0.0, y: -1.0 }, + }), + MouseMotionDirection::Right => self.mouse_motion.send(MouseMotion { + delta: Vec2 { x: 1.0, y: 0.0 }, + }), + MouseMotionDirection::Left => self.mouse_motion.send(MouseMotion { + delta: Vec2 { x: -1.0, y: 0.0 }, + }), } } - } - - /// Releases the specified `user_input` directly, using the specified gamepad - /// - /// Called by the methods of [`MockInput`]. - pub fn release_user_input(&mut self, input: impl Into) { - let input_to_release: UserInput = input.into(); - let (gamepad_buttons, keyboard_buttons, mouse_buttons) = input_to_release.raw_inputs(); - if let Some(ref mut gamepad_input) = self.gamepad { - for button in gamepad_buttons { - if let Some(associated_gamepad) = self.associated_gamepad { - let gamepad_button = GamepadButton(associated_gamepad, button); - gamepad_input.release(gamepad_button); - } + // Gamepad buttons + for button_type in raw_inputs.gamepad_buttons { + if let Some(gamepad) = gamepad { + self.gamepad_events.send(GamepadEventRaw { + gamepad, + event_type: GamepadEventType::ButtonChanged(button_type, 1.0), + }); } } - if let Some(ref mut keyboard_input) = self.keyboard { - for button in keyboard_buttons { - keyboard_input.release(button); + // Axis data + for (outer_axis_type, maybe_position_data) in raw_inputs.axis_data { + if let Some(position_data) = maybe_position_data { + match outer_axis_type { + AxisType::Gamepad(axis_type) => { + if let Some(gamepad) = gamepad { + self.gamepad_events.send(GamepadEventRaw { + gamepad, + event_type: GamepadEventType::AxisChanged(axis_type, position_data), + }); + } + } + AxisType::MouseWheel(axis_type) => { + match axis_type { + // FIXME: MouseScrollUnit is not recorded and is always assumed to be Pixel + MouseWheelAxisType::X => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: position_data, + y: 0.0, + }), + MouseWheelAxisType::Y => self.mouse_wheel.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: 0.0, + y: position_data, + }), + } + } + AxisType::MouseMotion(axis_type) => match axis_type { + MouseMotionAxisType::X => self.mouse_motion.send(MouseMotion { + delta: Vec2 { + x: position_data, + y: 0.0, + }, + }), + MouseMotionAxisType::Y => self.mouse_motion.send(MouseMotion { + delta: Vec2 { + x: 0.0, + y: position_data, + }, + }), + }, + } } } + } + + fn release_input(&mut self, input: impl Into) { + self.release_input_as_gamepad(input, self.guess_gamepad()) + } + + fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + // Releasing axis-like inputs deliberately has no effect; it's unclear what this would do - if let Some(ref mut mouse_input) = self.mouse { - for button in mouse_buttons { - mouse_input.release(button); + let input_to_release: UserInput = input.into(); + let raw_inputs = input_to_release.raw_inputs(); + + for button_type in raw_inputs.gamepad_buttons { + if let Some(gamepad) = gamepad { + self.gamepad_events.send(GamepadEventRaw { + gamepad, + event_type: GamepadEventType::ButtonChanged(button_type, 1.0), + }); } } - } -} -impl MockInput for World { - fn send_input(&mut self, input: impl Into) { - let gamepad = if let Some(gamepads) = self.get_resource::() { - gamepads.iter().next().copied() - } else { - None - }; + for button in raw_inputs.keycodes { + self.keyboard_events.send(KeyboardInput { + scan_code: u32::MAX, + key_code: Some(button), + state: ButtonState::Released, + }); + } - self.send_input_to_gamepad(input, gamepad); + for button in raw_inputs.mouse_buttons { + self.mouse_button_events.send(MouseButtonInput { + button, + state: ButtonState::Released, + }); + } } - fn send_input_to_gamepad(&mut self, input: impl Into, gamepad: Option) { - // You can make a system with this type signature if you'd like to mock user input - // in a non-exclusive system - let mut input_system_state: SystemState<( - Option>>, - Option>>, - Option>>, - )> = SystemState::new(self); + fn pressed(&self, input: impl Into) -> bool { + let input_streams: InputStreams = self.into(); + input_streams.input_pressed(&input.into()) + } - let (mut maybe_gamepad, mut maybe_keyboard, mut maybe_mouse) = - input_system_state.get_mut(self); + fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { + let mut input_streams: InputStreams = self.into(); + input_streams.associated_gamepad = gamepad; - let mut mutable_input_streams = MutableInputStreams { - gamepad: maybe_gamepad.as_deref_mut(), - keyboard: maybe_keyboard.as_deref_mut(), - mouse: maybe_mouse.as_deref_mut(), - associated_gamepad: gamepad, - }; + input_streams.input_pressed(&input.into()) + } - mutable_input_streams.send_user_input(input); + fn reset_inputs(&mut self) { + // WARNING: this *must* be updated when MutableInputStreams's fields change + // Note that we deliberately are not resetting either Gamepads or associated_gamepad + // as they are not actually input data + *self.gamepad_buttons = Default::default(); + *self.gamepad_axes = Default::default(); + *self.keycode = Default::default(); + *self.mouse_button = Default::default(); + *self.mouse_wheel = Default::default(); + *self.mouse_motion = Default::default(); } - fn release_input(&mut self, input: impl Into) { - let gamepad = if let Some(gamepads) = self.get_resource::() { - gamepads.iter().next().copied() - } else { - None - }; + #[cfg(feature = "ui")] + fn click_button(&mut self) { + panic!("Cannot use bevy_ui input mocking from `MutableInputStreams`, use an `App` or `World` instead.") + } - self.release_input_for_gamepad(input, gamepad); + #[cfg(feature = "ui")] + fn hover_button(&mut self) { + panic!("Cannot use bevy_ui input mocking from `MutableInputStreams`, use an `App` or `World` instead.") } +} - fn release_input_for_gamepad(&mut self, input: impl Into, gamepad: Option) { - let mut input_system_state: SystemState<( - Option>>, - Option>>, - Option>>, - )> = SystemState::new(self); +impl MockInput for World { + fn send_input(&mut self, input: impl Into) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, None); - let (mut maybe_gamepad, mut maybe_keyboard, mut maybe_mouse) = - input_system_state.get_mut(self); + mutable_input_streams.send_input(input); + } - let mut mutable_input_streams = MutableInputStreams { - gamepad: maybe_gamepad.as_deref_mut(), - keyboard: maybe_keyboard.as_deref_mut(), - mouse: maybe_mouse.as_deref_mut(), - associated_gamepad: gamepad, - }; + fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad); - mutable_input_streams.release_user_input(input); + mutable_input_streams.send_input_as_gamepad(input, gamepad); } - fn pressed(&mut self, input: impl Into) -> bool { - let gamepad = if let Some(gamepads) = self.get_resource::() { - gamepads.iter().next().copied() - } else { - None - }; + fn release_input(&mut self, input: impl Into) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, None); - self.pressed_for_gamepad(input, gamepad) + mutable_input_streams.release_input(input); } - fn pressed_for_gamepad( - &mut self, - input: impl Into, - gamepad: Option, - ) -> bool { - let mut input_system_state: SystemState<( - Option>>, - Option>>, - Option>>, - )> = SystemState::new(self); + fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad); + + mutable_input_streams.release_input_as_gamepad(input, gamepad); + } - let (maybe_gamepad, maybe_keyboard, maybe_mouse) = input_system_state.get(self); + fn pressed(&self, input: impl Into) -> bool { + self.pressed_for_gamepad(input, None) + } - let input_streams = InputStreams { - gamepad: maybe_gamepad.as_deref(), - keyboard: maybe_keyboard.as_deref(), - mouse: maybe_mouse.as_deref(), - associated_gamepad: gamepad, - }; + fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { + let input_streams = InputStreams::from_world(self, gamepad); input_streams.input_pressed(&input.into()) } @@ -339,27 +442,23 @@ impl MockInput for App { self.world.send_input(input); } - fn send_input_to_gamepad(&mut self, input: impl Into, gamepad: Option) { - self.world.send_input_to_gamepad(input, gamepad); + fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + self.world.send_input_as_gamepad(input, gamepad); } fn release_input(&mut self, input: impl Into) { self.world.release_input(input); } - fn release_input_for_gamepad(&mut self, input: impl Into, gamepad: Option) { - self.world.release_input_for_gamepad(input, gamepad); + fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + self.world.release_input_as_gamepad(input, gamepad); } - fn pressed(&mut self, input: impl Into) -> bool { + fn pressed(&self, input: impl Into) -> bool { self.world.pressed(input) } - fn pressed_for_gamepad( - &mut self, - input: impl Into, - gamepad: Option, - ) -> bool { + fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { self.world.pressed_for_gamepad(input, gamepad) } @@ -380,69 +479,133 @@ impl MockInput for App { #[cfg(test)] mod test { + use crate::input_mocking::MockInput; + use bevy::{input::InputPlugin, prelude::*}; #[test] - fn button_inputs() { - use crate::input_mocking::MockInput; - use bevy::prelude::*; - - let mut world = World::new(); - world.insert_resource(Input::::default()); - world.insert_resource(Input::::default()); - world.insert_resource(Input::::default()); - - // BLOCKED: cannot use the less artifical APIs due to - // https://github.com/bevyengine/bevy/issues/3808 - let gamepad = Some(Gamepad(0)); + fn ordinary_button_inputs() { + let mut app = App::new(); + app.add_plugin(InputPlugin); // Test that buttons are unpressed by default - assert!(!world.pressed(KeyCode::Space)); - assert!(!world.pressed(MouseButton::Right)); - assert!(!world.pressed_for_gamepad(GamepadButtonType::North, gamepad)); + assert!(!app.pressed(KeyCode::Space)); + assert!(!app.pressed(MouseButton::Right)); // Send inputs - world.send_input(KeyCode::Space); - world.send_input(MouseButton::Right); - world.send_input_to_gamepad(GamepadButtonType::North, gamepad); + app.send_input(KeyCode::Space); + app.send_input(MouseButton::Right); + app.update(); // Verify that checking the resource value directly works - let keyboard_input: &Input = world.resource(); + let keyboard_input: &Input = app.world.resource(); assert!(keyboard_input.pressed(KeyCode::Space)); // Test the convenient .pressed API - assert!(world.pressed(KeyCode::Space)); - assert!(world.pressed(MouseButton::Right)); - assert!(world.pressed_for_gamepad(GamepadButtonType::North, gamepad)); + assert!(app.pressed(KeyCode::Space)); + assert!(app.pressed(MouseButton::Right)); // Test that resetting inputs works - world.reset_inputs(); + app.reset_inputs(); + app.update(); - assert!(!world.pressed(KeyCode::Space)); - assert!(!world.pressed(MouseButton::Right)); - assert!(!world.pressed_for_gamepad(GamepadButtonType::North, gamepad)); + assert!(!app.pressed(KeyCode::Space)); + assert!(!app.pressed(MouseButton::Right)); + } + + #[test] + fn explicit_gamepad_button_inputs() { + let mut app = App::new(); + app.add_plugin(InputPlugin); + + let gamepad = Gamepad { id: 0 }; + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEvent { + gamepad, + event_type: GamepadEventType::Connected, + }); + app.update(); + + // Test that buttons are unpressed by default + assert!(!app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); + + // Send inputs + app.send_input_as_gamepad(GamepadButtonType::North, Some(gamepad)); + app.update(); + + // Checking the old-fashioned way + // FIXME: put this in a gamepad_button.rs integration test. + let gamepad_input = app.world.resource::>(); + assert!(gamepad_input.pressed(GamepadButton { + gamepad, + button_type: GamepadButtonType::North, + })); + + // Test the convenient .pressed API + assert!(app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); + + // Test that resetting inputs works + app.reset_inputs(); + app.update(); + + assert!(!app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); + } + + #[test] + fn implicit_gamepad_button_inputs() { + let mut app = App::new(); + app.add_plugin(InputPlugin); + + let gamepad = Gamepad { id: 0 }; + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEvent { + gamepad, + event_type: GamepadEventType::Connected, + }); + app.update(); + + // Test that buttons are unpressed by default + assert!(!app.pressed(GamepadButtonType::North)); + + // Send inputs + app.send_input(GamepadButtonType::North); + app.update(); + + // Test the convenient .pressed API + assert!(app.pressed(GamepadButtonType::North)); + + // Test that resetting inputs works + app.reset_inputs(); + app.update(); + + assert!(!app.pressed(GamepadButtonType::North)); } #[test] #[cfg(feature = "ui")] fn ui_inputs() { - use crate::input_mocking::MockInput; - use bevy_ecs::prelude::*; - use bevy_ui::Interaction; + use bevy::ecs::prelude::*; + use bevy::ui::Interaction; #[derive(Component)] struct ButtonMarker; - let mut world = World::new(); + let mut app = App::new(); + app.add_plugin(InputPlugin); + // Marked button - world.spawn().insert(Interaction::None).insert(ButtonMarker); + app.world + .spawn() + .insert(Interaction::None) + .insert(ButtonMarker); // Unmarked button - world.spawn().insert(Interaction::None); + app.world.spawn().insert(Interaction::None); // Click the button - world.click_button::(); + app.world.click_button::(); + app.update(); - let mut interaction_query = world.query::<(&Interaction, Option<&ButtonMarker>)>(); - for (interaction, maybe_marker) in interaction_query.iter(&world) { + let mut interaction_query = app.world.query::<(&Interaction, Option<&ButtonMarker>)>(); + for (interaction, maybe_marker) in interaction_query.iter(&app.world) { match maybe_marker { Some(_) => assert_eq!(*interaction, Interaction::Clicked), None => assert_eq!(*interaction, Interaction::None), @@ -450,18 +613,19 @@ mod test { } // Reset inputs - world.reset_inputs(); + app.world.reset_inputs(); - let mut interaction_query = world.query::<&Interaction>(); - for interaction in interaction_query.iter(&world) { + let mut interaction_query = app.world.query::<&Interaction>(); + for interaction in interaction_query.iter(&app.world) { assert_eq!(*interaction, Interaction::None) } // Hover over the button - world.hover_button::(); + app.hover_button::(); + app.update(); - let mut interaction_query = world.query::<(&Interaction, Option<&ButtonMarker>)>(); - for (interaction, maybe_marker) in interaction_query.iter(&world) { + let mut interaction_query = app.world.query::<(&Interaction, Option<&ButtonMarker>)>(); + for (interaction, maybe_marker) in interaction_query.iter(&app.world) { match maybe_marker { Some(_) => assert_eq!(*interaction, Interaction::Hovered), None => assert_eq!(*interaction, Interaction::None), @@ -469,10 +633,10 @@ mod test { } // Reset inputs - world.reset_inputs(); + app.world.reset_inputs(); - let mut interaction_query = world.query::<&Interaction>(); - for interaction in interaction_query.iter(&world) { + let mut interaction_query = app.world.query::<&Interaction>(); + for interaction in interaction_query.iter(&app.world) { assert_eq!(*interaction, Interaction::None) } } diff --git a/src/input_streams.rs b/src/input_streams.rs new file mode 100644 index 00000000..8b2c1131 --- /dev/null +++ b/src/input_streams.rs @@ -0,0 +1,481 @@ +//! Unified input streams for working with [`bevy::input`] data. + +use bevy::input::{ + gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadEventRaw, Gamepads}, + keyboard::{KeyCode, KeyboardInput}, + mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, + Axis, Input, +}; +use petitset::PetitSet; + +use bevy::ecs::prelude::{Events, ResMut, World}; +use bevy::ecs::system::SystemState; + +use crate::axislike::{ + AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualDPad, +}; +use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::user_input::{InputKind, UserInput}; + +/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). +/// +/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. +#[derive(Debug, Clone)] +pub struct InputStreams<'a> { + /// A [`GamepadButton`] [`Input`] stream + pub gamepad_buttons: &'a Input, + /// A [`GamepadButton`] [`Axis`] stream + pub gamepad_button_axes: &'a Axis, + /// A [`GamepadAxis`] [`Axis`] stream + pub gamepad_axes: &'a Axis, + /// A list of registered gamepads + pub gamepads: &'a Gamepads, + /// A [`KeyCode`] [`Input`] stream + pub keycode: &'a Input, + /// A [`MouseButton`] [`Input`] stream + pub mouse_button: &'a Input, + /// A [`MouseWheel`] event stream + pub mouse_wheel: &'a Events, + /// A [`MouseMotion`] event stream + pub mouse_motion: &'a Events, + /// The [`Gamepad`] that this struct will detect inputs from + pub associated_gamepad: Option, +} + +// Constructors +impl<'a> InputStreams<'a> { + /// Construct an [`InputStreams`] from a [`World`] + pub fn from_world(world: &'a World, gamepad: Option) -> Self { + let gamepad_buttons = world.resource::>(); + let gamepad_button_axes = world.resource::>(); + let gamepad_axes = world.resource::>(); + let gamepads = world.resource::(); + let keyboard = world.resource::>(); + let mouse = world.resource::>(); + let mouse_wheel = world.resource::>(); + let mouse_motion = world.resource::>(); + + InputStreams { + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + keycode: keyboard, + mouse_button: mouse, + mouse_wheel, + mouse_motion, + associated_gamepad: gamepad, + } + } +} + +// Input checking +impl<'a> InputStreams<'a> { + /// Guess which registered [`Gamepad`] should be used. + /// + /// If an associated gamepad is set, use that. + /// Otherwise use the first registered gamepad, if any. + pub fn guess_gamepad(&self) -> Option { + match self.associated_gamepad { + Some(gamepad) => Some(gamepad), + None => self.gamepads.iter().next().copied(), + } + } + + /// Is the `input` matched by the [`InputStreams`]? + pub fn input_pressed(&self, input: &UserInput) -> bool { + match input { + UserInput::Single(button) => self.button_pressed(*button), + UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + for button in [up, down, left, right] { + if self.button_pressed(*button) { + return true; + } + } + false + } + } + } + + /// Is at least one of the `inputs` pressed? + #[must_use] + pub fn any_pressed(&self, inputs: &PetitSet) -> bool { + for input in inputs.iter() { + if self.input_pressed(input) { + return true; + } + } + // If none of the inputs matched, return false + false + } + + /// Is the `button` pressed? + #[must_use] + pub fn button_pressed(&self, button: InputKind) -> bool { + match button { + InputKind::DualAxis(_) => { + let axis_pair = self.input_axis_pair(&UserInput::Single(button)).unwrap(); + + axis_pair.length() != 0.0 + } + InputKind::SingleAxis(_) => { + let value = self.input_value(&UserInput::Single(button)); + + value != 0.0 + } + InputKind::GamepadButton(gamepad_button) => { + if let Some(gamepad) = self.guess_gamepad() { + self.gamepad_buttons.pressed(GamepadButton { + gamepad, + button_type: gamepad_button, + }) + } else { + false + } + } + InputKind::Keyboard(keycode) => self.keycode.pressed(keycode), + InputKind::Mouse(mouse_button) => self.mouse_button.pressed(mouse_button), + InputKind::MouseWheel(mouse_wheel_direction) => { + let mut total_mouse_wheel_movement = 0.0; + + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_wheel.get_reader(); + + // PERF: this summing is computed for every individual input + // This should probably be computed once, and then cached / read + // Fix upstream! + for mouse_wheel_event in event_reader.iter(self.mouse_wheel) { + total_mouse_wheel_movement += match mouse_wheel_direction { + MouseWheelDirection::Up | MouseWheelDirection::Down => mouse_wheel_event.y, + MouseWheelDirection::Left | MouseWheelDirection::Right => { + mouse_wheel_event.x + } + } + } + + match mouse_wheel_direction { + MouseWheelDirection::Up | MouseWheelDirection::Right => { + total_mouse_wheel_movement > 0.0 + } + MouseWheelDirection::Down | MouseWheelDirection::Left => { + total_mouse_wheel_movement < 0.0 + } + } + } + // CLEANUP: refactor to share code with MouseWheel + InputKind::MouseMotion(mouse_motion_direction) => { + let mut total_mouse_movement = 0.0; + + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_motion.get_reader(); + + for mouse_motion_event in event_reader.iter(self.mouse_motion) { + total_mouse_movement += match mouse_motion_direction { + MouseMotionDirection::Up | MouseMotionDirection::Down => { + mouse_motion_event.delta.y + } + MouseMotionDirection::Left | MouseMotionDirection::Right => { + mouse_motion_event.delta.x + } + } + } + + match mouse_motion_direction { + MouseMotionDirection::Up | MouseMotionDirection::Right => { + total_mouse_movement > 0.0 + } + MouseMotionDirection::Down | MouseMotionDirection::Left => { + total_mouse_movement < 0.0 + } + } + } + } + } + + /// Are all of the `buttons` pressed? + #[must_use] + pub fn all_buttons_pressed(&self, buttons: &PetitSet) -> bool { + for &button in buttons.iter() { + // If any of the appropriate inputs failed to match, the action is considered pressed + if !self.button_pressed(button) { + return false; + } + } + // If none of the inputs failed to match, return true + true + } + + /// Get the "value" of the input. + /// + /// For binary inputs such as buttons, this will always be either `0.0` or `1.0`. For analog + /// inputs such as axes, this will be the axis value. + /// + /// [`UserInput::Chord`] inputs are also considered binary and will return `0.0` or `1.0` based + /// on whether the chord has been pressed. + /// + /// # Warning + /// + /// If you need to ensure that this value is always in the range `[-1., 1.]`, + /// be sure to clamp the reutrned data. + pub fn input_value(&self, input: &UserInput) -> f32 { + let use_button_value = || -> f32 { + if self.input_pressed(input) { + 1.0 + } else { + 0.0 + } + }; + + // Helper that takes the value returned by an axis and returns 0.0 if it is not within the + // triggering range. + let value_in_axis_range = |axis: &SingleAxis, value: f32| -> f32 { + if value >= axis.negative_low && value <= axis.positive_low { + 0.0 + } else { + value + } + }; + + match input { + UserInput::Single(InputKind::SingleAxis(single_axis)) => { + match single_axis.axis_type { + AxisType::Gamepad(axis_type) => { + if let Some(gamepad) = self.guess_gamepad() { + self.gamepad_axes + .get(GamepadAxis { gamepad, axis_type }) + .unwrap_or_default() + } else { + 0.0 + } + } + AxisType::MouseWheel(axis_type) => { + let mut total_mouse_wheel_movement = 0.0; + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_wheel.get_reader(); + + for mouse_wheel_event in event_reader.iter(self.mouse_wheel) { + total_mouse_wheel_movement += match axis_type { + MouseWheelAxisType::X => mouse_wheel_event.x, + MouseWheelAxisType::Y => mouse_wheel_event.y, + } + } + value_in_axis_range(single_axis, total_mouse_wheel_movement) + } + // CLEANUP: deduplicate code with MouseWheel + AxisType::MouseMotion(axis_type) => { + let mut total_mouse_motion_movement = 0.0; + // FIXME: verify that this works and doesn't double count events + let mut event_reader = self.mouse_motion.get_reader(); + + for mouse_wheel_event in event_reader.iter(self.mouse_motion) { + total_mouse_motion_movement += match axis_type { + MouseMotionAxisType::X => mouse_wheel_event.delta.x, + MouseMotionAxisType::Y => mouse_wheel_event.delta.y, + } + } + value_in_axis_range(single_axis, total_mouse_motion_movement) + } + } + } + UserInput::Single(InputKind::DualAxis(_)) => { + self.input_axis_pair(input).unwrap_or_default().length() + } + UserInput::VirtualDPad { .. } => { + self.input_axis_pair(input).unwrap_or_default().length() + } + // This is required because upstream bevy::input still waffles about whether triggers are buttons or axes + UserInput::Single(InputKind::GamepadButton(button_type)) => { + if let Some(gamepad) = self.guess_gamepad() { + // Get the value from the registered gamepad + self.gamepad_button_axes + .get(GamepadButton { + gamepad, + button_type: *button_type, + }) + .unwrap_or_else(use_button_value) + } else { + 0.0 + } + } + _ => use_button_value(), + } + } + + /// Get the axis pair associated to the user input. + /// + /// If `input` is not a [`DualAxis`] or [`VirtualDPad`], returns [`None`]. + /// + /// See [`ActionState::action_axis_pair()`] for usage. + /// + /// # Warning + /// + /// If you need to ensure that this value is always in the range `[-1., 1.]`, + /// be sure to clamp the returned data. + pub fn input_axis_pair(&self, input: &UserInput) -> Option { + match input { + UserInput::Single(InputKind::DualAxis(dual_axis)) => { + let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x))); + let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y))); + + if x > dual_axis.x.positive_low + || x < dual_axis.x.negative_low + || y > dual_axis.y.positive_low + || y < dual_axis.y.negative_low + { + Some(DualAxisData::new(x, y)) + } else { + Some(DualAxisData::new(0.0, 0.0)) + } + } + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + let x = self.input_value(&UserInput::Single(*right)).abs() + - self.input_value(&UserInput::Single(*left)).abs(); + let y = self.input_value(&UserInput::Single(*up)).abs() + - self.input_value(&UserInput::Single(*down)).abs(); + Some(DualAxisData::new(x, y)) + } + _ => None, + } + } +} + +/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs. +/// +/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. +// WARNING: If you update the fields of this type, you must also remember to update `InputMocking::reset_inputs`. +#[derive(Debug)] +pub struct MutableInputStreams<'a> { + /// A [`GamepadButton`] [`Input`] stream + pub gamepad_buttons: &'a mut Input, + /// A [`GamepadButton`] [`Axis`] stream + pub gamepad_button_axes: &'a mut Axis, + /// A [`GamepadAxis`] [`Axis`] stream + pub gamepad_axes: &'a mut Axis, + /// A list of registered [`Gamepads`] + pub gamepads: &'a mut Gamepads, + /// Events used for mocking gamepad-related inputs + pub gamepad_events: &'a mut Events, + + /// A [`KeyCode`] [`Input`] stream + pub keycode: &'a mut Input, + /// Events used for mocking keyboard-related inputs + pub keyboard_events: &'a mut Events, + + /// A [`MouseButton`] [`Input`] stream + pub mouse_button: &'a mut Input, + /// Events used for mocking [`MouseButton`] inputs + pub mouse_button_events: &'a mut Events, + /// A [`MouseWheel`] event stream + pub mouse_wheel: &'a mut Events, + /// A [`MouseMotion`] event stream + pub mouse_motion: &'a mut Events, + + /// The [`Gamepad`] that this struct will detect inputs from + pub associated_gamepad: Option, +} + +impl<'a> MutableInputStreams<'a> { + /// Construct a [`MutableInputStreams`] from the [`World`] + pub fn from_world(world: &'a mut World, gamepad: Option) -> Self { + let mut input_system_state: SystemState<( + ResMut>, + ResMut>, + ResMut>, + ResMut, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + ResMut>, + )> = SystemState::new(world); + + let ( + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + gamepad_events, + keyboard, + keyboard_events, + mouse, + mouse_button_events, + mouse_wheel, + mouse_motion, + ) = input_system_state.get_mut(world); + + MutableInputStreams { + gamepad_buttons: gamepad_buttons.into_inner(), + gamepad_button_axes: gamepad_button_axes.into_inner(), + gamepad_axes: gamepad_axes.into_inner(), + gamepads: gamepads.into_inner(), + gamepad_events: gamepad_events.into_inner(), + keycode: keyboard.into_inner(), + keyboard_events: keyboard_events.into_inner(), + mouse_button: mouse.into_inner(), + mouse_button_events: mouse_button_events.into_inner(), + mouse_wheel: mouse_wheel.into_inner(), + mouse_motion: mouse_motion.into_inner(), + associated_gamepad: gamepad, + } + } + + /// Guess which registered [`Gamepad`] should be used. + /// + /// If an associated gamepad is set, use that. + /// Otherwise use the first registered gamepad, if any. + pub fn guess_gamepad(&self) -> Option { + match self.associated_gamepad { + Some(gamepad) => Some(gamepad), + None => self.gamepads.iter().next().copied(), + } + } +} + +impl<'a> From> for InputStreams<'a> { + fn from(mutable_streams: MutableInputStreams<'a>) -> Self { + InputStreams { + // This absurd-looking &*(foo) pattern convinces the compiler + // that we want a reference to the underlying data with the correct lifetime + gamepad_buttons: &*(mutable_streams.gamepad_buttons), + gamepad_button_axes: &*(mutable_streams.gamepad_button_axes), + gamepad_axes: &*(mutable_streams.gamepad_axes), + gamepads: &*(mutable_streams.gamepads), + keycode: &*(mutable_streams.keycode), + mouse_button: &*(mutable_streams.mouse_button), + mouse_wheel: &*(mutable_streams.mouse_wheel), + mouse_motion: &*(mutable_streams.mouse_motion), + associated_gamepad: mutable_streams.associated_gamepad, + } + } +} + +impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> { + fn from(mutable_streams: &'a MutableInputStreams<'a>) -> Self { + InputStreams { + // This absurd-looking &*(foo) pattern convinces the compiler + // that we want a reference to the underlying data with the correct lifetime + gamepad_buttons: &*(mutable_streams.gamepad_buttons), + gamepad_button_axes: &*(mutable_streams.gamepad_button_axes), + gamepad_axes: &*(mutable_streams.gamepad_axes), + gamepads: &*(mutable_streams.gamepads), + keycode: &*(mutable_streams.keycode), + mouse_button: &*(mutable_streams.mouse_button), + mouse_wheel: &*(mutable_streams.mouse_wheel), + mouse_motion: &*(mutable_streams.mouse_motion), + associated_gamepad: mutable_streams.associated_gamepad, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 372429d2..bcc15211 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,19 +5,18 @@ use crate::action_state::ActionState; use crate::input_map::InputMap; -use bevy_ecs::prelude::*; +use bevy::ecs::prelude::*; use std::marker::PhantomData; pub mod action_state; +pub mod axislike; +pub mod buttonlike; pub mod clashing_inputs; mod display_impl; pub mod errors; pub mod input_map; -mod input_mocking; -// Re-export this at the root level for convenience -pub use input_mocking::MockInput; -pub mod axislike; -pub mod buttonlike; +pub mod input_mocking; +pub mod input_streams; pub mod orientation; pub mod plugin; pub mod systems; @@ -29,8 +28,11 @@ pub use leafwing_input_manager_macros::Actionlike; /// Everything you need to get started pub mod prelude { pub use crate::action_state::{ActionState, ActionStateDriver}; + pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad}; + pub use crate::buttonlike::MouseWheelDirection; pub use crate::clashing_inputs::ClashStrategy; pub use crate::input_map::InputMap; + pub use crate::input_mocking::MockInput; pub use crate::user_input::UserInput; pub use crate::plugin::InputManagerPlugin; diff --git a/src/orientation.rs b/src/orientation.rs index eec5d682..71fd5e98 100644 --- a/src/orientation.rs +++ b/src/orientation.rs @@ -7,8 +7,8 @@ pub use rotation_direction::RotationDirection; mod orientation_trait { use super::{Direction, Rotation, RotationDirection}; - use bevy_math::Quat; - use bevy_transform::components::{GlobalTransform, Transform}; + use bevy::math::Quat; + use bevy::transform::components::{GlobalTransform, Transform}; use core::fmt::Debug; /// A type that can represent a orientation in 2D space @@ -67,7 +67,7 @@ mod orientation_trait { let rotation_to = target_rotation - self_rotation; - if rotation_to <= Rotation::new(1800) { + if rotation_to.deci_degrees == 0 || rotation_to.deci_degrees >= 1800 { RotationDirection::Clockwise } else { RotationDirection::CounterClockwise @@ -97,8 +97,8 @@ mod orientation_trait { *self = target_orientation; } else { let delta_rotation = match self.rotation_direction(target_orientation) { - RotationDirection::Clockwise => max_rotation, - RotationDirection::CounterClockwise => -max_rotation, + RotationDirection::CounterClockwise => max_rotation, + RotationDirection::Clockwise => -max_rotation, }; let current_rotation: Rotation = (*self).into(); let new_rotation: Rotation = current_rotation + delta_rotation; @@ -194,8 +194,8 @@ mod rotation_direction { #[must_use] pub fn sign(self) -> isize { match self { - RotationDirection::Clockwise => 1, - RotationDirection::CounterClockwise => -1, + RotationDirection::Clockwise => -1, + RotationDirection::CounterClockwise => 1, } } @@ -220,43 +220,44 @@ mod rotation_direction { mod rotation { use crate::errors::NearlySingularConversion; - use bevy_ecs::prelude::Component; - use bevy_math::Vec2; + use bevy::ecs::prelude::Component; + use bevy::math::Vec2; use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use derive_more::Display; + use std::f32::consts::TAU; /// A discretized 2-dimensional rotation /// - /// Internally, these are stored in normalized tenths of a degree, and so can be cleanly added and reversed - /// without accumulating error. + /// Internally, these are stored in tenths of a degree, and so can be cleanly added + /// and reversed without accumulating error. /// /// # Example /// ```rust /// use leafwing_input_manager::orientation::{Rotation, Direction, Orientation}; - /// use core::f32::consts::{PI, TAU}; + /// use core::f32::consts::{FRAC_PI_2, PI, TAU}; /// - /// let three_o_clock = Rotation::from_degrees(90.0); - /// let six_o_clock = Rotation::from_radians(PI); - /// let nine_o_clock = Rotation::from_degrees(-90.0); + /// let east = Rotation::from_radians(0.0); + /// let north = Rotation::from_radians(FRAC_PI_2); + /// let west = Rotation::from_radians(PI); /// /// Rotation::default().assert_approx_eq(Rotation::from_radians(0.0)); /// Rotation::default().assert_approx_eq(Rotation::from_radians(TAU)); /// Rotation::default().assert_approx_eq(500.0 * Rotation::from_radians(TAU)); /// - /// (three_o_clock + six_o_clock).assert_approx_eq(nine_o_clock); - /// (nine_o_clock - three_o_clock).assert_approx_eq(six_o_clock); - /// (2.0 * nine_o_clock).assert_approx_eq(six_o_clock); - /// (six_o_clock / 2.0).assert_approx_eq(three_o_clock); + /// (north + north).assert_approx_eq(west); + /// (west - east).assert_approx_eq(west); + /// (2.0 * north).assert_approx_eq(west); + /// (west / 2.0).assert_approx_eq(north); /// - /// six_o_clock.assert_approx_eq(Rotation::SOUTH); + /// north.assert_approx_eq(Rotation::NORTH); /// - /// Direction::from(nine_o_clock).assert_approx_eq(Direction::WEST); + /// Direction::from(west).assert_approx_eq(Direction::WEST); /// ``` #[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Default, Display)] pub struct Rotation { /// Tenths of a degree, measured clockwise from midnight (x=0, y=1) /// - /// 3600 make up a full circle. + /// 3600 make up a full circle pub(crate) deci_degrees: u16, } @@ -290,23 +291,23 @@ mod rotation { pub const FULL_CIRCLE: u16 = 3600; /// The direction that points straight up - pub const NORTH: Rotation = Rotation { deci_degrees: 0 }; + pub const NORTH: Rotation = Rotation { deci_degrees: 900 }; /// The direction that points straight right - pub const EAST: Rotation = Rotation { deci_degrees: 900 }; + pub const EAST: Rotation = Rotation { deci_degrees: 0 }; /// The direction that points straight down - pub const SOUTH: Rotation = Rotation { deci_degrees: 1800 }; + pub const SOUTH: Rotation = Rotation { deci_degrees: 2700 }; /// The direction that points straight left - pub const WEST: Rotation = Rotation { deci_degrees: 2700 }; + pub const WEST: Rotation = Rotation { deci_degrees: 1800 }; /// The direction that points halfway between up and right pub const NORTHEAST: Rotation = Rotation { deci_degrees: 450 }; /// The direction that points halfway between down and right - pub const SOUTHEAST: Rotation = Rotation { deci_degrees: 1350 }; + pub const SOUTHEAST: Rotation = Rotation { deci_degrees: 3150 }; /// The direction that points halfway between down and left pub const SOUTHWEST: Rotation = Rotation { deci_degrees: 2250 }; /// The direction that points halfway between left and up - pub const NORTHWEST: Rotation = Rotation { deci_degrees: 3150 }; + pub const NORTHWEST: Rotation = Rotation { deci_degrees: 1350 }; } // Conversion methods @@ -318,7 +319,7 @@ mod rotation { /// /// # Example /// ```rust - /// use bevy_math::Vec2; + /// use bevy::math::Vec2; /// use leafwing_input_manager::orientation::Rotation; /// /// assert_eq!(Rotation::from_xy(Vec2::new(0.0, 1.0)), Ok(Rotation::NORTH)); @@ -328,7 +329,7 @@ mod rotation { if xy.length_squared() < f32::EPSILON * f32::EPSILON { Err(NearlySingularConversion) } else { - let radians = f32::atan2(xy.x, xy.y); + let radians = f32::atan2(xy.y, xy.x); Ok(Rotation::from_radians(radians)) } } @@ -338,30 +339,29 @@ mod rotation { #[must_use] pub fn into_xy(self) -> Vec2 { let radians = self.into_radians(); - Vec2::new(radians.sin(), radians.cos()) + Vec2::new(radians.cos(), radians.sin()) } - /// Construct a [`Direction`](crate::orientation::Direction) from radians, measured clockwise from midnight + /// Construct a [`Direction`](crate::orientation::Direction) from radians, + /// measured counterclockwise from the positive x axis #[must_use] #[inline] pub fn from_radians(radians: impl Into) -> Rotation { - use std::f32::consts::TAU; - - let normalized_radians: f32 = radians.into().rem_euclid(TAU); + let normalized_radians = radians.into().rem_euclid(TAU); Rotation { - deci_degrees: (normalized_radians * 3600. / TAU) as u16, + deci_degrees: (normalized_radians * (3600. / TAU)) as u16, } } - /// Converts this direction into radians, measured clockwise from midnight + /// Converts this direction into radians, measured counterclockwise from the positive x axis #[inline] #[must_use] pub fn into_radians(self) -> f32 { - self.deci_degrees as f32 * std::f32::consts::TAU / 3600. + self.deci_degrees as f32 * TAU / 3600. } - /// Construct a [`Direction`](crate::orientation::Direction) from degrees, measured clockwise from midnight + /// Construct a [`Direction`](crate::orientation::Direction) from degrees, measured counterclockwise from the positive x axis #[must_use] #[inline] pub fn from_degrees(degrees: impl Into) -> Rotation { @@ -372,7 +372,7 @@ mod rotation { } } - /// Converts this direction into degrees, measured clockwise from midnight + /// Converts this direction into degrees, measured counterclockwise from the positive x axis #[inline] #[must_use] pub fn into_degrees(self) -> f32 { @@ -407,7 +407,7 @@ mod rotation { impl SubAssign for Rotation { fn sub_assign(&mut self, rhs: Self) { // Be sure to avoid overflow when subtracting - if self.deci_degrees > rhs.deci_degrees { + if self.deci_degrees >= rhs.deci_degrees { self.deci_degrees = self.deci_degrees - rhs.deci_degrees; } else { self.deci_degrees = Rotation::FULL_CIRCLE - (rhs.deci_degrees - self.deci_degrees); @@ -454,20 +454,20 @@ mod rotation { } mod direction { - use bevy_ecs::prelude::Component; - use bevy_math::{const_vec2, Vec2, Vec3}; + use bevy::ecs::prelude::Component; + use bevy::math::{Vec2, Vec3}; use core::ops::{Add, Div, Mul, Neg, Sub}; use derive_more::Display; use std::f32::consts::SQRT_2; /// A 2D unit vector that represents a direction /// - /// Its magnitude is always one. + /// Its magnitude is always `1.0`. /// /// # Example /// ```rust /// use leafwing_input_manager::orientation::Direction; - /// use bevy_math::Vec2; + /// use bevy::math::Vec2; /// /// assert_eq!(Direction::NORTH.unit_vector(), Vec2::new(0.0, 1.0)); /// assert_eq!(Direction::try_from(Vec2::ONE), Ok(Direction::NORTHEAST)); @@ -481,35 +481,31 @@ mod direction { } impl Default for Direction { - /// [`Direction::NORTH`] is the default direction, + /// [`Direction::EAST`] is the default direction, /// as it is consistent with the default [`Rotation`] fn default() -> Direction { - Direction::NORTH + Direction::EAST } } impl Direction { /// Creates a new [`Direction`] from a [`Vec2`] /// - /// The [`Vec2`] will be normalized to have a magnitude of 1. + /// The [`Vec2`] stored internally will be normalized to have a magnitude of `1.0`. /// /// # Panics - /// Panics if the supplied vector has length zero. + /// + /// Panics if the length of the supplied vector has length zero or cannot be determined. + /// Use [`try_from`](TryFrom) to get a [`Result`] instead. #[must_use] #[inline] pub fn new(vec2: Vec2) -> Self { - if vec2.length_squared() == 0.0 { - panic!("Supplied a Vec2 with length 0 to a Direction.") - }; - - Self { - unit_vector: vec2.normalize(), - } + Self::try_from(vec2).unwrap() } /// Returns the raw underlying [`Vec2`] unit vector of this direction /// - /// This will always have a magnitude of 1, unless it is [`Direction::NEUTRAL`] + /// This will always have a length of `1.0` #[must_use] #[inline] pub const fn unit_vector(&self) -> Vec2 { @@ -521,36 +517,36 @@ mod direction { impl Direction { /// The direction that points straight up pub const NORTH: Direction = Direction { - unit_vector: const_vec2!([0.0, 1.0]), + unit_vector: Vec2::new(0.0, 1.0), }; /// The direction that points straight right pub const EAST: Direction = Direction { - unit_vector: const_vec2!([1.0, 0.0]), + unit_vector: Vec2::new(1.0, 0.0), }; /// The direction that points straight down pub const SOUTH: Direction = Direction { - unit_vector: const_vec2!([0.0, -1.0]), + unit_vector: Vec2::new(0.0, -1.0), }; /// The direction that points straight left pub const WEST: Direction = Direction { - unit_vector: const_vec2!([-1.0, 0.0]), + unit_vector: Vec2::new(-1.0, 0.0), }; /// The direction that points halfway between up and right pub const NORTHEAST: Direction = Direction { - unit_vector: const_vec2!([SQRT_2 / 2.0, SQRT_2 / 2.0]), + unit_vector: Vec2::new(SQRT_2 / 2.0, SQRT_2 / 2.0), }; /// The direction that points halfway between down and right pub const SOUTHEAST: Direction = Direction { - unit_vector: const_vec2!([SQRT_2 / 2.0, -SQRT_2 / 2.0]), + unit_vector: Vec2::new(SQRT_2 / 2.0, -SQRT_2 / 2.0), }; /// The direction that points halfway between down and left pub const SOUTHWEST: Direction = Direction { - unit_vector: const_vec2!([-SQRT_2 / 2.0, -SQRT_2 / 2.0]), + unit_vector: Vec2::new(-SQRT_2 / 2.0, -SQRT_2 / 2.0), }; /// The direction that points halfway between left and up pub const NORTHWEST: Direction = Direction { - unit_vector: const_vec2!([-SQRT_2 / 2.0, SQRT_2 / 2.0]), + unit_vector: Vec2::new(-SQRT_2 / 2.0, SQRT_2 / 2.0), }; } @@ -572,38 +568,38 @@ mod direction { impl Mul for Direction { type Output = Vec2; - fn mul(self, rhs: f32) -> Self::Output { - Vec2::new(self.unit_vector.x * rhs, self.unit_vector.y * rhs) + fn mul(self, rhs: f32) -> Vec2 { + self.unit_vector * rhs } } impl Mul for f32 { type Output = Vec2; - fn mul(self, rhs: Direction) -> Self::Output { - Vec2::new(self * rhs.unit_vector.x, self * rhs.unit_vector.y) + fn mul(self, rhs: Direction) -> Vec2 { + self * rhs.unit_vector } } impl Div for Direction { type Output = Vec2; - fn div(self, rhs: f32) -> Self::Output { - Vec2::new(self.unit_vector.x / rhs, self.unit_vector.y / rhs) + fn div(self, rhs: f32) -> Vec2 { + self.unit_vector / rhs } } impl Div for f32 { type Output = Vec2; - fn div(self, rhs: Direction) -> Self::Output { - Vec2::new(self / rhs.unit_vector.x, self / rhs.unit_vector.y) + fn div(self, rhs: Direction) -> Vec2 { + self / rhs.unit_vector } } impl From for Vec3 { fn from(direction: Direction) -> Vec3 { - Vec3::new(direction.unit_vector.x, direction.unit_vector.y, 0.0) + direction.unit_vector.extend(0.0) } } @@ -621,8 +617,8 @@ mod direction { mod conversions { use super::{Direction, Rotation}; use crate::errors::NearlySingularConversion; - use bevy_math::{Quat, Vec2, Vec3}; - use bevy_transform::components::{GlobalTransform, Transform}; + use bevy::math::{Quat, Vec2, Vec3}; + use bevy::transform::components::{GlobalTransform, Transform}; impl From for Direction { fn from(rotation: Rotation) -> Direction { @@ -634,8 +630,23 @@ mod conversions { impl From for Rotation { fn from(direction: Direction) -> Rotation { - let radians = f32::atan2(direction.unit_vector().x, direction.unit_vector().y); - Rotation::from_radians(radians) + let radians = direction.unit_vector.y.atan2(direction.unit_vector.x); + // This dirty little trick helps us nudge the two (of eight) cardinal directions onto + // the correct decidegree. 32-bit floating point math rounds to the wrong decidegree, + // which usually isn't a big deal, but can result in unexpected surprises when people + // are dealing only with cardinal directions. The underlying problem is that f32 values + // for 1.0 and -1.0 can't be represented exactly, so our unit vectors start with an + // approximate value and both `atan2` above and `from_radians` below magnify the + // imprecision. So, we cheat. + const APPROX_SOUTH: f32 = -1.5707964; + const APPROX_NORTHWEST: f32 = 2.3561945; + if radians == APPROX_NORTHWEST { + Rotation::new(1350) + } else if radians == APPROX_SOUTH { + Rotation::new(2700) + } else { + Rotation::from_radians(radians) + } } } @@ -657,12 +668,9 @@ mod conversions { type Error = NearlySingularConversion; fn try_from(vec2: Vec2) -> Result { - if vec2.length_squared() == 0.0 { - Err(NearlySingularConversion) - } else { - Ok(Direction { - unit_vector: vec2.normalize(), - }) + match vec2.try_normalize() { + Some(unit_vector) => Ok(Direction { unit_vector }), + None => Err(NearlySingularConversion), } } } @@ -682,21 +690,15 @@ mod conversions { impl From for Quat { fn from(rotation: Rotation) -> Self { - // This is needed to ensure the rotation direction is correct - Quat::from_rotation_z(-rotation.into_radians()) + Quat::from_rotation_z(rotation.into_radians()) } } impl From for Direction { fn from(quaternion: Quat) -> Self { - let vec2 = quaternion.mul_vec3(Vec3::Y).truncate(); - - if vec2 == Vec2::ZERO { - Direction::default() - } else { - Direction { - unit_vector: vec2.normalize(), - } + match quaternion.mul_vec3(Vec3::X).truncate().try_normalize() { + Some(unit_vector) => Direction { unit_vector }, + None => Default::default(), } } } @@ -716,7 +718,7 @@ mod conversions { impl From for Direction { fn from(transform: GlobalTransform) -> Self { - transform.rotation.into() + transform.to_scale_rotation_translation().1.into() } } @@ -740,7 +742,7 @@ mod conversions { impl From for Rotation { fn from(transform: GlobalTransform) -> Self { - transform.rotation.into() + transform.to_scale_rotation_translation().1.into() } } @@ -756,3 +758,60 @@ mod conversions { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn directions_end_up_even() { + let north_rot: Rotation = Direction::NORTH.into(); + assert_eq!( + north_rot, + Rotation::new(900), + "we want north to end up exact in decidegrees" + ); + let northeast_rot: Rotation = Direction::NORTHEAST.into(); + assert_eq!( + northeast_rot, + Rotation::new(450), + "we want northeast to end up exact in decidegrees" + ); + let northwest_rot: Rotation = Direction::NORTHWEST.into(); + assert_eq!( + northwest_rot, + Rotation::new(1350), + "we want northwest to end up exact in decidegrees" + ); + let south_rot: Rotation = Direction::SOUTH.into(); + assert_eq!( + south_rot, + Rotation::new(2700), + "we want south to end up exact in decidegrees" + ); + let southeast_rot: Rotation = Direction::SOUTHEAST.into(); + assert_eq!( + southeast_rot, + Rotation::new(3150), + "we want southeast to end up exact in decidegrees" + ); + let southwest_rot: Rotation = Direction::SOUTHWEST.into(); + assert_eq!( + southwest_rot, + Rotation::new(2250), + "we want southwest to end up exact in decidegrees" + ); + let east_rot: Rotation = Direction::EAST.into(); + assert_eq!( + east_rot, + Rotation::new(0), + "we want east to end up exact in decidegrees" + ); + let west_rot: Rotation = Direction::WEST.into(); + assert_eq!( + west_rot, + Rotation::new(1800), + "we want west to end up exact in decidegrees" + ); + } +} diff --git a/src/plugin.rs b/src/plugin.rs index 256bbb76..f95b5c4d 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -6,11 +6,11 @@ use core::hash::Hash; use core::marker::PhantomData; use std::fmt::Debug; -use bevy_app::{App, CoreStage, Plugin}; -use bevy_ecs::prelude::*; -use bevy_input::InputSystem; +use bevy::app::{App, CoreStage, Plugin}; +use bevy::ecs::prelude::*; +use bevy::input::InputSystem; #[cfg(feature = "ui")] -use bevy_ui::UiSystem; +use bevy::ui::UiSystem; /// A [`Plugin`] that collects [`Input`](bevy::input::Input) from disparate sources, producing an [`ActionState`](crate::action_state::ActionState) that can be conveniently checked /// diff --git a/src/systems.rs b/src/systems.rs index f1f43a4b..430b17fb 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -6,18 +6,23 @@ use crate::{ action_state::{ActionDiff, ActionState}, clashing_inputs::ClashStrategy, input_map::InputMap, + input_streams::InputStreams, plugin::ToggleActions, - user_input::InputStreams, Actionlike, }; -use bevy_core::Time; -use bevy_ecs::{prelude::*, schedule::ShouldRun}; -use bevy_input::{gamepad::GamepadButton, keyboard::KeyCode, mouse::MouseButton, Input}; -use bevy_utils::Instant; +use bevy::ecs::{prelude::*, schedule::ShouldRun}; +use bevy::input::{ + gamepad::{GamepadAxis, GamepadButton, Gamepads}, + keyboard::KeyCode, + mouse::{MouseButton, MouseMotion, MouseWheel}, + Axis, Input, +}; +use bevy::time::Time; +use bevy::utils::Instant; #[cfg(feature = "ui")] -use bevy_ui::Interaction; +use bevy::ui::Interaction; /// Advances actions timer. /// @@ -54,25 +59,38 @@ pub fn tick_action_state( /// Missing resources will be ignored, and treated as if none of the corresponding inputs were pressed #[allow(clippy::too_many_arguments)] pub fn update_action_state( - maybe_gamepad_input_stream: Option>>, - maybe_keyboard_input_stream: Option>>, - maybe_mouse_input_stream: Option>>, + gamepad_buttons: Res>, + gamepad_button_axes: Res>, + gamepad_axes: Res>, + gamepads: Res, + keycode: Res>, + mouse_button: Res>, + mouse_wheel: Res>, + mouse_motion: Res>, clash_strategy: Res, mut action_state: Option>>, mut input_map: Option>>, mut query: Query<(&mut ActionState, &InputMap)>, ) { - let gamepad = maybe_gamepad_input_stream.as_deref(); - - let keyboard = maybe_keyboard_input_stream.as_deref(); - - let mouse = maybe_mouse_input_stream.as_deref(); + let gamepad_buttons = gamepad_buttons.into_inner(); + let gamepad_button_axes = gamepad_button_axes.into_inner(); + let gamepad_axes = gamepad_axes.into_inner(); + let gamepads = gamepads.into_inner(); + let keycode = keycode.into_inner(); + let mouse_button = mouse_button.into_inner(); + let mouse_wheel = mouse_wheel.into_inner(); + let mouse_motion = mouse_motion.into_inner(); if let (Some(input_map), Some(action_state)) = (&mut input_map, &mut action_state) { let input_streams = InputStreams { - gamepad, - keyboard, - mouse, + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + keycode, + mouse_button, + mouse_wheel, + mouse_motion, associated_gamepad: input_map.gamepad(), }; @@ -81,9 +99,14 @@ pub fn update_action_state( for (mut action_state, input_map) in query.iter_mut() { let input_streams = InputStreams { - gamepad, - keyboard, - mouse, + gamepad_buttons, + gamepad_button_axes, + gamepad_axes, + gamepads, + keycode, + mouse_button, + mouse_wheel, + mouse_motion, associated_gamepad: input_map.gamepad(), }; @@ -109,7 +132,7 @@ pub fn update_action_state_from_interaction( } } -/// Generates an [`Events`](bevy_ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`] +/// Generates an [`Events`](bevy::ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`] /// /// The `ID` generic type should be a stable entity identifer, /// suitable to be sent across a network. @@ -136,7 +159,7 @@ pub fn generate_action_diffs( } } -/// Generates an [`Events`](bevy_ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`] +/// Generates an [`Events`](bevy::ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`] /// /// The `ID` generic type should be a stable entity identifer, /// suitable to be sent across a network. diff --git a/src/user_input.rs b/src/user_input.rs index e0c50971..61ccbecb 100644 --- a/src/user_input.rs +++ b/src/user_input.rs @@ -1,444 +1,573 @@ -//! Helpful abstractions over user inputs of all sorts - -use bevy_input::{ - gamepad::{Gamepad, GamepadButton, GamepadButtonType}, - keyboard::KeyCode, - mouse::MouseButton, - Input, -}; - -use bevy_utils::HashSet; -use petitset::PetitSet; -use serde::{Deserialize, Serialize}; - -/// Some combination of user input, which may cross [`Input`] boundaries -/// -/// Suitable for use in an [`InputMap`](crate::input_map::InputMap) -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum UserInput { - /// A single button - Single(InputButton), - /// A combination of buttons, pressed simultaneously - /// - /// Up to 8 (!!) buttons can be chorded together at once. - /// Chords are considered to belong to all of the [InputMode]s of their constituent buttons. - Chord(PetitSet), -} - -impl UserInput { - /// Creates a [`UserInput::Chord`] from an iterator of [`Button`]s - /// - /// If `buttons` has a length of 1, a [`UserInput::Single`] variant will be returned instead. - pub fn chord(buttons: impl IntoIterator>) -> Self { - // We can't just check the length unless we add an ExactSizeIterator bound :( - let mut length: u8 = 0; - - let mut set: PetitSet = PetitSet::default(); - for button in buttons { - length += 1; - set.insert(button.into()); - } - - match length { - 1 => UserInput::Single(set.into_iter().next().unwrap()), - _ => UserInput::Chord(set), - } - } - - /// Which [`InputMode`]s does this input contain? - pub fn input_modes(&self) -> PetitSet { - let mut set = PetitSet::default(); - match self { - UserInput::Single(button) => { - set.insert((*button).into()); - } - UserInput::Chord(buttons) => { - for &button in buttons.iter() { - set.insert(button.into()); - } - } - } - set - } - - /// Does this [`UserInput`] match the provided [`InputMode`]? - /// - /// For [`UserInput::Chord`], this will be true if any of the buttons in the combination match. - pub fn matches_input_mode(&self, input_mode: InputMode) -> bool { - // This is slightly faster than using Self::input_modes - // As we can return early - match self { - UserInput::Single(button) => { - let button_mode: InputMode = (*button).into(); - button_mode == input_mode - } - UserInput::Chord(set) => { - for button in set.iter() { - let button_mode: InputMode = (*button).into(); - if button_mode == input_mode { - return true; - } - } - false - } - } - } - - /// The number of buttons in the [`UserInput`] - pub fn len(&self) -> usize { - match self { - UserInput::Single(_) => 1, - UserInput::Chord(button_set) => button_set.len(), - } - } - - /// Is the number of buttons in the [`UserInput`] 0? - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// How many of the provided `buttons` are found in the [`UserInput`] - /// - /// # Example - /// ```rust - /// use bevy_input::keyboard::KeyCode::*; - /// use bevy_utils::HashSet; - /// use leafwing_input_manager::user_input::UserInput; - /// - /// let buttons = HashSet::from_iter([LControl.into(), LAlt.into()]); - /// let a: UserInput = A.into(); - /// let ctrl_a = UserInput::chord([LControl, A]); - /// let ctrl_alt_a = UserInput::chord([LControl, LAlt, A]); - /// - /// assert_eq!(a.n_matching(&buttons), 0); - /// assert_eq!(ctrl_a.n_matching(&buttons), 1); - /// assert_eq!(ctrl_alt_a.n_matching(&buttons), 2); - /// ``` - pub fn n_matching(&self, buttons: &HashSet) -> usize { - match self { - UserInput::Single(button) => { - if buttons.contains(button) { - 1 - } else { - 0 - } - } - UserInput::Chord(chord_buttons) => { - let mut n_matching = 0; - for button in buttons.iter() { - if chord_buttons.contains(button) { - n_matching += 1; - } - } - - n_matching - } - } - } - - /// Returns the raw inputs that make up this [`UserInput`] - pub fn raw_inputs(&self) -> (Vec, Vec, Vec) { - let mut gamepad_buttons: Vec = Vec::default(); - let mut keyboard_buttons: Vec = Vec::default(); - let mut mouse_buttons: Vec = Vec::default(); - - match self { - UserInput::Single(button) => match *button { - InputButton::Gamepad(variant) => gamepad_buttons.push(variant), - InputButton::Keyboard(variant) => keyboard_buttons.push(variant), - InputButton::Mouse(variant) => mouse_buttons.push(variant), - }, - UserInput::Chord(button_set) => { - for button in button_set.iter() { - match button { - InputButton::Gamepad(variant) => gamepad_buttons.push(*variant), - InputButton::Keyboard(variant) => keyboard_buttons.push(*variant), - InputButton::Mouse(variant) => mouse_buttons.push(*variant), - } - } - } - }; - - (gamepad_buttons, keyboard_buttons, mouse_buttons) - } -} - -impl From for UserInput { - fn from(input: InputButton) -> Self { - UserInput::Single(input) - } -} - -impl From for UserInput { - fn from(input: GamepadButtonType) -> Self { - UserInput::Single(InputButton::Gamepad(input)) - } -} - -impl From for UserInput { - fn from(input: KeyCode) -> Self { - UserInput::Single(InputButton::Keyboard(input)) - } -} - -impl From for UserInput { - fn from(input: MouseButton) -> Self { - UserInput::Single(InputButton::Mouse(input)) - } -} - -/// A button-like input type -/// -/// See [`Button`] for the value-ful equivalent. -/// Use the [`From`] or [`Into`] traits to convert from a [`InputButton`] to a [`InputMode`]. -/// -/// Unfortunately we cannot use a trait object here, as the types used by `Input` -/// require traits that are not object-safe. -/// -/// Please contact the maintainers if you need support for another type! -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum InputMode { - /// A gamepad - Gamepad, - /// A keyboard - Keyboard, - /// A mouse - Mouse, -} - -impl InputMode { - /// Iterates over the possible [`InputModes`](InputMode) - pub fn iter() -> InputModeIter { - InputModeIter::default() - } -} - -/// An iterator of [`InputModes`](InputMode) -/// -/// Created by calling [`InputMode::iter`] -#[derive(Debug, Clone, Default)] -pub struct InputModeIter { - cursor: u8, -} - -impl Iterator for InputModeIter { - type Item = InputMode; - - fn next(&mut self) -> Option { - let item = match self.cursor { - 0 => Some(InputMode::Gamepad), - 1 => Some(InputMode::Keyboard), - 2 => Some(InputMode::Mouse), - _ => None, - }; - if self.cursor <= 2 { - self.cursor += 1; - } - - item - } -} - -impl From for InputMode { - fn from(button: InputButton) -> Self { - match button { - InputButton::Gamepad(_) => InputMode::Gamepad, - InputButton::Keyboard(_) => InputMode::Keyboard, - InputButton::Mouse(_) => InputMode::Mouse, - } - } -} - -/// The values of a button-like input type -/// -/// See [`InputMode`] for the value-less equivalent. Commonly stored in the [`UserInput`] enum. -/// -/// Unfortunately we cannot use a trait object here, as the types used by `Input` -/// require traits that are not object-safe. -/// -/// Please contact the maintainers if you need support for another type! -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum InputButton { - /// A button on a gamepad - Gamepad(GamepadButtonType), - /// A button on a keyboard - Keyboard(KeyCode), - /// A button on a mouse - Mouse(MouseButton), -} - -impl From for InputButton { - fn from(input: GamepadButtonType) -> Self { - InputButton::Gamepad(input) - } -} - -impl From for InputButton { - fn from(input: KeyCode) -> Self { - InputButton::Keyboard(input) - } -} - -impl From for InputButton { - fn from(input: MouseButton) -> Self { - InputButton::Mouse(input) - } -} - -/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). -/// -/// Each of these streams is optional; if a stream does not exist, it is treated as if it were entirely unpressed. -/// -/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. -#[derive(Debug, Clone)] -pub struct InputStreams<'a> { - /// An optional [`GamepadButton`] [`Input`] stream - pub gamepad: Option<&'a Input>, - /// An optional [`KeyCode`] [`Input`] stream - pub keyboard: Option<&'a Input>, - /// An optional [`MouseButton`] [`Input`] stream - pub mouse: Option<&'a Input>, - /// The [`Gamepad`] that this struct will detect inputs from - pub associated_gamepad: Option, -} - -// Constructors -impl<'a> InputStreams<'a> { - /// Construct [`InputStreams`] with only a [`GamepadButton`] input stream - pub fn from_gamepad( - gamepad_input_stream: &'a Input, - associated_gamepad: Gamepad, - ) -> Self { - Self { - gamepad: Some(gamepad_input_stream), - keyboard: None, - mouse: None, - associated_gamepad: Some(associated_gamepad), - } - } - - /// Construct [`InputStreams`] with only a [`KeyCode`] input stream - pub fn from_keyboard(keyboard_input_stream: &'a Input) -> Self { - Self { - gamepad: None, - keyboard: Some(keyboard_input_stream), - mouse: None, - associated_gamepad: None, - } - } - - /// Construct [`InputStreams`] with only a [`GamepadButton`] input stream - pub fn from_mouse(mouse_input_stream: &'a Input) -> Self { - Self { - gamepad: None, - keyboard: None, - mouse: Some(mouse_input_stream), - associated_gamepad: None, - } - } -} - -// Input checking -impl<'a> InputStreams<'a> { - /// Is the `input` matched by the [`InputStreams`]? - pub fn input_pressed(&self, input: &UserInput) -> bool { - match input { - UserInput::Single(button) => self.button_pressed(*button), - UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), - } - } - - /// Is at least one of the `inputs` pressed? - #[must_use] - pub fn any_pressed(&self, inputs: &PetitSet) -> bool { - for input in inputs.iter() { - if self.input_pressed(input) { - return true; - } - } - // If none of the inputs matched, return false - false - } - - /// Is the `button` pressed? - #[must_use] - pub fn button_pressed(&self, button: InputButton) -> bool { - match button { - InputButton::Gamepad(gamepad_button) => { - // If no gamepad is registered, we know for sure that no match was found - if let Some(gamepad) = self.associated_gamepad { - if let Some(gamepad_stream) = self.gamepad { - gamepad_stream.pressed(GamepadButton(gamepad, gamepad_button)) - } else { - false - } - } else { - false - } - } - InputButton::Keyboard(keycode) => { - if let Some(keyboard_stream) = self.keyboard { - keyboard_stream.pressed(keycode) - } else { - false - } - } - InputButton::Mouse(mouse_button) => { - if let Some(mouse_stream) = self.mouse { - mouse_stream.pressed(mouse_button) - } else { - false - } - } - } - } - - /// Are all of the `buttons` pressed? - #[must_use] - pub fn all_buttons_pressed(&self, buttons: &PetitSet) -> bool { - for &button in buttons.iter() { - // If any of the appropriate inputs failed to match, the action is considered pressed - if !self.button_pressed(button) { - return false; - } - } - // If none of the inputs failed to match, return true - true - } -} - -/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs. -/// -/// Each of these streams is optional; if a stream does not exist, inputs sent to them will be ignored. -/// -/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources. -#[derive(Debug)] -pub struct MutableInputStreams<'a> { - /// An optional [`GamepadButton`] [`Input`] stream - pub gamepad: Option<&'a mut Input>, - /// An optional [`KeyCode`] [`Input`] stream - pub keyboard: Option<&'a mut Input>, - /// An optional [`MouseButton`] [`Input`] stream - pub mouse: Option<&'a mut Input>, - /// The [`Gamepad`] that this struct will detect inputs from - pub associated_gamepad: Option, -} - -impl<'a> From> for InputStreams<'a> { - fn from(mutable_streams: MutableInputStreams<'a>) -> Self { - let gamepad = mutable_streams.gamepad.map(|mutable_ref| &*mutable_ref); - - let keyboard = mutable_streams.keyboard.map(|mutable_ref| &*mutable_ref); - - let mouse = mutable_streams.mouse.map(|mutable_ref| &*mutable_ref); - - InputStreams { - gamepad, - keyboard, - mouse, - associated_gamepad: mutable_streams.associated_gamepad, - } - } -} +//! Helpful abstractions over user inputs of all sorts + +use bevy::input::{gamepad::GamepadButtonType, keyboard::KeyCode, mouse::MouseButton}; + +use bevy::utils::HashSet; +use petitset::PetitSet; +use serde::{Deserialize, Serialize}; + +use crate::{ + axislike::{AxisType, DualAxis, SingleAxis, VirtualDPad}, + buttonlike::{MouseMotionDirection, MouseWheelDirection}, +}; + +/// Some combination of user input, which may cross [`Input`]-mode boundaries +/// +/// Suitable for use in an [`InputMap`](crate::input_map::InputMap) +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum UserInput { + /// A single button + Single(InputKind), + /// A combination of buttons, pressed simultaneously + /// + /// Up to 8 (!!) buttons can be chorded together at once. + /// Chords are considered to belong to all of the [InputMode]s of their constituent buttons. + Chord(PetitSet), + /// A virtual DPad that you can get an [`AxisPair`] from + VirtualDPad(VirtualDPad), +} + +impl UserInput { + /// Creates a [`UserInput::Chord`] from an iterator of [`InputKind`]s + /// + /// If `inputs` has a length of 1, a [`UserInput::Single`] variant will be returned instead. + pub fn chord(inputs: impl IntoIterator>) -> Self { + // We can't just check the length unless we add an ExactSizeIterator bound :( + let mut length: u8 = 0; + + let mut set: PetitSet = PetitSet::default(); + for button in inputs { + length += 1; + set.insert(button.into()); + } + + match length { + 1 => UserInput::Single(set.into_iter().next().unwrap()), + _ => UserInput::Chord(set), + } + } + + /// The number of logical inputs that make up the [`UserInput`]. + /// + /// - A [`Single`][UserInput::Single] input returns 1 + /// - A [`Chord`][UserInput::Chord] returns the number of buttons in the chord + /// - A [`VirtualDPad`][UserInput::VirtualDPad] returns 1 + pub fn len(&self) -> usize { + match self { + UserInput::Single(_) => 1, + UserInput::Chord(button_set) => button_set.len(), + UserInput::VirtualDPad { .. } => 1, + } + } + + /// Is the number of buttons in the [`UserInput`] 0? + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// How many of the provided `buttons` are found in the [`UserInput`] + /// + /// # Example + /// ```rust + /// use bevy::input::keyboard::KeyCode::*; + /// use bevy::utils::HashSet; + /// use leafwing_input_manager::user_input::UserInput; + /// + /// let buttons = HashSet::from_iter([LControl.into(), LAlt.into()]); + /// let a: UserInput = A.into(); + /// let ctrl_a = UserInput::chord([LControl, A]); + /// let ctrl_alt_a = UserInput::chord([LControl, LAlt, A]); + /// + /// assert_eq!(a.n_matching(&buttons), 0); + /// assert_eq!(ctrl_a.n_matching(&buttons), 1); + /// assert_eq!(ctrl_alt_a.n_matching(&buttons), 2); + /// ``` + pub fn n_matching(&self, buttons: &HashSet) -> usize { + match self { + UserInput::Single(button) => { + if buttons.contains(button) { + 1 + } else { + 0 + } + } + UserInput::Chord(chord_buttons) => { + let mut n_matching = 0; + for button in buttons.iter() { + if chord_buttons.contains(button) { + n_matching += 1; + } + } + + n_matching + } + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + let mut n_matching = 0; + for button in buttons.iter() { + for dpad_button in [up, down, left, right] { + if button == dpad_button { + n_matching += 1; + } + } + } + + n_matching + } + } + } + + /// Returns the raw inputs that make up this [`UserInput`] + pub fn raw_inputs(&self) -> RawInputs { + let mut raw_inputs = RawInputs::default(); + + match self { + UserInput::Single(button) => match *button { + InputKind::DualAxis(dual_axis) => { + raw_inputs + .axis_data + .push((dual_axis.x.axis_type, dual_axis.x.value)); + raw_inputs + .axis_data + .push((dual_axis.y.axis_type, dual_axis.y.value)); + } + InputKind::SingleAxis(single_axis) => raw_inputs + .axis_data + .push((single_axis.axis_type, single_axis.value)), + InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button), + InputKind::Keyboard(button) => raw_inputs.keycodes.push(button), + InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button), + InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button), + InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button), + }, + UserInput::Chord(button_set) => { + for button in button_set.iter() { + match *button { + InputKind::DualAxis(dual_axis) => { + raw_inputs + .axis_data + .push((dual_axis.x.axis_type, dual_axis.x.value)); + raw_inputs + .axis_data + .push((dual_axis.y.axis_type, dual_axis.y.value)); + } + InputKind::SingleAxis(single_axis) => raw_inputs + .axis_data + .push((single_axis.axis_type, single_axis.value)), + InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button), + InputKind::Keyboard(button) => raw_inputs.keycodes.push(button), + InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button), + InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button), + InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button), + } + } + } + UserInput::VirtualDPad(VirtualDPad { + up, + down, + left, + right, + }) => { + for button in [up, down, left, right] { + match *button { + InputKind::DualAxis(dual_axis) => { + raw_inputs + .axis_data + .push((dual_axis.x.axis_type, dual_axis.x.value)); + raw_inputs + .axis_data + .push((dual_axis.y.axis_type, dual_axis.y.value)); + } + InputKind::SingleAxis(single_axis) => raw_inputs + .axis_data + .push((single_axis.axis_type, single_axis.value)), + InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button), + InputKind::Keyboard(button) => raw_inputs.keycodes.push(button), + InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button), + InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button), + InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button), + } + } + } + }; + + raw_inputs + } +} + +impl From for UserInput { + fn from(input: InputKind) -> Self { + UserInput::Single(input) + } +} + +impl From for UserInput { + fn from(input: DualAxis) -> Self { + UserInput::Single(InputKind::DualAxis(input)) + } +} + +impl From for UserInput { + fn from(input: SingleAxis) -> Self { + UserInput::Single(InputKind::SingleAxis(input)) + } +} + +impl From for UserInput { + fn from(input: VirtualDPad) -> Self { + UserInput::VirtualDPad(input) + } +} + +impl From for UserInput { + fn from(input: GamepadButtonType) -> Self { + UserInput::Single(InputKind::GamepadButton(input)) + } +} + +impl From for UserInput { + fn from(input: KeyCode) -> Self { + UserInput::Single(InputKind::Keyboard(input)) + } +} + +impl From for UserInput { + fn from(input: MouseButton) -> Self { + UserInput::Single(InputKind::Mouse(input)) + } +} + +impl From for UserInput { + fn from(input: MouseWheelDirection) -> Self { + UserInput::Single(InputKind::MouseWheel(input)) + } +} + +impl From for UserInput { + fn from(input: MouseMotionDirection) -> Self { + UserInput::Single(InputKind::MouseMotion(input)) + } +} + +/// The different kinds of supported input bindings. +/// +/// See [`InputMode`] for the value-less equivalent. Commonly stored in the [`UserInput`] enum. +/// +/// Unfortunately we cannot use a trait object here, as the types used by `Input` +/// require traits that are not object-safe. +/// +/// Please contact the maintainers if you need support for another type! +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum InputKind { + /// A button on a gamepad + GamepadButton(GamepadButtonType), + /// A single axis of continous motion + SingleAxis(SingleAxis), + /// Two paired axes of continous motion + DualAxis(DualAxis), + /// A button on a keyboard + Keyboard(KeyCode), + /// A button on a mouse + Mouse(MouseButton), + /// A discretized mousewheel movement + MouseWheel(MouseWheelDirection), + /// A discretized mouse movement + MouseMotion(MouseMotionDirection), +} + +impl From for InputKind { + fn from(input: DualAxis) -> Self { + InputKind::DualAxis(input) + } +} + +impl From for InputKind { + fn from(input: SingleAxis) -> Self { + InputKind::SingleAxis(input) + } +} + +impl From for InputKind { + fn from(input: GamepadButtonType) -> Self { + InputKind::GamepadButton(input) + } +} + +impl From for InputKind { + fn from(input: KeyCode) -> Self { + InputKind::Keyboard(input) + } +} + +impl From for InputKind { + fn from(input: MouseButton) -> Self { + InputKind::Mouse(input) + } +} + +impl From for InputKind { + fn from(input: MouseWheelDirection) -> Self { + InputKind::MouseWheel(input) + } +} + +impl From for InputKind { + fn from(input: MouseMotionDirection) -> Self { + InputKind::MouseMotion(input) + } +} + +/// The basic input events that make up a [`UserInput`]. +/// +/// Obtained by calling [`UserInput::raw_inputs()`]. +#[derive(Default, Debug, Clone, PartialEq)] +pub struct RawInputs { + /// Physical keyboard buttons + pub keycodes: Vec, + /// Mouse buttons + pub mouse_buttons: Vec, + /// Discretized mouse wheel inputs + pub mouse_wheel: Vec, + /// Discretized mouse motion inputs + pub mouse_motion: Vec, + /// Gamepad buttons, independent of a [`Gamepad`](bevy::input::gamepad::Gamepad) + pub gamepad_buttons: Vec, + /// Axis-like data + /// + /// The `f32` stores the magnitude of the axis motion, and is only used for input mocking. + pub axis_data: Vec<(AxisType, Option)>, +} + +#[cfg(test)] +impl RawInputs { + fn from_keycode(keycode: KeyCode) -> RawInputs { + RawInputs { + keycodes: vec![keycode], + ..Default::default() + } + } + + fn from_mouse_button(mouse_button: MouseButton) -> RawInputs { + RawInputs { + mouse_buttons: vec![mouse_button], + ..Default::default() + } + } + + fn from_gamepad_button(gamepad_button: GamepadButtonType) -> RawInputs { + RawInputs { + gamepad_buttons: vec![gamepad_button], + ..Default::default() + } + } + + fn from_mouse_wheel(direction: MouseWheelDirection) -> RawInputs { + RawInputs { + mouse_wheel: vec![direction], + ..Default::default() + } + } + + fn from_mouse_direction(direction: MouseMotionDirection) -> RawInputs { + RawInputs { + mouse_motion: vec![direction], + ..Default::default() + } + } + + fn from_dual_axis(axis: DualAxis) -> RawInputs { + RawInputs { + axis_data: vec![ + (axis.x.axis_type, axis.x.value), + (axis.y.axis_type, axis.y.value), + ], + ..Default::default() + } + } + + fn from_single_axis(axis: SingleAxis) -> RawInputs { + RawInputs { + axis_data: vec![(axis.axis_type, axis.value)], + ..Default::default() + } + } +} + +#[cfg(test)] +mod raw_input_tests { + use crate::{ + axislike::AxisType, + user_input::{InputKind, RawInputs, UserInput}, + }; + + #[test] + fn simple_chord() { + use bevy::input::gamepad::GamepadButtonType; + + let buttons = vec![GamepadButtonType::Start, GamepadButtonType::Select]; + let raw_inputs = UserInput::chord(buttons.clone()).raw_inputs(); + let expected = RawInputs { + gamepad_buttons: buttons, + ..Default::default() + }; + + assert_eq!(expected, raw_inputs); + } + + #[test] + fn mixed_chord() { + use crate::axislike::SingleAxis; + use bevy::input::gamepad::GamepadAxisType; + use bevy::input::gamepad::GamepadButtonType; + + let chord = UserInput::chord([ + InputKind::GamepadButton(GamepadButtonType::Start), + InputKind::SingleAxis(SingleAxis::symmetric(GamepadAxisType::LeftZ, 0.)), + ]); + + let raw = chord.raw_inputs(); + let expected = RawInputs { + gamepad_buttons: vec![GamepadButtonType::Start], + axis_data: vec![(AxisType::Gamepad(GamepadAxisType::LeftZ), None)], + ..Default::default() + }; + + assert_eq!(expected, raw); + } + + mod gamepad { + use crate::user_input::{RawInputs, UserInput}; + + #[test] + fn gamepad_button() { + use bevy::input::gamepad::GamepadButtonType; + + let button = GamepadButtonType::Start; + let expected = RawInputs::from_gamepad_button(button); + let raw = UserInput::from(button).raw_inputs(); + assert_eq!(expected, raw); + } + + #[test] + fn single_gamepad_axis() { + use crate::axislike::SingleAxis; + use bevy::input::gamepad::GamepadAxisType; + + let direction = SingleAxis::from_value(GamepadAxisType::LeftStickX, 1.0); + let expected = RawInputs::from_single_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + + #[test] + fn dual_gamepad_axis() { + use crate::axislike::DualAxis; + use bevy::input::gamepad::GamepadAxisType; + + let direction = DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.5, + 0.7, + ); + let expected = RawInputs::from_dual_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + } + + mod keyboard { + use crate::user_input::{RawInputs, UserInput}; + + #[test] + fn keyboard_button() { + use bevy::input::keyboard::KeyCode; + + let button = KeyCode::A; + let expected = RawInputs::from_keycode(button); + let raw = UserInput::from(button).raw_inputs(); + assert_eq!(expected, raw); + } + } + + mod mouse { + use crate::user_input::{RawInputs, UserInput}; + + #[test] + fn mouse_button() { + use bevy::input::mouse::MouseButton; + + let button = MouseButton::Left; + let expected = RawInputs::from_mouse_button(button); + let raw = UserInput::from(button).raw_inputs(); + assert_eq!(expected, raw); + } + + #[test] + fn mouse_wheel() { + use crate::buttonlike::MouseWheelDirection; + + let direction = MouseWheelDirection::Down; + let expected = RawInputs::from_mouse_wheel(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw); + } + + #[test] + fn mouse_motion() { + use crate::buttonlike::MouseMotionDirection; + + let direction = MouseMotionDirection::Up; + let expected = RawInputs::from_mouse_direction(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw); + } + + #[test] + fn single_mousewheel_axis() { + use crate::axislike::{MouseWheelAxisType, SingleAxis}; + + let direction = SingleAxis::from_value(MouseWheelAxisType::X, 1.0); + let expected = RawInputs::from_single_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + + #[test] + fn dual_mousewheel_axis() { + use crate::axislike::{DualAxis, MouseWheelAxisType}; + + let direction = + DualAxis::from_value(MouseWheelAxisType::X, MouseWheelAxisType::Y, 1.0, 1.0); + let expected = RawInputs::from_dual_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + + #[test] + fn single_mouse_motion_axis() { + use crate::axislike::{MouseMotionAxisType, SingleAxis}; + + let direction = SingleAxis::from_value(MouseMotionAxisType::X, 1.0); + let expected = RawInputs::from_single_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + + #[test] + fn dual_mouse_motion_axis() { + use crate::axislike::{DualAxis, MouseMotionAxisType}; + + let direction = + DualAxis::from_value(MouseMotionAxisType::X, MouseMotionAxisType::Y, 1.0, 1.0); + let expected = RawInputs::from_dual_axis(direction); + let raw = UserInput::from(direction).raw_inputs(); + assert_eq!(expected, raw) + } + } +} diff --git a/tests/clashes.rs b/tests/clashes.rs index 428d6fc7..62e35ade 100644 --- a/tests/clashes.rs +++ b/tests/clashes.rs @@ -1,8 +1,19 @@ +use bevy::ecs::system::SystemState; +use bevy::input::InputPlugin; use bevy::prelude::*; -use bevy_ecs::system::SystemState; -use bevy_utils::HashSet; +use bevy::utils::HashSet; +use leafwing_input_manager::input_streams::InputStreams; use leafwing_input_manager::prelude::*; -use leafwing_input_manager::user_input::InputStreams; + +fn test_app() -> App { + let mut app = App::new(); + + app.add_plugins(MinimalPlugins) + .add_plugin(InputPlugin) + .add_plugin(InputManagerPlugin::::default()) + .add_startup_system(spawn_input_map); + app +} #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug)] enum Action { @@ -53,26 +64,23 @@ impl ClashTestExt for App { ) { let pressed_actions: HashSet = HashSet::from_iter(pressed_actions.into_iter()); // SystemState is love, SystemState is life - let mut input_system_state: SystemState<(Query<&InputMap>, Res>)> = + let mut input_system_state: SystemState>> = SystemState::new(&mut self.world); - let (input_map_query, keyboard) = input_system_state.get(&self.world); - - let input_streams = InputStreams::from_keyboard(&*keyboard); + let input_map_query = input_system_state.get(&self.world); let input_map = input_map_query.single(); - - let keyboard_input = input_streams.keyboard.unwrap(); + let keyboard_input = self.world.resource::>(); for action in Action::variants() { if pressed_actions.contains(&action) { assert!( - input_map.pressed(action, &input_streams, clash_strategy), + input_map.pressed(action, &InputStreams::from_world(&self.world, None), clash_strategy), "{action:?} was incorrectly not pressed for {clash_strategy:?} when `Input` was \n {keyboard_input:?}." ); } else { assert!( - !input_map.pressed(action, &input_streams, clash_strategy), + !input_map.pressed(action, &InputStreams::from_world(&self.world, None), clash_strategy), "{action:?} was incorrectly pressed for {clash_strategy:?} when `Input` was \n {keyboard_input:?}" ); } @@ -81,18 +89,11 @@ impl ClashTestExt for App { } #[test] -fn input_clash_handling() { - use bevy_input::InputPlugin; - use leafwing_input_manager::MockInput; +fn two_inputs_clash_handling() { use Action::*; use KeyCode::*; - let mut app = App::new(); - - app.add_plugins(MinimalPlugins) - .add_plugin(InputPlugin) - .add_plugin(InputManagerPlugin::::default()) - .add_startup_system(spawn_input_map); + let mut app = test_app(); // Two inputs app.send_input(Key1); @@ -102,6 +103,14 @@ fn input_clash_handling() { app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, Two, OneAndTwo]); app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwo]); app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]); +} + +#[test] +fn three_inputs_clash_handling() { + use Action::*; + use KeyCode::*; + + let mut app = test_app(); // Three inputs app.reset_inputs(); @@ -116,6 +125,14 @@ fn input_clash_handling() { ); app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwoAndThree]); app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]); +} + +#[test] +fn modifier_clash_handling() { + use Action::*; + use KeyCode::*; + + let mut app = test_app(); // Modifier app.reset_inputs(); @@ -134,6 +151,14 @@ fn input_clash_handling() { [CtrlOne, OneAndTwoAndThree], ); app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]); +} + +#[test] +fn multiple_modifiers_clash_handling() { + use Action::*; + use KeyCode::*; + + let mut app = test_app(); // Multiple modifiers app.reset_inputs(); @@ -145,6 +170,14 @@ fn input_clash_handling() { app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, CtrlOne, AltOne, CtrlAltOne]); app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [CtrlAltOne]); app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One]); +} + +#[test] +fn action_order_clash_handling() { + use Action::*; + use KeyCode::*; + + let mut app = test_app(); // Action order app.reset_inputs(); diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs new file mode 100644 index 00000000..9b2e0d54 --- /dev/null +++ b/tests/gamepad_axis.rs @@ -0,0 +1,246 @@ +use bevy::input::gamepad::GamepadEventRaw; +use bevy::input::InputPlugin; +use bevy::prelude::*; +use leafwing_input_manager::axislike::{AxisType, DualAxisData}; +use leafwing_input_manager::prelude::*; + +#[derive(Actionlike, Clone, Copy, Debug)] +enum ButtonlikeTestAction { + Up, + Down, + Left, + Right, +} + +#[derive(Actionlike, Clone, Copy, Debug)] +enum AxislikeTestAction { + X, + Y, + XY, +} + +fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugin(InputPlugin) + .add_plugin(InputManagerPlugin::::default()) + .add_plugin(InputManagerPlugin::::default()) + .init_resource::>() + .init_resource::>(); + + // WARNING: you MUST register your gamepad during tests, or all gamepad input mocking will fail + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEventRaw { + // This MUST be consistent with any other mocked events + gamepad: Gamepad { id: 1 }, + event_type: GamepadEventType::Connected, + }); + + // Ensure that the gamepad is picked up by the appropriate system + app.update(); + // Ensure that the connection event is flushed through + app.update(); + + app +} + +#[test] +fn raw_gamepad_axis_events() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), + ButtonlikeTestAction::Up, + )])); + + let mut events = app.world.resource_mut::>(); + events.send(GamepadEventRaw { + gamepad: Gamepad { id: 1 }, + event_type: GamepadEventType::AxisChanged(GamepadAxisType::RightStickX, 1.0), + }); + + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(ButtonlikeTestAction::Up)); +} + +#[test] +fn game_pad_single_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + + app.send_input(input); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn game_pad_dual_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = DualAxis { + x: SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }, + y: SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(0.), + positive_low: 0.0, + negative_low: 0.0, + }, + }; + app.send_input(input); + let mut events = app.world.resource_mut::>(); + // Dual axis events are split out + assert_eq!(events.drain().count(), 2); +} + +#[test] +fn game_pad_single_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1), + AxislikeTestAction::X, + ), + ( + SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1), + AxislikeTestAction::Y, + ), + ])); + + // +X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // -X + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // +Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // -Y + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // 0 + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: Some(0.0), + // Usually a small deadzone threshold will be set + positive_low: 0.1, + negative_low: 0.1, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); + + // None + let input = SingleAxis { + axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + value: None, + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); +} + +#[test] +fn game_pad_dual_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::left_stick(), + AxislikeTestAction::XY, + )])); + + app.send_input(DualAxis::from_value( + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + 0.8, + 0.0, + )); + + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 0.8); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(0.8, 0.0) + ); +} + +#[test] +fn game_pad_virtualdpad() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + VirtualDPad::dpad(), + AxislikeTestAction::XY, + )])); + + app.send_input(GamepadButtonType::DPadLeft); + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + // This should be unit length, because we're working with a VirtualDpad + assert_eq!(action_state.value(AxislikeTestAction::XY), 1.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + // This should be unit length, because we're working with a VirtualDpad + DualAxisData::new(-1.0, 0.0) + ); +} diff --git a/tests/integration.rs b/tests/integration.rs index 4ed451d8..2e834411 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,8 +1,7 @@ #![cfg(test)] +use bevy::ecs::query::ChangeTrackers; use bevy::prelude::*; -use bevy_ecs::query::ChangeTrackers; use leafwing_input_manager::prelude::*; -use leafwing_input_manager::MockInput; #[derive(Actionlike, Clone, Copy, Debug)] enum Action { @@ -49,8 +48,8 @@ fn spawn_player(mut commands: Commands) { #[test] fn do_nothing() { - use bevy_input::InputPlugin; - use bevy_utils::Duration; + use bevy::input::InputPlugin; + use bevy::utils::Duration; let mut app = App::new(); @@ -77,8 +76,6 @@ fn do_nothing() { assert!(action_state.released(Action::PayRespects)); assert!(!action_state.just_released(Action::PayRespects)); - assert_eq!(action_state.reasons_pressed(Action::PayRespects).len(), 0); - assert_eq!(action_state.instant_started(Action::PayRespects), t0); assert_eq!( action_state.previous_duration(Action::PayRespects), @@ -93,7 +90,7 @@ fn do_nothing() { #[test] fn action_state_change_detection() { - use bevy_input::InputPlugin; + use bevy::input::InputPlugin; let mut app = App::new(); @@ -127,7 +124,7 @@ fn action_state_change_detection() { #[test] fn disable_input() { - use bevy_input::InputPlugin; + use bevy::input::InputPlugin; let mut app = App::new(); @@ -168,8 +165,8 @@ fn disable_input() { #[test] #[cfg(feature = "ui")] fn action_state_driver() { - use bevy_input::InputPlugin; - use bevy_ui::Interaction; + use bevy::input::InputPlugin; + use bevy::ui::Interaction; let mut app = App::new(); @@ -239,8 +236,8 @@ fn action_state_driver() { #[test] fn duration() { - use bevy_input::InputPlugin; - use bevy_utils::Duration; + use bevy::input::InputPlugin; + use bevy::utils::Duration; const RESPECTFUL_DURATION: Duration = Duration::from_millis(5); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs new file mode 100644 index 00000000..499e2e56 --- /dev/null +++ b/tests/mouse_motion.rs @@ -0,0 +1,289 @@ +use bevy::input::mouse::MouseMotion; +use bevy::input::InputPlugin; +use bevy::prelude::*; +use leafwing_input_manager::axislike::{AxisType, DualAxisData, MouseMotionAxisType}; +use leafwing_input_manager::buttonlike::MouseMotionDirection; +use leafwing_input_manager::prelude::*; + +#[derive(Actionlike, Clone, Copy, Debug)] +enum ButtonlikeTestAction { + Up, + Down, + Left, + Right, +} + +#[derive(Actionlike, Clone, Copy, Debug)] +enum AxislikeTestAction { + X, + Y, + XY, +} + +fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugin(InputPlugin) + .add_plugin(InputManagerPlugin::::default()) + .add_plugin(InputManagerPlugin::::default()) + .init_resource::>() + .init_resource::>(); + + app +} + +#[test] +fn raw_mouse_motion_events() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + SingleAxis::from_value(AxisType::MouseMotion(MouseMotionAxisType::Y), 1.0), + AxislikeTestAction::X, + )])); + + let mut events = app.world.resource_mut::>(); + events.send(MouseMotion { + delta: Vec2::new(0.0, 1.0), + }); + + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); +} + +#[test] +fn mouse_motion_discrete_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + app.send_input(MouseMotionDirection::Up); + let mut events = app.world.resource_mut::>(); + + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn mouse_motion_single_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + + app.send_input(input); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn mouse_motion_dual_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = DualAxis { + x: SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }, + y: SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + value: Some(0.), + positive_low: 0.0, + negative_low: 0.0, + }, + }; + app.send_input(input); + let mut events = app.world.resource_mut::>(); + // Dual axis events are split out + assert_eq!(events.drain().count(), 2); +} + +#[test] +fn mouse_motion_buttonlike() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (MouseMotionDirection::Up, ButtonlikeTestAction::Up), + (MouseMotionDirection::Down, ButtonlikeTestAction::Down), + (MouseMotionDirection::Left, ButtonlikeTestAction::Left), + (MouseMotionDirection::Right, ButtonlikeTestAction::Right), + ])); + + for action in ButtonlikeTestAction::variants() { + let input_map = app.world.resource::>(); + // Get the first associated input + let input = input_map.get(action).get_at(0).unwrap().clone(); + + app.send_input(input.clone()); + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(action), "failed for {input:?}"); + } +} + +#[test] +fn mouse_motion_buttonlike_cancels() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (MouseMotionDirection::Up, ButtonlikeTestAction::Up), + (MouseMotionDirection::Down, ButtonlikeTestAction::Down), + (MouseMotionDirection::Left, ButtonlikeTestAction::Left), + (MouseMotionDirection::Right, ButtonlikeTestAction::Right), + ])); + + app.send_input(MouseMotionDirection::Up); + app.send_input(MouseMotionDirection::Down); + + // Correctly flushes the world + app.update(); + + let action_state = app.world.resource::>(); + + assert!(!action_state.pressed(ButtonlikeTestAction::Up)); + assert!(!action_state.pressed(ButtonlikeTestAction::Down)); +} + +#[test] +fn mouse_motion_single_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (SingleAxis::mouse_motion_x(), AxislikeTestAction::X), + (SingleAxis::mouse_motion_y(), AxislikeTestAction::Y), + ])); + + // +X + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // -X + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // +Y + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // -Y + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // 0 + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + value: Some(0.0), + // Usually a small deadzone threshold will be set + positive_low: 0.1, + negative_low: 0.1, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); + + // None + let input = SingleAxis { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + value: None, + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); +} + +#[test] +fn mouse_motion_dual_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::mouse_motion(), + AxislikeTestAction::XY, + )])); + + app.send_input(DualAxis::from_value( + MouseMotionAxisType::X, + MouseMotionAxisType::Y, + 5.0, + 0.0, + )); + + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 5.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(5.0, 0.0) + ); +} + +#[test] +fn mouse_motion_virtualdpad() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + VirtualDPad::mouse_motion(), + AxislikeTestAction::XY, + )])); + + app.send_input(DualAxis::from_value( + MouseMotionAxisType::X, + MouseMotionAxisType::Y, + 0.0, + -2.0, + )); + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + // This should be unit length, because we're working with a VirtualDpad + assert_eq!(action_state.value(AxislikeTestAction::XY), 1.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + // This should be unit length, because we're working with a VirtualDpad + DualAxisData::new(0.0, -1.0) + ); +} diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs new file mode 100644 index 00000000..40536b24 --- /dev/null +++ b/tests/mouse_wheel.rs @@ -0,0 +1,290 @@ +use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; +use bevy::input::InputPlugin; +use bevy::prelude::*; +use leafwing_input_manager::axislike::{AxisType, DualAxisData, MouseWheelAxisType}; +use leafwing_input_manager::prelude::*; + +#[derive(Actionlike, Clone, Copy, Debug)] +enum ButtonlikeTestAction { + Up, + Down, + Left, + Right, +} + +#[derive(Actionlike, Clone, Copy, Debug)] +enum AxislikeTestAction { + X, + Y, + XY, +} + +fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugin(InputPlugin) + .add_plugin(InputManagerPlugin::::default()) + .add_plugin(InputManagerPlugin::::default()) + .init_resource::>() + .init_resource::>(); + + app +} + +#[test] +fn raw_mouse_wheel_events() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + MouseWheelDirection::Up, + ButtonlikeTestAction::Up, + )])); + + let mut events = app.world.resource_mut::>(); + events.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: 0.0, + y: 10.0, + }); + + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(ButtonlikeTestAction::Up)); +} + +#[test] +fn mouse_wheel_discrete_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + app.send_input(MouseWheelDirection::Up); + let mut events = app.world.resource_mut::>(); + + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn mouse_wheel_single_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + + app.send_input(input); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 1); +} + +#[test] +fn mouse_wheel_dual_axis_mocking() { + let mut app = test_app(); + let mut events = app.world.resource_mut::>(); + assert_eq!(events.drain().count(), 0); + + let input = DualAxis { + x: SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }, + y: SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + value: Some(0.), + positive_low: 0.0, + negative_low: 0.0, + }, + }; + app.send_input(input); + let mut events = app.world.resource_mut::>(); + // Dual axis events are split out + assert_eq!(events.drain().count(), 2); +} + +#[test] +fn mouse_wheel_buttonlike() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (MouseWheelDirection::Up, ButtonlikeTestAction::Up), + (MouseWheelDirection::Down, ButtonlikeTestAction::Down), + (MouseWheelDirection::Left, ButtonlikeTestAction::Left), + (MouseWheelDirection::Right, ButtonlikeTestAction::Right), + ])); + + for action in ButtonlikeTestAction::variants() { + let input_map = app.world.resource::>(); + // Get the first associated input + let input = input_map.get(action).get_at(0).unwrap().clone(); + + app.send_input(input.clone()); + app.update(); + + let action_state = app.world.resource::>(); + assert!(action_state.pressed(action), "failed for {input:?}"); + } +} + +#[test] +fn mouse_wheel_buttonlike_cancels() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (MouseWheelDirection::Up, ButtonlikeTestAction::Up), + (MouseWheelDirection::Down, ButtonlikeTestAction::Down), + (MouseWheelDirection::Left, ButtonlikeTestAction::Left), + (MouseWheelDirection::Right, ButtonlikeTestAction::Right), + ])); + + app.send_input(MouseWheelDirection::Up); + app.send_input(MouseWheelDirection::Down); + + // Correctly flushes the world + app.update(); + + let action_state = app.world.resource::>(); + + assert!(!action_state.pressed(ButtonlikeTestAction::Up)); + assert!(!action_state.pressed(ButtonlikeTestAction::Down)); +} + +#[test] +fn mouse_wheel_single_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([ + (SingleAxis::mouse_wheel_x(), AxislikeTestAction::X), + (SingleAxis::mouse_wheel_y(), AxislikeTestAction::Y), + ])); + + // +X + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // -X + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::X)); + + // +Y + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + value: Some(1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // -Y + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + value: Some(-1.), + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(action_state.pressed(AxislikeTestAction::Y)); + + // 0 + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + value: Some(0.0), + // Usually a small deadzone threshold will be set + positive_low: 0.1, + negative_low: 0.1, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); + + // None + let input = SingleAxis { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + value: None, + positive_low: 0.0, + negative_low: 0.0, + }; + app.send_input(input); + app.update(); + let action_state = app.world.resource::>(); + assert!(!action_state.pressed(AxislikeTestAction::Y)); +} + +#[test] +fn mouse_wheel_dual_axis() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + DualAxis::mouse_wheel(), + AxislikeTestAction::XY, + )])); + + app.send_input(DualAxis::from_value( + MouseWheelAxisType::X, + MouseWheelAxisType::Y, + 5.0, + 0.0, + )); + + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + assert_eq!(action_state.value(AxislikeTestAction::XY), 5.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + DualAxisData::new(5.0, 0.0) + ); +} + +#[test] +fn mouse_wheel_virtualdpad() { + let mut app = test_app(); + app.insert_resource(InputMap::new([( + VirtualDPad::mouse_wheel(), + AxislikeTestAction::XY, + )])); + + app.send_input(DualAxis::from_value( + MouseWheelAxisType::X, + MouseWheelAxisType::Y, + 0.0, + -2.0, + )); + app.update(); + + let action_state = app.world.resource::>(); + + assert!(action_state.pressed(AxislikeTestAction::XY)); + // This should be unit length, because we're working with a VirtualDpad + assert_eq!(action_state.value(AxislikeTestAction::XY), 1.0); + assert_eq!( + action_state.axis_pair(AxislikeTestAction::XY).unwrap(), + // This should be unit length, because we're working with a VirtualDpad + DualAxisData::new(0.0, -1.0) + ); +}