├── .github ├── dependabot.yml └── workflows │ ├── dependencies.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── codecov.yml ├── deny.toml ├── examples ├── all_conditions.rs ├── context_layering.rs ├── context_switch.rs ├── keybinding_menu.rs ├── local_multiplayer.rs └── simple_fly_cam.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── src ├── action_binding.rs ├── action_instances.rs ├── action_map.rs ├── action_value.rs ├── actions.rs ├── events.rs ├── input.rs ├── input_action.rs ├── input_binding.rs ├── input_condition.rs ├── input_condition │ ├── block_by.rs │ ├── chord.rs │ ├── condition_timer.rs │ ├── hold.rs │ ├── hold_and_release.rs │ ├── just_press.rs │ ├── press.rs │ ├── pulse.rs │ ├── release.rs │ └── tap.rs ├── input_modifier.rs ├── input_modifier │ ├── accumulate_by.rs │ ├── clamp.rs │ ├── dead_zone.rs │ ├── delta_scale.rs │ ├── exponential_curve.rs │ ├── negate.rs │ ├── scale.rs │ ├── smooth_nudge.rs │ └── swizzle_axis.rs ├── input_reader.rs ├── lib.rs ├── preset.rs └── trigger_tracker.rs └── tests ├── accumulation.rs ├── condition_kind.rs ├── consume_input.rs ├── context_gamepad.rs ├── dim.rs ├── fixed_timestep.rs ├── instances.rs ├── macro_hygiene.rs ├── mocking.rs ├── preset.rs ├── priority.rs ├── require_reset.rs └── state_and_value_merge.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | labels: 8 | - "dependencies" 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | labels: 14 | - "github actions" 15 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Dependencies 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - "Cargo.toml" 8 | - "Cargo.lock" 9 | - "deny.toml" 10 | pull_request: 11 | paths: 12 | - "Cargo.toml" 13 | - "Cargo.lock" 14 | - "deny.toml" 15 | schedule: 16 | - cron: "0 0 * * 0" 17 | env: 18 | CARGO_TERM_COLOR: always 19 | jobs: 20 | dependencies: 21 | name: Check dependencies 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Clone repo 25 | uses: actions/checkout@v4 26 | 27 | - name: Check dependencies 28 | uses: EmbarkStudios/cargo-deny-action@v2 29 | with: 30 | command-arguments: -D warnings 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - ".gitignore" 8 | - ".github/dependabot.yml" 9 | - "deny.toml" 10 | pull_request: 11 | paths-ignore: 12 | - ".gitignore" 13 | - ".github/dependabot.yml" 14 | - "deny.toml" 15 | env: 16 | CARGO_TERM_COLOR: always 17 | jobs: 18 | typos: 19 | name: Typos 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Clone repo 23 | uses: actions/checkout@v4 24 | 25 | - name: Check typos 26 | uses: crate-ci/typos@v1.32.0 27 | 28 | format: 29 | name: Format 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Clone repo 33 | uses: actions/checkout@v4 34 | 35 | - name: Cache crates 36 | uses: Swatinem/rust-cache@v2 37 | 38 | - name: Install Taplo 39 | run: cargo install --locked taplo-cli 40 | 41 | - name: Format 42 | run: | 43 | cargo fmt --all --check 44 | taplo fmt --check 45 | 46 | lint: 47 | name: Lint 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Clone repo 51 | uses: actions/checkout@v4 52 | 53 | - name: Install dependencies 54 | run: | 55 | sudo apt-get update 56 | sudo apt-get install --no-install-recommends libudev-dev 57 | 58 | - name: Install stable toolchain 59 | uses: dtolnay/rust-toolchain@stable 60 | 61 | - name: Cache crates 62 | uses: Swatinem/rust-cache@v2 63 | 64 | - name: Clippy 65 | run: cargo clippy --tests --examples -- -D warnings 66 | 67 | - name: Rustdoc 68 | run: cargo rustdoc -- -D warnings 69 | 70 | no-std-portable-atomic: 71 | name: Without atomics and std 72 | runs-on: ubuntu-latest 73 | steps: 74 | - name: Clone repo 75 | uses: actions/checkout@v4 76 | 77 | # Use the same target platform as Bevy (Game Boy Advance). 78 | - name: Instal stable toolchain 79 | uses: dtolnay/rust-toolchain@stable 80 | with: 81 | targets: thumbv6m-none-eabi 82 | 83 | - name: Cache crates 84 | uses: Swatinem/rust-cache@v2 85 | 86 | - name: Check compilation 87 | run: cargo check --target thumbv6m-none-eabi --features bevy/critical-section 88 | 89 | doctest: 90 | name: Doctest 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Clone repo 94 | uses: actions/checkout@v4 95 | 96 | - name: Install dependencies 97 | run: | 98 | sudo apt-get update 99 | sudo apt-get install --no-install-recommends libudev-dev 100 | 101 | - name: Instal stable toolchain 102 | uses: dtolnay/rust-toolchain@stable 103 | 104 | - name: Cache crates 105 | uses: Swatinem/rust-cache@v2 106 | 107 | - name: Test doc 108 | run: cargo test --doc 109 | 110 | test: 111 | name: Test 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: Clone repo 115 | uses: actions/checkout@v4 116 | 117 | - name: Install dependencies 118 | run: | 119 | sudo apt-get update 120 | sudo apt-get install --no-install-recommends libudev-dev 121 | 122 | - name: Instal stable toolchain 123 | uses: dtolnay/rust-toolchain@stable 124 | 125 | - name: Cache crates 126 | uses: Swatinem/rust-cache@v2 127 | 128 | - name: Install LLVM tools 129 | run: rustup component add llvm-tools-preview 130 | 131 | - name: Install Tarpaulin 132 | run: cargo install cargo-tarpaulin 133 | 134 | # Use less job to prevent OOM while compiling. 135 | - name: Test 136 | run: cargo tarpaulin --engine llvm --out lcov --jobs 3 137 | 138 | - name: Upload code coverage results 139 | if: github.actor != 'dependabot[bot]' 140 | uses: actions/upload-artifact@v4 141 | with: 142 | name: code-coverage-report 143 | path: lcov.info 144 | 145 | codecov: 146 | name: Upload to Codecov 147 | if: github.actor != 'dependabot[bot]' 148 | needs: [typos, format, lint, no-std-portable-atomic, doctest, test] 149 | runs-on: ubuntu-latest 150 | steps: 151 | - name: Clone repo 152 | uses: actions/checkout@v4 153 | 154 | - name: Download code coverage results 155 | uses: actions/download-artifact@v4 156 | with: 157 | name: code-coverage-report 158 | 159 | - name: Upload to Codecov 160 | uses: codecov/codecov-action@v5 161 | with: 162 | token: ${{ secrets.CODECOV_TOKEN }} 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | target 3 | 4 | # Code coverage 5 | html 6 | 7 | # For library 8 | Cargo.lock 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_enhanced_input" 3 | version = "0.12.0" 4 | authors = [ 5 | "Hennadii Chernyshchyk ", 6 | "Alice Cecile ", 7 | ] 8 | edition = "2024" 9 | description = "Input manager for Bevy, inspired by Unreal Engine Enhanced Input" 10 | readme = "README.md" 11 | repository = "https://github.com/projectharmonia/bevy_enhanced_input" 12 | keywords = ["bevy", "input"] 13 | categories = ["game-development"] 14 | license = "MIT OR Apache-2.0" 15 | include = ["/src", "/LICENSE*"] 16 | 17 | [dependencies] 18 | bevy_enhanced_input_macros = { path = "macros", version = "0.12.0" } 19 | bevy = { version = "0.16.0", default-features = false, features = [ 20 | "serialize", 21 | ] } 22 | log = "0.4" # Directly depend on `log` like other `no_std` Bevy crates, since `bevy_log` currently requires `std`. 23 | variadics_please = "1.0" 24 | serde = { version = "1.0", default-features = false, features = ["derive"] } 25 | bitflags = { version = "2.6", default-features = false, features = ["serde"] } 26 | 27 | [dev-dependencies] 28 | bevy = { version = "0.16.0", default-features = false, features = [ 29 | "bevy_gilrs", 30 | "bevy_log", 31 | "bevy_pbr", 32 | "bevy_ui_picking_backend", 33 | "bevy_ui", 34 | "bevy_window", 35 | "default_font", 36 | "tonemapping_luts", 37 | "x11", 38 | ] } 39 | test-log = "0.2" 40 | ron = "0.8" 41 | 42 | [lints.clippy] 43 | type_complexity = "allow" 44 | alloc_instead_of_core = "warn" 45 | std_instead_of_alloc = "warn" 46 | std_instead_of_core = "warn" 47 | 48 | [workspace] 49 | members = ["macros"] 50 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hennadii Chernyshchyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bevy Enhanced Input 2 | 3 | [![crates.io](https://img.shields.io/crates/v/bevy_enhanced_input)](https://crates.io/crates/bevy_enhanced_input) 4 | [![docs.rs](https://docs.rs/bevy_enhanced_input/badge.svg)](https://docs.rs/bevy_enhanced_input) 5 | [![license](https://img.shields.io/crates/l/bevy_enhanced_input)](#license) 6 | [![codecov](https://codecov.io/gh/projectharmonia/bevy_enhanced_input/graph/badge.svg?token=wirFEuKmMz)](https://codecov.io/gh/projectharmonia/bevy_enhanced_input) 7 | 8 | Input manager for [Bevy](https://bevyengine.org), inspired by [Unreal Engine Enhanced Input](https://dev.epicgames.com/documentation/en-us/unreal-engine/enhanced-input-in-unreal-engine). 9 | 10 | ## Features 11 | 12 | * Map inputs from various sources (keyboard, gamepad, etc.) to gameplay actions like `Jump`, `Move`, or `Attack`. 13 | * Assign actions to different contexts like `OnFoot` or `InCar`, controlled by `Actions` components. 14 | * Layer multiple contexts on a single entity, controlled by priority. 15 | * Apply modifiers to inputs, such as dead zones, inversion, scaling, etc., or create custom modifiers by implementing a trait. 16 | * Assign conditions for how and when an action is triggered, like "hold", "tap", "chord", etc. You can also create custom conditions by implementing a trait. 17 | * Control how actions accumulate input from sources and consume it. 18 | * React to actions with observers. 19 | 20 | ## Getting Started 21 | 22 | Check out the [quick start guide](https://docs.rs/bevy_enhanced_input) for more details. 23 | 24 | See also examples in the repo. [simple_fly_cam.rs](examples/simple_fly_cam.rs) should be a good starting point. 25 | 26 | Have any questions? Feel free to ask in the dedicated [`bevy_enhanced_input` channel](https://discord.com/channels/691052431525675048/1297361733886677036) in Bevy's Discord server. 27 | 28 | ## Bevy compatibility 29 | 30 | | bevy | bevy_enhanced_input | 31 | | ----------- | ------------------- | 32 | | 0.16.0 | 0.11-0.12 | 33 | | 0.15.0 | 0.4-0.10 | 34 | | 0.14.0 | 0.1-0.3 | 35 | 36 | ## License 37 | 38 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option. 39 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = [ 3 | "Apache-2.0 WITH LLVM-exception", 4 | "Apache-2.0", 5 | "BSD-2-Clause", 6 | "BSD-3-Clause", 7 | "CC0-1.0", 8 | "ISC", 9 | "MIT-0", 10 | "MIT", 11 | "Unicode-3.0", 12 | "Zlib", 13 | ] 14 | 15 | [bans] 16 | skip-tree = ["bevy"] 17 | 18 | [advisories] 19 | version = 2 20 | ignore = ["RUSTSEC-2024-0436"] 21 | -------------------------------------------------------------------------------- /examples/all_conditions.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates all available input conditions. 2 | //! Press keys from the number row on the keyboard to trigger actions and observe the output in console. 3 | 4 | use bevy::{log::LogPlugin, prelude::*}; 5 | use bevy_enhanced_input::prelude::*; 6 | 7 | fn main() { 8 | // Setup logging to display triggered events. 9 | let mut log_plugin = LogPlugin::default(); 10 | log_plugin.filter += ",bevy_enhanced_input=debug"; 11 | 12 | App::new() 13 | .add_plugins(( 14 | DefaultPlugins.set(log_plugin), 15 | EnhancedInputPlugin, 16 | GamePlugin, 17 | )) 18 | .run(); 19 | } 20 | 21 | struct GamePlugin; 22 | 23 | impl Plugin for GamePlugin { 24 | fn build(&self, app: &mut App) { 25 | app.add_input_context::() 26 | .add_observer(binding) 27 | .add_systems(Startup, spawn); 28 | } 29 | } 30 | 31 | fn spawn(mut commands: Commands) { 32 | commands.spawn(Actions::::default()); 33 | } 34 | 35 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 36 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 37 | actions 38 | .bind::() 39 | .to(PressAction::KEY) 40 | .with_conditions(Down::default()); 41 | actions 42 | .bind::() 43 | .to(JustPressAction::KEY) 44 | .with_conditions(Press::default()); 45 | actions 46 | .bind::() 47 | .to(HoldAction::KEY) 48 | .with_conditions(Hold::new(1.0)); 49 | actions 50 | .bind::() 51 | .to(HoldAndReleaseAction::KEY) 52 | .with_conditions(HoldAndRelease::new(1.0)); 53 | actions 54 | .bind::() 55 | .to(PulseAction::KEY) 56 | .with_conditions(Pulse::new(1.0)); 57 | actions 58 | .bind::() 59 | .to(ReleaseAction::KEY) 60 | .with_conditions(Release::default()); 61 | actions 62 | .bind::() 63 | .to(TapAction::KEY) 64 | .with_conditions(Tap::new(0.5)); 65 | actions.bind::().to(ChordMember1::KEY); 66 | actions.bind::().to(ChordMember2::KEY); 67 | actions.bind::().with_conditions(( 68 | Chord::::default(), 69 | Chord::::default(), 70 | )); 71 | actions.bind::().to(BlockerAction::KEY); 72 | actions 73 | .bind::() 74 | .to(BlockByAction::KEY) 75 | .with_conditions(BlockBy::::default()); 76 | } 77 | 78 | #[derive(InputContext)] 79 | struct Test; 80 | 81 | #[derive(Debug, InputAction)] 82 | #[input_action(output = bool)] 83 | struct PressAction; 84 | 85 | impl PressAction { 86 | const KEY: KeyCode = KeyCode::Digit1; 87 | } 88 | 89 | #[derive(Debug, InputAction)] 90 | #[input_action(output = bool)] 91 | struct JustPressAction; 92 | 93 | impl JustPressAction { 94 | const KEY: KeyCode = KeyCode::Digit2; 95 | } 96 | 97 | #[derive(Debug, InputAction)] 98 | #[input_action(output = bool)] 99 | struct HoldAction; 100 | 101 | impl HoldAction { 102 | const KEY: KeyCode = KeyCode::Digit3; 103 | } 104 | 105 | #[derive(Debug, InputAction)] 106 | #[input_action(output = bool)] 107 | struct HoldAndReleaseAction; 108 | 109 | impl HoldAndReleaseAction { 110 | const KEY: KeyCode = KeyCode::Digit4; 111 | } 112 | 113 | #[derive(Debug, InputAction)] 114 | #[input_action(output = bool)] 115 | struct PulseAction; 116 | 117 | impl PulseAction { 118 | const KEY: KeyCode = KeyCode::Digit5; 119 | } 120 | 121 | #[derive(Debug, InputAction)] 122 | #[input_action(output = bool)] 123 | struct ReleaseAction; 124 | 125 | impl ReleaseAction { 126 | const KEY: KeyCode = KeyCode::Digit6; 127 | } 128 | 129 | #[derive(Debug, InputAction)] 130 | #[input_action(output = bool)] 131 | struct TapAction; 132 | 133 | impl TapAction { 134 | const KEY: KeyCode = KeyCode::Digit7; 135 | } 136 | 137 | #[derive(Debug, InputAction)] 138 | #[input_action(output = bool)] 139 | struct ChordMember1; 140 | 141 | impl ChordMember1 { 142 | const KEY: KeyCode = KeyCode::Digit8; 143 | } 144 | 145 | #[derive(Debug, InputAction)] 146 | #[input_action(output = bool)] 147 | struct ChordMember2; 148 | 149 | impl ChordMember2 { 150 | const KEY: KeyCode = KeyCode::Digit9; 151 | } 152 | 153 | #[derive(Debug, InputAction)] 154 | #[input_action(output = bool)] 155 | struct BlockerAction; 156 | 157 | impl BlockerAction { 158 | const KEY: KeyCode = KeyCode::Digit0; 159 | } 160 | 161 | #[derive(Debug, InputAction)] 162 | #[input_action(output = bool)] 163 | struct ChordAction; 164 | 165 | #[derive(Debug, InputAction)] 166 | #[input_action(output = bool)] 167 | struct BlockByAction; 168 | 169 | impl BlockByAction { 170 | const KEY: KeyCode = KeyCode::Minus; 171 | } 172 | -------------------------------------------------------------------------------- /examples/context_layering.rs: -------------------------------------------------------------------------------- 1 | //! One context applied on top of another and overrides some of the bindings. 2 | 3 | use bevy::prelude::*; 4 | use bevy_enhanced_input::prelude::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_input_context::() 11 | .add_observer(regular_binding) 12 | .add_observer(swimming_binding) 13 | .add_observer(apply_movement) 14 | .add_observer(jump) 15 | .add_observer(exit_car) 16 | .add_observer(enter_car) 17 | .add_observer(brake) 18 | .add_systems(Startup, spawn) 19 | .run(); 20 | } 21 | 22 | fn spawn(mut commands: Commands) { 23 | commands.spawn(Actions::::default()); 24 | } 25 | 26 | fn regular_binding(trigger: Trigger>, mut players: Query<&mut Actions>) { 27 | let mut actions = players.get_mut(trigger.target()).unwrap(); 28 | actions 29 | .bind::() 30 | .to((Cardinal::wasd_keys(), Axial::left_stick())) 31 | .with_modifiers(DeadZone::default()); 32 | actions 33 | .bind::() 34 | .to((KeyCode::Space, GamepadButton::South)); 35 | actions 36 | .bind::() 37 | .to((KeyCode::Enter, GamepadButton::North)); 38 | } 39 | 40 | fn swimming_binding(trigger: Trigger>, mut players: Query<&mut Actions>) { 41 | let mut actions = players.get_mut(trigger.target()).unwrap(); 42 | // `Player` has lower priority, so `Brake` and `ExitCar` consume inputs first, 43 | // preventing `Rotate` and `EnterWater` from being triggered. 44 | // The consuming behavior can be configured in the `InputAction` trait. 45 | actions 46 | .bind::() 47 | .to((KeyCode::Space, GamepadButton::East)); 48 | actions 49 | .bind::() 50 | .to((KeyCode::Enter, GamepadButton::North)); 51 | } 52 | 53 | fn apply_movement(trigger: Trigger>) { 54 | info!("moving: {}", trigger.value); 55 | } 56 | 57 | fn jump(_trigger: Trigger>) { 58 | info!("jumping"); 59 | } 60 | 61 | fn enter_car(trigger: Trigger>, mut commands: Commands) { 62 | info!("entering car"); 63 | commands 64 | .entity(trigger.target()) 65 | .insert(Actions::::default()); 66 | } 67 | 68 | fn brake(_trigger: Trigger>) { 69 | info!("braking"); 70 | } 71 | 72 | fn exit_car(trigger: Trigger>, mut commands: Commands) { 73 | info!("exiting car"); 74 | commands 75 | .entity(trigger.target()) 76 | .remove::>(); 77 | } 78 | 79 | #[derive(InputContext)] 80 | struct Player; 81 | 82 | #[derive(Debug, InputAction)] 83 | #[input_action(output = Vec2)] 84 | struct Move; 85 | 86 | #[derive(Debug, InputAction)] 87 | #[input_action(output = bool)] 88 | struct Jump; 89 | 90 | /// Adds [`Driving`]. 91 | #[derive(Debug, InputAction)] 92 | #[input_action(output = bool)] 93 | struct EnterCar; 94 | 95 | /// Overrides some actions from [`Player`]. 96 | #[derive(InputContext)] 97 | #[input_context(priority = 1)] 98 | struct Driving; 99 | 100 | /// This action overrides [`Jump`] when the player is [`Driving`]. 101 | #[derive(Debug, InputAction)] 102 | #[input_action(output = bool)] 103 | struct Brake; 104 | 105 | /// Removes [`Driving`]. 106 | /// 107 | /// We set `require_reset` to `true` because [`EnterWater`] action uses the same input, 108 | /// and we want it to be triggerable only after the button is released. 109 | #[derive(Debug, InputAction)] 110 | #[input_action(output = bool, require_reset = true)] 111 | struct ExitCar; 112 | -------------------------------------------------------------------------------- /examples/context_switch.rs: -------------------------------------------------------------------------------- 1 | //! One context completely replaces another. 2 | 3 | use bevy::prelude::*; 4 | use bevy_enhanced_input::prelude::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_input_context::() 11 | .add_observer(player_binding) 12 | .add_observer(inventory_binding) 13 | .add_observer(apply_movement) 14 | .add_observer(attack) 15 | .add_observer(open_inventory) 16 | .add_observer(navigate_inventory) 17 | .add_observer(close_inventory) 18 | .add_systems(Startup, spawn) 19 | .run(); 20 | } 21 | 22 | fn spawn(mut commands: Commands) { 23 | commands.spawn(Actions::::default()); 24 | } 25 | 26 | fn player_binding(trigger: Trigger>, mut players: Query<&mut Actions>) { 27 | let mut actions = players.get_mut(trigger.target()).unwrap(); 28 | actions 29 | .bind::() 30 | .to((Cardinal::wasd_keys(), Axial::left_stick())) 31 | .with_modifiers(DeadZone::default()); 32 | actions 33 | .bind::() 34 | .to((MouseButton::Left, GamepadButton::West)); 35 | actions 36 | .bind::() 37 | .to((KeyCode::KeyI, GamepadButton::Select)); 38 | } 39 | 40 | fn inventory_binding( 41 | trigger: Trigger>, 42 | mut players: Query<&mut Actions>, 43 | ) { 44 | let mut actions = players.get_mut(trigger.target()).unwrap(); 45 | actions 46 | .bind::() 47 | .to((Cardinal::wasd_keys(), Axial::left_stick())) 48 | .with_conditions(Pulse::new(0.2)); // Avoid triggering every frame on hold for UI. 49 | actions 50 | .bind::() 51 | .to((KeyCode::KeyI, GamepadButton::Select)); 52 | } 53 | 54 | fn apply_movement(trigger: Trigger>) { 55 | info!("moving: {}", trigger.value); 56 | } 57 | 58 | fn attack(_trigger: Trigger>) { 59 | info!("attacking"); 60 | } 61 | 62 | fn open_inventory(trigger: Trigger>, mut commands: Commands) { 63 | info!("opening inventory"); 64 | commands 65 | .entity(trigger.target()) 66 | .remove::>() 67 | .insert(Actions::::default()); 68 | } 69 | 70 | fn navigate_inventory(_trigger: Trigger>) { 71 | info!("navigating inventory"); 72 | } 73 | 74 | fn close_inventory(trigger: Trigger>, mut commands: Commands) { 75 | info!("closing inventory"); 76 | commands 77 | .entity(trigger.target()) 78 | .remove::>() 79 | .insert(Actions::::default()); 80 | } 81 | 82 | #[derive(InputContext)] 83 | struct Player; 84 | 85 | #[derive(Debug, InputAction)] 86 | #[input_action(output = Vec2)] 87 | struct Move; 88 | 89 | #[derive(Debug, InputAction)] 90 | #[input_action(output = bool)] 91 | struct Attack; 92 | 93 | /// Switches context to [`Inventory`]. 94 | /// 95 | /// We set `require_reset` to `true` because [`CloseInventory`] action uses the same input, 96 | /// and we want it to be triggerable only after the button is released. 97 | #[derive(Debug, InputAction)] 98 | #[input_action(output = bool, require_reset = true)] 99 | struct OpenInventory; 100 | 101 | #[derive(InputContext)] 102 | struct Inventory; 103 | 104 | #[derive(Debug, InputAction)] 105 | #[input_action(output = Vec2)] 106 | struct NavigateInventory; 107 | 108 | /// Switches context to [`Player`]. 109 | /// 110 | /// See [`OpenInventory`] for details about `require_reset`. 111 | #[derive(Debug, InputAction)] 112 | #[input_action(output = bool, require_reset = true)] 113 | struct CloseInventory; 114 | -------------------------------------------------------------------------------- /examples/local_multiplayer.rs: -------------------------------------------------------------------------------- 1 | //! Two players that use the same context type, but with different bindings. 2 | 3 | use bevy::{input::gamepad::GamepadConnectionEvent, prelude::*}; 4 | use bevy_enhanced_input::prelude::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, EnhancedInputPlugin)) 9 | .init_resource::() 10 | .add_input_context::() 11 | .add_observer(binding) 12 | .add_observer(apply_movement) 13 | .add_systems(Startup, spawn) 14 | .add_systems( 15 | Update, 16 | update_gamepads.run_if(on_event::), 17 | ) 18 | .run(); 19 | } 20 | 21 | fn spawn( 22 | mut commands: Commands, 23 | mut meshes: ResMut>, 24 | mut materials: ResMut>, 25 | ) { 26 | commands.spawn(( 27 | Camera3d::default(), 28 | Transform::from_xyz(0.0, 25.0, 0.0).looking_at(-Vec3::Y, Vec3::Y), 29 | )); 30 | 31 | commands.spawn(( 32 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0)))), 33 | MeshMaterial3d(materials.add(Color::WHITE)), 34 | )); 35 | commands.spawn(( 36 | PointLight { 37 | shadows_enabled: true, 38 | ..Default::default() 39 | }, 40 | Transform::from_xyz(4.0, 8.0, 4.0), 41 | )); 42 | 43 | // Spawn two players with different assigned indices. 44 | let capsule = meshes.add(Capsule3d::new(0.5, 2.0)); 45 | commands.spawn(( 46 | Mesh3d(capsule.clone()), 47 | MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), 48 | Transform::from_xyz(0.0, 1.5, 8.0), 49 | Actions::::default(), 50 | Player::First, 51 | )); 52 | 53 | commands.spawn(( 54 | Mesh3d(capsule), 55 | MeshMaterial3d(materials.add(Color::srgb_u8(220, 90, 90))), 56 | Transform::from_xyz(0.0, 1.5, -8.0), 57 | Actions::::default(), 58 | Player::Second, 59 | )); 60 | } 61 | 62 | fn binding( 63 | trigger: Trigger>, 64 | gamepads: Query>, 65 | mut players: Query<(&Player, &mut Actions)>, 66 | ) { 67 | let (&player, mut actions) = players.get_mut(trigger.target()).unwrap(); 68 | 69 | // By default actions read inputs from all gamepads, 70 | // but for local multiplayer we need assign specific 71 | // gamepad index. If no gamepad with the given exists, 72 | // use a placeholder to disable gamepad input. 73 | let gamepad_entity = gamepads.iter().nth(player as usize); 74 | actions.set_gamepad(gamepad_entity.unwrap_or(Entity::PLACEHOLDER)); 75 | 76 | // Assign different bindings based player index. 77 | match player { 78 | Player::First => { 79 | actions 80 | .bind::() 81 | .to((Cardinal::wasd_keys(), Axial::left_stick())); 82 | } 83 | Player::Second => { 84 | actions 85 | .bind::() 86 | .to((Cardinal::arrow_keys(), Axial::left_stick())); 87 | } 88 | } 89 | 90 | // Can be called multiple times extend bindings. 91 | // In our case we add modifiers for all players. 92 | actions 93 | .bind::() 94 | .with_modifiers((DeadZone::default(), SmoothNudge::default())); 95 | } 96 | 97 | fn apply_movement(trigger: Trigger>, mut players: Query<&mut Transform>) { 98 | let mut transform = players.get_mut(trigger.target()).unwrap(); 99 | 100 | // Adjust axes for top-down movement. 101 | transform.translation.z -= trigger.value.x; 102 | transform.translation.x -= trigger.value.y; 103 | 104 | // Prevent from moving out of plane. 105 | transform.translation.z = transform.translation.z.clamp(-10.0, 10.0); 106 | transform.translation.x = transform.translation.x.clamp(-10.0, 10.0); 107 | } 108 | 109 | fn update_gamepads(mut commands: Commands) { 110 | commands.trigger(RebuildBindings); 111 | } 112 | 113 | /// Used as both input context and component. 114 | #[derive(InputContext, Component, Clone, Copy, PartialEq, Eq, Hash)] 115 | enum Player { 116 | First, 117 | Second, 118 | } 119 | 120 | /// A resource that tracks all connected gamepads to pick them by index. 121 | #[derive(Resource, Default, Deref, DerefMut)] 122 | struct Gamepads(Vec); 123 | 124 | #[derive(Debug, InputAction)] 125 | #[input_action(output = Vec2)] 126 | struct Move; 127 | -------------------------------------------------------------------------------- /examples/simple_fly_cam.rs: -------------------------------------------------------------------------------- 1 | //! Simple fly camera with a single context. 2 | 3 | use bevy::{prelude::*, window::CursorGrabMode}; 4 | use bevy_enhanced_input::prelude::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, EnhancedInputPlugin)) 9 | .add_input_context::() // All contexts should be registered. 10 | .add_observer(binding) // Add observer to setup bindings. 11 | .add_observer(apply_movement) 12 | .add_observer(capture_cursor) 13 | .add_observer(release_cursor) 14 | .add_observer(rotate) 15 | .add_systems(Startup, setup) 16 | .run(); 17 | } 18 | 19 | fn setup( 20 | mut commands: Commands, 21 | mut meshes: ResMut>, 22 | mut materials: ResMut>, 23 | mut window: Single<&mut Window>, 24 | ) { 25 | grab_cursor(&mut window, true); 26 | 27 | // Spawn a camera with `Actions` component. 28 | commands.spawn(( 29 | Camera3d::default(), 30 | Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), 31 | Actions::::default(), 32 | )); 33 | 34 | // Setup simple 3D scene. 35 | commands.spawn(( 36 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(25.0)))), 37 | MeshMaterial3d(materials.add(Color::WHITE)), 38 | )); 39 | commands.spawn(( 40 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 41 | MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), 42 | Transform::from_xyz(0.0, 0.5, 0.0), 43 | )); 44 | commands.spawn(( 45 | PointLight { 46 | shadows_enabled: true, 47 | ..Default::default() 48 | }, 49 | Transform::from_xyz(4.0, 8.0, 4.0), 50 | )); 51 | } 52 | 53 | // To define bindings for actions, write an observer for `Binding`. 54 | // It's also possible to create bindings before the insertion, 55 | // but this way you can conveniently reload bindings when settings change. 56 | fn binding(trigger: Trigger>, mut players: Query<&mut Actions>) { 57 | let mut actions = players.get_mut(trigger.target()).unwrap(); 58 | 59 | // Bindings like WASD or sticks are very common, 60 | // so we provide built-ins to assign all keys/axes at once. 61 | // We don't assign any conditions and in this case the action will 62 | // be triggered with any non-zero value. 63 | // An action can have multiple inputs bound to it 64 | // and will respond to any of them. 65 | actions 66 | .bind::() 67 | .to((Cardinal::wasd_keys(), Axial::left_stick())) 68 | .with_modifiers(( 69 | DeadZone::default(), // Apply non-uniform normalization to ensure consistent speed, otherwise diagonal movement will be faster. 70 | SmoothNudge::default(), // Make movement smooth and independent of the framerate. To only make it framerate-independent, use `DeltaScale`. 71 | Scale::splat(0.3), // Additionally multiply by a constant to achieve the desired speed. 72 | )); 73 | 74 | actions.bind::().to(( 75 | // You can attach modifiers to individual inputs as well. 76 | Input::mouse_motion().with_modifiers((Scale::splat(0.1), Negate::all())), 77 | Axial::right_stick().with_modifiers_each((Scale::splat(2.0), Negate::x())), 78 | )); 79 | 80 | actions.bind::().to(MouseButton::Left); 81 | actions.bind::().to(KeyCode::Escape); 82 | } 83 | 84 | fn apply_movement(trigger: Trigger>, mut transforms: Query<&mut Transform>) { 85 | let mut transform = transforms.get_mut(trigger.target()).unwrap(); 86 | 87 | // Move to the camera direction. 88 | let rotation = transform.rotation; 89 | 90 | // Movement consists of X and -Z components, so swap Y and Z with negation. 91 | // We could do it with modifiers, but it wold be weird for an action to return 92 | // a `Vec3` like this, so we doing it inside the function. 93 | let mut movement = trigger.value.extend(0.0).xzy(); 94 | movement.z = -movement.z; 95 | 96 | transform.translation += rotation * movement 97 | } 98 | 99 | fn rotate( 100 | trigger: Trigger>, 101 | mut players: Query<&mut Transform>, 102 | window: Single<&Window>, 103 | ) { 104 | if window.cursor_options.visible { 105 | return; 106 | } 107 | 108 | let mut transform = players.get_mut(trigger.target()).unwrap(); 109 | let (mut yaw, mut pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); 110 | 111 | yaw += trigger.value.x.to_radians(); 112 | pitch += trigger.value.y.to_radians(); 113 | 114 | transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0); 115 | } 116 | 117 | fn capture_cursor(_trigger: Trigger>, mut window: Single<&mut Window>) { 118 | grab_cursor(&mut window, true); 119 | } 120 | 121 | fn release_cursor(_trigger: Trigger>, mut window: Single<&mut Window>) { 122 | grab_cursor(&mut window, false); 123 | } 124 | 125 | fn grab_cursor(window: &mut Window, grab: bool) { 126 | window.cursor_options.grab_mode = if grab { 127 | CursorGrabMode::Confined 128 | } else { 129 | CursorGrabMode::None 130 | }; 131 | window.cursor_options.visible = !grab; 132 | } 133 | 134 | // Since it's possible to have multiple `Actions` components, you need 135 | // to define a marker and derive `InputContext` trait. 136 | #[derive(InputContext)] 137 | struct FlyCam; 138 | 139 | // All actions should implement the `InputAction` trait. 140 | // It can be done manually, but we provide a derive for convenience. 141 | // The only necessary parameter is `output`, which defines the output type. 142 | #[derive(Debug, InputAction)] 143 | #[input_action(output = Vec2)] 144 | struct Move; 145 | 146 | #[derive(Debug, InputAction)] 147 | #[input_action(output = bool)] 148 | struct CaptureCursor; 149 | 150 | #[derive(Debug, InputAction)] 151 | #[input_action(output = bool)] 152 | struct ReleaseCursor; 153 | 154 | #[derive(Debug, InputAction)] 155 | #[input_action(output = Vec2)] 156 | struct Rotate; 157 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_enhanced_input_macros" 3 | version = "0.12.0" 4 | authors = [ 5 | "Hennadii Chernyshchyk ", 6 | "Alice Cecile ", 7 | ] 8 | edition = "2021" 9 | description = "Bevy Enhanced Input Macros" 10 | readme = "../README.md" 11 | repository = "https://github.com/projectharmonia/bevy_enhanced_input" 12 | keywords = ["bevy", "input", "macros"] 13 | categories = ["game-development"] 14 | license = "MIT OR Apache-2.0" 15 | include = ["/src", "../LICENSE*"] 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | darling = "0.20" 22 | syn = { version = "2.0", features = ["full"] } 23 | quote = "1.0" 24 | proc-macro2 = "1.0" 25 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromDeriveInput; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::{parse_macro_input, DeriveInput, Ident}; 5 | 6 | #[derive(FromDeriveInput)] 7 | #[darling(attributes(input_action))] 8 | struct InputActionOpts { 9 | output: Ident, 10 | #[darling(default)] 11 | accumulation: Option, 12 | #[darling(default)] 13 | consume_input: Option, 14 | #[darling(default)] 15 | require_reset: Option, 16 | } 17 | 18 | #[derive(FromDeriveInput)] 19 | #[darling(attributes(input_context))] 20 | struct InputContextOpts { 21 | #[darling(default)] 22 | schedule: Option, 23 | #[darling(default)] 24 | priority: Option, 25 | } 26 | 27 | #[proc_macro_derive(InputAction, attributes(input_action))] 28 | pub fn input_action_derive(item: TokenStream) -> TokenStream { 29 | let input = parse_macro_input!(item as DeriveInput); 30 | 31 | #[expect(non_snake_case, reason = "item shortcuts")] 32 | let (Accumulation, InputAction) = ( 33 | quote! { ::bevy_enhanced_input::prelude::Accumulation }, 34 | quote! { ::bevy_enhanced_input::prelude::InputAction }, 35 | ); 36 | 37 | let opts = match InputActionOpts::from_derive_input(&input) { 38 | Ok(value) => value, 39 | Err(e) => { 40 | return e.write_errors().into(); 41 | } 42 | }; 43 | 44 | let struct_name = input.ident; 45 | let output = opts.output; 46 | let accumulation = if let Some(accumulation) = opts.accumulation { 47 | quote! { 48 | const ACCUMULATION: #Accumulation = #Accumulation::#accumulation; 49 | } 50 | } else { 51 | Default::default() 52 | }; 53 | let consume_input = if let Some(consume) = opts.consume_input { 54 | quote! { 55 | const CONSUME_INPUT: bool = #consume; 56 | } 57 | } else { 58 | Default::default() 59 | }; 60 | let require_reset = if let Some(reset) = opts.require_reset { 61 | quote! { 62 | const REQUIRE_RESET: bool = #reset; 63 | } 64 | } else { 65 | Default::default() 66 | }; 67 | 68 | let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); 69 | 70 | TokenStream::from(quote! { 71 | impl #impl_generics #InputAction for #struct_name #type_generics #where_clause { 72 | type Output = #output; 73 | #accumulation 74 | #consume_input 75 | #require_reset 76 | } 77 | }) 78 | } 79 | 80 | #[proc_macro_derive(InputContext, attributes(input_context))] 81 | pub fn input_context_derive(item: TokenStream) -> TokenStream { 82 | let input = parse_macro_input!(item as DeriveInput); 83 | 84 | #[expect(non_snake_case, reason = "item shortcuts")] 85 | let InputContext = quote! { ::bevy_enhanced_input::prelude::InputContext }; 86 | 87 | let opts = match InputContextOpts::from_derive_input(&input) { 88 | Ok(value) => value, 89 | Err(e) => { 90 | return e.write_errors().into(); 91 | } 92 | }; 93 | 94 | let struct_name = input.ident; 95 | let priority = if let Some(priority) = opts.priority { 96 | quote! { 97 | const PRIORITY: usize = #priority; 98 | } 99 | } else { 100 | Default::default() 101 | }; 102 | let schedule = if let Some(schedule) = opts.schedule { 103 | quote! { #schedule } 104 | } else { 105 | quote! { ::bevy::app::PreUpdate } 106 | }; 107 | 108 | let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); 109 | 110 | TokenStream::from(quote! { 111 | impl #impl_generics #InputContext for #struct_name #type_generics #where_clause { 112 | type Schedule = #schedule; 113 | #priority 114 | } 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /src/action_map.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | any::{self, TypeId}, 3 | fmt::Debug, 4 | }; 5 | 6 | use bevy::{prelude::*, utils::TypeIdMap}; 7 | use log::debug; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::{input_action::ActionOutput, prelude::*}; 11 | 12 | /// Maps markers that implement [`InputAction`] to their data (state, value, etc.). 13 | /// 14 | /// Stored inside [`Actions`]. 15 | /// 16 | /// Accessible from [`InputCondition::evaluate`] and [`InputModifier::apply`]. 17 | #[derive(Default, Deref, DerefMut)] 18 | pub struct ActionMap(TypeIdMap); 19 | 20 | impl ActionMap { 21 | /// Returns associated state for action `A`. 22 | pub fn action(&self) -> Option<&Action> { 23 | self.get(&TypeId::of::()) 24 | } 25 | } 26 | 27 | /// Data associated with an [`InputAction`] marker. 28 | /// 29 | /// Stored inside [`ActionMap`]. 30 | #[derive(Clone, Copy)] 31 | pub struct Action { 32 | state: ActionState, 33 | events: ActionEvents, 34 | value: ActionValue, 35 | elapsed_secs: f32, 36 | fired_secs: f32, 37 | trigger_events: fn(&Self, &mut Commands, Entity), 38 | } 39 | 40 | impl Action { 41 | /// Creates a new instance associated with action `A`. 42 | /// 43 | /// [`Self::trigger_events`] will trigger events for `A`. 44 | #[must_use] 45 | pub(crate) fn new() -> Self { 46 | Self { 47 | state: Default::default(), 48 | events: ActionEvents::empty(), 49 | value: ActionValue::zero(A::Output::DIM), 50 | elapsed_secs: 0.0, 51 | fired_secs: 0.0, 52 | trigger_events: Self::trigger_events_typed::, 53 | } 54 | } 55 | 56 | /// Updates internal state. 57 | pub(crate) fn update( 58 | &mut self, 59 | time: &Time, 60 | state: ActionState, 61 | value: impl Into, 62 | ) { 63 | match self.state { 64 | ActionState::None => { 65 | self.elapsed_secs = 0.0; 66 | self.fired_secs = 0.0; 67 | } 68 | ActionState::Ongoing => { 69 | self.elapsed_secs += time.delta_secs(); 70 | self.fired_secs = 0.0; 71 | } 72 | ActionState::Fired => { 73 | self.elapsed_secs += time.delta_secs(); 74 | self.fired_secs += time.delta_secs(); 75 | } 76 | } 77 | 78 | self.events = ActionEvents::new(self.state, state); 79 | self.state = state; 80 | self.value = value.into(); 81 | } 82 | 83 | /// Triggers events resulting from a state transition after [`Self::update`]. 84 | /// 85 | /// See also [`Self::new`] and [`ActionEvents`]. 86 | pub(crate) fn trigger_events(&self, commands: &mut Commands, entity: Entity) { 87 | (self.trigger_events)(self, commands, entity); 88 | } 89 | 90 | /// A typed version of [`Self::trigger_events`]. 91 | fn trigger_events_typed(&self, commands: &mut Commands, entity: Entity) { 92 | for (_, event) in self.events.iter_names() { 93 | match event { 94 | ActionEvents::STARTED => { 95 | trigger_and_log::( 96 | commands, 97 | entity, 98 | Started:: { 99 | value: A::Output::as_output(self.value), 100 | state: self.state, 101 | }, 102 | ); 103 | } 104 | ActionEvents::ONGOING => { 105 | trigger_and_log::( 106 | commands, 107 | entity, 108 | Ongoing:: { 109 | value: A::Output::as_output(self.value), 110 | state: self.state, 111 | elapsed_secs: self.elapsed_secs, 112 | }, 113 | ); 114 | } 115 | ActionEvents::FIRED => { 116 | trigger_and_log::( 117 | commands, 118 | entity, 119 | Fired:: { 120 | value: A::Output::as_output(self.value), 121 | state: self.state, 122 | fired_secs: self.fired_secs, 123 | elapsed_secs: self.elapsed_secs, 124 | }, 125 | ); 126 | } 127 | ActionEvents::CANCELED => { 128 | trigger_and_log::( 129 | commands, 130 | entity, 131 | Canceled:: { 132 | value: A::Output::as_output(self.value), 133 | state: self.state, 134 | elapsed_secs: self.elapsed_secs, 135 | }, 136 | ); 137 | } 138 | ActionEvents::COMPLETED => { 139 | trigger_and_log::( 140 | commands, 141 | entity, 142 | Completed:: { 143 | value: A::Output::as_output(self.value), 144 | state: self.state, 145 | fired_secs: self.fired_secs, 146 | elapsed_secs: self.elapsed_secs, 147 | }, 148 | ); 149 | } 150 | _ => unreachable!("iteration should yield only named flags"), 151 | } 152 | } 153 | } 154 | 155 | /// Returns the current state. 156 | pub fn state(&self) -> ActionState { 157 | self.state 158 | } 159 | 160 | /// Returns events triggered by a transition of [`Self::state`] since the last update. 161 | pub fn events(&self) -> ActionEvents { 162 | self.events 163 | } 164 | 165 | /// Returns the value since the last update. 166 | /// 167 | /// Unlike when reading values from triggers, this returns [`ActionValue`] since actions 168 | /// are stored in a type-erased format. 169 | pub fn value(&self) -> ActionValue { 170 | self.value 171 | } 172 | 173 | /// Time the action was in [`ActionState::Ongoing`] and [`ActionState::Fired`] states. 174 | pub fn elapsed_secs(&self) -> f32 { 175 | self.elapsed_secs 176 | } 177 | 178 | /// Time the action was in [`ActionState::Fired`] state. 179 | pub fn fired_secs(&self) -> f32 { 180 | self.fired_secs 181 | } 182 | } 183 | 184 | fn trigger_and_log(commands: &mut Commands, entity: Entity, event: E) { 185 | debug!( 186 | "triggering `{event:?}` for `{}` for `{entity}`", 187 | any::type_name::() 188 | ); 189 | commands.trigger_targets(event, entity); 190 | } 191 | 192 | /// State for [`Action`]. 193 | /// 194 | /// States are ordered by their significance. 195 | /// 196 | /// See also [`ActionEvents`] and [`ActionBinding`](). 197 | #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 198 | pub enum ActionState { 199 | /// Condition is not triggered. 200 | #[default] 201 | None, 202 | /// Condition has started triggering, but has not yet finished. 203 | /// 204 | /// For example, [`Hold`] condition requires its state to be 205 | /// maintained over several frames. 206 | Ongoing, 207 | /// The condition has been met. 208 | Fired, 209 | } 210 | -------------------------------------------------------------------------------- /src/action_value.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use bevy::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Value from [`Input`](crate::input::Input) for [`Action`](crate::action_map::Action). 7 | /// 8 | /// Can be optionally modified by [`InputModifier`](crate::input_modifier::InputModifier) 9 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)] 10 | pub enum ActionValue { 11 | Bool(bool), 12 | Axis1D(f32), 13 | Axis2D(Vec2), 14 | Axis3D(Vec3), 15 | } 16 | 17 | impl ActionValue { 18 | /// Creates a zero-initialized value for the specified dimension. 19 | pub fn zero(dim: ActionValueDim) -> Self { 20 | match dim { 21 | ActionValueDim::Bool => ActionValue::Bool(false), 22 | ActionValueDim::Axis1D => ActionValue::Axis1D(0.0), 23 | ActionValueDim::Axis2D => ActionValue::Axis2D(Vec2::ZERO), 24 | ActionValueDim::Axis3D => ActionValue::Axis3D(Vec3::ZERO), 25 | } 26 | } 27 | 28 | /// Returns dimension. 29 | pub fn dim(self) -> ActionValueDim { 30 | match self { 31 | Self::Bool(_) => ActionValueDim::Bool, 32 | Self::Axis1D(_) => ActionValueDim::Axis1D, 33 | Self::Axis2D(_) => ActionValueDim::Axis2D, 34 | Self::Axis3D(_) => ActionValueDim::Axis3D, 35 | } 36 | } 37 | 38 | /// Converts the value into the specified variant based on the dimension. 39 | /// 40 | /// If the new dimension is larger, the additional axes will be set to zero. 41 | /// If the new dimension is smaller, the extra axes will be discarded. 42 | pub fn convert(self, dim: ActionValueDim) -> Self { 43 | match dim { 44 | ActionValueDim::Bool => self.as_bool().into(), 45 | ActionValueDim::Axis1D => self.as_axis1d().into(), 46 | ActionValueDim::Axis2D => self.as_axis2d().into(), 47 | ActionValueDim::Axis3D => self.as_axis3d().into(), 48 | } 49 | } 50 | 51 | /// Returns `true` if the value in sufficiently large. 52 | pub fn is_actuated(self, actuation: f32) -> bool { 53 | self.as_axis3d().length_squared() >= actuation * actuation 54 | } 55 | 56 | /// Returns the value as a boolean. 57 | /// 58 | /// If the value is not [`ActionValue::Bool`], 59 | /// it returns `false` if the value is zero, and `true` otherwise. 60 | pub fn as_bool(self) -> bool { 61 | match self { 62 | Self::Bool(value) => value, 63 | Self::Axis1D(value) => value != 0.0, 64 | Self::Axis2D(value) => value != Vec2::ZERO, 65 | Self::Axis3D(value) => value != Vec3::ZERO, 66 | } 67 | } 68 | 69 | /// Returns the value as a 1-dimensional axis. 70 | /// 71 | /// For [`ActionValue::Bool`], it returns `1.0` if `true`, otherwise `0.0`. 72 | /// For multi-dimensional values, it returns the X axis. 73 | pub fn as_axis1d(self) -> f32 { 74 | match self { 75 | Self::Bool(value) => { 76 | if value { 77 | 1.0 78 | } else { 79 | 0.0 80 | } 81 | } 82 | Self::Axis1D(value) => value, 83 | Self::Axis2D(value) => value.x, 84 | Self::Axis3D(value) => value.x, 85 | } 86 | } 87 | 88 | /// Returns the value as a 2-dimensional axis. 89 | /// 90 | /// For [`ActionValue::Bool`], it returns [`Vec2::X`] if `true`, otherwise [`Vec2::ZERO`]. 91 | /// For [`ActionValue::Axis1D`], it maps the value to the X axis. 92 | /// For [`ActionValue::Axis3D`], it returns the X and Y axes. 93 | pub fn as_axis2d(self) -> Vec2 { 94 | match self { 95 | Self::Bool(value) => { 96 | if value { 97 | Vec2::X 98 | } else { 99 | Vec2::ZERO 100 | } 101 | } 102 | Self::Axis1D(value) => Vec2::X * value, 103 | Self::Axis2D(value) => value, 104 | Self::Axis3D(value) => value.xy(), 105 | } 106 | } 107 | 108 | /// Returns the value as a 3-dimensional axis . 109 | /// 110 | /// For [`ActionValue::Bool`], it returns [`Vec3::X`] if `true`, otherwise [`Vec3::ZERO`]. 111 | /// For [`ActionValue::Axis1D`], it maps the value to the X axis. 112 | /// For [`ActionValue::Axis2D`], it maps the value to the X and Y axes. 113 | pub fn as_axis3d(self) -> Vec3 { 114 | match self { 115 | Self::Bool(value) => { 116 | if value { 117 | Vec3::X 118 | } else { 119 | Vec3::ZERO 120 | } 121 | } 122 | Self::Axis1D(value) => Vec3::X * value, 123 | Self::Axis2D(value) => value.extend(0.0), 124 | Self::Axis3D(value) => value, 125 | } 126 | } 127 | } 128 | 129 | /// A dimension discriminant for [`ActionValue`]. 130 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] 131 | pub enum ActionValueDim { 132 | Bool, 133 | Axis1D, 134 | Axis2D, 135 | Axis3D, 136 | } 137 | 138 | impl From for ActionValue { 139 | fn from(value: bool) -> Self { 140 | ActionValue::Bool(value) 141 | } 142 | } 143 | 144 | impl From for ActionValue { 145 | fn from(value: f32) -> Self { 146 | ActionValue::Axis1D(value) 147 | } 148 | } 149 | 150 | impl From for ActionValue { 151 | fn from(value: Vec2) -> Self { 152 | ActionValue::Axis2D(value) 153 | } 154 | } 155 | 156 | impl From for ActionValue { 157 | fn from(value: Vec3) -> Self { 158 | ActionValue::Axis3D(value) 159 | } 160 | } 161 | 162 | impl From<(f32, f32)> for ActionValue { 163 | fn from(value: (f32, f32)) -> Self { 164 | ActionValue::Axis2D(value.into()) 165 | } 166 | } 167 | 168 | impl From<(f32, f32, f32)> for ActionValue { 169 | fn from(value: (f32, f32, f32)) -> Self { 170 | ActionValue::Axis3D(value.into()) 171 | } 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | use super::*; 177 | 178 | #[test] 179 | fn bool_conversion() { 180 | let value = ActionValue::Bool(true); 181 | assert_eq!(value.convert(ActionValueDim::Bool), true.into()); 182 | assert_eq!(value.convert(ActionValueDim::Axis1D), 1.0.into()); 183 | assert_eq!(value.convert(ActionValueDim::Axis2D), (1.0, 0.0).into()); 184 | assert_eq!( 185 | value.convert(ActionValueDim::Axis3D), 186 | (1.0, 0.0, 0.0).into() 187 | ); 188 | } 189 | 190 | #[test] 191 | fn axis1d_conversion() { 192 | let value = ActionValue::Axis1D(1.0); 193 | assert_eq!(value.convert(ActionValueDim::Bool), true.into()); 194 | assert_eq!(value.convert(ActionValueDim::Axis1D), 1.0.into()); 195 | assert_eq!(value.convert(ActionValueDim::Axis2D), (1.0, 0.0).into()); 196 | assert_eq!( 197 | value.convert(ActionValueDim::Axis3D), 198 | (1.0, 0.0, 0.0).into() 199 | ); 200 | } 201 | 202 | #[test] 203 | fn axis2d_conversion() { 204 | let value = ActionValue::Axis2D(Vec2::ONE); 205 | assert_eq!(value.convert(ActionValueDim::Bool), true.into()); 206 | assert_eq!(value.convert(ActionValueDim::Axis1D), 1.0.into()); 207 | assert_eq!(value.convert(ActionValueDim::Axis2D), Vec2::ONE.into()); 208 | assert_eq!( 209 | value.convert(ActionValueDim::Axis3D), 210 | (1.0, 1.0, 0.0).into() 211 | ); 212 | } 213 | 214 | #[test] 215 | fn axis3d_conversion() { 216 | let value = ActionValue::Axis3D(Vec3::ONE); 217 | assert_eq!(value.convert(ActionValueDim::Bool), true.into()); 218 | assert_eq!(value.convert(ActionValueDim::Axis1D), 1.0.into()); 219 | assert_eq!(value.convert(ActionValueDim::Axis2D), Vec2::ONE.into()); 220 | assert_eq!(value.convert(ActionValueDim::Axis3D), Vec3::ONE.into()); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/input_action.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use bevy::prelude::*; 4 | 5 | use crate::prelude::*; 6 | 7 | /// Marker for a gameplay-related action. 8 | /// 9 | /// Needs to be bound to actions using [`Actions::bind`](crate::actions::Actions::bind). 10 | /// 11 | /// To implement the trait you can use the [`InputAction`](bevy_enhanced_input_macros::InputAction) 12 | /// derive to reduce boilerplate: 13 | /// 14 | /// ``` 15 | /// # use bevy::prelude::*; 16 | /// # use bevy_enhanced_input::prelude::*; 17 | /// #[derive(Debug, InputAction)] 18 | /// #[input_action(output = Vec2)] 19 | /// struct Move; 20 | /// ``` 21 | /// 22 | /// Optionally you can pass `consume_input` and/or `accumulation`: 23 | /// 24 | /// ``` 25 | /// # use bevy::prelude::*; 26 | /// # use bevy_enhanced_input::prelude::*; 27 | /// #[derive(Debug, InputAction)] 28 | /// #[input_action(output = Vec2, accumulation = Cumulative, consume_input = false)] 29 | /// struct Move; 30 | /// ``` 31 | /// 32 | /// All parameters match corresponding data in the trait. 33 | pub trait InputAction: Debug + Send + Sync + 'static { 34 | /// What type of value this action will output. 35 | /// 36 | /// - Use [`bool`] for button-like actions (e.g., `Jump`). 37 | /// - Use [`f32`] for single-axis actions (e.g., `Zoom`). 38 | /// - For multi-axis actions, like `Move`, use [`Vec2`] or [`Vec3`]. 39 | /// 40 | /// This type will also be used for `value` field on events 41 | /// e.g. [`Fired::value`], [`Canceled::value`]. 42 | type Output: ActionOutput; 43 | 44 | /// Specifies whether this action should swallow any [`Input`]s 45 | /// bound to it or allow them to pass through to affect other actions. 46 | /// 47 | /// Inputs are consumed when the action state is not equal to 48 | /// [`ActionState::None`]. For details, see [`Actions`]. 49 | /// 50 | /// Consuming is global and affect actions in all contexts. 51 | const CONSUME_INPUT: bool = true; 52 | 53 | /// Associated accumulation behavior. 54 | const ACCUMULATION: Accumulation = Accumulation::Cumulative; 55 | 56 | /// Require inputs to be zero before the first activation and continue to consume them 57 | /// even after context removal until inputs become zero again. 58 | /// 59 | /// This way new instances won't react to currently held inputs until they are released. 60 | /// This prevents unintended behavior where switching or layering contexts using the same key 61 | /// could cause an immediate switch back, as buttons are rarely pressed for only a single frame. 62 | const REQUIRE_RESET: bool = false; 63 | } 64 | 65 | /// Marks a type which can be used as [`InputAction::Output`]. 66 | pub trait ActionOutput: Send + Sync + Debug + Clone + Copy + Into { 67 | /// Dimension of this output. 68 | const DIM: ActionValueDim; 69 | 70 | /// Converts the value into the action output type. 71 | /// 72 | /// # Panics 73 | /// 74 | /// Panics if the value represents a different type. 75 | fn as_output(value: ActionValue) -> Self; 76 | } 77 | 78 | impl ActionOutput for bool { 79 | const DIM: ActionValueDim = ActionValueDim::Bool; 80 | 81 | fn as_output(value: ActionValue) -> Self { 82 | let ActionValue::Bool(value) = value else { 83 | unreachable!("output value should be bool"); 84 | }; 85 | value 86 | } 87 | } 88 | 89 | impl ActionOutput for f32 { 90 | const DIM: ActionValueDim = ActionValueDim::Axis1D; 91 | 92 | fn as_output(value: ActionValue) -> Self { 93 | let ActionValue::Axis1D(value) = value else { 94 | unreachable!("output value should be axis 1D"); 95 | }; 96 | value 97 | } 98 | } 99 | 100 | impl ActionOutput for Vec2 { 101 | const DIM: ActionValueDim = ActionValueDim::Axis2D; 102 | 103 | fn as_output(value: ActionValue) -> Self { 104 | let ActionValue::Axis2D(value) = value else { 105 | unreachable!("output value should be axis 2D"); 106 | }; 107 | value 108 | } 109 | } 110 | 111 | impl ActionOutput for Vec3 { 112 | const DIM: ActionValueDim = ActionValueDim::Axis3D; 113 | 114 | fn as_output(value: ActionValue) -> Self { 115 | let ActionValue::Axis3D(value) = value else { 116 | unreachable!("output value should be axis 3D"); 117 | }; 118 | value 119 | } 120 | } 121 | 122 | /// Defines how [`ActionValue`] is calculated when multiple inputs are evaluated with the 123 | /// same most significant [`ActionState`] (excluding [`ActionState::None`]). 124 | #[derive(Default, Clone, Copy, Debug)] 125 | pub enum Accumulation { 126 | /// Cumulatively add the key values for each mapping. 127 | /// 128 | /// For example, given values of 0.5 and -0.3, the input action's value would be 0.2. 129 | /// 130 | /// Usually used for things like WASD movement, when you want pressing W and S to cancel each other out. 131 | #[default] 132 | Cumulative, 133 | /// Take the value from the mapping with the highest absolute value. 134 | /// 135 | /// For example, given values of 0.5 and -1.5, the input action's value would be -1.5. 136 | MaxAbs, 137 | } 138 | -------------------------------------------------------------------------------- /src/input_binding.rs: -------------------------------------------------------------------------------- 1 | use alloc::{boxed::Box, vec::Vec}; 2 | use core::iter; 3 | 4 | use crate::{input_condition::IntoConditions, input_modifier::IntoModifiers, prelude::*}; 5 | 6 | /// Associated input for [`ActionBinding`]. 7 | #[derive(Debug)] 8 | pub struct InputBinding { 9 | pub input: Input, 10 | pub modifiers: Vec>, 11 | pub conditions: Vec>, 12 | 13 | /// Whether the input output a non-zero value. 14 | /// 15 | /// Prevents newly created contexts from reacting to currently held inputs 16 | /// until they’re released. 17 | /// 18 | /// Used only if [`ActionBinding`](crate::action_binding::ActionBinding::require_reset) is set. 19 | pub(crate) first_activation: bool, 20 | } 21 | 22 | impl InputBinding { 23 | /// Creates a new instance without modifiers and conditions. 24 | pub fn new(input: impl Into) -> Self { 25 | Self { 26 | input: input.into(), 27 | modifiers: Default::default(), 28 | conditions: Default::default(), 29 | first_activation: true, 30 | } 31 | } 32 | } 33 | 34 | impl> From for InputBinding { 35 | fn from(input: I) -> Self { 36 | Self::new(input) 37 | } 38 | } 39 | 40 | /// A trait to ergonomically add modifiers or conditions to any type that can be converted into a binding. 41 | pub trait BindingBuilder { 42 | /// Adds input-level modifiers. 43 | /// 44 | /// For action-level conditions see 45 | /// [`ActionBinding::with_modifiers`](crate::action_binding::ActionBinding::with_modifiers). 46 | /// 47 | /// # Examples 48 | /// 49 | /// Single modifier: 50 | /// 51 | /// ``` 52 | /// # use bevy::prelude::*; 53 | /// # use bevy_enhanced_input::prelude::*; 54 | /// # let mut actions = Actions::::default(); 55 | /// actions.bind::() 56 | /// .to(KeyCode::Space.with_modifiers(Scale::splat(2.0))); 57 | /// # #[derive(InputContext)] 58 | /// # struct Player; 59 | /// # #[derive(Debug, InputAction)] 60 | /// # #[input_action(output = f32)] 61 | /// # struct Jump; 62 | /// ``` 63 | /// 64 | /// Multiple modifiers: 65 | /// 66 | /// ``` 67 | /// # use bevy::prelude::*; 68 | /// # use bevy_enhanced_input::prelude::*; 69 | /// # let mut actions = Actions::::default(); 70 | /// actions.bind::() 71 | /// .to(KeyCode::Space.with_modifiers((Scale::splat(2.0), Negate::all()))); 72 | /// # #[derive(InputContext)] 73 | /// # struct Player; 74 | /// # #[derive(Debug, InputAction)] 75 | /// # #[input_action(output = f32)] 76 | /// # struct Jump; 77 | /// ``` 78 | #[must_use] 79 | fn with_modifiers(self, modifiers: impl IntoModifiers) -> InputBinding; 80 | 81 | /// Adds input-level conditions. 82 | /// 83 | /// You can also apply modifiers to multiple inputs using [`IntoBindings::with_modifiers_each`] 84 | /// 85 | /// For action-level conditions see 86 | /// [`ActionBinding::with_conditions`](crate::action_binding::ActionBinding::with_conditions). 87 | /// 88 | /// # Examples 89 | /// 90 | /// Single condition: 91 | /// 92 | /// ``` 93 | /// # use bevy::prelude::*; 94 | /// # use bevy_enhanced_input::prelude::*; 95 | /// # let mut actions = Actions::::default(); 96 | /// actions.bind::() 97 | /// .to(KeyCode::Space.with_conditions(Release::default())); 98 | /// # #[derive(InputContext)] 99 | /// # struct Player; 100 | /// # #[derive(Debug, InputAction)] 101 | /// # #[input_action(output = bool)] 102 | /// # struct Jump; 103 | /// ``` 104 | /// 105 | /// Multiple conditions: 106 | /// 107 | /// ``` 108 | /// # use bevy::prelude::*; 109 | /// # use bevy_enhanced_input::prelude::*; 110 | /// # let mut actions = Actions::::default(); 111 | /// actions.bind::() 112 | /// .to(KeyCode::Space.with_conditions((Release::default(), Press::default()))); 113 | /// # #[derive(Debug, InputAction)] 114 | /// # #[input_action(output = bool)] 115 | /// # struct Jump; 116 | /// # #[derive(InputContext)] 117 | /// # struct Player; 118 | /// ``` 119 | #[must_use] 120 | fn with_conditions(self, conditions: impl IntoConditions) -> InputBinding; 121 | } 122 | 123 | impl> BindingBuilder for T { 124 | fn with_modifiers(self, modifiers: impl IntoModifiers) -> InputBinding { 125 | let mut binding = self.into(); 126 | binding.modifiers.extend(modifiers.into_modifiers()); 127 | binding 128 | } 129 | 130 | fn with_conditions(self, conditions: impl IntoConditions) -> InputBinding { 131 | let mut binding = self.into(); 132 | binding.conditions.extend(conditions.into_conditions()); 133 | binding 134 | } 135 | } 136 | 137 | /// Conversion into iterator of bindings that could be passed into 138 | /// [`ActionBinding::to`](crate::action_binding::ActionBinding::to). 139 | /// 140 | /// Can be manually implemented to provide custom modifiers or conditions. 141 | /// See [`preset`](crate::preset) for examples. 142 | pub trait IntoBindings { 143 | /// Returns an iterator over bindings. 144 | fn into_bindings(self) -> impl Iterator; 145 | 146 | /// Adds modifiers to **each** binding. 147 | /// 148 | ///
149 | /// 150 | /// Avoid using this with modifiers like [`DeadZone`], as this method applies 151 | /// the modifier to each input **individually** rather than to all bindings. 152 | /// 153 | ///
154 | /// 155 | /// # Examples 156 | /// 157 | /// Negate each gamepad axis for the stick: 158 | /// 159 | /// ``` 160 | /// # use bevy::prelude::*; 161 | /// # use bevy_enhanced_input::prelude::*; 162 | /// # let mut actions = Actions::::default(); 163 | /// actions.bind::() 164 | /// .to(( 165 | /// Input::mouse_motion(), 166 | /// Axial::left_stick().with_modifiers_each(Negate::all()), // Will be applied to each axis. 167 | /// )) 168 | /// .with_modifiers(DeadZone::default()); // Modifiers like `DeadZone` need to be applied at the action level! 169 | /// # #[derive(InputContext)] 170 | /// # struct Player; 171 | /// # #[derive(Debug, InputAction)] 172 | /// # #[input_action(output = bool)] 173 | /// # struct Move; 174 | /// ``` 175 | fn with_modifiers_each( 176 | self, 177 | modifiers: M, 178 | ) -> WithModifiersEach 179 | where 180 | Self: Sized, 181 | { 182 | WithModifiersEach { 183 | bindings: self, 184 | modifiers, 185 | } 186 | } 187 | 188 | /// Adds condition to **each** binding. 189 | /// 190 | /// Similar to [`Self::with_modifiers_each`]. 191 | fn with_conditions_each( 192 | self, 193 | conditions: C, 194 | ) -> WithConditionsEach 195 | where 196 | Self: Sized, 197 | { 198 | WithConditionsEach { 199 | bindings: self, 200 | conditions, 201 | } 202 | } 203 | } 204 | 205 | impl> IntoBindings for I { 206 | fn into_bindings(self) -> impl Iterator { 207 | iter::once(self.into()) 208 | } 209 | } 210 | 211 | impl + Copy> IntoBindings for &Vec { 212 | fn into_bindings(self) -> impl Iterator { 213 | self.as_slice().into_bindings() 214 | } 215 | } 216 | 217 | impl + Copy, const N: usize> IntoBindings for &[I; N] { 218 | fn into_bindings(self) -> impl Iterator { 219 | self.as_slice().into_bindings() 220 | } 221 | } 222 | 223 | impl + Copy> IntoBindings for &[I] { 224 | fn into_bindings(self) -> impl Iterator { 225 | self.iter().copied().map(Into::into) 226 | } 227 | } 228 | 229 | macro_rules! impl_tuple_binds { 230 | ($($name:ident),+) => { 231 | impl<$($name),+> IntoBindings for ($($name,)+) 232 | where 233 | $($name: IntoBindings),+ 234 | { 235 | #[allow(non_snake_case)] 236 | fn into_bindings(self) -> impl Iterator { 237 | let ($($name,)+) = self; 238 | core::iter::empty() 239 | $(.chain($name.into_bindings()))+ 240 | } 241 | } 242 | }; 243 | } 244 | 245 | variadics_please::all_tuples!(impl_tuple_binds, 1, 15, I); 246 | 247 | /// Bindings with assigned modifiers. 248 | /// 249 | /// See also [`IntoBindings::with_modifiers_each`] 250 | pub struct WithModifiersEach { 251 | bindings: I, 252 | modifiers: M, 253 | } 254 | 255 | impl IntoBindings for WithModifiersEach { 256 | fn into_bindings(self) -> impl Iterator { 257 | self.bindings 258 | .into_bindings() 259 | .map(move |binding| binding.with_modifiers(self.modifiers.clone())) 260 | } 261 | } 262 | 263 | /// Bindings with assigned conditions. 264 | /// 265 | /// See also [`IntoBindings::with_conditions_each`] 266 | pub struct WithConditionsEach { 267 | bindings: I, 268 | conditions: C, 269 | } 270 | 271 | impl IntoBindings for WithConditionsEach { 272 | fn into_bindings(self) -> impl Iterator { 273 | self.bindings 274 | .into_bindings() 275 | .map(move |binding| binding.with_conditions(self.conditions.clone())) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/input_condition.rs: -------------------------------------------------------------------------------- 1 | pub mod block_by; 2 | pub mod chord; 3 | pub mod condition_timer; 4 | pub mod hold; 5 | pub mod hold_and_release; 6 | pub mod just_press; 7 | pub mod press; 8 | pub mod pulse; 9 | pub mod release; 10 | pub mod tap; 11 | 12 | use alloc::boxed::Box; 13 | use core::{fmt::Debug, iter}; 14 | 15 | use bevy::prelude::*; 16 | 17 | use crate::{action_map::ActionMap, prelude::*}; 18 | 19 | /// Default actuation threshold for all conditions. 20 | pub const DEFAULT_ACTUATION: f32 = 0.5; 21 | 22 | /// Defines how input activates. 23 | /// 24 | /// Conditions analyze the input, checking for minimum actuation values 25 | /// and validating patterns like short taps, prolonged holds, or the typical "press" 26 | /// or "release" events. 27 | /// 28 | /// Can be applied both to inputs and actions. 29 | /// See [`ActionBinding::with_conditions`] and [`BindingBuilder::with_conditions`]. 30 | pub trait InputCondition: Sync + Send + Debug + 'static { 31 | /// Returns calculates state. 32 | /// 33 | /// `actions` is a state of other actions within the currently evaluating context. 34 | fn evaluate( 35 | &mut self, 36 | action_map: &ActionMap, 37 | time: &Time, 38 | value: ActionValue, 39 | ) -> ActionState; 40 | 41 | /// Returns how the condition is combined with others. 42 | fn kind(&self) -> ConditionKind { 43 | ConditionKind::Explicit 44 | } 45 | } 46 | 47 | /// Determines how a condition contributes to the final [`ActionState`]. 48 | /// 49 | /// If no conditions are provided, the state will be set to [`ActionState::Fired`] 50 | /// on any non-zero value, functioning similarly to a [`Down`] condition 51 | /// with a zero actuation threshold. 52 | /// 53 | /// For details about how actions are combined, see [`Actions`]. 54 | pub enum ConditionKind { 55 | /// The most significant [`ActionState`] from all explicit conditions will be the 56 | /// resulting state. 57 | Explicit, 58 | /// Like [`Self::Explicit`], but [`ActionState::Fired`] will be set only if all 59 | /// implicit conditions return it. 60 | /// 61 | /// Otherwise, the most significant state will be capped at [`ActionState::Ongoing`]. 62 | Implicit, 63 | /// Any blocking condition that returns [`ActionState::None`] will override 64 | /// the state with [`ActionState::None`]. 65 | /// 66 | /// Doesn't contribute to the state on its own. 67 | Blocker, 68 | } 69 | 70 | /// Conversion into iterator of bindings that could be passed into 71 | /// [`ActionBinding::with_conditions`] and [`BindingBuilder::with_conditions`]. 72 | pub trait IntoConditions { 73 | /// Returns an iterator over conditions. 74 | fn into_conditions(self) -> impl Iterator>; 75 | } 76 | 77 | impl IntoConditions for I { 78 | fn into_conditions(self) -> impl Iterator> { 79 | iter::once(Box::new(self) as Box) 80 | } 81 | } 82 | 83 | macro_rules! impl_tuple_condition { 84 | ($($name:ident),+) => { 85 | impl<$($name),+> IntoConditions for ($($name,)+) 86 | where 87 | $($name: InputCondition),+ 88 | { 89 | #[allow(non_snake_case)] 90 | fn into_conditions(self) -> impl Iterator> { 91 | let ($($name,)+) = self; 92 | core::iter::empty() 93 | $(.chain(iter::once(Box::new($name) as Box)))+ 94 | } 95 | } 96 | }; 97 | } 98 | 99 | variadics_please::all_tuples!(impl_tuple_condition, 1, 15, I); 100 | -------------------------------------------------------------------------------- /src/input_condition/block_by.rs: -------------------------------------------------------------------------------- 1 | use core::{any, marker::PhantomData}; 2 | 3 | use bevy::prelude::*; 4 | use log::warn; 5 | 6 | use crate::{action_map::ActionMap, prelude::*}; 7 | 8 | /// Requires another action to not be fired within the same context. 9 | #[derive(Debug)] 10 | pub struct BlockBy { 11 | /// Action that blocks this condition when active. 12 | marker: PhantomData
, 13 | } 14 | 15 | impl Default for BlockBy { 16 | fn default() -> Self { 17 | Self { 18 | marker: PhantomData, 19 | } 20 | } 21 | } 22 | 23 | impl Clone for BlockBy { 24 | fn clone(&self) -> Self { 25 | *self 26 | } 27 | } 28 | 29 | impl Copy for BlockBy {} 30 | 31 | impl InputCondition for BlockBy { 32 | fn evaluate( 33 | &mut self, 34 | action_map: &ActionMap, 35 | _time: &Time, 36 | _value: ActionValue, 37 | ) -> ActionState { 38 | if let Some(action) = action_map.action::() { 39 | if action.state() == ActionState::Fired { 40 | return ActionState::None; 41 | } 42 | } else { 43 | // TODO: use `warn_once` when `bevy_log` becomes `no_std` compatible. 44 | warn!( 45 | "action `{}` is not present in context", 46 | any::type_name::() 47 | ); 48 | } 49 | 50 | ActionState::Fired 51 | } 52 | 53 | fn kind(&self) -> ConditionKind { 54 | ConditionKind::Blocker 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use core::any::TypeId; 61 | 62 | use bevy_enhanced_input_macros::InputAction; 63 | 64 | use super::*; 65 | 66 | #[test] 67 | fn block() { 68 | let mut condition = BlockBy::::default(); 69 | let mut action = Action::new::(); 70 | let time = Time::default(); 71 | action.update(&time, ActionState::Fired, true); 72 | let mut action_map = ActionMap::default(); 73 | action_map.insert(TypeId::of::(), action); 74 | 75 | assert_eq!( 76 | condition.evaluate(&action_map, &time, true.into()), 77 | ActionState::None, 78 | ); 79 | } 80 | 81 | #[test] 82 | fn missing_action() { 83 | let mut condition = BlockBy::::default(); 84 | let action_map = ActionMap::default(); 85 | let time = Time::default(); 86 | 87 | assert_eq!( 88 | condition.evaluate(&action_map, &time, true.into()), 89 | ActionState::Fired, 90 | ); 91 | } 92 | 93 | #[derive(Debug, InputAction)] 94 | #[input_action(output = bool)] 95 | struct TestAction; 96 | } 97 | -------------------------------------------------------------------------------- /src/input_condition/chord.rs: -------------------------------------------------------------------------------- 1 | use core::{any, marker::PhantomData}; 2 | 3 | use bevy::prelude::*; 4 | use log::warn; 5 | 6 | use crate::{action_map::ActionMap, prelude::*}; 7 | 8 | /// Requires action `A` to be fired within the same context. 9 | /// 10 | /// Inherits [`ActionState`] from the specified action. 11 | #[derive(Debug)] 12 | pub struct Chord { 13 | /// Required action. 14 | marker: PhantomData, 15 | } 16 | 17 | impl Default for Chord { 18 | fn default() -> Self { 19 | Self { 20 | marker: PhantomData, 21 | } 22 | } 23 | } 24 | 25 | impl Clone for Chord { 26 | fn clone(&self) -> Self { 27 | *self 28 | } 29 | } 30 | 31 | impl Copy for Chord {} 32 | 33 | impl InputCondition for Chord { 34 | fn evaluate( 35 | &mut self, 36 | action_map: &ActionMap, 37 | _time: &Time, 38 | _value: ActionValue, 39 | ) -> ActionState { 40 | if let Some(action) = action_map.action::() { 41 | // Inherit state from the chorded action. 42 | action.state() 43 | } else { 44 | // TODO: use `warn_once` when `bevy_log` becomes `no_std` compatible. 45 | warn!( 46 | "action `{}` is not present in context", 47 | any::type_name::() 48 | ); 49 | ActionState::None 50 | } 51 | } 52 | 53 | fn kind(&self) -> ConditionKind { 54 | ConditionKind::Implicit 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use core::any::TypeId; 61 | 62 | use bevy_enhanced_input_macros::InputAction; 63 | 64 | use super::*; 65 | 66 | #[test] 67 | fn chord() { 68 | let mut condition = Chord::::default(); 69 | let mut action = Action::new::(); 70 | let time = Time::default(); 71 | action.update(&time, ActionState::Fired, true); 72 | let mut action_map = ActionMap::default(); 73 | action_map.insert(TypeId::of::(), action); 74 | 75 | assert_eq!( 76 | condition.evaluate(&action_map, &time, true.into()), 77 | ActionState::Fired, 78 | ); 79 | } 80 | 81 | #[test] 82 | fn missing_action() { 83 | let mut condition = Chord::::default(); 84 | let action_map = ActionMap::default(); 85 | let time = Time::default(); 86 | 87 | assert_eq!( 88 | condition.evaluate(&action_map, &time, true.into()), 89 | ActionState::None, 90 | ); 91 | } 92 | 93 | #[derive(Debug, InputAction)] 94 | #[input_action(output = bool)] 95 | struct TestAction; 96 | } 97 | -------------------------------------------------------------------------------- /src/input_condition/condition_timer.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | /// Helper for building triggers that have firing conditions governed by elapsed time. 4 | #[derive(Clone, Copy, Default, Debug)] 5 | pub struct ConditionTimer { 6 | /// If set to `true`, [`Time::relative_speed`] will be applied to the held duration. 7 | /// 8 | /// By default is set to `false`. 9 | pub relative_speed: bool, 10 | 11 | duration: f32, 12 | } 13 | 14 | impl ConditionTimer { 15 | /// Creates a new instance with the given time passed. 16 | pub fn with_duration(duration: f32) -> Self { 17 | Self { 18 | duration, 19 | ..Default::default() 20 | } 21 | } 22 | 23 | pub fn update(&mut self, timer: &Time) { 24 | // Time returns already scaled results. 25 | // Unscale if configured. 26 | let scale = if self.relative_speed { 27 | 1.0 28 | } else { 29 | timer.relative_speed() 30 | }; 31 | 32 | self.duration += timer.delta_secs() / scale; 33 | } 34 | 35 | pub fn reset(&mut self) { 36 | self.duration = 0.0; 37 | } 38 | 39 | pub fn duration(&self) -> f32 { 40 | self.duration 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use core::time::Duration; 47 | 48 | use super::*; 49 | 50 | #[test] 51 | fn absolute() { 52 | let mut time = Time::::default(); 53 | time.set_relative_speed(0.5); 54 | time.advance_by(Duration::from_millis(200 / 2)); // Advance needs to be scaled manually. 55 | 56 | let mut timer = ConditionTimer::default(); 57 | timer.update(&time); 58 | assert_eq!(timer.duration(), 0.2); 59 | } 60 | 61 | #[test] 62 | fn relative() { 63 | let mut time = Time::::default(); 64 | time.set_relative_speed(0.5); 65 | time.advance_by(Duration::from_millis(200 / 2)); // Advance needs to be scaled manually. 66 | 67 | let mut timer = ConditionTimer { 68 | relative_speed: true, 69 | ..Default::default() 70 | }; 71 | timer.update(&time); 72 | assert_eq!(timer.duration(), 0.1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/input_condition/hold.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Ongoing`] when the input becomes actuated and 7 | /// [`ActionState::Fired`] when input remained actuated for [`Self::hold_time`] seconds. 8 | /// 9 | /// Returns [`ActionState::None`] when the input stops being actuated earlier than [`Self::hold_time`] seconds. 10 | /// May optionally fire once, or repeatedly fire. 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct Hold { 13 | /// How long does the input have to be held to cause trigger. 14 | pub hold_time: f32, 15 | 16 | /// Should this trigger fire only once, or fire every frame once the hold time threshold is met? 17 | pub one_shot: bool, 18 | 19 | /// Trigger threshold. 20 | pub actuation: f32, 21 | 22 | timer: ConditionTimer, 23 | 24 | fired: bool, 25 | } 26 | 27 | impl Hold { 28 | #[must_use] 29 | pub fn new(hold_time: f32) -> Self { 30 | Self { 31 | hold_time, 32 | one_shot: false, 33 | actuation: DEFAULT_ACTUATION, 34 | timer: Default::default(), 35 | fired: false, 36 | } 37 | } 38 | 39 | #[must_use] 40 | pub fn one_shot(mut self, one_shot: bool) -> Self { 41 | self.one_shot = one_shot; 42 | self 43 | } 44 | 45 | #[must_use] 46 | pub fn with_actuation(mut self, actuation: f32) -> Self { 47 | self.actuation = actuation; 48 | self 49 | } 50 | 51 | /// Enables or disables time dilation. 52 | #[must_use] 53 | pub fn relative_speed(mut self, relative: bool) -> Self { 54 | self.timer.relative_speed = relative; 55 | self 56 | } 57 | } 58 | 59 | impl InputCondition for Hold { 60 | fn evaluate( 61 | &mut self, 62 | _action_map: &ActionMap, 63 | time: &Time, 64 | value: ActionValue, 65 | ) -> ActionState { 66 | let actuated = value.is_actuated(self.actuation); 67 | if actuated { 68 | self.timer.update(time); 69 | } else { 70 | self.timer.reset(); 71 | } 72 | 73 | let is_first_trigger = !self.fired; 74 | self.fired = self.timer.duration() >= self.hold_time; 75 | 76 | if self.fired { 77 | if is_first_trigger || !self.one_shot { 78 | ActionState::Fired 79 | } else { 80 | ActionState::None 81 | } 82 | } else if actuated { 83 | ActionState::Ongoing 84 | } else { 85 | ActionState::None 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use core::time::Duration; 93 | 94 | use super::*; 95 | 96 | #[test] 97 | fn hold() { 98 | let mut condition = Hold::new(1.0); 99 | let action_map = ActionMap::default(); 100 | let mut time = Time::default(); 101 | 102 | assert_eq!( 103 | condition.evaluate(&action_map, &time, 1.0.into()), 104 | ActionState::Ongoing, 105 | ); 106 | 107 | time.advance_by(Duration::from_secs(1)); 108 | assert_eq!( 109 | condition.evaluate(&action_map, &time, 1.0.into()), 110 | ActionState::Fired, 111 | ); 112 | assert_eq!( 113 | condition.evaluate(&action_map, &time, 1.0.into()), 114 | ActionState::Fired, 115 | ); 116 | assert_eq!( 117 | condition.evaluate(&action_map, &time, 0.0.into()), 118 | ActionState::None 119 | ); 120 | 121 | time.advance_by(Duration::ZERO); 122 | assert_eq!( 123 | condition.evaluate(&action_map, &time, 1.0.into()), 124 | ActionState::Ongoing, 125 | ); 126 | } 127 | 128 | #[test] 129 | fn one_shot() { 130 | let mut hold = Hold::new(1.0).one_shot(true); 131 | let action_map = ActionMap::default(); 132 | let mut time = Time::default(); 133 | time.advance_by(Duration::from_secs(1)); 134 | 135 | assert_eq!( 136 | hold.evaluate(&action_map, &time, 1.0.into()), 137 | ActionState::Fired 138 | ); 139 | assert_eq!( 140 | hold.evaluate(&action_map, &time, 1.0.into()), 141 | ActionState::None 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/input_condition/hold_and_release.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Ongoing`] when input becomes actuated and [`ActionState::Fired`] 7 | /// when the input is released after having been actuated for [`Self::hold_time`] seconds. 8 | /// 9 | /// Returns [`ActionState::None`] when the input stops being actuated earlier than [`Self::hold_time`] seconds. 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct HoldAndRelease { 12 | /// How long does the input have to be held to cause trigger. 13 | pub hold_time: f32, 14 | 15 | /// Trigger threshold. 16 | pub actuation: f32, 17 | 18 | timer: ConditionTimer, 19 | } 20 | 21 | impl HoldAndRelease { 22 | #[must_use] 23 | pub fn new(hold_time: f32) -> Self { 24 | Self { 25 | hold_time, 26 | actuation: DEFAULT_ACTUATION, 27 | timer: Default::default(), 28 | } 29 | } 30 | 31 | #[must_use] 32 | pub fn with_actuation(mut self, actuation: f32) -> Self { 33 | self.actuation = actuation; 34 | self 35 | } 36 | 37 | /// Enables or disables time dilation. 38 | #[must_use] 39 | pub fn relative_speed(mut self, relative: bool) -> Self { 40 | self.timer.relative_speed = relative; 41 | self 42 | } 43 | } 44 | 45 | impl InputCondition for HoldAndRelease { 46 | fn evaluate( 47 | &mut self, 48 | _action_map: &ActionMap, 49 | time: &Time, 50 | value: ActionValue, 51 | ) -> ActionState { 52 | // Evaluate the updated held duration prior to checking for actuation. 53 | // This stops us failing to trigger if the input is released on the 54 | // threshold frame due to held duration being 0. 55 | self.timer.update(time); 56 | let held_duration = self.timer.duration(); 57 | 58 | if value.is_actuated(self.actuation) { 59 | ActionState::Ongoing 60 | } else { 61 | self.timer.reset(); 62 | // Trigger if we've passed the threshold and released. 63 | if held_duration >= self.hold_time { 64 | ActionState::Fired 65 | } else { 66 | ActionState::None 67 | } 68 | } 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use core::time::Duration; 75 | 76 | use super::*; 77 | 78 | #[test] 79 | fn hold_and_release() { 80 | let mut condition = HoldAndRelease::new(1.0); 81 | let action_map = ActionMap::default(); 82 | let mut time = Time::default(); 83 | 84 | assert_eq!( 85 | condition.evaluate(&action_map, &time, 1.0.into()), 86 | ActionState::Ongoing, 87 | ); 88 | 89 | time.advance_by(Duration::from_secs(1)); 90 | assert_eq!( 91 | condition.evaluate(&action_map, &time, 0.0.into()), 92 | ActionState::Fired 93 | ); 94 | 95 | time.advance_by(Duration::ZERO); 96 | assert_eq!( 97 | condition.evaluate(&action_map, &time, 1.0.into()), 98 | ActionState::Ongoing, 99 | ); 100 | assert_eq!( 101 | condition.evaluate(&action_map, &time, 0.0.into()), 102 | ActionState::None 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/input_condition/just_press.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Like [`super::press::Down`] but returns [`ActionState::Fired`] only once until the next actuation. 7 | /// 8 | /// Holding the input will not cause further triggers. 9 | #[derive(Clone, Copy, Debug)] 10 | pub struct Press { 11 | /// Trigger threshold. 12 | pub actuation: f32, 13 | actuated: bool, 14 | } 15 | 16 | impl Press { 17 | #[must_use] 18 | pub fn new(actuation: f32) -> Self { 19 | Self { 20 | actuation, 21 | actuated: false, 22 | } 23 | } 24 | } 25 | 26 | impl Default for Press { 27 | fn default() -> Self { 28 | Self::new(DEFAULT_ACTUATION) 29 | } 30 | } 31 | 32 | impl InputCondition for Press { 33 | fn evaluate( 34 | &mut self, 35 | _action_map: &ActionMap, 36 | _time: &Time, 37 | value: ActionValue, 38 | ) -> ActionState { 39 | let previously_actuated = self.actuated; 40 | self.actuated = value.is_actuated(self.actuation); 41 | 42 | if self.actuated && !previously_actuated { 43 | ActionState::Fired 44 | } else { 45 | ActionState::None 46 | } 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | 54 | #[test] 55 | fn press() { 56 | let mut condition = Press::default(); 57 | let action_map = ActionMap::default(); 58 | let time = Time::default(); 59 | 60 | assert_eq!( 61 | condition.evaluate(&action_map, &time, 0.0.into()), 62 | ActionState::None 63 | ); 64 | assert_eq!( 65 | condition.evaluate(&action_map, &time, 1.0.into()), 66 | ActionState::Fired, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/input_condition/press.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Fired`] when the input exceeds the actuation threshold. 7 | #[derive(Clone, Copy, Debug)] 8 | pub struct Down { 9 | /// Trigger threshold. 10 | pub actuation: f32, 11 | } 12 | 13 | impl Down { 14 | #[must_use] 15 | pub fn new(actuation: f32) -> Self { 16 | Self { actuation } 17 | } 18 | } 19 | 20 | impl Default for Down { 21 | fn default() -> Self { 22 | Self::new(DEFAULT_ACTUATION) 23 | } 24 | } 25 | 26 | impl InputCondition for Down { 27 | fn evaluate( 28 | &mut self, 29 | _action_map: &ActionMap, 30 | _time: &Time, 31 | value: ActionValue, 32 | ) -> ActionState { 33 | if value.is_actuated(self.actuation) { 34 | ActionState::Fired 35 | } else { 36 | ActionState::None 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn down() { 47 | let mut condition = Down::new(1.0); 48 | let action_map = ActionMap::default(); 49 | let time = Time::default(); 50 | 51 | assert_eq!( 52 | condition.evaluate(&action_map, &time, 0.0.into()), 53 | ActionState::None 54 | ); 55 | assert_eq!( 56 | condition.evaluate(&action_map, &time, 1.0.into()), 57 | ActionState::Fired, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/input_condition/pulse.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Ongoing`] when input becomes actuated and [`ActionState::Fired`] 7 | /// each [`Self::interval`] seconds. 8 | /// 9 | /// Note: [`Completed`] only fires when the repeat limit is reached or when input is released 10 | /// immediately after being triggered. Otherwise, [`Canceled`] is fired when input is released. 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct Pulse { 13 | /// Time in seconds between each triggering while input is held. 14 | pub interval: f32, 15 | 16 | /// Number of times the condition can be triggered (0 means no limit). 17 | pub trigger_limit: u32, 18 | 19 | /// Whether to trigger when the input first exceeds the actuation threshold or wait for the first interval. 20 | pub trigger_on_start: bool, 21 | 22 | /// Trigger threshold. 23 | pub actuation: f32, 24 | 25 | timer: ConditionTimer, 26 | 27 | trigger_count: u32, 28 | } 29 | 30 | impl Pulse { 31 | #[must_use] 32 | pub fn new(interval: f32) -> Self { 33 | Self { 34 | interval, 35 | trigger_limit: 0, 36 | trigger_on_start: true, 37 | trigger_count: 0, 38 | actuation: DEFAULT_ACTUATION, 39 | timer: Default::default(), 40 | } 41 | } 42 | 43 | #[must_use] 44 | pub fn with_trigger_limit(mut self, trigger_limit: u32) -> Self { 45 | self.trigger_limit = trigger_limit; 46 | self 47 | } 48 | 49 | #[must_use] 50 | pub fn trigger_on_start(mut self, trigger_on_start: bool) -> Self { 51 | self.trigger_on_start = trigger_on_start; 52 | self 53 | } 54 | 55 | #[must_use] 56 | pub fn with_actuation(mut self, actuation: f32) -> Self { 57 | self.actuation = actuation; 58 | self 59 | } 60 | 61 | /// Enables or disables time dilation. 62 | #[must_use] 63 | pub fn relative_speed(mut self, relative: bool) -> Self { 64 | self.timer.relative_speed = relative; 65 | self 66 | } 67 | } 68 | 69 | impl InputCondition for Pulse { 70 | fn evaluate( 71 | &mut self, 72 | _action_map: &ActionMap, 73 | time: &Time, 74 | value: ActionValue, 75 | ) -> ActionState { 76 | if value.is_actuated(self.actuation) { 77 | self.timer.update(time); 78 | 79 | if self.trigger_limit == 0 || self.trigger_count < self.trigger_limit { 80 | let trigger_count = if self.trigger_on_start { 81 | self.trigger_count 82 | } else { 83 | self.trigger_count + 1 84 | }; 85 | 86 | // If the repeat count limit has not been reached. 87 | if self.timer.duration() >= self.interval * trigger_count as f32 { 88 | // Trigger when held duration exceeds the interval threshold. 89 | self.trigger_count += 1; 90 | ActionState::Fired 91 | } else { 92 | ActionState::Ongoing 93 | } 94 | } else { 95 | ActionState::None 96 | } 97 | } else { 98 | self.timer.reset(); 99 | 100 | self.trigger_count = 0; 101 | ActionState::None 102 | } 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use core::time::Duration; 109 | 110 | use super::*; 111 | 112 | #[test] 113 | fn tap() { 114 | let mut condition = Pulse::new(1.0); 115 | let action_map = ActionMap::default(); 116 | let mut time = Time::default(); 117 | 118 | assert_eq!( 119 | condition.evaluate(&action_map, &time, 1.0.into()), 120 | ActionState::Fired, 121 | ); 122 | 123 | time.advance_by(Duration::from_millis(500)); 124 | assert_eq!( 125 | condition.evaluate(&action_map, &time, 1.0.into()), 126 | ActionState::Ongoing, 127 | ); 128 | assert_eq!( 129 | condition.evaluate(&action_map, &time, 1.0.into()), 130 | ActionState::Fired, 131 | ); 132 | 133 | time.advance_by(Duration::ZERO); 134 | assert_eq!( 135 | condition.evaluate(&action_map, &time, 1.0.into()), 136 | ActionState::Ongoing, 137 | ); 138 | assert_eq!( 139 | condition.evaluate(&action_map, &time, 0.0.into()), 140 | ActionState::None 141 | ); 142 | } 143 | 144 | #[test] 145 | fn not_trigger_on_start() { 146 | let mut condition = Pulse::new(1.0).trigger_on_start(false); 147 | let action_map = ActionMap::default(); 148 | let time = Time::default(); 149 | 150 | assert_eq!( 151 | condition.evaluate(&action_map, &time, 1.0.into()), 152 | ActionState::Ongoing, 153 | ); 154 | } 155 | 156 | #[test] 157 | fn trigger_limit() { 158 | let mut condition = Pulse::new(1.0).with_trigger_limit(1); 159 | let action_map = ActionMap::default(); 160 | let time = Time::default(); 161 | 162 | assert_eq!( 163 | condition.evaluate(&action_map, &time, 1.0.into()), 164 | ActionState::Fired, 165 | ); 166 | assert_eq!( 167 | condition.evaluate(&action_map, &time, 1.0.into()), 168 | ActionState::None 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/input_condition/release.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Ongoing`] when the input exceeds the actuation threshold and 7 | /// [`ActionState::Fired`] once when the input drops back below the actuation threshold. 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Release { 10 | /// Trigger threshold. 11 | pub actuation: f32, 12 | actuated: bool, 13 | } 14 | 15 | impl Release { 16 | #[must_use] 17 | pub fn new(actuation: f32) -> Self { 18 | Self { 19 | actuation, 20 | actuated: false, 21 | } 22 | } 23 | } 24 | 25 | impl Default for Release { 26 | fn default() -> Self { 27 | Self::new(DEFAULT_ACTUATION) 28 | } 29 | } 30 | 31 | impl InputCondition for Release { 32 | fn evaluate( 33 | &mut self, 34 | _action_map: &ActionMap, 35 | _time: &Time, 36 | value: ActionValue, 37 | ) -> ActionState { 38 | let previously_actuated = self.actuated; 39 | self.actuated = value.is_actuated(self.actuation); 40 | 41 | if self.actuated { 42 | // Ongoing on hold. 43 | ActionState::Ongoing 44 | } else if previously_actuated { 45 | // Fired on release. 46 | ActionState::Fired 47 | } else { 48 | ActionState::None 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | 57 | #[test] 58 | fn release() { 59 | let mut condition = Release::default(); 60 | let action_map = ActionMap::default(); 61 | let time = Time::default(); 62 | 63 | assert_eq!( 64 | condition.evaluate(&action_map, &time, 0.0.into()), 65 | ActionState::None 66 | ); 67 | assert_eq!( 68 | condition.evaluate(&action_map, &time, 1.0.into()), 69 | ActionState::Ongoing 70 | ); 71 | assert_eq!( 72 | condition.evaluate(&action_map, &time, 0.0.into()), 73 | ActionState::Fired 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/input_condition/tap.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use super::DEFAULT_ACTUATION; 4 | use crate::{action_map::ActionMap, prelude::*}; 5 | 6 | /// Returns [`ActionState::Ongoing`] when input becomes actuated and [`ActionState::Fired`] 7 | /// when the input is released within the [`Self::release_time`] seconds. 8 | /// 9 | /// Returns [`ActionState::None`] when the input is actuated more than [`Self::release_time`] seconds. 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct Tap { 12 | /// Time window within which the action must be released to register as a tap. 13 | pub release_time: f32, 14 | 15 | /// Trigger threshold. 16 | pub actuation: f32, 17 | 18 | timer: ConditionTimer, 19 | actuated: bool, 20 | } 21 | 22 | impl Tap { 23 | #[must_use] 24 | pub fn new(release_time: f32) -> Self { 25 | Self { 26 | release_time, 27 | actuation: DEFAULT_ACTUATION, 28 | timer: Default::default(), 29 | actuated: false, 30 | } 31 | } 32 | 33 | #[must_use] 34 | pub fn with_actuation(mut self, actuation: f32) -> Self { 35 | self.actuation = actuation; 36 | self 37 | } 38 | 39 | /// Enables or disables time dilation. 40 | #[must_use] 41 | pub fn relative_speed(mut self, relative: bool) -> Self { 42 | self.timer.relative_speed = relative; 43 | self 44 | } 45 | } 46 | 47 | impl InputCondition for Tap { 48 | fn evaluate( 49 | &mut self, 50 | _action_map: &ActionMap, 51 | time: &Time, 52 | value: ActionValue, 53 | ) -> ActionState { 54 | let last_actuated = self.actuated; 55 | let last_held_duration = self.timer.duration(); 56 | self.actuated = value.is_actuated(self.actuation); 57 | if self.actuated { 58 | self.timer.update(time); 59 | } else { 60 | self.timer.reset(); 61 | } 62 | 63 | if last_actuated && !self.actuated && last_held_duration <= self.release_time { 64 | // Only trigger if pressed then released quickly enough. 65 | ActionState::Fired 66 | } else if self.timer.duration() >= self.release_time { 67 | // Once we pass the threshold halt all triggering until released. 68 | ActionState::None 69 | } else if self.actuated { 70 | ActionState::Ongoing 71 | } else { 72 | ActionState::None 73 | } 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use core::time::Duration; 80 | 81 | use super::*; 82 | 83 | #[test] 84 | fn tap() { 85 | let mut condition = Tap::new(1.0); 86 | let action_map = ActionMap::default(); 87 | let mut time = Time::default(); 88 | 89 | assert_eq!( 90 | condition.evaluate(&action_map, &time, 1.0.into()), 91 | ActionState::Ongoing, 92 | ); 93 | 94 | time.advance_by(Duration::from_secs(1)); 95 | assert_eq!( 96 | condition.evaluate(&action_map, &time, 0.0.into()), 97 | ActionState::Fired, 98 | ); 99 | 100 | time.advance_by(Duration::ZERO); 101 | assert_eq!( 102 | condition.evaluate(&action_map, &time, 0.0.into()), 103 | ActionState::None 104 | ); 105 | 106 | time.advance_by(Duration::from_secs(2)); 107 | assert_eq!( 108 | condition.evaluate(&action_map, &time, 1.0.into()), 109 | ActionState::None 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/input_modifier.rs: -------------------------------------------------------------------------------- 1 | pub mod accumulate_by; 2 | pub mod clamp; 3 | pub mod dead_zone; 4 | pub mod delta_scale; 5 | pub mod exponential_curve; 6 | pub mod negate; 7 | pub mod scale; 8 | pub mod smooth_nudge; 9 | pub mod swizzle_axis; 10 | 11 | use alloc::boxed::Box; 12 | use core::{fmt::Debug, iter}; 13 | 14 | use bevy::prelude::*; 15 | 16 | use crate::{action_map::ActionMap, prelude::*}; 17 | 18 | /// Pre-processor that alter the raw input values. 19 | /// 20 | /// Input modifiers are useful for applying sensitivity settings, smoothing input over multiple frames, 21 | /// or changing how input maps to axes. 22 | /// 23 | /// Can be applied both to inputs and actions. 24 | /// See [`ActionBinding::with_modifiers`] and [`BindingBuilder::with_modifiers`]. 25 | pub trait InputModifier: Sync + Send + Debug + 'static { 26 | /// Returns pre-processed value. 27 | /// 28 | /// Called each frame. 29 | fn apply( 30 | &mut self, 31 | action_map: &ActionMap, 32 | time: &Time, 33 | value: ActionValue, 34 | ) -> ActionValue; 35 | } 36 | 37 | /// Conversion into iterator of bindings that could be passed into 38 | /// [`ActionBinding::with_modifiers`] and [`BindingBuilder::with_modifiers`]. 39 | pub trait IntoModifiers { 40 | /// Returns an iterator over modifiers. 41 | fn into_modifiers(self) -> impl Iterator>; 42 | } 43 | 44 | impl IntoModifiers for I { 45 | fn into_modifiers(self) -> impl Iterator> { 46 | iter::once(Box::new(self) as Box) 47 | } 48 | } 49 | 50 | macro_rules! impl_tuple_modifiers { 51 | ($($name:ident),+) => { 52 | impl<$($name),+> IntoModifiers for ($($name,)+) 53 | where 54 | $($name: InputModifier),+ 55 | { 56 | #[allow(non_snake_case)] 57 | fn into_modifiers(self) -> impl Iterator> { 58 | let ($($name,)+) = self; 59 | core::iter::empty() 60 | $(.chain(iter::once(Box::new($name) as Box)))+ 61 | } 62 | } 63 | }; 64 | } 65 | 66 | variadics_please::all_tuples!(impl_tuple_modifiers, 1, 15, I); 67 | -------------------------------------------------------------------------------- /src/input_modifier/accumulate_by.rs: -------------------------------------------------------------------------------- 1 | use core::{any, marker::PhantomData}; 2 | 3 | use bevy::prelude::*; 4 | use log::warn; 5 | 6 | use crate::{action_map::ActionMap, prelude::*}; 7 | 8 | /// Produces accumulated value when another action is fired within the same context. 9 | /// 10 | /// Continuously adds input values together as long as action `A` is [`ActionState::Fired`]. 11 | /// When the action is inactive, it resets the accumulation with the current frame's input value. 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct AccumulateBy { 14 | /// Action that activates accumulation. 15 | marker: PhantomData, 16 | 17 | /// The accumulated value across frames. 18 | value: Vec3, 19 | } 20 | 21 | impl Default for AccumulateBy { 22 | fn default() -> Self { 23 | Self { 24 | marker: PhantomData, 25 | value: Default::default(), 26 | } 27 | } 28 | } 29 | 30 | impl InputModifier for AccumulateBy { 31 | fn apply( 32 | &mut self, 33 | action_map: &ActionMap, 34 | _time: &Time, 35 | value: ActionValue, 36 | ) -> ActionValue { 37 | if let Some(action) = action_map.action::() { 38 | if action.state() == ActionState::Fired { 39 | self.value += value.as_axis3d(); 40 | } else { 41 | self.value = value.as_axis3d(); 42 | } 43 | ActionValue::Axis3D(self.value).convert(value.dim()) 44 | } else { 45 | // TODO: use `warn_once` when `bevy_log` becomes `no_std` compatible. 46 | warn!( 47 | "action `{}` is not present in context", 48 | any::type_name::() 49 | ); 50 | value 51 | } 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use core::any::TypeId; 58 | 59 | use bevy_enhanced_input_macros::InputAction; 60 | 61 | use super::*; 62 | 63 | #[test] 64 | fn accumulation_active() { 65 | let mut modifier = AccumulateBy::::default(); 66 | let mut action = Action::new::(); 67 | let time = Time::default(); 68 | action.update(&time, ActionState::Fired, true); 69 | let mut action_map = ActionMap::default(); 70 | action_map.insert(TypeId::of::(), action); 71 | 72 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 73 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 2.0.into()); 74 | } 75 | 76 | #[test] 77 | fn accumulation_inactive() { 78 | let mut modifier = AccumulateBy::::default(); 79 | let action = Action::new::(); 80 | let time = Time::default(); 81 | let mut action_map = ActionMap::default(); 82 | action_map.insert(TypeId::of::(), action); 83 | 84 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 85 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 86 | } 87 | 88 | #[test] 89 | fn missing_action() { 90 | let mut modifier = AccumulateBy::::default(); 91 | let action_map = ActionMap::default(); 92 | let time = Time::default(); 93 | 94 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 95 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 96 | } 97 | 98 | #[derive(Debug, InputAction)] 99 | #[input_action(output = bool)] 100 | struct TestAction; 101 | } 102 | -------------------------------------------------------------------------------- /src/input_modifier/clamp.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Restricts input to a certain interval independently along each axis. 6 | /// 7 | /// [`ActionValue::Bool`] will be converted into [`ActionValue::Axis1D`] before clamping. 8 | /// 9 | /// # Examples 10 | /// 11 | /// Bind only positive or negative direction of [`GamepadAxis::LeftStickY`]. 12 | /// 13 | /// ``` 14 | /// use bevy::prelude::*; 15 | /// use bevy_enhanced_input::prelude::*; 16 | /// 17 | /// fn binding( 18 | /// trigger: Trigger>, 19 | /// mut ui: Query<&mut Actions>, 20 | /// ) { 21 | /// let mut actions = ui.get_mut(trigger.target()).unwrap(); 22 | /// actions 23 | /// .bind::() 24 | /// .to(GamepadAxis::LeftStickY) 25 | /// .with_modifiers(Clamp::pos()); 26 | /// actions 27 | /// .bind::() 28 | /// .to(GamepadAxis::LeftStickY) 29 | /// .with_modifiers(Clamp::neg()); 30 | /// } 31 | /// 32 | /// #[derive(InputContext)] 33 | /// struct Ui; 34 | /// 35 | /// #[derive(Debug, InputAction)] 36 | /// #[input_action(output = bool)] 37 | /// struct Up; 38 | /// 39 | /// #[derive(Debug, InputAction)] 40 | /// #[input_action(output = bool)] 41 | /// struct Down; 42 | /// ``` 43 | #[derive(Clone, Copy, Debug)] 44 | pub struct Clamp { 45 | /// Minimum value per axis. 46 | pub min: Vec3, 47 | /// Maximum value per axis. 48 | pub max: Vec3, 49 | } 50 | 51 | impl Clamp { 52 | /// Creates a new instance that restricts all axes only to positive numbers. 53 | /// 54 | /// Any negative values will become 0.0. 55 | #[must_use] 56 | pub fn pos() -> Self { 57 | Self::splat(0.0, f32::MAX) 58 | } 59 | 60 | /// Creates a new instance that restricts all axes only to negative numbers. 61 | /// 62 | /// Any positive values will become 0.0. 63 | #[must_use] 64 | pub fn neg() -> Self { 65 | Self::splat(f32::MIN, 0.0) 66 | } 67 | 68 | /// Creates a new instance with all axes set to `min` and `max`. 69 | #[must_use] 70 | pub fn splat(min: f32, max: f32) -> Self { 71 | Self::new(Vec3::splat(min), Vec3::splat(max)) 72 | } 73 | 74 | #[must_use] 75 | pub fn new(min: Vec3, max: Vec3) -> Self { 76 | Self { min, max } 77 | } 78 | } 79 | 80 | impl InputModifier for Clamp { 81 | fn apply( 82 | &mut self, 83 | _action_map: &ActionMap, 84 | _time: &Time, 85 | value: ActionValue, 86 | ) -> ActionValue { 87 | match value { 88 | ActionValue::Bool(value) => { 89 | let value: f32 = if value { 1.0 } else { 0.0 }; 90 | value.clamp(self.min.x, self.max.x).into() 91 | } 92 | ActionValue::Axis1D(value) => value.clamp(self.min.x, self.max.x).into(), 93 | ActionValue::Axis2D(value) => value.clamp(self.min.xy(), self.max.xy()).into(), 94 | ActionValue::Axis3D(value) => value.clamp(self.min, self.max).into(), 95 | } 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | 103 | #[test] 104 | fn clamping() { 105 | let mut modifier = Clamp::splat(0.0, 1.0); 106 | let action_map = ActionMap::default(); 107 | let time = Time::default(); 108 | 109 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 110 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 111 | assert_eq!(modifier.apply(&action_map, &time, 2.0.into()), 1.0.into()); 112 | assert_eq!( 113 | modifier.apply(&action_map, &time, (-1.0).into()), 114 | 0.0.into() 115 | ); 116 | 117 | assert_eq!( 118 | modifier.apply(&action_map, &time, Vec2::new(-1.0, 2.0).into()), 119 | Vec2::new(0.0, 1.0).into() 120 | ); 121 | 122 | assert_eq!( 123 | modifier.apply(&action_map, &time, Vec3::new(-2.0, 0.5, 3.0).into()), 124 | Vec3::new(0.0, 0.5, 1.0).into() 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/input_modifier/dead_zone.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Remaps input values within the range [Self::lower_threshold] to [Self::upper_threshold] onto the range 0 to 1. 6 | /// Values outside this range are clamped. 7 | /// 8 | /// This modifier acts as a normalizer, suitable for both analog and digital inputs (e.g., keyboards and gamepad sticks). 9 | /// Apply at the action level to ensure consistent diagonal movement speeds across different input sources. 10 | /// 11 | /// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`]. 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct DeadZone { 14 | /// Defines how axes are processed. 15 | /// 16 | /// By default set to [`DeadZoneKind::Radial`]. 17 | pub kind: DeadZoneKind, 18 | 19 | /// Threshold below which input is ignored. 20 | /// 21 | /// By default set to 0.2. 22 | pub lower_threshold: f32, 23 | 24 | /// Threshold above which input is clamped to 1. 25 | /// 26 | /// By default set to 1.0. 27 | pub upper_threshold: f32, 28 | } 29 | 30 | impl DeadZone { 31 | #[must_use] 32 | pub fn new(kind: DeadZoneKind) -> Self { 33 | Self { 34 | kind, 35 | lower_threshold: 0.2, 36 | upper_threshold: 1.0, 37 | } 38 | } 39 | 40 | #[must_use] 41 | pub fn with_lower_threshold(mut self, lower_threshold: f32) -> Self { 42 | self.lower_threshold = lower_threshold; 43 | self 44 | } 45 | 46 | #[must_use] 47 | pub fn with_upper_threshold(mut self, upper_threshold: f32) -> Self { 48 | self.upper_threshold = upper_threshold; 49 | self 50 | } 51 | 52 | fn dead_zone(self, axis_value: f32) -> f32 { 53 | // Translate and scale the input to the +/- 1 range after removing the dead zone. 54 | let lower_bound = (axis_value.abs() - self.lower_threshold).max(0.0); 55 | let scaled_value = lower_bound / (self.upper_threshold - self.lower_threshold); 56 | scaled_value.min(1.0) * axis_value.signum() 57 | } 58 | } 59 | 60 | impl Default for DeadZone { 61 | fn default() -> Self { 62 | Self::new(Default::default()) 63 | } 64 | } 65 | 66 | impl InputModifier for DeadZone { 67 | fn apply( 68 | &mut self, 69 | _action_map: &ActionMap, 70 | _time: &Time, 71 | value: ActionValue, 72 | ) -> ActionValue { 73 | match value { 74 | ActionValue::Bool(value) => { 75 | let value = if value { 1.0 } else { 0.0 }; 76 | self.dead_zone(value).into() 77 | } 78 | ActionValue::Axis1D(value) => self.dead_zone(value).into(), 79 | ActionValue::Axis2D(mut value) => match self.kind { 80 | DeadZoneKind::Radial => { 81 | (value.normalize_or_zero() * self.dead_zone(value.length())).into() 82 | } 83 | DeadZoneKind::Axial => { 84 | value.x = self.dead_zone(value.x); 85 | value.y = self.dead_zone(value.y); 86 | value.into() 87 | } 88 | }, 89 | ActionValue::Axis3D(mut value) => match self.kind { 90 | DeadZoneKind::Radial => { 91 | (value.normalize_or_zero() * self.dead_zone(value.length())).into() 92 | } 93 | DeadZoneKind::Axial => { 94 | value.x = self.dead_zone(value.x); 95 | value.y = self.dead_zone(value.y); 96 | value.z = self.dead_zone(value.z); 97 | value.into() 98 | } 99 | }, 100 | } 101 | } 102 | } 103 | 104 | /// Dead zone behavior. 105 | #[derive(Default, Clone, Copy, Debug)] 106 | pub enum DeadZoneKind { 107 | /// Apply dead zone logic to all axes simultaneously. 108 | /// 109 | /// This gives smooth input (circular/spherical coverage). 110 | /// For [`ActionValue::Axis1D`] and [`ActionValue::Bool`] 111 | /// this works identically to [`Self::Axial`]. 112 | #[default] 113 | Radial, 114 | /// Apply dead zone to axes individually. 115 | /// 116 | /// This will result in input being chamfered at the corners 117 | /// for [`ActionValue::Axis2D`]/[`ActionValue::Axis2D`]. 118 | Axial, 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #[test] 126 | fn radial() { 127 | let mut modifier = DeadZone::new(DeadZoneKind::Radial); 128 | let action_map = ActionMap::default(); 129 | let time = Time::default(); 130 | 131 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 132 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 133 | 134 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 135 | assert_eq!(modifier.apply(&action_map, &time, 0.5.into()), 0.375.into()); 136 | assert_eq!(modifier.apply(&action_map, &time, 0.2.into()), 0.0.into()); 137 | assert_eq!(modifier.apply(&action_map, &time, 2.0.into()), 1.0.into()); 138 | 139 | assert_eq!( 140 | modifier.apply(&action_map, &time, (Vec2::ONE * 0.5).into()), 141 | (Vec2::ONE * 0.4482233).into() 142 | ); 143 | assert_eq!( 144 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 145 | (Vec2::ONE * 0.70710677).into() 146 | ); 147 | assert_eq!( 148 | modifier.apply(&action_map, &time, (Vec2::ONE * 0.2).into()), 149 | (Vec2::ONE * 0.07322331).into() 150 | ); 151 | 152 | assert_eq!( 153 | modifier.apply(&action_map, &time, (Vec3::ONE * 0.5).into()), 154 | (Vec3::ONE * 0.48066244).into() 155 | ); 156 | assert_eq!( 157 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 158 | (Vec3::ONE * 0.57735026).into() 159 | ); 160 | assert_eq!( 161 | modifier.apply(&action_map, &time, (Vec3::ONE * 0.2).into()), 162 | (Vec3::ONE * 0.105662435).into() 163 | ); 164 | } 165 | 166 | #[test] 167 | fn axial() { 168 | let mut modifier = DeadZone::new(DeadZoneKind::Axial); 169 | let action_map = ActionMap::default(); 170 | let time = Time::default(); 171 | 172 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 173 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 174 | 175 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 176 | assert_eq!(modifier.apply(&action_map, &time, 0.5.into()), 0.375.into()); 177 | assert_eq!(modifier.apply(&action_map, &time, 0.2.into()), 0.0.into()); 178 | assert_eq!(modifier.apply(&action_map, &time, 2.0.into()), 1.0.into()); 179 | 180 | assert_eq!( 181 | modifier.apply(&action_map, &time, (Vec2::ONE * 0.5).into()), 182 | (Vec2::ONE * 0.375).into() 183 | ); 184 | assert_eq!( 185 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 186 | Vec2::ONE.into() 187 | ); 188 | assert_eq!( 189 | modifier.apply(&action_map, &time, (Vec2::ONE * 0.2).into()), 190 | Vec2::ZERO.into() 191 | ); 192 | 193 | assert_eq!( 194 | modifier.apply(&action_map, &time, (Vec3::ONE * 0.5).into()), 195 | (Vec3::ONE * 0.375).into() 196 | ); 197 | assert_eq!( 198 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 199 | Vec3::ONE.into() 200 | ); 201 | assert_eq!( 202 | modifier.apply(&action_map, &time, (Vec3::ONE * 0.2).into()), 203 | Vec3::ZERO.into() 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/input_modifier/delta_scale.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Multiplies the input value by delta time for this frame. 6 | /// 7 | /// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`]. 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct DeltaScale; 10 | 11 | impl InputModifier for DeltaScale { 12 | fn apply( 13 | &mut self, 14 | _action_map: &ActionMap, 15 | time: &Time, 16 | value: ActionValue, 17 | ) -> ActionValue { 18 | match value { 19 | ActionValue::Bool(value) => { 20 | let value = if value { 1.0 } else { 0.0 }; 21 | (value * time.delta_secs()).into() 22 | } 23 | ActionValue::Axis1D(value) => (value * time.delta_secs()).into(), 24 | ActionValue::Axis2D(value) => (value * time.delta_secs()).into(), 25 | ActionValue::Axis3D(value) => (value * time.delta_secs()).into(), 26 | } 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use core::time::Duration; 33 | 34 | use super::*; 35 | 36 | #[test] 37 | fn scaling() { 38 | let action_map = ActionMap::default(); 39 | let mut time = Time::default(); 40 | time.advance_by(Duration::from_millis(500)); 41 | 42 | assert_eq!( 43 | DeltaScale.apply(&action_map, &time, true.into()), 44 | 0.5.into() 45 | ); 46 | assert_eq!( 47 | DeltaScale.apply(&action_map, &time, false.into()), 48 | 0.0.into() 49 | ); 50 | assert_eq!( 51 | DeltaScale.apply(&action_map, &time, 0.5.into()), 52 | 0.25.into() 53 | ); 54 | assert_eq!( 55 | DeltaScale.apply(&action_map, &time, Vec2::ONE.into()), 56 | (0.5, 0.5).into() 57 | ); 58 | assert_eq!( 59 | DeltaScale.apply(&action_map, &time, Vec3::ONE.into()), 60 | (0.5, 0.5, 0.5).into() 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/input_modifier/exponential_curve.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Response curve exponential. 6 | /// 7 | /// Apply a simple exponential response curve to input values, per axis. 8 | /// 9 | /// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`]. 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct ExponentialCurve { 12 | /// Curve exponent. 13 | pub exp: Vec3, 14 | } 15 | 16 | impl ExponentialCurve { 17 | /// Creates a new exponential curve with all axes set to `value` 18 | #[must_use] 19 | pub fn splat(value: f32) -> Self { 20 | Self::new(Vec3::splat(value)) 21 | } 22 | 23 | #[must_use] 24 | pub fn new(exp: Vec3) -> Self { 25 | Self { exp } 26 | } 27 | } 28 | 29 | impl InputModifier for ExponentialCurve { 30 | fn apply( 31 | &mut self, 32 | _action_map: &ActionMap, 33 | _time: &Time, 34 | value: ActionValue, 35 | ) -> ActionValue { 36 | match value { 37 | ActionValue::Bool(value) => { 38 | let value = if value { 1.0 } else { 0.0 }; 39 | apply_exp(value, self.exp.x).into() 40 | } 41 | ActionValue::Axis1D(value) => apply_exp(value, self.exp.x).into(), 42 | ActionValue::Axis2D(mut value) => { 43 | value.x = apply_exp(value.x, self.exp.x); 44 | value.y = apply_exp(value.y, self.exp.y); 45 | value.into() 46 | } 47 | ActionValue::Axis3D(mut value) => { 48 | value.x = apply_exp(value.x, self.exp.x); 49 | value.y = apply_exp(value.y, self.exp.y); 50 | value.z = apply_exp(value.z, self.exp.z); 51 | value.into() 52 | } 53 | } 54 | } 55 | } 56 | 57 | fn apply_exp(value: f32, exp: f32) -> f32 { 58 | ops::powf(value.abs(), exp).copysign(value) 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | 65 | #[test] 66 | fn exp() { 67 | let action_map = ActionMap::default(); 68 | let time = Time::default(); 69 | let mut modifier = ExponentialCurve::splat(2.0); 70 | 71 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 72 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 73 | assert_eq!( 74 | modifier.apply(&action_map, &time, (-0.5).into()), 75 | (-0.25).into() 76 | ); 77 | assert_eq!(modifier.apply(&action_map, &time, 0.5.into()), 0.25.into()); 78 | assert_eq!( 79 | modifier.apply(&action_map, &time, (Vec2::ONE * 2.0).into()), 80 | (Vec2::ONE * 4.0).into() 81 | ); 82 | assert_eq!( 83 | modifier.apply(&action_map, &time, (Vec3::ONE * 2.0).into()), 84 | (Vec3::ONE * 4.0).into() 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/input_modifier/negate.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Inverts value per axis. 6 | /// 7 | /// By default, all axes are inverted. 8 | /// 9 | /// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`]. 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct Negate { 12 | /// Whether to inverse the X axis. 13 | pub x: bool, 14 | 15 | /// Whether to inverse the Y axis. 16 | pub y: bool, 17 | 18 | /// Whether to inverse the Z axis. 19 | pub z: bool, 20 | } 21 | 22 | impl Negate { 23 | /// Returns [`Self`] with inversion for all axes set to `invert` 24 | #[must_use] 25 | pub fn splat(invert: bool) -> Self { 26 | Self { 27 | x: invert, 28 | y: invert, 29 | z: invert, 30 | } 31 | } 32 | 33 | /// Returns [`Self`] with none of the axes inverted. 34 | pub fn none() -> Self { 35 | Self::splat(false) 36 | } 37 | 38 | /// Returns [`Self`] with all of the axes inverted. 39 | pub fn all() -> Self { 40 | Self::splat(true) 41 | } 42 | 43 | /// Returns [`Self`] with the X axis inverted. 44 | #[must_use] 45 | pub fn x() -> Self { 46 | Self { 47 | x: true, 48 | ..Self::none() 49 | } 50 | } 51 | 52 | /// Returns [`Self`] with the Y axis inverted. 53 | #[must_use] 54 | pub fn y() -> Self { 55 | Self { 56 | y: true, 57 | ..Self::none() 58 | } 59 | } 60 | 61 | /// Returns [`Self`] with the Z axis inverted. 62 | #[must_use] 63 | pub fn z() -> Self { 64 | Self { 65 | z: true, 66 | ..Self::none() 67 | } 68 | } 69 | } 70 | 71 | impl InputModifier for Negate { 72 | fn apply( 73 | &mut self, 74 | _action_map: &ActionMap, 75 | _time: &Time, 76 | value: ActionValue, 77 | ) -> ActionValue { 78 | match value { 79 | ActionValue::Bool(value) => { 80 | let value = if value { 1.0 } else { 0.0 }; 81 | self.apply(_action_map, _time, value.into()) 82 | } 83 | ActionValue::Axis1D(value) => { 84 | if self.x { 85 | (-value).into() 86 | } else { 87 | value.into() 88 | } 89 | } 90 | ActionValue::Axis2D(mut value) => { 91 | if self.x { 92 | value.x = -value.x; 93 | } 94 | if self.y { 95 | value.y = -value.y; 96 | } 97 | value.into() 98 | } 99 | ActionValue::Axis3D(mut value) => { 100 | if self.x { 101 | value.x = -value.x; 102 | } 103 | if self.y { 104 | value.y = -value.y; 105 | } 106 | if self.z { 107 | value.z = -value.z; 108 | } 109 | value.into() 110 | } 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | 119 | #[test] 120 | fn x() { 121 | let mut modifier = Negate::x(); 122 | let action_map = ActionMap::default(); 123 | let time = Time::default(); 124 | 125 | assert_eq!( 126 | modifier.apply(&action_map, &time, true.into()), 127 | (-1.0).into() 128 | ); 129 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 130 | assert_eq!( 131 | modifier.apply(&action_map, &time, 0.5.into()), 132 | (-0.5).into() 133 | ); 134 | assert_eq!( 135 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 136 | (-1.0, 1.0).into() 137 | ); 138 | assert_eq!( 139 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 140 | (-1.0, 1.0, 1.0).into(), 141 | ); 142 | } 143 | 144 | #[test] 145 | fn y() { 146 | let mut modifier = Negate::y(); 147 | let action_map = ActionMap::default(); 148 | let time = Time::default(); 149 | 150 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 151 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 152 | assert_eq!(modifier.apply(&action_map, &time, 0.5.into()), 0.5.into()); 153 | assert_eq!( 154 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 155 | (1.0, -1.0).into() 156 | ); 157 | assert_eq!( 158 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 159 | (1.0, -1.0, 1.0).into(), 160 | ); 161 | } 162 | 163 | #[test] 164 | fn z() { 165 | let mut modifier = Negate::z(); 166 | let action_map = ActionMap::default(); 167 | let time = Time::default(); 168 | 169 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 170 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 171 | assert_eq!(modifier.apply(&action_map, &time, 0.5.into()), 0.5.into()); 172 | assert_eq!( 173 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 174 | Vec2::ONE.into() 175 | ); 176 | assert_eq!( 177 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 178 | (1.0, 1.0, -1.0).into(), 179 | ); 180 | } 181 | 182 | #[test] 183 | fn all() { 184 | let mut modifier = Negate::all(); 185 | let action_map = ActionMap::default(); 186 | let time = Time::default(); 187 | 188 | assert_eq!( 189 | modifier.apply(&action_map, &time, true.into()), 190 | (-1.0).into() 191 | ); 192 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 193 | assert_eq!( 194 | modifier.apply(&action_map, &time, 0.5.into()), 195 | (-0.5).into() 196 | ); 197 | assert_eq!( 198 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 199 | Vec2::NEG_ONE.into(), 200 | ); 201 | assert_eq!( 202 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 203 | Vec3::NEG_ONE.into(), 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/input_modifier/scale.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Scales input independently along each axis by a specified factor. 6 | /// 7 | /// [`ActionValue::Bool`] will be converted into [`ActionValue::Axis1D`]. 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Scale { 10 | /// The factor applied to the input value. 11 | /// 12 | /// For example, if the factor is set to `Vec3::new(2.0, 2.0, 2.0)`, each input axis will be multiplied by 2.0. 13 | pub factor: Vec3, 14 | } 15 | 16 | impl Scale { 17 | /// Creates a new instance with all axes set to `value`. 18 | #[must_use] 19 | pub fn splat(value: f32) -> Self { 20 | Self::new(Vec3::splat(value)) 21 | } 22 | 23 | #[must_use] 24 | pub fn new(factor: Vec3) -> Self { 25 | Self { factor } 26 | } 27 | } 28 | 29 | impl InputModifier for Scale { 30 | fn apply( 31 | &mut self, 32 | _action_map: &ActionMap, 33 | _time: &Time, 34 | value: ActionValue, 35 | ) -> ActionValue { 36 | match value { 37 | ActionValue::Bool(value) => { 38 | let value = if value { 1.0 } else { 0.0 }; 39 | (value * self.factor.x).into() 40 | } 41 | ActionValue::Axis1D(value) => (value * self.factor.x).into(), 42 | ActionValue::Axis2D(value) => (value * self.factor.xy()).into(), 43 | ActionValue::Axis3D(value) => (value * self.factor).into(), 44 | } 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | 52 | #[test] 53 | fn scaling() { 54 | let mut modifier = Scale::splat(2.0); 55 | let action_map = ActionMap::default(); 56 | let time = Time::default(); 57 | 58 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 2.0.into()); 59 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 60 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 2.0.into()); 61 | assert_eq!( 62 | modifier.apply(&action_map, &time, Vec2::ONE.into()), 63 | (2.0, 2.0).into() 64 | ); 65 | assert_eq!( 66 | modifier.apply(&action_map, &time, Vec3::ONE.into()), 67 | (2.0, 2.0, 2.0).into() 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/input_modifier/smooth_nudge.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Produces a smoothed value of the current and previous input value. 6 | /// 7 | /// See [`StableInterpolate::smooth_nudge`] for details. 8 | /// 9 | /// [`ActionValue::Bool`] will be transformed into [`ActionValue::Axis1D`]. 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct SmoothNudge { 12 | /// Multiplier for delta time, determines the rate of smoothing. 13 | /// 14 | /// By default set to 8.0, an ad-hoc value that usually produces nice results. 15 | pub decay_rate: f32, 16 | 17 | current_value: Vec3, 18 | } 19 | 20 | impl SmoothNudge { 21 | #[must_use] 22 | pub fn new(decay_rate: f32) -> Self { 23 | Self { 24 | decay_rate, 25 | current_value: Default::default(), 26 | } 27 | } 28 | } 29 | 30 | impl Default for SmoothNudge { 31 | fn default() -> Self { 32 | Self::new(8.0) 33 | } 34 | } 35 | 36 | impl InputModifier for SmoothNudge { 37 | fn apply( 38 | &mut self, 39 | _action_map: &ActionMap, 40 | time: &Time, 41 | value: ActionValue, 42 | ) -> ActionValue { 43 | if let ActionValue::Bool(value) = value { 44 | let value = if value { 1.0 } else { 0.0 }; 45 | return self.apply(_action_map, time, value.into()); 46 | } 47 | 48 | let target_value = value.as_axis3d(); 49 | if self.current_value.distance_squared(target_value) < 1e-4 { 50 | // Snap to the target if the distance is too small. 51 | self.current_value = target_value; 52 | return value; 53 | } 54 | 55 | self.current_value 56 | .smooth_nudge(&target_value, self.decay_rate, time.delta_secs()); 57 | 58 | ActionValue::Axis3D(self.current_value).convert(value.dim()) 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use core::time::Duration; 65 | 66 | use super::*; 67 | 68 | #[test] 69 | fn lerp() { 70 | let mut modifier = SmoothNudge::default(); 71 | let action_map = ActionMap::default(); 72 | let mut time = Time::default(); 73 | time.advance_by(Duration::from_millis(100)); 74 | 75 | assert_eq!( 76 | modifier.apply(&action_map, &time, 0.5.into()), 77 | 0.27533552.into() 78 | ); 79 | assert_eq!( 80 | modifier.apply(&action_map, &time, 1.0.into()), 81 | 0.6743873.into() 82 | ); 83 | } 84 | 85 | #[test] 86 | fn bool_as_axis1d() { 87 | let mut modifier = SmoothNudge::default(); 88 | let action_map = ActionMap::default(); 89 | let mut time = Time::default(); 90 | time.advance_by(Duration::from_millis(100)); 91 | 92 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 93 | assert_eq!( 94 | modifier.apply(&action_map, &time, true.into()), 95 | 0.55067104.into() 96 | ); 97 | } 98 | 99 | #[test] 100 | fn snapping() { 101 | let mut modifier = SmoothNudge::default(); 102 | let action_map = ActionMap::default(); 103 | let mut time = Time::default(); 104 | time.advance_by(Duration::from_millis(100)); 105 | modifier.current_value = Vec3::X * 0.99; 106 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 107 | modifier.current_value = Vec3::X * 0.98; 108 | assert_ne!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/input_modifier/swizzle_axis.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{action_map::ActionMap, prelude::*}; 4 | 5 | /// Swizzle axis components of an input value. 6 | /// 7 | /// Useful for things like mapping a 1D input onto the Y axis of a 2D action. 8 | /// 9 | /// It tries to preserve the original dimension. However, if an axis from the original 10 | /// is promoted to a higher dimension, the value's type changes. Missing axes will be replaced with zero. 11 | /// 12 | /// For example, [`ActionValue::Bool`] will remain unchanged for [`Self::XZY`] (X in the first place). 13 | /// But for variants like [`Self::YXZ`] (where X becomes the second component), it will be 14 | /// converted into [`ActionValue::Axis2D`] with Y set to the value. 15 | #[derive(Clone, Copy, Debug)] 16 | pub enum SwizzleAxis { 17 | /// Swap X and Y axis. Useful for binding 1D inputs to the Y axis for 2D actions. 18 | YXZ, 19 | /// Swap X and Z axis. 20 | ZYX, 21 | /// Swap Y and Z axis. 22 | XZY, 23 | /// Reorder all axes, Y first. 24 | YZX, 25 | /// Reorder all axes, Z first. 26 | ZXY, 27 | } 28 | 29 | impl InputModifier for SwizzleAxis { 30 | fn apply( 31 | &mut self, 32 | _action_map: &ActionMap, 33 | _time: &Time, 34 | value: ActionValue, 35 | ) -> ActionValue { 36 | match value { 37 | ActionValue::Bool(value) => { 38 | let value = if value { 1.0 } else { 0.0 }; 39 | self.apply(_action_map, _time, value.into()) 40 | } 41 | ActionValue::Axis1D(value) => match self { 42 | SwizzleAxis::YXZ | SwizzleAxis::ZXY => (Vec2::Y * value).into(), 43 | SwizzleAxis::ZYX | SwizzleAxis::YZX => (Vec3::Z * value).into(), 44 | SwizzleAxis::XZY => value.into(), 45 | }, 46 | ActionValue::Axis2D(value) => match self { 47 | SwizzleAxis::YXZ => value.yx().into(), 48 | SwizzleAxis::ZYX => (0.0, value.y, value.x).into(), 49 | SwizzleAxis::XZY => (value.x, 0.0, value.y).into(), 50 | SwizzleAxis::YZX => (value.y, 0.0, value.x).into(), 51 | SwizzleAxis::ZXY => (0.0, value.x, value.y).into(), 52 | }, 53 | ActionValue::Axis3D(value) => match self { 54 | SwizzleAxis::YXZ => value.yxz().into(), 55 | SwizzleAxis::ZYX => value.zyx().into(), 56 | SwizzleAxis::XZY => value.xzy().into(), 57 | SwizzleAxis::YZX => value.yzx().into(), 58 | SwizzleAxis::ZXY => value.zxy().into(), 59 | }, 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn yxz() { 70 | let mut modifier = SwizzleAxis::YXZ; 71 | let action_map = ActionMap::default(); 72 | let time = Time::default(); 73 | 74 | assert_eq!( 75 | modifier.apply(&action_map, &time, true.into()), 76 | Vec2::Y.into() 77 | ); 78 | assert_eq!( 79 | modifier.apply(&action_map, &time, false.into()), 80 | Vec2::ZERO.into() 81 | ); 82 | assert_eq!( 83 | modifier.apply(&action_map, &time, 1.0.into()), 84 | Vec2::Y.into() 85 | ); 86 | assert_eq!( 87 | modifier.apply(&action_map, &time, (0.0, 1.0).into()), 88 | (1.0, 0.0).into() 89 | ); 90 | assert_eq!( 91 | modifier.apply(&action_map, &time, (0.0, 1.0, 2.0).into()), 92 | (1.0, 0.0, 2.0).into(), 93 | ); 94 | } 95 | 96 | #[test] 97 | fn zyx() { 98 | let mut modifier = SwizzleAxis::ZYX; 99 | let action_map = ActionMap::default(); 100 | let time = Time::default(); 101 | 102 | assert_eq!( 103 | modifier.apply(&action_map, &time, true.into()), 104 | Vec3::Z.into() 105 | ); 106 | assert_eq!( 107 | modifier.apply(&action_map, &time, false.into()), 108 | Vec3::ZERO.into() 109 | ); 110 | assert_eq!( 111 | modifier.apply(&action_map, &time, 1.0.into()), 112 | Vec3::Z.into() 113 | ); 114 | assert_eq!( 115 | modifier.apply(&action_map, &time, (0.0, 1.0).into()), 116 | (0.0, 1.0, 0.0).into() 117 | ); 118 | assert_eq!( 119 | modifier.apply(&action_map, &time, (0.0, 1.0, 2.0).into()), 120 | (2.0, 1.0, 0.0).into(), 121 | ); 122 | } 123 | 124 | #[test] 125 | fn xzy() { 126 | let mut modifier = SwizzleAxis::XZY; 127 | let action_map = ActionMap::default(); 128 | let time = Time::default(); 129 | 130 | assert_eq!(modifier.apply(&action_map, &time, true.into()), 1.0.into()); 131 | assert_eq!(modifier.apply(&action_map, &time, false.into()), 0.0.into()); 132 | assert_eq!(modifier.apply(&action_map, &time, 1.0.into()), 1.0.into()); 133 | assert_eq!( 134 | modifier.apply(&action_map, &time, (0.0, 1.0).into()), 135 | (0.0, 0.0, 1.0).into() 136 | ); 137 | assert_eq!( 138 | modifier.apply(&action_map, &time, (0.0, 1.0, 2.0).into()), 139 | (0.0, 2.0, 1.0).into(), 140 | ); 141 | } 142 | 143 | #[test] 144 | fn yzx() { 145 | let mut modifier = SwizzleAxis::YZX; 146 | let action_map = ActionMap::default(); 147 | let time = Time::default(); 148 | 149 | assert_eq!( 150 | modifier.apply(&action_map, &time, true.into()), 151 | Vec3::Z.into() 152 | ); 153 | assert_eq!( 154 | modifier.apply(&action_map, &time, false.into()), 155 | Vec3::ZERO.into() 156 | ); 157 | assert_eq!( 158 | modifier.apply(&action_map, &time, 1.0.into()), 159 | Vec3::Z.into() 160 | ); 161 | assert_eq!( 162 | modifier.apply(&action_map, &time, (0.0, 1.0).into()), 163 | (1.0, 0.0, 0.0).into() 164 | ); 165 | assert_eq!( 166 | modifier.apply(&action_map, &time, (0.0, 1.0, 2.0).into()), 167 | (1.0, 2.0, 0.0).into(), 168 | ); 169 | } 170 | 171 | #[test] 172 | fn zxy() { 173 | let mut modifier = SwizzleAxis::ZXY; 174 | let action_map = ActionMap::default(); 175 | let time = Time::default(); 176 | 177 | assert_eq!( 178 | modifier.apply(&action_map, &time, true.into()), 179 | Vec2::Y.into() 180 | ); 181 | assert_eq!( 182 | modifier.apply(&action_map, &time, false.into()), 183 | Vec2::ZERO.into() 184 | ); 185 | assert_eq!( 186 | modifier.apply(&action_map, &time, 1.0.into()), 187 | Vec2::Y.into() 188 | ); 189 | assert_eq!( 190 | modifier.apply(&action_map, &time, (0.0, 1.0).into()), 191 | (0.0, 0.0, 1.0).into() 192 | ); 193 | assert_eq!( 194 | modifier.apply(&action_map, &time, (0.0, 1.0, 2.0).into()), 195 | (2.0, 0.0, 1.0).into(), 196 | ); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/trigger_tracker.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | 3 | use bevy::prelude::*; 4 | use log::trace; 5 | 6 | use crate::{action_map::ActionMap, prelude::*}; 7 | 8 | /// Helper to calculate [`ActionState`] based on its modifiers and conditions. 9 | /// 10 | /// Could be used to track both input-level state and action-level state. 11 | pub(crate) struct TriggerTracker { 12 | value: ActionValue, 13 | found_explicit: bool, 14 | any_explicit_fired: bool, 15 | found_active: bool, 16 | found_implicit: bool, 17 | all_implicits_fired: bool, 18 | blocked: bool, 19 | } 20 | 21 | impl TriggerTracker { 22 | #[must_use] 23 | pub(crate) fn new(value: ActionValue) -> Self { 24 | Self { 25 | value, 26 | found_explicit: false, 27 | any_explicit_fired: false, 28 | found_active: false, 29 | found_implicit: false, 30 | all_implicits_fired: true, 31 | blocked: false, 32 | } 33 | } 34 | 35 | pub(crate) fn apply_modifiers( 36 | &mut self, 37 | action_map: &ActionMap, 38 | time: &Time, 39 | modifiers: &mut [Box], 40 | ) { 41 | for modifier in modifiers { 42 | let new_value = modifier.apply(action_map, time, self.value); 43 | trace!( 44 | "`{modifier:?}` changes `{:?}` to `{new_value:?}`", 45 | self.value 46 | ); 47 | 48 | self.value = new_value; 49 | } 50 | } 51 | 52 | pub(crate) fn apply_conditions( 53 | &mut self, 54 | action_map: &ActionMap, 55 | time: &Time, 56 | conditions: &mut [Box], 57 | ) { 58 | // Note: No early outs permitted! 59 | // All conditions must be evaluated to update their internal state/delta time. 60 | for condition in conditions { 61 | let state = condition.evaluate(action_map, time, self.value); 62 | trace!("`{condition:?}` returns state `{state:?}`"); 63 | match condition.kind() { 64 | ConditionKind::Explicit => { 65 | self.found_explicit = true; 66 | self.any_explicit_fired |= state == ActionState::Fired; 67 | self.found_active |= state != ActionState::None; 68 | } 69 | ConditionKind::Implicit => { 70 | self.found_implicit = true; 71 | self.all_implicits_fired &= state == ActionState::Fired; 72 | self.found_active |= state != ActionState::None; 73 | } 74 | ConditionKind::Blocker => { 75 | self.blocked |= state == ActionState::None; 76 | } 77 | } 78 | } 79 | } 80 | 81 | pub(crate) fn state(&self) -> ActionState { 82 | if self.blocked { 83 | return ActionState::None; 84 | } 85 | 86 | if !self.found_explicit && !self.found_implicit { 87 | if self.value.as_bool() { 88 | return ActionState::Fired; 89 | } else { 90 | return ActionState::None; 91 | } 92 | } 93 | 94 | if (!self.found_explicit || self.any_explicit_fired) && self.all_implicits_fired { 95 | ActionState::Fired 96 | } else if self.found_active { 97 | ActionState::Ongoing 98 | } else { 99 | ActionState::None 100 | } 101 | } 102 | 103 | pub(crate) fn value(&self) -> ActionValue { 104 | self.value 105 | } 106 | 107 | /// Replaces the state with `other`. 108 | /// 109 | /// Preserves the value dimension. 110 | pub(crate) fn overwrite(&mut self, other: TriggerTracker) { 111 | let dim = self.value.dim(); 112 | *self = other; 113 | self.value = self.value.convert(dim); 114 | } 115 | 116 | /// Merges two trackers. 117 | /// 118 | /// Preserves the value dimension. 119 | pub(crate) fn combine(&mut self, other: Self, accumulation: Accumulation) { 120 | let accumulated = match accumulation { 121 | Accumulation::MaxAbs => { 122 | let mut value = self.value.as_axis3d().to_array(); 123 | let other_value = other.value.as_axis3d().to_array(); 124 | for (axis, other_axis) in value.iter_mut().zip(other_value) { 125 | if axis.abs() < other_axis.abs() { 126 | *axis = other_axis; 127 | } 128 | } 129 | value.into() 130 | } 131 | Accumulation::Cumulative => self.value.as_axis3d() + other.value.as_axis3d(), 132 | }; 133 | 134 | self.value = ActionValue::Axis3D(accumulated).convert(self.value.dim()); 135 | self.found_explicit |= other.found_explicit; 136 | self.any_explicit_fired |= other.any_explicit_fired; 137 | self.found_active |= other.found_active; 138 | self.found_implicit |= other.found_implicit; 139 | self.all_implicits_fired &= other.all_implicits_fired; 140 | self.blocked |= other.blocked; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/accumulation.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn max_abs() { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | let mut keys = app.world_mut().resource_mut::>(); 18 | keys.press(KeyCode::KeyW); 19 | keys.press(KeyCode::KeyS); 20 | 21 | app.update(); 22 | 23 | let actions = app.world().get::>(entity).unwrap(); 24 | assert_eq!(actions.value::().unwrap(), Vec2::Y.into()); 25 | } 26 | 27 | #[test] 28 | fn cumulative() { 29 | let mut app = App::new(); 30 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 31 | .add_input_context::() 32 | .add_observer(binding) 33 | .finish(); 34 | 35 | let entity = app.world_mut().spawn(Actions::::default()).id(); 36 | 37 | app.update(); 38 | 39 | let mut keys = app.world_mut().resource_mut::>(); 40 | keys.press(KeyCode::ArrowUp); 41 | keys.press(KeyCode::ArrowDown); 42 | 43 | app.update(); 44 | 45 | let actions = app.world().get::>(entity).unwrap(); 46 | assert_eq!( 47 | actions.value::().unwrap(), 48 | Vec2::ZERO.into(), 49 | "up and down should cancel each other" 50 | ); 51 | } 52 | 53 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 54 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 55 | actions.bind::().to(Cardinal::wasd_keys()); 56 | actions.bind::().to(Cardinal::arrow_keys()); 57 | } 58 | 59 | #[derive(InputContext)] 60 | struct Test; 61 | 62 | #[derive(Debug, InputAction)] 63 | #[input_action(output = Vec2, accumulation = MaxAbs)] 64 | struct MaxAbs; 65 | 66 | #[derive(Debug, InputAction)] 67 | #[input_action(output = Vec2, accumulation = Cumulative)] 68 | struct Cumulative; 69 | -------------------------------------------------------------------------------- /tests/condition_kind.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn explicit() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | let actions = app.world().get::>(entity).unwrap(); 18 | let action = actions.get::()?; 19 | assert_eq!(action.value(), false.into()); 20 | assert_eq!(action.state(), ActionState::None); 21 | 22 | app.world_mut() 23 | .resource_mut::>() 24 | .press(Explicit::KEY); 25 | 26 | app.update(); 27 | 28 | let actions = app.world().get::>(entity).unwrap(); 29 | let action = actions.get::()?; 30 | assert_eq!(action.value(), true.into()); 31 | assert_eq!(action.state(), ActionState::Fired); 32 | 33 | app.world_mut() 34 | .resource_mut::>() 35 | .release(Explicit::KEY); 36 | 37 | app.update(); 38 | 39 | let actions = app.world().get::>(entity).unwrap(); 40 | let action = actions.get::()?; 41 | assert_eq!(action.value(), false.into()); 42 | assert_eq!(action.state(), ActionState::None); 43 | 44 | Ok(()) 45 | } 46 | 47 | #[test] 48 | fn implicit() -> Result<()> { 49 | let mut app = App::new(); 50 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 51 | .add_input_context::() 52 | .add_observer(binding) 53 | .finish(); 54 | 55 | let entity = app.world_mut().spawn(Actions::::default()).id(); 56 | 57 | app.update(); 58 | 59 | let actions = app.world().get::>(entity).unwrap(); 60 | let action = actions.get::()?; 61 | assert_eq!(action.value(), false.into()); 62 | assert_eq!(action.state(), ActionState::None); 63 | 64 | let actions = app.world().get::>(entity).unwrap(); 65 | let action = actions.get::()?; 66 | assert_eq!(action.value(), false.into()); 67 | assert_eq!(action.state(), ActionState::None); 68 | 69 | app.world_mut() 70 | .resource_mut::>() 71 | .press(ReleaseAction::KEY); 72 | 73 | app.update(); 74 | 75 | let actions = app.world().get::>(entity).unwrap(); 76 | let action = actions.get::()?; 77 | assert_eq!(action.value(), true.into()); 78 | assert_eq!(action.state(), ActionState::Ongoing); 79 | 80 | let actions = app.world().get::>(entity).unwrap(); 81 | let action = actions.get::()?; 82 | assert_eq!(action.value(), false.into()); 83 | assert_eq!(action.state(), ActionState::Ongoing); 84 | 85 | app.world_mut() 86 | .resource_mut::>() 87 | .release(ReleaseAction::KEY); 88 | 89 | app.update(); 90 | 91 | let actions = app.world().get::>(entity).unwrap(); 92 | let action = actions.get::()?; 93 | assert_eq!(action.value(), false.into()); 94 | assert_eq!(action.state(), ActionState::Fired); 95 | 96 | let actions = app.world().get::>(entity).unwrap(); 97 | let action = actions.get::()?; 98 | assert_eq!(action.value(), false.into()); 99 | assert_eq!(action.state(), ActionState::Fired); 100 | 101 | app.update(); 102 | 103 | let actions = app.world().get::>(entity).unwrap(); 104 | let action = actions.get::()?; 105 | assert_eq!(action.value(), false.into()); 106 | assert_eq!(action.state(), ActionState::None); 107 | 108 | let actions = app.world().get::>(entity).unwrap(); 109 | let action = actions.get::()?; 110 | assert_eq!(action.value(), false.into()); 111 | assert_eq!(action.state(), ActionState::None); 112 | 113 | Ok(()) 114 | } 115 | 116 | #[test] 117 | fn blocker() -> Result<()> { 118 | let mut app = App::new(); 119 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 120 | .add_input_context::() 121 | .add_observer(binding) 122 | .finish(); 123 | 124 | let entity = app.world_mut().spawn(Actions::::default()).id(); 125 | 126 | app.update(); 127 | 128 | let actions = app.world().get::>(entity).unwrap(); 129 | let action = actions.get::()?; 130 | assert_eq!(action.value(), false.into()); 131 | assert_eq!(action.state(), ActionState::None); 132 | 133 | let actions = app.world().get::>(entity).unwrap(); 134 | let action = actions.get::()?; 135 | assert_eq!(action.value(), false.into()); 136 | assert_eq!(action.state(), ActionState::None); 137 | 138 | let mut keys = app.world_mut().resource_mut::>(); 139 | keys.press(ReleaseAction::KEY); 140 | keys.press(Blocker::KEY); 141 | 142 | app.update(); 143 | 144 | let actions = app.world().get::>(entity).unwrap(); 145 | let action = actions.get::()?; 146 | assert_eq!(action.value(), true.into()); 147 | assert_eq!(action.state(), ActionState::Ongoing); 148 | 149 | let actions = app.world().get::>(entity).unwrap(); 150 | let action = actions.get::()?; 151 | assert_eq!(action.value(), true.into()); 152 | assert_eq!(action.state(), ActionState::Fired); 153 | 154 | app.world_mut() 155 | .resource_mut::>() 156 | .release(ReleaseAction::KEY); 157 | 158 | app.update(); 159 | 160 | let actions = app.world().get::>(entity).unwrap(); 161 | let action = actions.get::()?; 162 | assert_eq!(action.value(), false.into()); 163 | assert_eq!(action.state(), ActionState::Fired); 164 | 165 | let actions = app.world().get::>(entity).unwrap(); 166 | let action = actions.get::()?; 167 | assert_eq!(action.value(), true.into()); 168 | assert_eq!(action.state(), ActionState::None); 169 | 170 | app.update(); 171 | 172 | let actions = app.world().get::>(entity).unwrap(); 173 | let action = actions.get::()?; 174 | assert_eq!(action.value(), false.into()); 175 | assert_eq!(action.state(), ActionState::None); 176 | 177 | let actions = app.world().get::>(entity).unwrap(); 178 | let action = actions.get::()?; 179 | assert_eq!(action.value(), true.into()); 180 | assert_eq!(action.state(), ActionState::Fired); 181 | 182 | Ok(()) 183 | } 184 | 185 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 186 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 187 | actions 188 | .bind::() 189 | .to(ReleaseAction::KEY) 190 | .with_conditions(Release::default()); 191 | actions 192 | .bind::() 193 | .with_conditions(Down::default()) 194 | .to(Explicit::KEY); 195 | actions 196 | .bind::() 197 | .with_conditions(Chord::::default()); 198 | actions 199 | .bind::() 200 | .to(Blocker::KEY) 201 | .with_conditions(BlockBy::::default()); 202 | } 203 | 204 | #[derive(InputContext)] 205 | struct Player; 206 | 207 | #[derive(Debug, InputAction)] 208 | #[input_action(output = bool)] 209 | struct ReleaseAction; 210 | 211 | impl ReleaseAction { 212 | const KEY: KeyCode = KeyCode::KeyA; 213 | } 214 | 215 | #[derive(Debug, InputAction)] 216 | #[input_action(output = bool)] 217 | struct Explicit; 218 | 219 | impl Explicit { 220 | const KEY: KeyCode = KeyCode::KeyB; 221 | } 222 | 223 | #[derive(Debug, InputAction)] 224 | #[input_action(output = bool)] 225 | struct Implicit; 226 | 227 | #[derive(Debug, InputAction)] 228 | #[input_action(output = bool)] 229 | struct Blocker; 230 | 231 | impl Blocker { 232 | const KEY: KeyCode = KeyCode::KeyD; 233 | } 234 | -------------------------------------------------------------------------------- /tests/consume_input.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn consume() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(consume_only_binding) 11 | .finish(); 12 | 13 | let entity1 = app 14 | .world_mut() 15 | .spawn(Actions::::default()) 16 | .id(); 17 | let entity2 = app 18 | .world_mut() 19 | .spawn(Actions::::default()) 20 | .id(); 21 | 22 | app.update(); 23 | 24 | app.world_mut() 25 | .resource_mut::>() 26 | .press(KEY); 27 | 28 | app.update(); 29 | 30 | let entity1_ctx = app.world().get::>(entity1).unwrap(); 31 | assert_eq!(entity1_ctx.state::()?, ActionState::Fired); 32 | 33 | let entity2_ctx = app.world().get::>(entity2).unwrap(); 34 | assert_eq!( 35 | entity2_ctx.state::()?, 36 | ActionState::None, 37 | "only first entity with the same mappings that consume inputs should receive them" 38 | ); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn passthrough() -> Result<()> { 45 | let mut app = App::new(); 46 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 47 | .add_input_context::() 48 | .add_observer(passthrough_only_binding) 49 | .finish(); 50 | 51 | let entity1 = app 52 | .world_mut() 53 | .spawn(Actions::::default()) 54 | .id(); 55 | let entity2 = app 56 | .world_mut() 57 | .spawn(Actions::::default()) 58 | .id(); 59 | 60 | app.update(); 61 | 62 | app.world_mut() 63 | .resource_mut::>() 64 | .press(KEY); 65 | 66 | app.update(); 67 | 68 | let entity1_ctx = app 69 | .world() 70 | .get::>(entity1) 71 | .unwrap(); 72 | assert_eq!(entity1_ctx.state::()?, ActionState::Fired); 73 | 74 | let entity2_ctx = app 75 | .world() 76 | .get::>(entity2) 77 | .unwrap(); 78 | assert_eq!( 79 | entity2_ctx.state::()?, 80 | ActionState::Fired, 81 | "actions that doesn't consume inputs should still fire" 82 | ); 83 | 84 | Ok(()) 85 | } 86 | 87 | #[test] 88 | fn consume_then_passthrough() -> Result<()> { 89 | let mut app = App::new(); 90 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 91 | .add_input_context::() 92 | .add_observer(consume_then_passthrough_binding) 93 | .finish(); 94 | 95 | let entity = app 96 | .world_mut() 97 | .spawn(Actions::::default()) 98 | .id(); 99 | 100 | app.update(); 101 | 102 | app.world_mut() 103 | .resource_mut::>() 104 | .press(KEY); 105 | 106 | app.update(); 107 | 108 | let actions = app 109 | .world() 110 | .get::>(entity) 111 | .unwrap(); 112 | assert_eq!(actions.state::()?, ActionState::Fired); 113 | assert_eq!( 114 | actions.state::()?, 115 | ActionState::None, 116 | "action should be consumed" 117 | ); 118 | 119 | Ok(()) 120 | } 121 | 122 | #[test] 123 | fn passthrough_then_consume() -> Result<()> { 124 | let mut app = App::new(); 125 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 126 | .add_input_context::() 127 | .add_observer(passthrough_then_consume_binding) 128 | .finish(); 129 | 130 | let entity = app 131 | .world_mut() 132 | .spawn(Actions::::default()) 133 | .id(); 134 | 135 | app.update(); 136 | 137 | app.world_mut() 138 | .resource_mut::>() 139 | .press(KEY); 140 | 141 | app.update(); 142 | 143 | let actions = app 144 | .world() 145 | .get::>(entity) 146 | .unwrap(); 147 | assert_eq!(actions.state::()?, ActionState::Fired); 148 | assert_eq!(actions.state::()?, ActionState::Fired); 149 | 150 | Ok(()) 151 | } 152 | 153 | fn consume_only_binding( 154 | trigger: Trigger>, 155 | mut actions: Query<&mut Actions>, 156 | ) { 157 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 158 | actions.bind::().to(KEY); 159 | } 160 | 161 | fn passthrough_only_binding( 162 | trigger: Trigger>, 163 | mut actions: Query<&mut Actions>, 164 | ) { 165 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 166 | actions.bind::().to(KEY); 167 | } 168 | 169 | fn consume_then_passthrough_binding( 170 | trigger: Trigger>, 171 | mut actions: Query<&mut Actions>, 172 | ) { 173 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 174 | actions.bind::().to(KEY); 175 | actions.bind::().to(KEY); 176 | } 177 | 178 | fn passthrough_then_consume_binding( 179 | trigger: Trigger>, 180 | mut actions: Query<&mut Actions>, 181 | ) { 182 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 183 | actions.bind::().to(KEY); 184 | actions.bind::().to(KEY); 185 | } 186 | 187 | #[derive(InputContext)] 188 | struct PassthroughOnly; 189 | 190 | #[derive(InputContext)] 191 | struct ConsumeOnly; 192 | 193 | #[derive(InputContext)] 194 | struct PassthroughThenConsume; 195 | 196 | #[derive(InputContext)] 197 | struct ConsumeThenPassthrough; 198 | 199 | /// A key used by both [`Consume`] and [`Passthrough`] actions. 200 | const KEY: KeyCode = KeyCode::KeyA; 201 | 202 | #[derive(Debug, InputAction)] 203 | #[input_action(output = bool, consume_input = true)] 204 | struct Consume; 205 | 206 | #[derive(Debug, InputAction)] 207 | #[input_action(output = bool, consume_input = false)] 208 | struct Passthrough; 209 | -------------------------------------------------------------------------------- /tests/context_gamepad.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn any() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(any_gamepad_binding) 11 | .finish(); 12 | 13 | let gamepad_entity1 = app.world_mut().spawn(Gamepad::default()).id(); 14 | let gamepad_entity2 = app.world_mut().spawn(Gamepad::default()).id(); 15 | 16 | let context_entity = app.world_mut().spawn(Actions::::default()).id(); 17 | 18 | app.update(); 19 | 20 | let mut gamepad1 = app.world_mut().get_mut::(gamepad_entity1).unwrap(); 21 | gamepad1.analog_mut().set(TestAction::BUTTON, 1.0); 22 | 23 | app.update(); 24 | 25 | let actions = app 26 | .world() 27 | .get::>(context_entity) 28 | .unwrap(); 29 | assert_eq!(actions.state::()?, ActionState::Fired); 30 | 31 | let mut gamepad1 = app.world_mut().get_mut::(gamepad_entity1).unwrap(); 32 | gamepad1.analog_mut().set(TestAction::BUTTON, 0.0); 33 | 34 | let mut gamepad2 = app.world_mut().get_mut::(gamepad_entity2).unwrap(); 35 | gamepad2.analog_mut().set(TestAction::BUTTON, 1.0); 36 | 37 | app.update(); 38 | 39 | let actions = app 40 | .world() 41 | .get::>(context_entity) 42 | .unwrap(); 43 | assert_eq!(actions.state::()?, ActionState::Fired); 44 | 45 | Ok(()) 46 | } 47 | 48 | #[test] 49 | fn by_id() -> Result<()> { 50 | let mut app = App::new(); 51 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 52 | .add_input_context::() 53 | .add_observer(single_gamepad_binding) 54 | .finish(); 55 | 56 | let gamepad_entity1 = app.world_mut().spawn(Gamepad::default()).id(); 57 | let gamepad_entity2 = app.world_mut().spawn(Gamepad::default()).id(); 58 | 59 | let context_entity = app 60 | .world_mut() 61 | .spawn(( 62 | Actions::::default(), 63 | SingleGamepad(gamepad_entity1), 64 | )) 65 | .id(); 66 | 67 | app.update(); 68 | 69 | let mut gamepad1 = app.world_mut().get_mut::(gamepad_entity1).unwrap(); 70 | gamepad1.analog_mut().set(TestAction::BUTTON, 1.0); 71 | 72 | app.update(); 73 | 74 | let actions = app 75 | .world() 76 | .get::>(context_entity) 77 | .unwrap(); 78 | assert_eq!(actions.state::()?, ActionState::Fired); 79 | 80 | let mut gamepad1 = app.world_mut().get_mut::(gamepad_entity1).unwrap(); 81 | gamepad1.analog_mut().set(TestAction::BUTTON, 0.0); 82 | 83 | let mut gamepad2 = app.world_mut().get_mut::(gamepad_entity2).unwrap(); 84 | gamepad2.analog_mut().set(TestAction::BUTTON, 1.0); 85 | 86 | app.update(); 87 | 88 | let actions = app 89 | .world() 90 | .get::>(context_entity) 91 | .unwrap(); 92 | assert_eq!(actions.state::()?, ActionState::None); 93 | 94 | Ok(()) 95 | } 96 | 97 | fn any_gamepad_binding( 98 | trigger: Trigger>, 99 | mut actions: Query<&mut Actions>, 100 | ) { 101 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 102 | actions.bind::().to(TestAction::BUTTON); 103 | } 104 | 105 | fn single_gamepad_binding( 106 | trigger: Trigger>, 107 | mut actions: Query<(&mut Actions, &mut SingleGamepad)>, 108 | ) { 109 | let (mut actions, gamepad) = actions.get_mut(trigger.target()).unwrap(); 110 | actions.set_gamepad(**gamepad); 111 | actions.bind::().to(TestAction::BUTTON); 112 | } 113 | 114 | #[derive(InputContext)] 115 | struct AnyGamepad; 116 | 117 | #[derive(Component, Deref, InputContext)] 118 | struct SingleGamepad(Entity); 119 | 120 | #[derive(Debug, InputAction)] 121 | #[input_action(output = bool)] 122 | struct TestAction; 123 | 124 | impl TestAction { 125 | const BUTTON: GamepadButton = GamepadButton::South; 126 | } 127 | -------------------------------------------------------------------------------- /tests/dim.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn bool() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | app.world_mut() 18 | .resource_mut::>() 19 | .press(Bool::KEY); 20 | 21 | app.update(); 22 | 23 | let actions = app.world().get::>(entity).unwrap(); 24 | assert_eq!(actions.value::()?, true.into()); 25 | 26 | app.world_mut() 27 | .resource_mut::>() 28 | .release(Bool::KEY); 29 | 30 | app.update(); 31 | 32 | let actions = app.world().get::>(entity).unwrap(); 33 | assert_eq!(actions.value::()?, false.into()); 34 | 35 | Ok(()) 36 | } 37 | 38 | #[test] 39 | fn axis1d() -> Result<()> { 40 | let mut app = App::new(); 41 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 42 | .add_input_context::() 43 | .add_observer(binding) 44 | .finish(); 45 | 46 | let entity = app.world_mut().spawn(Actions::::default()).id(); 47 | 48 | app.update(); 49 | 50 | app.world_mut() 51 | .resource_mut::>() 52 | .press(Axis1D::KEY); 53 | 54 | app.update(); 55 | 56 | let actions = app.world().get::>(entity).unwrap(); 57 | assert_eq!(actions.value::()?, 1.0.into()); 58 | 59 | app.world_mut() 60 | .resource_mut::>() 61 | .release(Axis1D::KEY); 62 | 63 | app.update(); 64 | 65 | let actions = app.world().get::>(entity).unwrap(); 66 | assert_eq!(actions.value::()?, 0.0.into()); 67 | 68 | Ok(()) 69 | } 70 | 71 | #[test] 72 | fn axis2d() -> Result<()> { 73 | let mut app = App::new(); 74 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 75 | .add_input_context::() 76 | .add_observer(binding) 77 | .finish(); 78 | 79 | let entity = app.world_mut().spawn(Actions::::default()).id(); 80 | 81 | app.update(); 82 | 83 | app.world_mut() 84 | .resource_mut::>() 85 | .press(Axis2D::KEY); 86 | 87 | app.update(); 88 | 89 | let actions = app.world().get::>(entity).unwrap(); 90 | assert_eq!(actions.value::()?, (1.0, 0.0).into()); 91 | 92 | app.world_mut() 93 | .resource_mut::>() 94 | .release(Axis2D::KEY); 95 | 96 | app.update(); 97 | 98 | let actions = app.world().get::>(entity).unwrap(); 99 | assert_eq!(actions.value::()?, Vec2::ZERO.into()); 100 | 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn axis3d() -> Result<()> { 106 | let mut app = App::new(); 107 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 108 | .add_input_context::() 109 | .add_observer(binding) 110 | .finish(); 111 | 112 | let entity = app.world_mut().spawn(Actions::::default()).id(); 113 | 114 | app.update(); 115 | 116 | app.world_mut() 117 | .resource_mut::>() 118 | .press(Axis3D::KEY); 119 | 120 | app.update(); 121 | 122 | let actions = app.world().get::>(entity).unwrap(); 123 | assert_eq!(actions.value::()?, (1.0, 0.0, 0.0).into()); 124 | 125 | app.world_mut() 126 | .resource_mut::>() 127 | .release(Axis3D::KEY); 128 | 129 | app.update(); 130 | 131 | let actions = app.world().get::>(entity).unwrap(); 132 | assert_eq!(actions.value::()?, Vec3::ZERO.into()); 133 | 134 | Ok(()) 135 | } 136 | 137 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 138 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 139 | actions.bind::().to(Bool::KEY); 140 | actions.bind::().to(Axis1D::KEY); 141 | actions.bind::().to(Axis2D::KEY); 142 | actions.bind::().to(Axis3D::KEY); 143 | } 144 | 145 | #[derive(InputContext)] 146 | struct Test; 147 | 148 | #[derive(Debug, InputAction)] 149 | #[input_action(output = bool)] 150 | struct Bool; 151 | 152 | impl Bool { 153 | const KEY: KeyCode = KeyCode::KeyA; 154 | } 155 | 156 | #[derive(Debug, InputAction)] 157 | #[input_action(output = f32)] 158 | struct Axis1D; 159 | 160 | impl Axis1D { 161 | const KEY: KeyCode = KeyCode::KeyB; 162 | } 163 | 164 | #[derive(Debug, InputAction)] 165 | #[input_action(output = Vec2)] 166 | struct Axis2D; 167 | 168 | impl Axis2D { 169 | const KEY: KeyCode = KeyCode::KeyC; 170 | } 171 | 172 | #[derive(Debug, InputAction)] 173 | #[input_action(output = Vec3)] 174 | struct Axis3D; 175 | 176 | impl Axis3D { 177 | const KEY: KeyCode = KeyCode::KeyD; 178 | } 179 | -------------------------------------------------------------------------------- /tests/fixed_timestep.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*, time::TimeUpdateStrategy}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn once_in_two_frames() -> Result<()> { 7 | let time_step = Time::::default().timestep() / 2; 8 | 9 | let mut app = App::new(); 10 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 11 | .insert_resource(TimeUpdateStrategy::ManualDuration(time_step)) 12 | .add_input_context::() 13 | .add_observer(binding) 14 | .finish(); 15 | 16 | let entity = app.world_mut().spawn(Actions::::default()).id(); 17 | 18 | app.world_mut() 19 | .resource_mut::>() 20 | .press(TestAction::KEY); 21 | 22 | for frame in 0..2 { 23 | app.update(); 24 | 25 | let actions = app.world().get::>(entity).unwrap(); 26 | assert!( 27 | actions.events::()?.is_empty(), 28 | "shouldn't fire on frame {frame}" 29 | ); 30 | } 31 | 32 | for frame in 2..4 { 33 | app.update(); 34 | 35 | let actions = app.world().get::>(entity).unwrap(); 36 | assert_eq!( 37 | actions.events::()?, 38 | ActionEvents::STARTED | ActionEvents::FIRED, 39 | "should maintain start-firing on frame {frame}" 40 | ); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | fn twice_in_one_frame() -> Result<()> { 48 | let time_step = Time::::default().timestep() * 2; 49 | 50 | let mut app = App::new(); 51 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 52 | .insert_resource(TimeUpdateStrategy::ManualDuration(time_step)) 53 | .add_input_context::() 54 | .add_observer(binding) 55 | .finish(); 56 | 57 | let entity = app.world_mut().spawn(Actions::::default()).id(); 58 | 59 | app.world_mut() 60 | .resource_mut::>() 61 | .press(TestAction::KEY); 62 | 63 | app.update(); 64 | 65 | let actions = app.world().get::>(entity).unwrap(); 66 | assert!( 67 | actions.events::()?.is_empty(), 68 | "`FixedMain` should never run on the first frame" 69 | ); 70 | 71 | app.update(); 72 | 73 | let actions = app.world().get::>(entity).unwrap(); 74 | assert_eq!( 75 | actions.events::()?, 76 | ActionEvents::FIRED, 77 | "should run twice, so it shouldn't be started on the second run" 78 | ); 79 | 80 | Ok(()) 81 | } 82 | 83 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 84 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 85 | actions.bind::().to(TestAction::KEY); 86 | } 87 | 88 | #[derive(InputContext)] 89 | #[input_context(schedule = FixedPreUpdate)] 90 | struct Test; 91 | 92 | #[derive(Debug, InputAction)] 93 | #[input_action(output = bool)] 94 | struct TestAction; 95 | 96 | impl TestAction { 97 | const KEY: KeyCode = KeyCode::KeyA; 98 | } 99 | -------------------------------------------------------------------------------- /tests/instances.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn removal() { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | app.world_mut().entity_mut(entity).remove::>(); 18 | 19 | app.world_mut() 20 | .resource_mut::>() 21 | .press(TestAction::KEY); 22 | 23 | app.world_mut() 24 | .add_observer(|_: Trigger>| { 25 | panic!("action shouldn't trigger"); 26 | }); 27 | 28 | app.update(); 29 | } 30 | 31 | #[test] 32 | fn rebuild() { 33 | let mut app = App::new(); 34 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 35 | .add_input_context::() 36 | .add_observer(binding) 37 | .finish(); 38 | 39 | let entity = app.world_mut().spawn(Actions::::default()).id(); 40 | 41 | app.update(); 42 | 43 | app.world_mut() 44 | .entity_mut(entity) 45 | .insert(Actions::::default()); 46 | 47 | app.update(); 48 | 49 | let actions = app.world().get::>(entity).unwrap(); 50 | assert!(actions.get::().is_ok()); 51 | } 52 | 53 | #[test] 54 | fn rebuild_all() -> Result<()> { 55 | let mut app = App::new(); 56 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 57 | .add_input_context::() 58 | .add_observer(binding) 59 | .finish(); 60 | 61 | let entity = app.world_mut().spawn(Actions::::default()).id(); 62 | 63 | app.update(); 64 | 65 | app.world_mut() 66 | .resource_mut::>() 67 | .press(TestAction::KEY); 68 | 69 | app.update(); 70 | 71 | let actions = app.world().get::>(entity).unwrap(); 72 | assert_eq!(actions.state::()?, ActionState::Fired); 73 | 74 | app.world_mut().trigger(RebuildBindings); 75 | app.world_mut().flush(); 76 | 77 | let actions = app.world().get::>(entity).unwrap(); 78 | assert_eq!( 79 | actions.state::()?, 80 | ActionState::None, 81 | "state should reset on rebuild" 82 | ); 83 | 84 | Ok(()) 85 | } 86 | 87 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 88 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 89 | actions.bind::().to(TestAction::KEY); 90 | } 91 | 92 | #[derive(InputContext)] 93 | struct Test; 94 | 95 | #[derive(Debug, InputAction)] 96 | #[input_action(output = bool)] 97 | struct TestAction; 98 | 99 | impl TestAction { 100 | const KEY: KeyCode = KeyCode::KeyA; 101 | } 102 | -------------------------------------------------------------------------------- /tests/macro_hygiene.rs: -------------------------------------------------------------------------------- 1 | #![expect( 2 | dead_code, 3 | reason = "if any of these types are used, \ 4 | then the macro is using one of these types, \ 5 | instead of the actual `bevy_enhanced_input` type, \ 6 | which breaks macro hygiene" 7 | )] 8 | 9 | use bevy::prelude::{Vec2, Vec3}; 10 | 11 | struct InputAction; 12 | struct Accumulation; 13 | struct ActionValueDim; 14 | struct InputContext; 15 | 16 | #[derive(Debug, bevy_enhanced_input::prelude::InputAction)] 17 | #[input_action(output = bool, accumulation = Cumulative)] 18 | struct Action1; 19 | 20 | #[derive(Debug, bevy_enhanced_input::prelude::InputAction)] 21 | #[input_action(output = f32)] 22 | struct Action2; 23 | 24 | #[derive(Debug, bevy_enhanced_input::prelude::InputAction)] 25 | #[input_action(output = Vec2)] 26 | struct Action3; 27 | 28 | #[derive(Debug, bevy_enhanced_input::prelude::InputAction)] 29 | #[input_action(output = Vec3)] 30 | struct Action4; 31 | 32 | #[derive(Debug, bevy_enhanced_input::prelude::InputContext)] 33 | struct Marker; 34 | -------------------------------------------------------------------------------- /tests/mocking.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | 3 | use bevy::{input::InputPlugin, prelude::*, time::TimeUpdateStrategy}; 4 | use bevy_enhanced_input::prelude::*; 5 | use test_log::test; 6 | 7 | #[test] 8 | fn updates() { 9 | let mut app = App::new(); 10 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 11 | .add_input_context::() 12 | .finish(); 13 | 14 | let mut actions = Actions::::default(); 15 | actions.mock_once::(ActionState::Fired, true); 16 | let entity = app.world_mut().spawn(actions).id(); 17 | 18 | app.update(); 19 | 20 | let actions = app.world().get::>(entity).unwrap(); 21 | let action = actions.get::().unwrap(); 22 | assert_eq!(action.value(), true.into()); 23 | assert_eq!(action.state(), ActionState::Fired); 24 | assert_eq!(action.events(), ActionEvents::FIRED | ActionEvents::STARTED); 25 | 26 | app.update(); 27 | 28 | let actions = app.world().get::>(entity).unwrap(); 29 | let action = actions.get::().unwrap(); 30 | assert_eq!(action.value(), false.into()); 31 | assert_eq!(action.state(), ActionState::None); 32 | assert_eq!(action.events(), ActionEvents::COMPLETED); 33 | } 34 | 35 | #[test] 36 | fn duration() { 37 | let mut app = App::new(); 38 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 39 | .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(1))) 40 | .add_input_context::() 41 | .finish(); 42 | 43 | // Update once to get a non-zero delta-time. 44 | app.update(); 45 | 46 | let mut actions = Actions::::default(); 47 | actions.mock::(ActionState::Fired, true, Duration::from_millis(2)); 48 | let entity = app.world_mut().spawn(actions).id(); 49 | 50 | app.update(); 51 | 52 | let actions = app.world().get::>(entity).unwrap(); 53 | let action = actions.get::().unwrap(); 54 | assert_eq!(action.value(), true.into()); 55 | assert_eq!(action.state(), ActionState::Fired); 56 | assert_eq!(action.events(), ActionEvents::FIRED | ActionEvents::STARTED); 57 | 58 | app.update(); 59 | 60 | let actions = app.world().get::>(entity).unwrap(); 61 | let action = actions.get::().unwrap(); 62 | assert_eq!(action.value(), true.into()); 63 | assert_eq!(action.state(), ActionState::Fired); 64 | assert_eq!(action.events(), ActionEvents::FIRED); 65 | 66 | app.update(); 67 | 68 | let actions = app.world().get::>(entity).unwrap(); 69 | let action = actions.get::().unwrap(); 70 | assert_eq!(action.value(), false.into()); 71 | assert_eq!(action.state(), ActionState::None); 72 | assert_eq!(action.events(), ActionEvents::COMPLETED); 73 | } 74 | 75 | #[test] 76 | fn manual() { 77 | let mut app = App::new(); 78 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 79 | .add_input_context::() 80 | .finish(); 81 | 82 | let mut actions = Actions::::default(); 83 | actions.mock::(ActionState::Fired, true, MockSpan::Manual); 84 | let entity = app.world_mut().spawn(actions).id(); 85 | 86 | app.update(); 87 | 88 | let actions = app.world().get::>(entity).unwrap(); 89 | let action = actions.get::().unwrap(); 90 | assert_eq!(action.value(), true.into()); 91 | assert_eq!(action.state(), ActionState::Fired); 92 | assert_eq!(action.events(), ActionEvents::FIRED | ActionEvents::STARTED); 93 | 94 | app.update(); 95 | 96 | let mut actions = app.world_mut().get_mut::>(entity).unwrap(); 97 | let action = actions.get::().unwrap(); 98 | assert_eq!(action.value(), true.into()); 99 | assert_eq!(action.state(), ActionState::Fired); 100 | assert_eq!(action.events(), ActionEvents::FIRED); 101 | 102 | actions.clear_mock::(); 103 | 104 | app.update(); 105 | 106 | let actions = app.world().get::>(entity).unwrap(); 107 | let action = actions.get::().unwrap(); 108 | assert_eq!(action.value(), false.into()); 109 | assert_eq!(action.state(), ActionState::None); 110 | assert_eq!(action.events(), ActionEvents::COMPLETED); 111 | } 112 | 113 | #[derive(InputContext)] 114 | struct Test; 115 | 116 | #[derive(Debug, InputAction)] 117 | #[input_action(output = bool, consume_input = true)] 118 | struct TestAction; 119 | -------------------------------------------------------------------------------- /tests/preset.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn keys() { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | for (key, dir) in [ 18 | (KeyCode::KeyW, UP), 19 | (KeyCode::KeyA, LEFT), 20 | (KeyCode::KeyS, DOWN), 21 | (KeyCode::KeyD, RIGHT), 22 | (KeyCode::ArrowUp, UP), 23 | (KeyCode::ArrowLeft, LEFT), 24 | (KeyCode::ArrowDown, DOWN), 25 | (KeyCode::ArrowRight, RIGHT), 26 | (KeyCode::NumpadSubtract, LEFT), 27 | (KeyCode::NumpadAdd, RIGHT), 28 | (KeyCode::Digit0, FORWARD), 29 | (KeyCode::Digit1, BACKWARD), 30 | (KeyCode::Digit2, LEFT), 31 | (KeyCode::Digit3, RIGHT), 32 | (KeyCode::Digit4, UP), 33 | (KeyCode::Digit5, DOWN), 34 | ] { 35 | app.world_mut() 36 | .resource_mut::>() 37 | .press(key); 38 | 39 | app.update(); 40 | 41 | let actions = app.world().get::>(entity).unwrap(); 42 | assert_eq!( 43 | actions.value::().unwrap(), 44 | dir.into(), 45 | "`{key:?}` should result in `{dir}`" 46 | ); 47 | 48 | app.world_mut() 49 | .resource_mut::>() 50 | .release(key); 51 | 52 | app.update(); 53 | } 54 | } 55 | 56 | #[test] 57 | fn dpad() { 58 | let mut app = App::new(); 59 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 60 | .add_input_context::() 61 | .add_observer(binding) 62 | .finish(); 63 | 64 | let gamepad_entity = app.world_mut().spawn(Gamepad::default()).id(); 65 | let ctx_entity = app.world_mut().spawn(Actions::::default()).id(); 66 | 67 | app.update(); 68 | 69 | for (button, dir) in [ 70 | (GamepadButton::DPadUp, UP), 71 | (GamepadButton::DPadLeft, LEFT), 72 | (GamepadButton::DPadDown, DOWN), 73 | (GamepadButton::DPadRight, RIGHT), 74 | ] { 75 | let mut gamepad = app.world_mut().get_mut::(gamepad_entity).unwrap(); 76 | gamepad.analog_mut().set(button, 1.0); 77 | 78 | app.update(); 79 | 80 | let actions = app.world().get::>(ctx_entity).unwrap(); 81 | assert_eq!( 82 | actions.value::().unwrap(), 83 | dir.into(), 84 | "`{button:?}` should result in `{dir}`" 85 | ); 86 | 87 | let mut gamepad = app.world_mut().get_mut::(gamepad_entity).unwrap(); 88 | gamepad.analog_mut().set(button, 0.0); 89 | 90 | app.update(); 91 | } 92 | } 93 | 94 | #[test] 95 | fn sticks() { 96 | let mut app = App::new(); 97 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 98 | .add_input_context::() 99 | .add_observer(binding) 100 | .finish(); 101 | 102 | let gamepad_entity = app.world_mut().spawn(Gamepad::default()).id(); 103 | let ctx_entity = app.world_mut().spawn(Actions::::default()).id(); 104 | 105 | app.update(); 106 | 107 | for (axis, dirs) in [ 108 | (GamepadAxis::LeftStickX, [LEFT, RIGHT]), 109 | (GamepadAxis::RightStickX, [LEFT, RIGHT]), 110 | (GamepadAxis::LeftStickY, [DOWN, UP]), 111 | (GamepadAxis::RightStickY, [DOWN, UP]), 112 | ] { 113 | for (dir, value) in dirs.into_iter().zip([-1.0, 1.0]) { 114 | let mut gamepad = app.world_mut().get_mut::(gamepad_entity).unwrap(); 115 | gamepad.analog_mut().set(axis, value); 116 | 117 | app.update(); 118 | 119 | let actions = app.world().get::>(ctx_entity).unwrap(); 120 | assert_eq!( 121 | actions.value::().unwrap(), 122 | dir.into(), 123 | "`{axis:?}` should result in `{dir}`" 124 | ); 125 | 126 | let mut gamepad = app.world_mut().get_mut::(gamepad_entity).unwrap(); 127 | gamepad.analog_mut().set(axis, 0.0); 128 | 129 | app.update(); 130 | } 131 | } 132 | } 133 | 134 | const RIGHT: Vec3 = Vec3::X; 135 | const LEFT: Vec3 = Vec3::NEG_X; 136 | const BACKWARD: Vec3 = Vec3::Z; 137 | const FORWARD: Vec3 = Vec3::NEG_Z; 138 | const UP: Vec3 = Vec3::Y; 139 | const DOWN: Vec3 = Vec3::NEG_Y; 140 | 141 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 142 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 143 | actions.bind::().to(( 144 | Cardinal::wasd_keys(), 145 | Cardinal::arrow_keys(), 146 | Cardinal::dpad_buttons(), 147 | Bidirectional { 148 | positive: KeyCode::NumpadAdd, 149 | negative: KeyCode::NumpadSubtract, 150 | }, 151 | Axial::left_stick(), 152 | Axial::right_stick(), 153 | Spatial { 154 | forward: KeyCode::Digit0, 155 | backward: KeyCode::Digit1, 156 | left: KeyCode::Digit2, 157 | right: KeyCode::Digit3, 158 | up: KeyCode::Digit4, 159 | down: KeyCode::Digit5, 160 | }, 161 | )); 162 | } 163 | 164 | #[derive(InputContext)] 165 | struct Test; 166 | 167 | #[derive(Debug, InputAction)] 168 | #[input_action(output = Vec3, consume_input = true)] 169 | struct TestAction; 170 | -------------------------------------------------------------------------------- /tests/priority.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn prioritization() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_input_context::() 11 | .add_observer(first_binding) 12 | .add_observer(second_binding) 13 | .finish(); 14 | 15 | let entity = app 16 | .world_mut() 17 | .spawn((Actions::::default(), Actions::::default())) 18 | .id(); 19 | 20 | app.update(); 21 | 22 | let mut keys = app.world_mut().resource_mut::>(); 23 | keys.press(CONSUME_KEY); 24 | keys.press(PASSTHROUGH_KEY); 25 | 26 | app.update(); 27 | 28 | let first = app.world().get::>(entity).unwrap(); 29 | assert_eq!(first.state::()?, ActionState::Fired); 30 | assert_eq!(first.state::()?, ActionState::Fired); 31 | 32 | let second = app.world().get::>(entity).unwrap(); 33 | assert_eq!( 34 | second.state::()?, 35 | ActionState::None, 36 | "action should be consumed by component input with a higher priority" 37 | ); 38 | assert_eq!( 39 | second.state::()?, 40 | ActionState::Fired, 41 | "actions that doesn't consume inputs should still be triggered" 42 | ); 43 | 44 | Ok(()) 45 | } 46 | 47 | fn first_binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 48 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 49 | actions.bind::().to(CONSUME_KEY); 50 | actions.bind::().to(PASSTHROUGH_KEY); 51 | } 52 | 53 | fn second_binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 54 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 55 | actions.bind::().to(CONSUME_KEY); 56 | actions.bind::().to(PASSTHROUGH_KEY); 57 | } 58 | 59 | #[derive(InputContext)] 60 | #[input_context(priority = 1)] 61 | struct First; 62 | 63 | #[derive(InputContext)] 64 | struct Second; 65 | 66 | /// A key used by both [`FirstConsume`] and [`SecondConsume`] actions. 67 | const CONSUME_KEY: KeyCode = KeyCode::KeyA; 68 | 69 | /// A key used by both [`FirstPassthrough`] and [`SecondPassthrough`] actions. 70 | const PASSTHROUGH_KEY: KeyCode = KeyCode::KeyB; 71 | 72 | #[derive(Debug, InputAction)] 73 | #[input_action(output = bool, consume_input = true)] 74 | struct FirstConsume; 75 | 76 | #[derive(Debug, InputAction)] 77 | #[input_action(output = bool, consume_input = true)] 78 | struct SecondConsume; 79 | 80 | #[derive(Debug, InputAction)] 81 | #[input_action(output = bool, consume_input = false)] 82 | struct FirstPassthrough; 83 | 84 | #[derive(Debug, InputAction)] 85 | #[input_action(output = bool, consume_input = false)] 86 | struct SecondPassthrough; 87 | -------------------------------------------------------------------------------- /tests/require_reset.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn layering() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_input_context::() 11 | .add_observer(first_binding) 12 | .add_observer(second_binding) 13 | .finish(); 14 | 15 | let entity = app 16 | .world_mut() 17 | .spawn((Actions::::default(), Actions::::default())) 18 | .id(); 19 | 20 | app.update(); 21 | 22 | app.world_mut() 23 | .resource_mut::>() 24 | .press(TestAction::KEY); 25 | 26 | app.update(); 27 | 28 | let first = app.world().get::>(entity).unwrap(); 29 | assert_eq!(first.state::()?, ActionState::Fired); 30 | 31 | let second = app.world().get::>(entity).unwrap(); 32 | assert_eq!(second.state::()?, ActionState::None); 33 | 34 | app.world_mut() 35 | .entity_mut(entity) 36 | .remove::>(); 37 | 38 | app.update(); 39 | 40 | let second = app.world().get::>(entity).unwrap(); 41 | assert_eq!( 42 | second.state::()?, 43 | ActionState::None, 44 | "action should still be consumed even after removal" 45 | ); 46 | 47 | app.world_mut() 48 | .resource_mut::>() 49 | .release(TestAction::KEY); 50 | 51 | app.update(); 52 | 53 | app.world_mut() 54 | .resource_mut::>() 55 | .press(TestAction::KEY); 56 | 57 | app.update(); 58 | 59 | let second = app.world().get::>(entity).unwrap(); 60 | assert_eq!(second.state::()?, ActionState::Fired); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn switching() -> Result<()> { 67 | let mut app = App::new(); 68 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 69 | .add_input_context::() 70 | .add_input_context::() 71 | .add_observer(first_binding) 72 | .add_observer(second_binding) 73 | .finish(); 74 | 75 | let entity = app.world_mut().spawn(Actions::::default()).id(); 76 | 77 | app.update(); 78 | 79 | app.world_mut() 80 | .resource_mut::>() 81 | .press(TestAction::KEY); 82 | 83 | app.update(); 84 | 85 | let actions = app.world().get::>(entity).unwrap(); 86 | assert_eq!(actions.state::()?, ActionState::Fired); 87 | 88 | app.world_mut() 89 | .entity_mut(entity) 90 | .remove::>() 91 | .insert(Actions::::default()); 92 | 93 | app.update(); 94 | 95 | let second = app.world().get::>(entity).unwrap(); 96 | assert_eq!( 97 | second.state::()?, 98 | ActionState::None, 99 | "action should still be consumed even after removal" 100 | ); 101 | 102 | app.world_mut() 103 | .resource_mut::>() 104 | .release(TestAction::KEY); 105 | 106 | app.update(); 107 | 108 | app.world_mut() 109 | .resource_mut::>() 110 | .press(TestAction::KEY); 111 | 112 | app.update(); 113 | 114 | let second = app.world().get::>(entity).unwrap(); 115 | assert_eq!(second.state::()?, ActionState::Fired); 116 | 117 | Ok(()) 118 | } 119 | 120 | fn first_binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 121 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 122 | actions.bind::().to(TestAction::KEY); 123 | } 124 | 125 | fn second_binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 126 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 127 | actions.bind::().to(TestAction::KEY); 128 | } 129 | 130 | #[derive(InputContext)] 131 | #[input_context(priority = 1)] 132 | struct First; 133 | 134 | #[derive(InputContext)] 135 | struct Second; 136 | 137 | #[derive(Debug, InputAction)] 138 | #[input_action(output = bool, require_reset = true)] 139 | struct TestAction; 140 | 141 | impl TestAction { 142 | const KEY: KeyCode = KeyCode::KeyA; 143 | } 144 | -------------------------------------------------------------------------------- /tests/state_and_value_merge.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::InputPlugin, prelude::*}; 2 | use bevy_enhanced_input::prelude::*; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn input_level() -> Result<()> { 7 | let mut app = App::new(); 8 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 9 | .add_input_context::() 10 | .add_observer(binding) 11 | .finish(); 12 | 13 | let entity = app.world_mut().spawn(Actions::::default()).id(); 14 | 15 | app.update(); 16 | 17 | app.world_mut() 18 | .resource_mut::>() 19 | .press(InputLevel::KEY1); 20 | 21 | app.update(); 22 | 23 | let actions = app.world().get::>(entity).unwrap(); 24 | let action = actions.get::()?; 25 | assert_eq!(action.value(), (Vec2::Y * 2.0).into()); 26 | assert_eq!(action.state(), ActionState::Ongoing); 27 | 28 | app.world_mut() 29 | .resource_mut::>() 30 | .press(ChordMember::KEY); 31 | 32 | app.update(); 33 | 34 | let actions = app.world().get::>(entity).unwrap(); 35 | let action = actions.get::()?; 36 | assert_eq!(action.value(), (Vec2::Y * 2.0).into()); 37 | assert_eq!(action.state(), ActionState::Fired); 38 | 39 | let mut keys = app.world_mut().resource_mut::>(); 40 | keys.release(InputLevel::KEY1); 41 | keys.press(InputLevel::KEY2); 42 | 43 | app.update(); 44 | 45 | let actions = app.world().get::>(entity).unwrap(); 46 | let action = actions.get::()?; 47 | assert_eq!(action.value(), Vec2::NEG_Y.into()); 48 | assert_eq!(action.state(), ActionState::Fired); 49 | 50 | app.world_mut() 51 | .resource_mut::>() 52 | .press(InputLevel::KEY1); 53 | 54 | app.update(); 55 | 56 | let actions = app.world().get::>(entity).unwrap(); 57 | let action = actions.get::()?; 58 | assert_eq!(action.value(), Vec2::Y.into()); 59 | assert_eq!(action.state(), ActionState::Fired); 60 | 61 | app.world_mut() 62 | .resource_mut::>() 63 | .press(Blocker::KEY); 64 | 65 | app.update(); 66 | 67 | let actions = app.world().get::>(entity).unwrap(); 68 | let action = actions.get::()?; 69 | assert_eq!(action.value(), Vec2::ZERO.into()); 70 | assert_eq!( 71 | action.state(), 72 | ActionState::None, 73 | "if a blocker condition fails, it should override other conditions" 74 | ); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[test] 80 | fn action_level() -> Result<()> { 81 | let mut app = App::new(); 82 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 83 | .add_input_context::() 84 | .add_observer(binding) 85 | .finish(); 86 | 87 | let entity = app.world_mut().spawn(Actions::::default()).id(); 88 | 89 | app.update(); 90 | 91 | app.world_mut() 92 | .resource_mut::>() 93 | .press(ActionLevel::KEY1); 94 | 95 | app.update(); 96 | 97 | let actions = app.world().get::>(entity).unwrap(); 98 | let action = actions.get::()?; 99 | assert_eq!(action.value(), (Vec2::NEG_Y * 2.0).into()); 100 | assert_eq!(action.state(), ActionState::Ongoing); 101 | 102 | app.world_mut() 103 | .resource_mut::>() 104 | .press(ChordMember::KEY); 105 | 106 | app.update(); 107 | 108 | let actions = app.world().get::>(entity).unwrap(); 109 | let action = actions.get::()?; 110 | assert_eq!(action.value(), (Vec2::NEG_Y * 2.0).into()); 111 | assert_eq!(action.state(), ActionState::Fired); 112 | 113 | let mut keys = app.world_mut().resource_mut::>(); 114 | keys.release(ActionLevel::KEY1); 115 | keys.press(ActionLevel::KEY2); 116 | 117 | app.update(); 118 | 119 | let actions = app.world().get::>(entity).unwrap(); 120 | let action = actions.get::()?; 121 | assert_eq!(action.value(), (Vec2::NEG_Y * 2.0).into()); 122 | assert_eq!(action.state(), ActionState::Fired); 123 | 124 | app.world_mut() 125 | .resource_mut::>() 126 | .press(ActionLevel::KEY1); 127 | 128 | app.update(); 129 | 130 | let actions = app.world().get::>(entity).unwrap(); 131 | let action = actions.get::()?; 132 | assert_eq!(action.value(), (Vec2::NEG_Y * 4.0).into()); 133 | assert_eq!(action.state(), ActionState::Fired); 134 | 135 | app.world_mut() 136 | .resource_mut::>() 137 | .press(Blocker::KEY); 138 | 139 | app.update(); 140 | 141 | let actions = app.world().get::>(entity).unwrap(); 142 | let action = actions.get::()?; 143 | assert_eq!(action.value(), (Vec2::NEG_Y * 4.0).into()); 144 | assert_eq!( 145 | action.state(), 146 | ActionState::None, 147 | "if a blocker condition fails, it should override other conditions" 148 | ); 149 | 150 | Ok(()) 151 | } 152 | 153 | #[test] 154 | fn both_levels() -> Result<()> { 155 | let mut app = App::new(); 156 | app.add_plugins((MinimalPlugins, InputPlugin, EnhancedInputPlugin)) 157 | .add_input_context::() 158 | .add_observer(binding) 159 | .finish(); 160 | 161 | let entity = app.world_mut().spawn(Actions::::default()).id(); 162 | 163 | app.update(); 164 | 165 | app.world_mut() 166 | .resource_mut::>() 167 | .press(BothLevels::KEY1); 168 | 169 | app.update(); 170 | 171 | let actions = app.world().get::>(entity).unwrap(); 172 | let action = actions.get::()?; 173 | assert_eq!(action.value(), (Vec2::Y * 2.0).into()); 174 | assert_eq!(action.state(), ActionState::Ongoing); 175 | 176 | app.world_mut() 177 | .resource_mut::>() 178 | .press(ChordMember::KEY); 179 | 180 | app.update(); 181 | 182 | let actions = app.world().get::>(entity).unwrap(); 183 | let action = actions.get::()?; 184 | assert_eq!(action.value(), (Vec2::Y * 2.0).into()); 185 | assert_eq!(action.state(), ActionState::Fired); 186 | 187 | let mut keys = app.world_mut().resource_mut::>(); 188 | keys.release(BothLevels::KEY1); 189 | keys.press(BothLevels::KEY2); 190 | 191 | app.update(); 192 | 193 | let actions = app.world().get::>(entity).unwrap(); 194 | let action = actions.get::()?; 195 | assert_eq!(action.value(), Vec2::NEG_Y.into()); 196 | assert_eq!(action.state(), ActionState::Fired); 197 | 198 | app.world_mut() 199 | .resource_mut::>() 200 | .press(BothLevels::KEY1); 201 | 202 | app.update(); 203 | 204 | let actions = app.world().get::>(entity).unwrap(); 205 | let action = actions.get::()?; 206 | assert_eq!(action.value(), Vec2::Y.into()); 207 | assert_eq!(action.state(), ActionState::Fired); 208 | 209 | app.world_mut() 210 | .resource_mut::>() 211 | .press(Blocker::KEY); 212 | 213 | app.update(); 214 | 215 | let actions = app.world().get::>(entity).unwrap(); 216 | let action = actions.get::()?; 217 | assert_eq!(action.value(), Vec2::Y.into()); 218 | assert_eq!( 219 | action.state(), 220 | ActionState::None, 221 | "if a blocker condition fails, it should override other conditions" 222 | ); 223 | 224 | Ok(()) 225 | } 226 | 227 | fn binding(trigger: Trigger>, mut actions: Query<&mut Actions>) { 228 | let mut actions = actions.get_mut(trigger.target()).unwrap(); 229 | 230 | let down = Down::default(); 231 | let release = Release::default(); 232 | let chord = Chord::::default(); 233 | let block_by = BlockBy::::default(); 234 | let swizzle_axis = SwizzleAxis::YXZ; 235 | let negate = Negate::all(); 236 | let scale = Scale::splat(2.0); 237 | 238 | actions.bind::().to(ChordMember::KEY); 239 | actions.bind::().to(Blocker::KEY); 240 | actions.bind::().to(( 241 | InputLevel::KEY1.with_modifiers(scale), 242 | InputLevel::KEY2.with_modifiers(negate), 243 | ) 244 | .with_modifiers_each(swizzle_axis) 245 | .with_conditions_each((chord, block_by, down, release))); 246 | actions 247 | .bind::() 248 | .to((ActionLevel::KEY1, ActionLevel::KEY2)) 249 | .with_conditions((down, release, chord, block_by)) 250 | .with_modifiers((swizzle_axis, negate, scale)); 251 | actions 252 | .bind::() 253 | .to(( 254 | BothLevels::KEY1.with_modifiers(scale), 255 | BothLevels::KEY2.with_modifiers(negate), 256 | ) 257 | .with_conditions_each(down)) 258 | .with_conditions((release, chord, block_by)) 259 | .with_modifiers(swizzle_axis); 260 | } 261 | 262 | #[derive(InputContext)] 263 | struct Test; 264 | 265 | #[derive(Debug, InputAction)] 266 | #[input_action(output = Vec2)] 267 | struct InputLevel; 268 | 269 | impl InputLevel { 270 | const KEY1: KeyCode = KeyCode::KeyA; 271 | const KEY2: KeyCode = KeyCode::KeyB; 272 | } 273 | 274 | #[derive(Debug, InputAction)] 275 | #[input_action(output = Vec2)] 276 | struct ActionLevel; 277 | 278 | impl ActionLevel { 279 | const KEY1: KeyCode = KeyCode::KeyC; 280 | const KEY2: KeyCode = KeyCode::KeyD; 281 | } 282 | 283 | #[derive(Debug, InputAction)] 284 | #[input_action(output = Vec2)] 285 | struct BothLevels; 286 | 287 | impl BothLevels { 288 | const KEY1: KeyCode = KeyCode::KeyE; 289 | const KEY2: KeyCode = KeyCode::KeyF; 290 | } 291 | 292 | #[derive(Debug, InputAction)] 293 | #[input_action(output = bool)] 294 | struct ChordMember; 295 | 296 | impl ChordMember { 297 | const KEY: KeyCode = KeyCode::KeyG; 298 | } 299 | 300 | #[derive(Debug, InputAction)] 301 | #[input_action(output = bool)] 302 | struct Blocker; 303 | 304 | impl Blocker { 305 | const KEY: KeyCode = KeyCode::KeyH; 306 | } 307 | --------------------------------------------------------------------------------