Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Move input mocking to new traits #570

Merged
merged 21 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ and a single input can result in multiple actions being triggered, which can be

| Bevy | leafwing-input-manager |
| ---- | ---------------------- |
| 0.14 | 0.14 |
| 0.14 | 0.14..0.15 |
| 0.13 | 0.13 |
| 0.12 | 0.11..0.12 |
| 0.11 | 0.10 |
Expand All @@ -27,6 +27,7 @@ and a single input can result in multiple actions being triggered, which can be

- Full keyboard, mouse and joystick support for button-like and axis inputs
- Dual axis support for analog inputs from gamepads and joysticks
- Expressive and easy-to-use axis processing for deadzones, sensitivity and clamping
- Bind arbitrary button inputs into virtual D-Pads
- Effortlessly wire UI buttons to game state with one simple component!
- When clicked, your button will press the appropriate action on the corresponding entity
Expand All @@ -37,13 +38,12 @@ and a single input can result in multiple actions being triggered, which can be
- Ergonomic insertion API that seamlessly blends multiple input types for you
- Can't decide between `input_map.insert(Action::Jump, KeyCode::Space)` and `input_map.insert(Action::Jump, GamepadButtonType::South)`? Have both!
- Full support for arbitrary button combinations: chord your heart out.
- `input_map.insert(Action::Console, InputChord::new([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))`
- `input_map.insert(Action::Console, ButtonlikeChord::new([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))`
- Sophisticated input disambiguation with the `ClashStrategy` enum: stop triggering individual buttons when you meant to press a chord!
- Create an arbitrary number of strongly typed disjoint action sets by adding multiple copies of this plugin: decouple your camera and player state
- Local multiplayer support: freely bind keys to distinct entities, rather than worrying about singular global state
- Networked multiplayer support: serializable structs, and a space-conscious `ActionDiff` representation to send on the wire
- Powerful and easy-to-use input mocking API for integration testing your Bevy applications
- `app.press_input(KeyCode::KeyB)` or `world.press_input(UserInput::chord([KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY])`
- Control which state this plugin is active in: stop wandering around while in a menu!
- Leafwing Studio's trademark `#![forbid(missing_docs)]`

Expand Down
6 changes: 0 additions & 6 deletions RELEASE-CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# LWIM Release Checklist

## Adding a new input kind

1. Ensure that `reset_inputs` for `MutableInputStreams` is resetting all relevant fields.
2. Ensure that `RawInputs` struct has fields that cover all necessary input types.
3. Ensure that `press_input`, `send_axis_values` and `release_input` check all possible fields on `RawInputs`.

## Before release

1. Ensure no tests (other than ones in the README) are ignored.
Expand Down
17 changes: 4 additions & 13 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,11 @@ Input processors allow you to create custom logic for axis-like input manipulati

- removed `ToggleActions` resource in favor of new methods on `ActionState`: `disable_all`, `disable(action)`, `enable_all`, `enable(action)`, and `disabled(action)`.

### MockInput
### Input mocking

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

### QueryInput

- added new methods for the `QueryInput` trait
- `fn read_axis_value` and `read_dual_axis_values`
- as well as methods for working with a specific gamepad
- implemented the methods for `InputStreams`, `World`, and `App`
- `MockInput`, `RawInputs` and `MutableInputStreams` have been removed in favor of methods on the `Buttonlike`, `Axislike` and `DualAxislike` traits
- for example, rather than `app.press_input(KeyCode::Space)` call `KeyCode::Space.press(app.world_mut())`
- existing methods for quickly checking the value of buttons and axes have been moved to the `FetchUserInput` trait and retained for testing purposes

### Bugs

Expand Down
10 changes: 6 additions & 4 deletions benches/input_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use bevy::{
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use leafwing_input_manager::input_map::UpdatedActions;
use leafwing_input_manager::plugin::AccumulatorPlugin;
use leafwing_input_manager::prelude::Buttonlike;
use leafwing_input_manager::{
input_streams::InputStreams,
prelude::{ClashStrategy, InputMap, MockInput},
prelude::{ClashStrategy, InputMap},
Actionlike,
};

Expand Down Expand Up @@ -75,9 +77,9 @@ pub fn criterion_benchmark(c: &mut Criterion) {

// Constructing our test app / input stream outside the timed benchmark
let mut app = App::new();
app.add_plugins(InputPlugin);
app.press_input(KeyCode::KeyA);
app.press_input(KeyCode::KeyB);
app.add_plugins((InputPlugin, AccumulatorPlugin));
KeyCode::KeyA.press(app.world_mut());
KeyCode::KeyB.press(app.world_mut());
app.update();

let input_streams = InputStreams::from_world(app.world(), None);
Expand Down
5 changes: 2 additions & 3 deletions examples/send_actions_over_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ fn main() {
client_app.update();

// Sending inputs to the client
client_app.press_input(KeyCode::Space);
client_app.press_input(MouseButton::Left);
KeyCode::Space.press(client_app.world_mut());
MouseButton::Left.press(client_app.world_mut());

// These are converted into actions when the client_app's `Schedule` runs
client_app.update();
Expand All @@ -94,7 +94,6 @@ fn main() {
assert!(player_state.pressed(&FpsAction::Shoot));

// If we wait a tick, the buttons will be released
client_app.reset_inputs();
client_app.update();
let mut player_state_query = client_app.world_mut().query::<&ActionState<FpsAction>>();
let player_state = player_state_query.iter(client_app.world()).next().unwrap();
Expand Down
34 changes: 14 additions & 20 deletions src/action_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,10 @@ impl<A: Actionlike> ActionState<A> {
///
/// # Caution
///
/// - To access the [`ButtonData`] regardless of whether the `action` has been triggered,
/// To access the [`ButtonData`] regardless of whether the `action` has been triggered,
/// use [`unwrap_or_default`](Option::unwrap_or_default) on the returned [`Option`].
///
/// - To insert a default [`ButtonData`] if it doesn't exist,
/// To insert a default [`ButtonData`] if it doesn't exist,
/// use [`button_data_mut_or_default`](Self::button_data_mut_or_default) method.
///
/// # Returns
Expand Down Expand Up @@ -351,10 +351,10 @@ impl<A: Actionlike> ActionState<A> {
///
/// # Caution
///
/// - To access the [`AxisData`] regardless of whether the `action` has been triggered,
/// To access the [`AxisData`] regardless of whether the `action` has been triggered,
/// use [`unwrap_or_default`](Option::unwrap_or_default) on the returned [`Option`].
///
/// - To insert a default [`AxisData`] if it doesn't exist,
/// To insert a default [`AxisData`] if it doesn't exist,
/// use [`axis_data_mut_or_default`](Self::axis_data_mut_or_default) method.
///
/// # Returns
Expand Down Expand Up @@ -412,10 +412,10 @@ impl<A: Actionlike> ActionState<A> {
///
/// # Caution
///
/// - To access the [`DualAxisData`] regardless of whether the `action` has been triggered,
/// To access the [`DualAxisData`] regardless of whether the `action` has been triggered,
/// use [`unwrap_or_default`](Option::unwrap_or_default) on the returned [`Option`].
///
/// - To insert a default [`ButtonData`] if it doesn't exist,
/// To insert a default [`ButtonData`] if it doesn't exist,
/// use [`dual_axis_data_mut_or_default`](Self::dual_axis_data_mut_or_default) method.
///
/// # Returns
Expand Down Expand Up @@ -454,16 +454,11 @@ impl<A: Actionlike> ActionState<A> {
///
/// Different kinds of bindings have different ways of calculating the value:
///
/// - Binary buttons will have a value of `0.0` when the button is not pressed, and a value of
/// `1.0` when the button is pressed.
/// - Binary buttons will have a value of `0.0` when the button is not pressed, and a value of `1.0` when the button is pressed.
/// - Some axes, such as an analog stick, will have a value in the range `[-1.0, 1.0]`.
/// - Some axes, such as a variable trigger, will have a value in the range `[0.0, 1.0]`.
/// - Some buttons will also return a value in the range `[0.0, 1.0]`, such as analog gamepad
/// triggers which may be tracked as buttons or axes. Examples of these include the Xbox LT/RT
/// triggers and the Playstation L2/R2 triggers. See also the `axis_inputs` example in the
/// repository.
/// - Dual axis inputs will return the magnitude of its [`Vec2`] and will be in the range
/// `0.0..=1.0`.
/// - Some buttons will also return a value in the range `[0.0, 1.0]`, such as analog gamepad triggers which may be tracked as buttons or axes. Examples of these include the Xbox LT/Rtriggers and the Playstation L2/R2 triggers. See also the `axis_inputs` example in the repository.
/// - Dual axis inputs will return the magnitude of its [`Vec2`] and will be in the range `0.0..=1.0`.
/// - Chord inputs will return the value of its first input.
///
/// If multiple inputs trigger the same game action at the same time, the value of each
Expand Down Expand Up @@ -937,10 +932,9 @@ mod tests {
use crate::action_state::ActionState;
use crate::clashing_inputs::ClashStrategy;
use crate::input_map::InputMap;
use crate::input_mocking::MockInput;
use crate::input_streams::InputStreams;
use crate::plugin::AccumulatorPlugin;
use crate::prelude::ButtonlikeChord;
use crate::prelude::{Buttonlike, ButtonlikeChord};
use bevy::input::InputPlugin;
use bevy::prelude::*;
use bevy::utils::{Duration, Instant};
Expand Down Expand Up @@ -984,7 +978,7 @@ mod tests {
assert!(!action_state.just_released(&Action::Run));

// Pressing
app.press_input(KeyCode::KeyR);
KeyCode::KeyR.press(app.world_mut());
// Process the input events into Input<KeyCode> data
app.update();
let input_streams = InputStreams::from_world(app.world(), None);
Expand All @@ -1006,7 +1000,7 @@ mod tests {
assert!(!action_state.just_released(&Action::Run));

// Releasing
app.release_input(KeyCode::KeyR);
KeyCode::KeyR.release(app.world_mut());
app.update();
let input_streams = InputStreams::from_world(app.world(), None);

Expand Down Expand Up @@ -1059,7 +1053,7 @@ mod tests {
assert!(action_state.released(&Action::OneAndTwo));

// Pressing One
app.press_input(Digit1);
Digit1.press(app.world_mut());
app.update();
let input_streams = InputStreams::from_world(app.world(), None);

Expand All @@ -1080,7 +1074,7 @@ mod tests {
assert!(action_state.released(&Action::OneAndTwo));

// Pressing Two
app.press_input(Digit2);
Digit2.press(app.world_mut());
app.update();
let input_streams = InputStreams::from_world(app.world(), None);

Expand Down
30 changes: 19 additions & 11 deletions src/clashing_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ mod tests {
}

mod basic_functionality {
use crate::{input_mocking::MockInput, prelude::ModifierKey};
use crate::prelude::{AccumulatedMouseMovement, AccumulatedMouseScroll, ModifierKey};
use bevy::input::InputPlugin;
use Action::*;

Expand Down Expand Up @@ -572,11 +572,13 @@ mod tests {
fn resolve_prioritize_longest() {
let mut app = App::new();
app.add_plugins(InputPlugin);
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();

let input_map = test_input_map();
let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
app.press_input(Digit1);
app.press_input(Digit2);
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
app.update();

assert_eq!(
Expand All @@ -601,7 +603,7 @@ mod tests {
let chord_clash = input_map
.possible_clash(&OneAndTwo, &OneAndTwoAndThree)
.unwrap();
app.press_input(Digit3);
Digit3.press(app.world_mut());
app.update();

let input_streams = InputStreams::from_world(app.world(), None);
Expand All @@ -620,10 +622,12 @@ mod tests {
fn handle_clashes() {
let mut app = App::new();
app.add_plugins(InputPlugin);
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();

app.press_input(Digit1);
app.press_input(Digit2);
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
app.update();

let mut button_data = HashMap::new();
Expand All @@ -650,10 +654,12 @@ mod tests {
fn handle_clashes_dpad_chord() {
let mut app = App::new();
app.add_plugins(InputPlugin);
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();

app.press_input(ControlLeft);
app.press_input(ArrowUp);
ControlLeft.press(app.world_mut());
ArrowUp.press(app.world_mut());
app.update();

// Both the DPad and the chord are pressed,
Expand Down Expand Up @@ -700,11 +706,13 @@ mod tests {
fn which_pressed() {
let mut app = App::new();
app.add_plugins(InputPlugin);
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();

app.press_input(Digit1);
app.press_input(Digit2);
app.press_input(ControlLeft);
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
ControlLeft.press(app.world_mut());
app.update();

let action_data = input_map.process_actions(
Expand Down
Loading