From e788e3bc83a6c53f5a6c2490e37f79e989b2e808 Mon Sep 17 00:00:00 2001 From: s-puig <39652109+s-puig@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:07:20 +0200 Subject: [PATCH] Implement gamepads as entities (#12770) # Objective - Significantly improve the ergonomics of gamepads and allow new features Gamepads are a bit unergonomic to work with, they use resources but unlike other inputs, they are not limited to a single gamepad, to get around this it uses an identifier (Gamepad) to interact with anything causing all sorts of issues. 1. There are too many: Gamepads, GamepadSettings, GamepadInfo, ButtonInput, 2 Axis. 2. ButtonInput/Axis generic methods become really inconvenient to use e.g. any_pressed() 3. GamepadButton/Axis structs are unnecessary boilerplate: ```rust for gamepad in gamepads.iter() { if button_inputs.just_pressed(GamepadButton::new(gamepad, GamepadButtonType::South)) { info!("{:?} just pressed South", gamepad); } else if button_inputs.just_released(GamepadButton::new(gamepad, GamepadButtonType::South)) { info!("{:?} just released South", gamepad); } } ``` 4. Projects often need to create resources to store the selected gamepad and have to manually check if their gamepad is still valid anyways. - Previously attempted by #3419 and #12674 ## Solution - Implement gamepads as entities. Using entities solves all the problems above and opens new possibilities. 1. Reduce boilerplate and allows iteration ```rust let is_pressed = gamepads_buttons.iter().any(|buttons| buttons.pressed(GamepadButtonType::South)) ``` 2. ButtonInput/Axis generic methods become ergonomic again ```rust gamepad_buttons.any_just_pressed([GamepadButtonType::Start, GamepadButtonType::Select]) ``` 3. Reduces the number of public components significantly (Gamepad, GamepadSettings, GamepadButtons, GamepadAxes) 4. Components are highly convenient. Gamepad optional features could now be expressed naturally (`Option or Option`), allows devs to attach their own components and filter them, so code like this becomes possible: ```rust fn move_player( player: Query<&Transform, With>>, gamepads_buttons: Query<&GamepadButtons, With>>, ) { if let Ok(gamepad_buttons) = gamepads_buttons.get_single() { if gamepad_buttons.pressed(GamepadButtonType::South) { // move player } } } ``` --- ## Follow-up - [ ] Run conditions? - [ ] Rumble component # Changelog ## Added TODO ## Changed TODO ## Removed TODO ## Migration Guide TODO --------- Co-authored-by: Carter Anderson --- crates/bevy_gilrs/src/converter.rs | 60 +- crates/bevy_gilrs/src/gilrs_system.rs | 128 +- crates/bevy_gilrs/src/lib.rs | 26 +- crates/bevy_gilrs/src/rumble.rs | 10 +- crates/bevy_input/src/axis.rs | 57 +- crates/bevy_input/src/button_input.rs | 37 +- crates/bevy_input/src/gamepad.rs | 1922 ++++++++++++++++++------ crates/bevy_input/src/lib.rs | 45 +- examples/input/gamepad_input.rs | 33 +- examples/input/gamepad_input_events.rs | 19 +- examples/input/gamepad_rumble.rs | 34 +- examples/tools/gamepad_viewer.rs | 91 +- 12 files changed, 1665 insertions(+), 797 deletions(-) diff --git a/crates/bevy_gilrs/src/converter.rs b/crates/bevy_gilrs/src/converter.rs index e5ce04aa9c..521d11070c 100644 --- a/crates/bevy_gilrs/src/converter.rs +++ b/crates/bevy_gilrs/src/converter.rs @@ -1,42 +1,38 @@ -use bevy_input::gamepad::{Gamepad, GamepadAxisType, GamepadButtonType}; +use bevy_input::gamepad::{GamepadAxis, GamepadButton}; -pub fn convert_gamepad_id(gamepad_id: gilrs::GamepadId) -> Gamepad { - Gamepad::new(gamepad_id.into()) -} - -pub fn convert_button(button: gilrs::Button) -> Option { +pub fn convert_button(button: gilrs::Button) -> Option { match button { - gilrs::Button::South => Some(GamepadButtonType::South), - gilrs::Button::East => Some(GamepadButtonType::East), - gilrs::Button::North => Some(GamepadButtonType::North), - gilrs::Button::West => Some(GamepadButtonType::West), - gilrs::Button::C => Some(GamepadButtonType::C), - gilrs::Button::Z => Some(GamepadButtonType::Z), - gilrs::Button::LeftTrigger => Some(GamepadButtonType::LeftTrigger), - gilrs::Button::LeftTrigger2 => Some(GamepadButtonType::LeftTrigger2), - gilrs::Button::RightTrigger => Some(GamepadButtonType::RightTrigger), - gilrs::Button::RightTrigger2 => Some(GamepadButtonType::RightTrigger2), - gilrs::Button::Select => Some(GamepadButtonType::Select), - gilrs::Button::Start => Some(GamepadButtonType::Start), - gilrs::Button::Mode => Some(GamepadButtonType::Mode), - gilrs::Button::LeftThumb => Some(GamepadButtonType::LeftThumb), - gilrs::Button::RightThumb => Some(GamepadButtonType::RightThumb), - gilrs::Button::DPadUp => Some(GamepadButtonType::DPadUp), - gilrs::Button::DPadDown => Some(GamepadButtonType::DPadDown), - gilrs::Button::DPadLeft => Some(GamepadButtonType::DPadLeft), - gilrs::Button::DPadRight => Some(GamepadButtonType::DPadRight), + gilrs::Button::South => Some(GamepadButton::South), + gilrs::Button::East => Some(GamepadButton::East), + gilrs::Button::North => Some(GamepadButton::North), + gilrs::Button::West => Some(GamepadButton::West), + gilrs::Button::C => Some(GamepadButton::C), + gilrs::Button::Z => Some(GamepadButton::Z), + gilrs::Button::LeftTrigger => Some(GamepadButton::LeftTrigger), + gilrs::Button::LeftTrigger2 => Some(GamepadButton::LeftTrigger2), + gilrs::Button::RightTrigger => Some(GamepadButton::RightTrigger), + gilrs::Button::RightTrigger2 => Some(GamepadButton::RightTrigger2), + gilrs::Button::Select => Some(GamepadButton::Select), + gilrs::Button::Start => Some(GamepadButton::Start), + gilrs::Button::Mode => Some(GamepadButton::Mode), + gilrs::Button::LeftThumb => Some(GamepadButton::LeftThumb), + gilrs::Button::RightThumb => Some(GamepadButton::RightThumb), + gilrs::Button::DPadUp => Some(GamepadButton::DPadUp), + gilrs::Button::DPadDown => Some(GamepadButton::DPadDown), + gilrs::Button::DPadLeft => Some(GamepadButton::DPadLeft), + gilrs::Button::DPadRight => Some(GamepadButton::DPadRight), gilrs::Button::Unknown => None, } } -pub fn convert_axis(axis: gilrs::Axis) -> Option { +pub fn convert_axis(axis: gilrs::Axis) -> Option { match axis { - gilrs::Axis::LeftStickX => Some(GamepadAxisType::LeftStickX), - gilrs::Axis::LeftStickY => Some(GamepadAxisType::LeftStickY), - gilrs::Axis::LeftZ => Some(GamepadAxisType::LeftZ), - gilrs::Axis::RightStickX => Some(GamepadAxisType::RightStickX), - gilrs::Axis::RightStickY => Some(GamepadAxisType::RightStickY), - gilrs::Axis::RightZ => Some(GamepadAxisType::RightZ), + gilrs::Axis::LeftStickX => Some(GamepadAxis::LeftStickX), + gilrs::Axis::LeftStickY => Some(GamepadAxis::LeftStickY), + gilrs::Axis::LeftZ => Some(GamepadAxis::LeftZ), + gilrs::Axis::RightStickX => Some(GamepadAxis::RightStickX), + gilrs::Axis::RightStickY => Some(GamepadAxis::RightStickY), + gilrs::Axis::RightZ => Some(GamepadAxis::RightZ), // The `axis_dpad_to_button` gilrs filter should filter out all DPadX and DPadY events. If // it doesn't then we probably need an entry added to the following repo and an update to // GilRs to use the updated database: https://github.com/gabomdq/SDL_GameControllerDB diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs index 6a54249be7..4ea9d2bf78 100644 --- a/crates/bevy_gilrs/src/gilrs_system.rs +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -1,103 +1,113 @@ use crate::{ - converter::{convert_axis, convert_button, convert_gamepad_id}, - Gilrs, + converter::{convert_axis, convert_button}, + Gilrs, GilrsGamepads, }; +use bevy_ecs::event::EventWriter; +use bevy_ecs::prelude::Commands; #[cfg(target_arch = "wasm32")] use bevy_ecs::system::NonSendMut; -use bevy_ecs::{ - event::EventWriter, - system::{Res, ResMut}, -}; -use bevy_input::{ - gamepad::{ - GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnection, - GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadSettings, - }, - prelude::{GamepadAxis, GamepadButton}, - Axis, +use bevy_ecs::system::ResMut; +use bevy_input::gamepad::{ + GamepadConnection, GamepadConnectionEvent, GamepadInfo, RawGamepadAxisChangedEvent, + RawGamepadButtonChangedEvent, RawGamepadEvent, }; use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter}; pub fn gilrs_event_startup_system( + mut commands: Commands, #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, - mut events: EventWriter, + mut gamepads: ResMut, + mut events: EventWriter, ) { for (id, gamepad) in gilrs.0.get().gamepads() { + // Create entity and add to mapping + let entity = commands.spawn_empty().id(); + gamepads.id_to_entity.insert(id, entity); + gamepads.entity_to_id.insert(entity, id); + let info = GamepadInfo { name: gamepad.name().into(), }; - events.send( - GamepadConnectionEvent { - gamepad: convert_gamepad_id(id), - connection: GamepadConnection::Connected(info), - } - .into(), - ); + events.send(GamepadConnectionEvent { + gamepad: entity, + connection: GamepadConnection::Connected(info), + }); } } pub fn gilrs_event_system( + mut commands: Commands, #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, - mut events: EventWriter, - mut gamepad_buttons: ResMut>, - gamepad_axis: Res>, - gamepad_settings: Res, + mut gamepads: ResMut, + mut events: EventWriter, + mut connection_events: EventWriter, + mut button_events: EventWriter, + mut axis_event: EventWriter, ) { let gilrs = gilrs.0.get(); while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) { gilrs.update(&gilrs_event); - - let gamepad = convert_gamepad_id(gilrs_event.id); match gilrs_event.event { EventType::Connected => { let pad = gilrs.gamepad(gilrs_event.id); + let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| { + let entity = commands.spawn_empty().id(); + gamepads.id_to_entity.insert(gilrs_event.id, entity); + gamepads.entity_to_id.insert(entity, gilrs_event.id); + entity + }); + let info = GamepadInfo { name: pad.name().into(), }; events.send( - GamepadConnectionEvent::new(gamepad, GamepadConnection::Connected(info)).into(), + GamepadConnectionEvent::new(entity, GamepadConnection::Connected(info.clone())) + .into(), ); + connection_events.send(GamepadConnectionEvent::new( + entity, + GamepadConnection::Connected(info), + )); } EventType::Disconnected => { - events.send( - GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected).into(), - ); + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + let event = GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected); + events.send(event.clone().into()); + connection_events.send(event); } EventType::ButtonChanged(gilrs_button, raw_value, _) => { - if let Some(button_type) = convert_button(gilrs_button) { - let button = GamepadButton::new(gamepad, button_type); - let old_value = gamepad_buttons.get(button); - let button_settings = gamepad_settings.get_button_axis_settings(button); - - // Only send events that pass the user-defined change threshold - if let Some(filtered_value) = button_settings.filter(raw_value, old_value) { - events.send( - GamepadButtonChangedEvent::new(gamepad, button_type, filtered_value) - .into(), - ); - // Update the current value prematurely so that `old_value` is correct in - // future iterations of the loop. - gamepad_buttons.set(button, filtered_value); - } - } + let Some(button) = convert_button(gilrs_button) else { + continue; + }; + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + events.send(RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into()); + button_events.send(RawGamepadButtonChangedEvent::new( + gamepad, button, raw_value, + )); } EventType::AxisChanged(gilrs_axis, raw_value, _) => { - if let Some(axis_type) = convert_axis(gilrs_axis) { - let axis = GamepadAxis::new(gamepad, axis_type); - let old_value = gamepad_axis.get(axis); - let axis_settings = gamepad_settings.get_axis_settings(axis); - - // Only send events that pass the user-defined change threshold - if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) { - events.send( - GamepadAxisChangedEvent::new(gamepad, axis_type, filtered_value).into(), - ); - } - } + let Some(axis) = convert_axis(gilrs_axis) else { + continue; + }; + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + events.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into()); + axis_event.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value)); } _ => (), }; diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index 0b03ea23df..f9e9bc8dfd 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -15,9 +15,10 @@ mod gilrs_system; mod rumble; use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate}; +use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; use bevy_input::InputSystem; -use bevy_utils::{synccell::SyncCell, tracing::error}; +use bevy_utils::{synccell::SyncCell, tracing::error, HashMap}; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; use rumble::{play_gilrs_rumble, RunningRumbleEffects}; @@ -25,6 +26,27 @@ use rumble::{play_gilrs_rumble, RunningRumbleEffects}; #[cfg_attr(not(target_arch = "wasm32"), derive(Resource))] pub(crate) struct Gilrs(pub SyncCell); +/// A [`resource`](Resource) with the mapping of connected [`gilrs::GamepadId`] and their [`Entity`]. +#[derive(Debug, Default, Resource)] +pub(crate) struct GilrsGamepads { + /// Mapping of [`Entity`] to [`gilrs::GamepadId`]. + pub(crate) entity_to_id: EntityHashMap, + /// Mapping of [`gilrs::GamepadId`] to [`Entity`]. + pub(crate) id_to_entity: HashMap, +} + +impl GilrsGamepads { + /// Returns the [`Entity`] assigned to a connected [`gilrs::GamepadId`]. + pub fn get_entity(&self, gamepad_id: gilrs::GamepadId) -> Option { + self.id_to_entity.get(&gamepad_id).copied() + } + + /// Returns the [`gilrs::GamepadId`] assigned to a gamepad [`Entity`]. + pub fn get_gamepad_id(&self, entity: Entity) -> Option { + self.entity_to_id.get(&entity).copied() + } +} + /// Plugin that provides gamepad handling to an [`App`]. #[derive(Default)] pub struct GilrsPlugin; @@ -45,7 +67,7 @@ impl Plugin for GilrsPlugin { app.insert_non_send_resource(Gilrs(SyncCell::new(gilrs))); #[cfg(not(target_arch = "wasm32"))] app.insert_resource(Gilrs(SyncCell::new(gilrs))); - + app.init_resource::(); app.init_resource::() .add_systems(PreStartup, gilrs_event_startup_system) .add_systems(PreUpdate, gilrs_event_system.before(InputSystem)) diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 4d96ca4e30..62c6b0dc7d 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -1,5 +1,5 @@ //! Handle user specified rumble request events. -use crate::Gilrs; +use crate::{Gilrs, GilrsGamepads}; use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource}; #[cfg(target_arch = "wasm32")] use bevy_ecs::system::NonSendMut; @@ -16,8 +16,6 @@ use gilrs::{ }; use thiserror::Error; -use crate::converter::convert_gamepad_id; - /// A rumble effect that is currently in effect. struct RunningRumble { /// Duration from app startup when this effect will be finished @@ -84,6 +82,7 @@ fn get_base_effects( fn handle_rumble_request( running_rumbles: &mut RunningRumbleEffects, gilrs: &mut gilrs::Gilrs, + gamepads: &GilrsGamepads, rumble: GamepadRumbleRequest, current_time: Duration, ) -> Result<(), RumbleError> { @@ -91,7 +90,7 @@ fn handle_rumble_request( let (gamepad_id, _) = gilrs .gamepads() - .find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad) + .find(|(pad_id, _)| *pad_id == gamepads.get_gamepad_id(gamepad).unwrap()) .ok_or(RumbleError::GamepadNotFound)?; match rumble { @@ -129,6 +128,7 @@ pub(crate) fn play_gilrs_rumble( time: Res>, #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, + gamepads: Res, mut requests: EventReader, mut running_rumbles: ResMut, ) { @@ -146,7 +146,7 @@ pub(crate) fn play_gilrs_rumble( // Add new effects. for rumble in requests.read().cloned() { let gamepad = rumble.gamepad(); - match handle_rumble_request(&mut running_rumbles, gilrs, rumble, current_time) { + match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) { Ok(()) => {} Err(RumbleError::GilrsError(err)) => { if let ff::Error::FfNotSupported(_) = err { diff --git a/crates/bevy_input/src/axis.rs b/crates/bevy_input/src/axis.rs index 5e84b4bb4a..df16c0babf 100644 --- a/crates/bevy_input/src/axis.rs +++ b/crates/bevy_input/src/axis.rs @@ -57,7 +57,7 @@ where /// Returns the unclamped position data of the provided `input_device`. /// - /// This value may be outside of the [`Axis::MIN`] and [`Axis::MAX`] range. + /// This value may be outside the [`Axis::MIN`] and [`Axis::MAX`] range. /// /// Use for things like camera zoom, where you want devices like mouse wheels to be able to /// exceed the normal range. If being able to move faster on one input device @@ -70,18 +70,11 @@ where pub fn remove(&mut self, input_device: T) -> Option { self.axis_data.remove(&input_device) } - /// Returns an iterator of all the input devices that have position data - pub fn devices(&self) -> impl ExactSizeIterator { - self.axis_data.keys() - } } #[cfg(test)] mod tests { - use crate::{ - gamepad::{Gamepad, GamepadButton, GamepadButtonType}, - Axis, - }; + use crate::{gamepad::GamepadButton, Axis}; #[test] fn test_axis_set() { @@ -100,13 +93,11 @@ mod tests { ]; for (value, expected) in cases { - let gamepad_button = - GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger); let mut axis = Axis::::default(); - axis.set(gamepad_button, value); + axis.set(GamepadButton::RightTrigger, value); - let actual = axis.get(gamepad_button); + let actual = axis.get(GamepadButton::RightTrigger); assert_eq!(expected, actual); } } @@ -116,48 +107,16 @@ mod tests { let cases = [-1.0, -0.9, -0.1, 0.0, 0.1, 0.9, 1.0]; for value in cases { - let gamepad_button = - GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger); let mut axis = Axis::::default(); - axis.set(gamepad_button, value); - assert!(axis.get(gamepad_button).is_some()); + axis.set(GamepadButton::RightTrigger, value); + assert!(axis.get(GamepadButton::RightTrigger).is_some()); - axis.remove(gamepad_button); - let actual = axis.get(gamepad_button); + axis.remove(GamepadButton::RightTrigger); + let actual = axis.get(GamepadButton::RightTrigger); let expected = None; assert_eq!(expected, actual); } } - - #[test] - fn test_axis_devices() { - let mut axis = Axis::::default(); - assert_eq!(axis.devices().count(), 0); - - axis.set( - GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger), - 0.1, - ); - assert_eq!(axis.devices().count(), 1); - - axis.set( - GamepadButton::new(Gamepad::new(1), GamepadButtonType::LeftTrigger), - 0.5, - ); - assert_eq!(axis.devices().count(), 2); - - axis.set( - GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger), - -0.1, - ); - assert_eq!(axis.devices().count(), 2); - - axis.remove(GamepadButton::new( - Gamepad::new(1), - GamepadButtonType::RightTrigger, - )); - assert_eq!(axis.devices().count(), 1); - } } diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 559c7eda90..31c27f7330 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -73,17 +73,13 @@ use { /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, Update}; /// # use bevy_ecs::{prelude::{IntoSystemConfigs, Res, Resource, resource_changed}, schedule::Condition}; -/// # use bevy_input::{ButtonInput, prelude::{GamepadButton, KeyCode, MouseButton}}; +/// # use bevy_input::{ButtonInput, prelude::{KeyCode, MouseButton}}; /// /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) /// .add_systems( /// Update, -/// print_gamepad.run_if(resource_changed::>), -/// ) -/// .add_systems( -/// Update, /// print_mouse.run_if(resource_changed::>), /// ) /// .add_systems( @@ -93,10 +89,6 @@ use { /// .run(); /// } /// -/// fn print_gamepad(gamepad: Res>) { -/// println!("Gamepad: {:?}", gamepad.get_pressed().collect::>()); -/// } -/// /// fn print_mouse(mouse: Res>) { /// println!("Mouse: {:?}", mouse.get_pressed().collect::>()); /// } @@ -115,33 +107,6 @@ use { /// } /// ``` /// -/// Accepting input from multiple devices: -/// ```no_run -/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, Update}; -/// # use bevy_ecs::{prelude::IntoSystemConfigs, schedule::Condition}; -/// # use bevy_input::{ButtonInput, common_conditions::{input_just_pressed}, prelude::{GamepadButton, Gamepad, GamepadButtonType, KeyCode}}; -/// -/// fn main() { -/// App::new() -/// .add_plugins(DefaultPlugins) -/// .add_systems( -/// Update, -/// something_used.run_if( -/// input_just_pressed(KeyCode::KeyE) -/// .or(input_just_pressed(GamepadButton::new( -/// Gamepad::new(0), -/// GamepadButtonType::West, -/// ))), -/// ), -/// ) -/// .run(); -/// } -/// -/// fn something_used() { -/// println!("Generic use-ish button pressed."); -/// } -/// ``` -/// /// ## Note /// /// When adding this resource for a new input type, you should: diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 63007631cd..2fee5a4db5 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -3,16 +3,282 @@ use crate::{Axis, ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, + component::Component, + entity::Entity, event::{Event, EventReader, EventWriter}, - system::{Res, ResMut, Resource}, + system::{Commands, Query}, }; -use bevy_utils::{tracing::info, Duration, HashMap}; -use thiserror::Error; +use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] -use { - bevy_ecs::reflect::ReflectResource, - bevy_reflect::{std_traits::ReflectDefault, Reflect}, +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use bevy_utils::{ + tracing::{info, warn}, + Duration, HashMap, }; +use thiserror::Error; + +/// A gamepad event. +/// +/// This event type is used over the [`GamepadConnectionEvent`], +/// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when +/// the in-frame relative ordering of events is important. +/// +/// This event is produced by `bevy_input` +#[derive(Event, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub enum GamepadEvent { + /// A gamepad has been connected or disconnected. + Connection(GamepadConnectionEvent), + /// A button of the gamepad has been triggered. + Button(GamepadButtonChangedEvent), + /// An axis of the gamepad has been triggered. + Axis(GamepadAxisChangedEvent), +} + +impl From for GamepadEvent { + fn from(value: GamepadConnectionEvent) -> Self { + Self::Connection(value) + } +} + +impl From for GamepadEvent { + fn from(value: GamepadButtonChangedEvent) -> Self { + Self::Button(value) + } +} + +impl From for GamepadEvent { + fn from(value: GamepadAxisChangedEvent) -> Self { + Self::Axis(value) + } +} + +/// A raw gamepad event. +/// +/// This event type is used over the [`GamepadConnectionEvent`], +/// [`RawGamepadButtonChangedEvent`] and [`RawGamepadAxisChangedEvent`] when +/// the in-frame relative ordering of events is important. +/// +/// This event type is used by `bevy_input` to feed its components. +#[derive(Event, Debug, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum RawGamepadEvent { + /// A gamepad has been connected or disconnected. + Connection(GamepadConnectionEvent), + /// A button of the gamepad has been triggered. + Button(RawGamepadButtonChangedEvent), + /// An axis of the gamepad has been triggered. + Axis(RawGamepadAxisChangedEvent), +} + +impl From for RawGamepadEvent { + fn from(value: GamepadConnectionEvent) -> Self { + Self::Connection(value) + } +} + +impl From for RawGamepadEvent { + fn from(value: RawGamepadButtonChangedEvent) -> Self { + Self::Button(value) + } +} + +impl From for RawGamepadEvent { + fn from(value: RawGamepadAxisChangedEvent) -> Self { + Self::Axis(value) + } +} + +/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`] +#[derive(Event, Debug, Copy, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct RawGamepadButtonChangedEvent { + /// The gamepad on which the button is triggered. + pub gamepad: Entity, + /// The type of the triggered button. + pub button: GamepadButton, + /// The value of the button. + pub value: f32, +} + +impl RawGamepadButtonChangedEvent { + /// Creates a [`RawGamepadButtonChangedEvent`]. + pub fn new(gamepad: Entity, button_type: GamepadButton, value: f32) -> Self { + Self { + gamepad, + button: button_type, + value, + } + } +} + +/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`] +#[derive(Event, Debug, Copy, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct RawGamepadAxisChangedEvent { + /// The gamepad on which the axis is triggered. + pub gamepad: Entity, + /// The type of the triggered axis. + pub axis: GamepadAxis, + /// The value of the axis. + pub value: f32, +} + +impl RawGamepadAxisChangedEvent { + /// Creates a [`RawGamepadAxisChangedEvent`]. + pub fn new(gamepad: Entity, axis_type: GamepadAxis, value: f32) -> Self { + Self { + gamepad, + axis: axis_type, + value, + } + } +} + +/// A Gamepad connection event. Created when a connection to a gamepad +/// is established and when a gamepad is disconnected. +#[derive(Event, Debug, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadConnectionEvent { + /// The gamepad whose connection status changed. + pub gamepad: Entity, + /// The change in the gamepads connection. + pub connection: GamepadConnection, +} + +impl GamepadConnectionEvent { + /// Creates a [`GamepadConnectionEvent`]. + pub fn new(gamepad: Entity, connection: GamepadConnection) -> Self { + Self { + gamepad, + connection, + } + } + + /// Is the gamepad connected? + pub fn connected(&self) -> bool { + matches!(self.connection, GamepadConnection::Connected(_)) + } + + /// Is the gamepad disconnected? + pub fn disconnected(&self) -> bool { + !self.connected() + } +} + +/// [`GamepadButton`] event triggered by a digital state change +#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadButtonStateChangedEvent { + /// The entity that represents this gamepad. + pub entity: Entity, + /// The gamepad button assigned to the event. + pub button: GamepadButton, + /// The pressed state of the button. + pub state: ButtonState, +} + +impl GamepadButtonStateChangedEvent { + /// Creates a new [`GamepadButtonStateChangedEvent`] + pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self { + Self { + entity, + button, + state, + } + } +} + +/// [`GamepadButton`] event triggered by an analog state change +#[derive(Event, Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadButtonChangedEvent { + /// The entity that represents this gamepad. + pub entity: Entity, + /// The gamepad button assigned to the event. + pub button: GamepadButton, + /// The pressed state of the button. + pub state: ButtonState, + /// The analog value of the button. + pub value: f32, +} + +impl GamepadButtonChangedEvent { + /// Creates a new [`GamepadButtonChangedEvent`] + pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self { + Self { + entity, + button, + state, + value, + } + } +} + +/// [`GamepadAxis`] event triggered by an analog state change +#[derive(Event, Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadAxisChangedEvent { + /// The entity that represents this gamepad. + pub entity: Entity, + /// The gamepad axis assigned to the event. + pub axis: GamepadAxis, + /// The value of this axis. + pub value: f32, +} + +impl GamepadAxisChangedEvent { + /// Creates a new [`GamepadAxisChangedEvent`] + pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self { + Self { + entity, + axis, + value, + } + } +} /// Errors that occur when setting axis settings for gamepad input. #[derive(Error, Debug, PartialEq)] @@ -69,40 +335,175 @@ pub enum ButtonSettingsError { }, } -#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; - -/// A gamepad with an associated `ID`. +/// The [`Gamepad`] [`component`](Component) stores a connected gamepad's metadata such as the `name` and its [`GamepadButton`] and [`GamepadAxis`]. /// -/// ## Usage +/// The [`entity`](Entity) representing a gamepad and its [`minimal components`](GamepadSettings) are automatically managed. /// -/// The primary way to access the individual connected gamepads is done through the [`Gamepads`] -/// `bevy` resource. It is also used inside of [`GamepadConnectionEvent`]s to correspond a gamepad -/// with a connection event. +/// # Usage /// -/// ## Note +/// The only way to obtain a [`Gamepad`] is by [`query`](Query). /// -/// The `ID` of a gamepad is fixed until the gamepad disconnects or the app is restarted. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, Hash, PartialEq) -)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] +/// # Examples +/// +/// ``` +/// # use bevy_input::gamepad::{Gamepad, GamepadAxis, GamepadButton}; +/// # use bevy_ecs::system::Query; +/// # +/// fn gamepad_usage_system(gamepads: Query<&Gamepad>) { +/// for gamepad in gamepads.iter() { +/// println!("{}", gamepad.name()); +/// +/// if gamepad.just_pressed(GamepadButton::North) { +/// println!("{} just pressed North", gamepad.name()) +/// } +/// +/// if let Some(left_stick_x) = gamepad.get(GamepadAxis::LeftStickX) { +/// println!("left stick X: {}", left_stick_x) +/// } +/// } +/// } +/// ``` +#[derive(Component, Debug)] +#[require(GamepadSettings)] pub struct Gamepad { - /// The `ID` of the gamepad. - pub id: usize, + info: GamepadInfo, + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + pub(crate) digital: ButtonInput, + /// [`Axis`] of [`GamepadButton`] representing their analog state. + pub(crate) analog: Axis, } impl Gamepad { - /// Creates a new [`Gamepad`]. - pub fn new(id: usize) -> Self { - Self { id } + /// Creates a gamepad with the given metadata + fn new(info: GamepadInfo) -> Self { + let mut analog = Axis::default(); + for button in GamepadButton::all().iter().copied() { + analog.set(button.into(), 0.0); + } + for axis_type in GamepadAxis::all().iter().copied() { + analog.set(axis_type.into(), 0.0); + } + Self { + info, + analog, + digital: ButtonInput::default(), + } + } + + /// The name of the gamepad. + /// + /// This name is generally defined by the OS. + /// + /// For example on Windows the name may be "HID-compliant game controller". + pub fn name(&self) -> &str { + self.info.name.as_str() + } + + /// Returns the analog data of the provided [`GamepadAxis`] or [`GamepadButton`]. + /// + /// This will be clamped between [[`Axis::MIN`],[`Axis::MAX`]]. + pub fn get(&self, input: impl Into) -> Option { + self.analog.get(input.into()) + } + + /// Returns the unclamped analog data of the provided [`GamepadAxis`] or [`GamepadButton`]. + /// + /// This value may be outside the [`Axis::MIN`] and [`Axis::MAX`] range. + pub fn get_unclamped(&self, input: impl Into) -> Option { + self.analog.get_unclamped(input.into()) + } + + /// Returns the left stick as a [`Vec2`] + pub fn left_stick(&self) -> Vec2 { + Vec2 { + x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0), + y: self.get(GamepadAxis::LeftStickY).unwrap_or(0.0), + } + } + + /// Returns the right stick as a [`Vec2`] + pub fn right_stick(&self) -> Vec2 { + Vec2 { + x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0), + y: self.get(GamepadAxis::RightStickY).unwrap_or(0.0), + } + } + + /// Returns the directional pad as a [`Vec2`] + pub fn dpad(&self) -> Vec2 { + Vec2 { + x: self.get(GamepadButton::DPadRight).unwrap_or(0.0) + - self.get(GamepadButton::DPadLeft).unwrap_or(0.0), + y: self.get(GamepadButton::DPadUp).unwrap_or(0.0) + - self.get(GamepadButton::DPadDown).unwrap_or(0.0), + } + } + + /// Returns `true` if the [`GamepadButton`] has been pressed. + pub fn pressed(&self, button_type: GamepadButton) -> bool { + self.digital.pressed(button_type) + } + + /// Returns `true` if any item in [`GamepadButton`] has been pressed. + pub fn any_pressed(&self, button_inputs: impl IntoIterator) -> bool { + button_inputs + .into_iter() + .any(|button_type| self.pressed(button_type)) + } + + /// Returns `true` if all items in [`GamepadButton`] have been pressed. + pub fn all_pressed(&self, button_inputs: impl IntoIterator) -> bool { + button_inputs + .into_iter() + .all(|button_type| self.pressed(button_type)) + } + + /// Returns `true` if the [`GamepadButton`] has been pressed during the current frame. + /// + /// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_released`]. + pub fn just_pressed(&self, button_type: GamepadButton) -> bool { + self.digital.just_pressed(button_type) + } + + /// Returns `true` if any item in [`GamepadButton`] has been pressed during the current frame. + pub fn any_just_pressed(&self, button_inputs: impl IntoIterator) -> bool { + button_inputs + .into_iter() + .any(|button_type| self.just_pressed(button_type)) + } + + /// Returns `true` if all items in [`GamepadButton`] have been just pressed. + pub fn all_just_pressed(&self, button_inputs: impl IntoIterator) -> bool { + button_inputs + .into_iter() + .all(|button_type| self.just_pressed(button_type)) + } + + /// Returns `true` if the [`GamepadButton`] has been released during the current frame. + /// + /// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_pressed`]. + pub fn just_released(&self, button_type: GamepadButton) -> bool { + self.digital.just_released(button_type) + } + + /// Returns `true` if any item in [`GamepadButton`] has just been released. + pub fn any_just_released( + &self, + button_inputs: impl IntoIterator, + ) -> bool { + button_inputs + .into_iter() + .any(|button_type| self.just_released(button_type)) + } + + /// Returns `true` if all items in [`GamepadButton`] have just been released. + pub fn all_just_released( + &self, + button_inputs: impl IntoIterator, + ) -> bool { + button_inputs + .into_iter() + .all(|button_type| self.just_released(button_type)) } } @@ -123,57 +524,12 @@ pub struct GamepadInfo { pub name: String, } -/// A collection of connected [`Gamepad`]s. +/// Represents gamepad input types that are mapped in the range [0.0, 1.0]. /// /// ## Usage /// -/// It is stored in a `bevy` resource which tracks all of the currently connected [`Gamepad`]s. -/// -/// ## Updating -/// -/// The [`Gamepad`]s are registered and deregistered in the [`gamepad_connection_system`] -/// whenever a [`GamepadConnectionEvent`] is received. -#[derive(Resource, Default, Debug)] -pub struct Gamepads { - /// The collection of the connected [`Gamepad`]s. - gamepads: HashMap, -} - -impl Gamepads { - /// Returns `true` if the `gamepad` is connected. - pub fn contains(&self, gamepad: Gamepad) -> bool { - self.gamepads.contains_key(&gamepad) - } - - /// Returns an iterator over registered [`Gamepad`]s in an arbitrary order. - pub fn iter(&self) -> impl Iterator + '_ { - self.gamepads.keys().copied() - } - - /// The name of the gamepad if this one is connected. - pub fn name(&self, gamepad: Gamepad) -> Option<&str> { - self.gamepads.get(&gamepad).map(|g| g.name.as_str()) - } - - /// Registers the `gamepad`, marking it as connected. - fn register(&mut self, gamepad: Gamepad, info: GamepadInfo) { - self.gamepads.insert(gamepad, info); - } - - /// Deregisters the `gamepad`, marking it as disconnected. - fn deregister(&mut self, gamepad: Gamepad) { - self.gamepads.remove(&gamepad); - } -} - -/// A type of a [`GamepadButton`]. -/// -/// ## Usage -/// -/// This is used to determine which button has changed its value when receiving a -/// [`GamepadButtonChangedEvent`]. It is also used in the [`GamepadButton`] -/// which in turn is used to create the [`ButtonInput`] or -/// [`Axis`] `bevy` resources. +/// This is used to determine which button has changed its value when receiving gamepad button events +/// It is also used in the [`Gamepad`] component. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr( feature = "bevy_reflect", @@ -185,7 +541,7 @@ impl Gamepads { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub enum GamepadButtonType { +pub enum GamepadButton { /// The bottom action button of the action pad (i.e. PS: Cross, Xbox: A). South, /// The right action button of the action pad (i.e. PS: Circle, Xbox: B). @@ -233,77 +589,39 @@ pub enum GamepadButtonType { Other(u8), } -/// A button of a [`Gamepad`]. -/// -/// ## Usage -/// -/// It is used as the generic `T` value of an [`ButtonInput`] and [`Axis`] to create `bevy` resources. These -/// resources store the data of the buttons of a gamepad and can be accessed inside of a system. -/// -/// ## Updating -/// -/// The gamepad button resources are updated inside of the [`gamepad_button_event_system`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, Hash, PartialEq) -)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadButton { - /// The gamepad on which the button is located on. - pub gamepad: Gamepad, - /// The type of the button. - pub button_type: GamepadButtonType, -} - impl GamepadButton { - /// Creates a new [`GamepadButton`]. - /// - /// # Examples - /// - /// ``` - /// # use bevy_input::gamepad::{GamepadButton, GamepadButtonType, Gamepad}; - /// # - /// let gamepad_button = GamepadButton::new( - /// Gamepad::new(1), - /// GamepadButtonType::South, - /// ); - /// ``` - pub fn new(gamepad: Gamepad, button_type: GamepadButtonType) -> Self { - Self { - gamepad, - button_type, - } + /// Returns an array of all the standard [`GamepadButton`] + pub const fn all() -> [GamepadButton; 19] { + [ + GamepadButton::South, + GamepadButton::East, + GamepadButton::North, + GamepadButton::West, + GamepadButton::C, + GamepadButton::Z, + GamepadButton::LeftTrigger, + GamepadButton::LeftTrigger2, + GamepadButton::RightTrigger, + GamepadButton::RightTrigger2, + GamepadButton::Select, + GamepadButton::Start, + GamepadButton::Mode, + GamepadButton::LeftThumb, + GamepadButton::RightThumb, + GamepadButton::DPadUp, + GamepadButton::DPadDown, + GamepadButton::DPadLeft, + GamepadButton::DPadRight, + ] } } -/// A gamepad button input event. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadButtonInput { - /// The gamepad button assigned to the event. - pub button: GamepadButton, - /// The pressed state of the button. - pub state: ButtonState, -} - -/// A type of a [`GamepadAxis`]. +/// Represents gamepad input types that are mapped in the range [-1.0, 1.0] /// /// ## Usage /// /// This is used to determine which axis has changed its value when receiving a -/// [`GamepadAxisChangedEvent`]. It is also used in the [`GamepadAxis`] -/// which in turn is used to create the [`Axis`] `bevy` resource. +/// gamepad axis event. It is also used in the [`Gamepad`] component. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -311,7 +629,7 @@ pub struct GamepadButtonInput { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub enum GamepadAxisType { +pub enum GamepadAxis { /// The horizontal value of the left stick. LeftStickX, /// The vertical value of the left stick. @@ -330,68 +648,57 @@ pub enum GamepadAxisType { Other(u8), } -/// An axis of a [`Gamepad`]. -/// -/// ## Usage -/// -/// It is used as the generic `T` value of an [`Axis`] to create `bevy` resources. These -/// resources store the data of the axes of a gamepad and can be accessed inside of a system. -/// -/// ## Updating -/// -/// The gamepad axes resources are updated inside of the [`gamepad_axis_event_system`]. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadAxis { - /// The gamepad on which the axis is located on. - pub gamepad: Gamepad, - /// The type of the axis. - pub axis_type: GamepadAxisType, -} - impl GamepadAxis { - /// Creates a new [`GamepadAxis`]. - /// - /// # Examples - /// - /// ``` - /// # use bevy_input::gamepad::{GamepadAxis, GamepadAxisType, Gamepad}; - /// # - /// let gamepad_axis = GamepadAxis::new( - /// Gamepad::new(1), - /// GamepadAxisType::LeftStickX, - /// ); - /// ``` - pub fn new(gamepad: Gamepad, axis_type: GamepadAxisType) -> Self { - Self { gamepad, axis_type } + /// Returns an array of all the standard [`GamepadAxis`]. + pub const fn all() -> [GamepadAxis; 6] { + [ + GamepadAxis::LeftStickX, + GamepadAxis::LeftStickY, + GamepadAxis::LeftZ, + GamepadAxis::RightStickX, + GamepadAxis::RightStickY, + GamepadAxis::RightZ, + ] } } -/// Settings for all [`Gamepad`]s. +/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`] +// This is done so Gamepad can share a single Axis and simplifies the API by having only one get/get_unclamped method +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +pub enum GamepadInput { + /// A [`GamepadAxis`] + Axis(GamepadAxis), + /// A [`GamepadButton`] + Button(GamepadButton), +} + +impl From for GamepadInput { + fn from(value: GamepadAxis) -> Self { + GamepadInput::Axis(value) + } +} + +impl From for GamepadInput { + fn from(value: GamepadButton) -> Self { + GamepadInput::Button(value) + } +} + +/// Gamepad settings component. /// /// ## Usage /// -/// It is used to create a `bevy` resource that stores the settings of every [`GamepadButton`] and -/// [`GamepadAxis`]. If no user defined [`ButtonSettings`], [`AxisSettings`], or [`ButtonAxisSettings`] +/// It is used to create a `bevy` component that stores the settings of [`GamepadButton`] and [`GamepadAxis`] in [`Gamepad`]. +/// If no user defined [`ButtonSettings`], [`AxisSettings`], or [`ButtonAxisSettings`] /// are defined, the default settings of each are used as a fallback accordingly. /// /// ## Note /// -/// The [`GamepadSettings`] are used inside of `bevy_gilrs` to determine when raw gamepad events from `gilrs`, -/// should register as a [`GamepadEvent`]. Events that don't meet the change thresholds defined in [`GamepadSettings`] -/// will not register. To modify these settings, mutate the corresponding resource. -#[derive(Resource, Default, Debug)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, Default, Resource) -)] - +/// The [`GamepadSettings`] are used to determine when raw gamepad events +/// should register. Events that don't meet the change thresholds defined in [`GamepadSettings`] +/// will not register. To modify these settings, mutate the corresponding component. +#[derive(Component, Clone, Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] pub struct GamepadSettings { /// The default button settings. pub default_button_settings: ButtonSettings, @@ -408,18 +715,17 @@ pub struct GamepadSettings { } impl GamepadSettings { - /// Returns the [`ButtonSettings`] of the `button`. + /// Returns the [`ButtonSettings`] of the [`GamepadButton`]. /// /// If no user defined [`ButtonSettings`] are specified the default [`ButtonSettings`] get returned. /// /// # Examples /// /// ``` - /// # use bevy_input::gamepad::{GamepadSettings, GamepadButton, Gamepad, GamepadButtonType}; + /// # use bevy_input::gamepad::{GamepadSettings, GamepadButton}; /// # /// # let settings = GamepadSettings::default(); - /// let button = GamepadButton::new(Gamepad::new(1), GamepadButtonType::South); - /// let button_settings = settings.get_button_settings(button); + /// let button_settings = settings.get_button_settings(GamepadButton::South); /// ``` pub fn get_button_settings(&self, button: GamepadButton) -> &ButtonSettings { self.button_settings @@ -427,18 +733,17 @@ impl GamepadSettings { .unwrap_or(&self.default_button_settings) } - /// Returns the [`AxisSettings`] of the `axis`. + /// Returns the [`AxisSettings`] of the [`GamepadAxis`]. /// /// If no user defined [`AxisSettings`] are specified the default [`AxisSettings`] get returned. /// /// # Examples /// /// ``` - /// # use bevy_input::gamepad::{GamepadSettings, GamepadAxis, Gamepad, GamepadAxisType}; + /// # use bevy_input::gamepad::{GamepadSettings, GamepadAxis}; /// # /// # let settings = GamepadSettings::default(); - /// let axis = GamepadAxis::new(Gamepad::new(1), GamepadAxisType::LeftStickX); - /// let axis_settings = settings.get_axis_settings(axis); + /// let axis_settings = settings.get_axis_settings(GamepadAxis::LeftStickX); /// ``` pub fn get_axis_settings(&self, axis: GamepadAxis) -> &AxisSettings { self.axis_settings @@ -446,18 +751,17 @@ impl GamepadSettings { .unwrap_or(&self.default_axis_settings) } - /// Returns the [`ButtonAxisSettings`] of the `button`. + /// Returns the [`ButtonAxisSettings`] of the [`GamepadButton`]. /// /// If no user defined [`ButtonAxisSettings`] are specified the default [`ButtonAxisSettings`] get returned. /// /// # Examples /// /// ``` - /// # use bevy_input::gamepad::{GamepadSettings, GamepadButton, Gamepad, GamepadButtonType}; + /// # use bevy_input::gamepad::{GamepadSettings, GamepadButton}; /// # /// # let settings = GamepadSettings::default(); - /// let button = GamepadButton::new(Gamepad::new(1), GamepadButtonType::South); - /// let button_axis_settings = settings.get_button_axis_settings(button); + /// let button_axis_settings = settings.get_button_axis_settings(GamepadButton::South); /// ``` pub fn get_button_axis_settings(&self, button: GamepadButton) -> &ButtonAxisSettings { self.button_axis_settings @@ -468,12 +772,12 @@ impl GamepadSettings { /// Manages settings for gamepad buttons. /// -/// It is used inside of [`GamepadSettings`] to define the threshold for a gamepad button +/// It is used inside [`GamepadSettings`] to define the threshold for a [`GamepadButton`] /// to be considered pressed or released. A button is considered pressed if the `press_threshold` /// value is surpassed and released if the `release_threshold` value is undercut. /// /// Allowed values: `0.0 <= ``release_threshold`` <= ``press_threshold`` <= 1.0` -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] pub struct ButtonSettings { press_threshold: f32, @@ -624,7 +928,7 @@ impl ButtonSettings { /// Settings for a [`GamepadAxis`]. /// -/// It is used inside of the [`GamepadSettings`] to define the sensitivity range and +/// It is used inside the [`GamepadSettings`] to define the sensitivity range and /// threshold for an axis. /// Values that are higher than `livezone_upperbound` will be rounded up to 1.0. /// Values that are lower than `livezone_lowerbound` will be rounded down to -1.0. @@ -951,7 +1255,7 @@ impl AxisSettings { /// Settings for a [`GamepadButton`]. /// -/// It is used inside of the [`GamepadSettings`] to define the sensitivity range and +/// It is used inside the [`GamepadSettings`] to define the sensitivity range and /// threshold for a button axis. /// /// ## Logic @@ -961,10 +1265,6 @@ impl AxisSettings { /// - Otherwise, values will not be rounded. /// /// The valid range is from 0.0 to 1.0, inclusive. -/// -/// ## Updating -/// -/// The current value of a button is received through the [`GamepadButtonChangedEvent`]. #[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] pub struct ButtonAxisSettings { @@ -1028,47 +1328,40 @@ impl ButtonAxisSettings { } } -/// Handles [`GamepadConnectionEvent`]s and updates gamepad resources. +/// Handles [`GamepadConnectionEvent`]s events. /// -/// Updates the [`Gamepads`] resource and resets and/or initializes -/// the [`Axis`] and [`ButtonInput`] resources. +/// On connection, adds the components representing a [`Gamepad`] to the entity. +/// On disconnection, removes the [`Gamepad`] and other related components. +/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection /// /// ## Note /// /// Whenever a [`Gamepad`] connects or disconnects, an information gets printed to the console using the [`info!`] macro. pub fn gamepad_connection_system( - mut gamepads: ResMut, + mut commands: Commands, mut connection_events: EventReader, - mut axis: ResMut>, - mut button_axis: ResMut>, - mut button_input: ResMut>, ) { for connection_event in connection_events.read() { - let gamepad = connection_event.gamepad; - - if let GamepadConnection::Connected(info) = &connection_event.connection { - gamepads.register(gamepad, info.clone()); - info!("{:?} Connected", gamepad); - - for button_type in &ALL_BUTTON_TYPES { - let gamepad_button = GamepadButton::new(gamepad, *button_type); - button_input.reset(gamepad_button); - button_axis.set(gamepad_button, 0.0); + let id = connection_event.gamepad; + match &connection_event.connection { + GamepadConnection::Connected(info) => { + let Some(gamepad) = commands.get_entity(id) else { + warn!("Gamepad {:} removed before handling connection event.", id); + continue; + }; + gamepad.insert(Gamepad::new(info.clone())); + info!("Gamepad {:?} connected.", id); } - for axis_type in &ALL_AXIS_TYPES { - axis.set(GamepadAxis::new(gamepad, *axis_type), 0.0); - } - } else { - gamepads.deregister(gamepad); - info!("{:?} Disconnected", gamepad); - - for button_type in &ALL_BUTTON_TYPES { - let gamepad_button = GamepadButton::new(gamepad, *button_type); - button_input.reset(gamepad_button); - button_axis.remove(gamepad_button); - } - for axis_type in &ALL_AXIS_TYPES { - axis.remove(GamepadAxis::new(gamepad, *axis_type)); + GamepadConnection::Disconnected => { + let Some(gamepad) = commands.get_entity(id) else { + warn!("Gamepad {:} removed before handling disconnection event. You can ignore this if you manually removed it.", id); + continue; + }; + // Gamepad entities are left alive to preserve their state (e.g. [`GamepadSettings`]). + // Instead of despawning, we remove Gamepad components that don't need to preserve state + // and re-add them if they ever reconnect. + gamepad.remove::(); + info!("Gamepad {:} disconnected.", id); } } } @@ -1089,243 +1382,104 @@ pub enum GamepadConnection { Disconnected, } -/// A Gamepad connection event. Created when a connection to a gamepad -/// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadConnectionEvent { - /// The gamepad whose connection status changed. - pub gamepad: Gamepad, - /// The change in the gamepads connection. - pub connection: GamepadConnection, -} - -impl GamepadConnectionEvent { - /// Creates a [`GamepadConnectionEvent`]. - pub fn new(gamepad: Gamepad, connection: GamepadConnection) -> Self { - Self { - gamepad, - connection, - } - } - - /// Is the gamepad connected? - pub fn connected(&self) -> bool { - matches!(self.connection, GamepadConnection::Connected(_)) - } - - /// Is the gamepad disconnected? - pub fn disconnected(&self) -> bool { - !self.connected() - } -} - -/// Gamepad event for when the "value" on the axis changes -/// by an amount larger than the threshold defined in [`GamepadSettings`]. -#[derive(Event, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadAxisChangedEvent { - /// The gamepad on which the axis is triggered. - pub gamepad: Gamepad, - /// The type of the triggered axis. - pub axis_type: GamepadAxisType, - /// The value of the axis. - pub value: f32, -} - -impl GamepadAxisChangedEvent { - /// Creates a [`GamepadAxisChangedEvent`]. - pub fn new(gamepad: Gamepad, axis_type: GamepadAxisType, value: f32) -> Self { - Self { - gamepad, - axis_type, - value, - } - } -} - -/// Gamepad event for when the "value" (amount of pressure) on the button -/// changes by an amount larger than the threshold defined in [`GamepadSettings`]. -#[derive(Event, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct GamepadButtonChangedEvent { - /// The gamepad on which the button is triggered. - pub gamepad: Gamepad, - /// The type of the triggered button. - pub button_type: GamepadButtonType, - /// The value of the button. - pub value: f32, -} - -impl GamepadButtonChangedEvent { - /// Creates a [`GamepadButtonChangedEvent`]. - pub fn new(gamepad: Gamepad, button_type: GamepadButtonType, value: f32) -> Self { - Self { - gamepad, - button_type, - value, - } - } -} - -/// Uses [`GamepadAxisChangedEvent`]s to update the relevant [`ButtonInput`] and [`Axis`] values. -pub fn gamepad_axis_event_system( - mut gamepad_axis: ResMut>, - mut axis_events: EventReader, +/// Consumes [`RawGamepadEvent`] events, filters them using their [`GamepadSettings`] and if successful, +/// updates the [`Gamepad`] and sends [`GamepadAxisChangedEvent`], [`GamepadButtonStateChangedEvent`], [`GamepadButtonChangedEvent`] events. +pub fn gamepad_event_processing_system( + mut gamepads: Query<(&mut Gamepad, &GamepadSettings)>, + mut raw_events: EventReader, + mut processed_events: EventWriter, + mut processed_axis_events: EventWriter, + mut processed_digital_events: EventWriter, + mut processed_analog_events: EventWriter, ) { - for axis_event in axis_events.read() { - let axis = GamepadAxis::new(axis_event.gamepad, axis_event.axis_type); - gamepad_axis.set(axis, axis_event.value); + // Clear digital buttons state + for (mut gamepad, _) in gamepads.iter_mut() { + gamepad.bypass_change_detection().digital.clear(); } -} -/// Uses [`GamepadButtonChangedEvent`]s to update the relevant [`ButtonInput`] and [`Axis`] values. -pub fn gamepad_button_event_system( - mut button_changed_events: EventReader, - mut button_input: ResMut>, - mut button_input_events: EventWriter, - settings: Res, -) { - for button_event in button_changed_events.read() { - let button = GamepadButton::new(button_event.gamepad, button_event.button_type); - let value = button_event.value; - let button_property = settings.get_button_settings(button); - - if button_property.is_released(value) { - // Check if button was previously pressed - if button_input.pressed(button) { - button_input_events.send(GamepadButtonInput { - button, - state: ButtonState::Released, - }); + for event in raw_events.read() { + match event { + // Connections require inserting/removing components so they are done in a separate system + RawGamepadEvent::Connection(send_event) => { + processed_events.send(GamepadEvent::from(send_event.clone())); } - // We don't have to check if the button was previously pressed here - // because that check is performed within Input::release() - button_input.release(button); - } else if button_property.is_pressed(value) { - // Check if button was previously not pressed - if !button_input.pressed(button) { - button_input_events.send(GamepadButtonInput { - button, - state: ButtonState::Pressed, - }); + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent { + gamepad, + axis, + value, + }) => { + let (gamepad, axis, value) = (*gamepad, *axis, *value); + let Ok((mut gamepad_axis, gamepad_settings)) = gamepads.get_mut(gamepad) else { + continue; + }; + let Some(filtered_value) = gamepad_settings + .get_axis_settings(axis) + .filter(value, gamepad_axis.get(axis)) + else { + continue; + }; + + gamepad_axis.analog.set(axis.into(), filtered_value); + let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value); + processed_axis_events.send(send_event); + processed_events.send(GamepadEvent::from(send_event)); } - button_input.press(button); - }; - } -} + RawGamepadEvent::Button(RawGamepadButtonChangedEvent { + gamepad, + button, + value, + }) => { + let (gamepad, button, value) = (*gamepad, *button, *value); + let Ok((mut gamepad_buttons, settings)) = gamepads.get_mut(gamepad) else { + continue; + }; + let Some(filtered_value) = settings + .get_button_axis_settings(button) + .filter(value, gamepad_buttons.get(button)) + else { + continue; + }; + let button_settings = settings.get_button_settings(button); + gamepad_buttons.analog.set(button.into(), filtered_value); -/// A gamepad event. -/// -/// This event type is used over the [`GamepadConnectionEvent`], -/// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when -/// the in-frame relative ordering of events is important. -#[derive(Event, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub enum GamepadEvent { - /// A gamepad has been connected or disconnected. - Connection(GamepadConnectionEvent), - /// A button of the gamepad has been triggered. - Button(GamepadButtonChangedEvent), - /// An axis of the gamepad has been triggered. - Axis(GamepadAxisChangedEvent), -} + if button_settings.is_released(filtered_value) { + // Check if button was previously pressed + if gamepad_buttons.pressed(button) { + processed_digital_events.send(GamepadButtonStateChangedEvent::new( + gamepad, + button, + ButtonState::Released, + )); + } + // We don't have to check if the button was previously pressed here + // because that check is performed within Input::release() + gamepad_buttons.digital.release(button); + } else if button_settings.is_pressed(filtered_value) { + // Check if button was previously not pressed + if !gamepad_buttons.pressed(button) { + processed_digital_events.send(GamepadButtonStateChangedEvent::new( + gamepad, + button, + ButtonState::Pressed, + )); + } + gamepad_buttons.digital.press(button); + }; -impl From for GamepadEvent { - fn from(value: GamepadConnectionEvent) -> Self { - Self::Connection(value) - } -} - -impl From for GamepadEvent { - fn from(value: GamepadButtonChangedEvent) -> Self { - Self::Button(value) - } -} - -impl From for GamepadEvent { - fn from(value: GamepadAxisChangedEvent) -> Self { - Self::Axis(value) - } -} - -/// Splits the [`GamepadEvent`] event stream into its component events. -pub fn gamepad_event_system( - mut gamepad_events: EventReader, - mut connection_events: EventWriter, - mut button_events: EventWriter, - mut axis_events: EventWriter, - mut button_input: ResMut>, -) { - button_input.bypass_change_detection().clear(); - for gamepad_event in gamepad_events.read() { - match gamepad_event { - GamepadEvent::Connection(connection_event) => { - connection_events.send(connection_event.clone()); - } - GamepadEvent::Button(button_event) => { - button_events.send(button_event.clone()); - } - GamepadEvent::Axis(axis_event) => { - axis_events.send(axis_event.clone()); + let button_state = if gamepad_buttons.digital.pressed(button) { + ButtonState::Pressed + } else { + ButtonState::Released + }; + let send_event = + GamepadButtonChangedEvent::new(gamepad, button, button_state, filtered_value); + processed_analog_events.send(send_event); + processed_events.send(GamepadEvent::from(send_event)); } } } } -/// An array of every [`GamepadButtonType`] variant. -const ALL_BUTTON_TYPES: [GamepadButtonType; 19] = [ - GamepadButtonType::South, - GamepadButtonType::East, - GamepadButtonType::North, - GamepadButtonType::West, - GamepadButtonType::C, - GamepadButtonType::Z, - GamepadButtonType::LeftTrigger, - GamepadButtonType::LeftTrigger2, - GamepadButtonType::RightTrigger, - GamepadButtonType::RightTrigger2, - GamepadButtonType::Select, - GamepadButtonType::Start, - GamepadButtonType::Mode, - GamepadButtonType::LeftThumb, - GamepadButtonType::RightThumb, - GamepadButtonType::DPadUp, - GamepadButtonType::DPadDown, - GamepadButtonType::DPadLeft, - GamepadButtonType::DPadRight, -]; - -/// An array of every [`GamepadAxisType`] variant. -const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [ - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - GamepadAxisType::LeftZ, - GamepadAxisType::RightStickX, - GamepadAxisType::RightStickY, - GamepadAxisType::RightZ, -]; - /// The intensity at which a gamepad's force-feedback motors may rumble. #[derive(Clone, Copy, Debug, PartialEq)] pub struct GamepadRumbleIntensity { @@ -1385,7 +1539,7 @@ impl GamepadRumbleIntensity { } } -/// An event that controls force-feedback rumbling of a [`Gamepad`]. +/// An event that controls force-feedback rumbling of a [`Gamepad`] [`entity`](Entity). /// /// # Notes /// @@ -1394,16 +1548,16 @@ impl GamepadRumbleIntensity { /// # Example /// /// ``` -/// # use bevy_input::gamepad::{Gamepad, Gamepads, GamepadRumbleRequest, GamepadRumbleIntensity}; -/// # use bevy_ecs::prelude::{EventWriter, Res}; +/// # use bevy_input::gamepad::{Gamepad, GamepadRumbleRequest, GamepadRumbleIntensity}; +/// # use bevy_ecs::prelude::{EventWriter, Res, Query, Entity, With}; /// # use bevy_utils::Duration; /// fn rumble_gamepad_system( /// mut rumble_requests: EventWriter, -/// gamepads: Res +/// gamepads: Query>, /// ) { -/// for gamepad in gamepads.iter() { +/// for entity in gamepads.iter() { /// rumble_requests.send(GamepadRumbleRequest::Add { -/// gamepad, +/// gamepad: entity, /// intensity: GamepadRumbleIntensity::MAX, /// duration: Duration::from_secs_f32(0.5), /// }); @@ -1432,18 +1586,18 @@ pub enum GamepadRumbleRequest { /// How intense the rumble should be. intensity: GamepadRumbleIntensity, /// The gamepad to rumble. - gamepad: Gamepad, + gamepad: Entity, }, - /// Stop all running rumbles on the given [`Gamepad`]. + /// Stop all running rumbles on the given [`Entity`]. Stop { /// The gamepad to stop rumble. - gamepad: Gamepad, + gamepad: Entity, }, } impl GamepadRumbleRequest { - /// Get the [`Gamepad`] associated with this request. - pub fn gamepad(&self) -> Gamepad { + /// Get the [`Entity`] associated with this request. + pub fn gamepad(&self) -> Entity { match self { Self::Add { gamepad, .. } | Self::Stop { gamepad } => *gamepad, } @@ -1452,9 +1606,20 @@ impl GamepadRumbleRequest { #[cfg(test)] mod tests { - use crate::gamepad::{AxisSettingsError, ButtonSettingsError}; - - use super::{AxisSettings, ButtonAxisSettings, ButtonSettings}; + use super::{ + gamepad_connection_system, gamepad_event_processing_system, AxisSettings, + AxisSettingsError, ButtonAxisSettings, ButtonSettings, ButtonSettingsError, Gamepad, + GamepadAxis, GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, + GamepadButtonStateChangedEvent, + GamepadConnection::{Connected, Disconnected}, + GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadSettings, + RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, RawGamepadEvent, + }; + use crate::ButtonState; + use bevy_app::{App, PreUpdate}; + use bevy_ecs::entity::Entity; + use bevy_ecs::event::Events; + use bevy_ecs::schedule::IntoSystemConfigs; fn test_button_axis_settings_filter( settings: ButtonAxisSettings, @@ -1784,4 +1949,789 @@ mod tests { axis_settings.try_set_livezone_upperbound(0.1) ); } + + struct TestContext { + pub app: App, + } + + impl TestContext { + pub fn new() -> Self { + let mut app = App::new(); + app.add_systems( + PreUpdate, + ( + gamepad_connection_system, + gamepad_event_processing_system.after(gamepad_connection_system), + ), + ) + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + Self { app } + } + + pub fn update(&mut self) { + self.app.update(); + } + + pub fn send_gamepad_connection_event(&mut self, gamepad: Option) -> Entity { + let gamepad = gamepad.unwrap_or_else(|| self.app.world_mut().spawn_empty().id()); + self.app + .world_mut() + .resource_mut::>() + .send(GamepadConnectionEvent::new( + gamepad, + Connected(GamepadInfo { + name: String::from("Gamepad test"), + }), + )); + gamepad + } + + pub fn send_gamepad_disconnection_event(&mut self, gamepad: Entity) { + self.app + .world_mut() + .resource_mut::>() + .send(GamepadConnectionEvent::new(gamepad, Disconnected)); + } + + pub fn send_raw_gamepad_event(&mut self, event: RawGamepadEvent) { + self.app + .world_mut() + .resource_mut::>() + .send(event); + } + + pub fn send_raw_gamepad_event_batch( + &mut self, + events: impl IntoIterator, + ) { + self.app + .world_mut() + .resource_mut::>() + .send_batch(events); + } + } + + #[test] + fn connection_event() { + let mut ctx = TestContext::new(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + ctx.send_gamepad_connection_event(None); + ctx.update(); + assert_eq!( + ctx.app + .world_mut() + .query::<(&Gamepad, &GamepadSettings)>() + .iter(ctx.app.world()) + .len(), + 1 + ); + } + + #[test] + fn disconnection_event() { + let mut ctx = TestContext::new(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + let entity = ctx.send_gamepad_connection_event(None); + ctx.update(); + assert_eq!( + ctx.app + .world_mut() + .query::<(&Gamepad, &GamepadSettings)>() + .iter(ctx.app.world()) + .len(), + 1 + ); + ctx.send_gamepad_disconnection_event(entity); + ctx.update(); + // Gamepad component should be removed + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .is_err()); + // Settings should be kept + assert!(ctx + .app + .world_mut() + .query::<&GamepadSettings>() + .get(ctx.app.world(), entity) + .is_ok()); + + // Mistakenly sending a second disconnection event shouldn't break anything + ctx.send_gamepad_disconnection_event(entity); + ctx.update(); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .is_err()); + assert!(ctx + .app + .world_mut() + .query::<&GamepadSettings>() + .get(ctx.app.world(), entity) + .is_ok()); + } + + #[test] + fn connection_disconnection_frame_event() { + let mut ctx = TestContext::new(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + let entity = ctx.send_gamepad_connection_event(None); + ctx.send_gamepad_disconnection_event(entity); + ctx.update(); + // Gamepad component should be removed + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .is_err()); + // Settings should be kept + assert!(ctx + .app + .world_mut() + .query::<&GamepadSettings>() + .get(ctx.app.world(), entity) + .is_ok()); + } + + #[test] + fn reconnection_event() { + let button_settings = ButtonSettings::new(0.7, 0.2).expect("correct parameters"); + let mut ctx = TestContext::new(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + let entity = ctx.send_gamepad_connection_event(None); + ctx.update(); + let mut settings = ctx + .app + .world_mut() + .query::<&mut GamepadSettings>() + .get_mut(ctx.app.world_mut(), entity) + .expect("be alive"); + assert_ne!(settings.default_button_settings, button_settings); + settings.default_button_settings = button_settings.clone(); + ctx.send_gamepad_disconnection_event(entity); + ctx.update(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + ctx.send_gamepad_connection_event(Some(entity)); + ctx.update(); + let settings = ctx + .app + .world_mut() + .query::<&GamepadSettings>() + .get(ctx.app.world(), entity) + .expect("be alive"); + assert_eq!(settings.default_button_settings, button_settings); + } + + #[test] + fn reconnection_same_frame_event() { + let mut ctx = TestContext::new(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + let entity = ctx.send_gamepad_connection_event(None); + ctx.send_gamepad_disconnection_event(entity); + ctx.update(); + assert_eq!( + ctx.app + .world_mut() + .query::<&Gamepad>() + .iter(ctx.app.world()) + .len(), + 0 + ); + assert!(ctx + .app + .world_mut() + .query::<(Entity, &GamepadSettings)>() + .get(ctx.app.world(), entity) + .is_ok()); + } + + #[test] + fn gamepad_axis_valid() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + ctx.app + .world_mut() + .resource_mut::>() + .send_batch([ + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickY, + 0.5, + )), + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::RightStickX, + 0.6, + )), + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::RightZ, + -0.4, + )), + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::RightStickY, + -0.8, + )), + ]); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 4 + ); + } + + #[test] + fn gamepad_axis_threshold_filter() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let settings = GamepadSettings::default().default_axis_settings; + // Set of events to ensure they are being properly filtered + let base_value = 0.5; + let events = [ + // Event above threshold + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + base_value, + )), + // Event below threshold, should be filtered + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + base_value + settings.threshold - 0.01, + )), + // Event above threshold + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + base_value + settings.threshold + 0.01, + )), + ]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 2 + ); + } + + #[test] + fn gamepad_axis_deadzone_filter() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let settings = GamepadSettings::default().default_axis_settings; + + // Set of events to ensure they are being properly filtered + let events = [ + // Event below deadzone upperbound should be filtered + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.deadzone_upperbound - 0.01, + )), + // Event above deadzone lowerbound should be filtered + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.deadzone_lowerbound + 0.01, + )), + ]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 0 + ); + } + + #[test] + fn gamepad_axis_deadzone_rounded() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let settings = GamepadSettings::default().default_axis_settings; + + // Set of events to ensure they are being properly filtered + let events = [ + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + 1.0, + )), + // Event below deadzone upperbound should be rounded to 0 + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.deadzone_upperbound - 0.01, + )), + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + 1.0, + )), + // Event above deadzone lowerbound should be rounded to 0 + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.deadzone_lowerbound + 0.01, + )), + ]; + let results = [1.0, 0.0, 1.0, 0.0]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + + let events = ctx + .app + .world() + .resource::>(); + let mut event_reader = events.get_cursor(); + for (event, result) in event_reader.read(events).zip(results) { + assert_eq!(event.value, result); + } + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 4 + ); + } + + #[test] + fn gamepad_axis_livezone_filter() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let settings = GamepadSettings::default().default_axis_settings; + + // Set of events to ensure they are being properly filtered + let events = [ + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + 1.0, + )), + // Event above livezone upperbound should be filtered + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.livezone_upperbound + 0.01, + )), + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + -1.0, + )), + // Event below livezone lowerbound should be filtered + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.livezone_lowerbound - 0.01, + )), + ]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 2 + ); + } + + #[test] + fn gamepad_axis_livezone_rounded() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let settings = GamepadSettings::default().default_axis_settings; + + // Set of events to ensure they are being properly filtered + let events = [ + // Event above livezone upperbound should be rounded to 1 + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.livezone_upperbound + 0.01, + )), + // Event below livezone lowerbound should be rounded to -1 + RawGamepadEvent::Axis(RawGamepadAxisChangedEvent::new( + entity, + GamepadAxis::LeftStickX, + settings.livezone_lowerbound - 0.01, + )), + ]; + let results = [1.0, -1.0]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + + let events = ctx + .app + .world() + .resource::>(); + let mut event_reader = events.get_cursor(); + for (event, result) in event_reader.read(events).zip(results) { + assert_eq!(event.value, result); + } + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 2 + ); + } + + #[test] + fn gamepad_buttons_pressed() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let digital_settings = GamepadSettings::default().default_button_settings; + + let events = [RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.press_threshold, + ))]; + ctx.app + .world_mut() + .resource_mut::>() + .send_batch(events); + ctx.update(); + + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 1 + ); + let events = ctx + .app + .world() + .resource::>(); + let mut event_reader = events.get_cursor(); + for event in event_reader.read(events) { + assert_eq!(event.button, GamepadButton::DPadDown); + assert_eq!(event.state, ButtonState::Pressed); + } + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); + + ctx.app + .world_mut() + .resource_mut::>() + .clear(); + ctx.update(); + + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 0 + ); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); + } + + #[test] + fn gamepad_buttons_just_pressed() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let digital_settings = GamepadSettings::default().default_button_settings; + + ctx.send_raw_gamepad_event(RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.press_threshold, + ))); + ctx.update(); + + // Check it is flagged for this frame + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_pressed(GamepadButton::DPadDown)); + ctx.update(); + + //Check it clears next frame + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_pressed(GamepadButton::DPadDown)); + } + #[test] + fn gamepad_buttons_released() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let digital_settings = GamepadSettings::default().default_button_settings; + + ctx.send_raw_gamepad_event(RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.press_threshold, + ))); + ctx.update(); + + ctx.app + .world_mut() + .resource_mut::>() + .clear(); + ctx.send_raw_gamepad_event(RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold - 0.01, + ))); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 1 + ); + let events = ctx + .app + .world() + .resource::>(); + let mut event_reader = events.get_cursor(); + for event in event_reader.read(events) { + assert_eq!(event.button, GamepadButton::DPadDown); + assert_eq!(event.state, ButtonState::Released); + } + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); + ctx.app + .world_mut() + .resource_mut::>() + .clear(); + ctx.update(); + + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 0 + ); + } + + #[test] + fn gamepad_buttons_just_released() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let digital_settings = GamepadSettings::default().default_button_settings; + + ctx.send_raw_gamepad_event_batch([ + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.press_threshold, + )), + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold - 0.01, + )), + ]); + ctx.update(); + + // Check it is flagged for this frame + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_released(GamepadButton::DPadDown)); + ctx.update(); + + //Check it clears next frame + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_released(GamepadButton::DPadDown)); + } + + #[test] + fn gamepad_buttons_axis() { + let mut ctx = TestContext::new(); + + // Create test gamepad + let entity = ctx.send_gamepad_connection_event(None); + let digital_settings = GamepadSettings::default().default_button_settings; + let analog_settings = GamepadSettings::default().default_button_axis_settings; + + // Test events + let events = [ + // Should trigger event + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.press_threshold, + )), + // Should trigger event + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold, + )), + // Shouldn't trigger a state changed event + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold - analog_settings.threshold * 1.01, + )), + // Shouldn't trigger any event + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold - (analog_settings.threshold * 1.5), + )), + // Shouldn't trigger a state changed event + RawGamepadEvent::Button(RawGamepadButtonChangedEvent::new( + entity, + GamepadButton::DPadDown, + digital_settings.release_threshold - (analog_settings.threshold * 2.02), + )), + ]; + ctx.send_raw_gamepad_event_batch(events); + ctx.update(); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 2 + ); + assert_eq!( + ctx.app + .world() + .resource::>() + .len(), + 4 + ); + } } diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index b4dac1ee7f..ddcfa5bf17 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -30,9 +30,7 @@ pub use button_input::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - gamepad::{ - Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, Gamepads, - }, + gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadSettings}, keyboard::KeyCode, mouse::MouseButton, touch::{TouchInput, Touches}, @@ -54,10 +52,10 @@ use mouse::{ use touch::{touch_screen_input_system, TouchInput, Touches}; use gamepad::{ - gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system, - gamepad_event_system, GamepadAxis, GamepadAxisChangedEvent, GamepadButton, - GamepadButtonChangedEvent, GamepadButtonInput, GamepadConnectionEvent, GamepadEvent, - GamepadRumbleRequest, GamepadSettings, Gamepads, + gamepad_connection_system, gamepad_event_processing_system, GamepadAxisChangedEvent, + GamepadButtonChangedEvent, GamepadButtonStateChangedEvent, GamepadConnection, + GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadRumbleRequest, GamepadSettings, + RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, RawGamepadEvent, }; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] @@ -98,30 +96,22 @@ impl Plugin for InputPlugin { .add_event::() .add_event::() // gamepad + .add_event::() .add_event::() .add_event::() - .add_event::() + .add_event::() .add_event::() - .add_event::() + .add_event::() + .add_event::() + .add_event::() .add_event::() - .init_resource::() - .init_resource::() - .init_resource::>() - .init_resource::>() - .init_resource::>() .init_resource::() .init_resource::() .add_systems( PreUpdate, ( - gamepad_event_system, - gamepad_connection_system.after(gamepad_event_system), - gamepad_button_event_system - .after(gamepad_event_system) - .after(gamepad_connection_system), - gamepad_axis_event_system - .after(gamepad_event_system) - .after(gamepad_connection_system), + gamepad_connection_system, + gamepad_event_processing_system.after(gamepad_connection_system), ) .in_set(InputSystem), ) @@ -141,8 +131,15 @@ impl Plugin for InputPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::(); diff --git a/examples/input/gamepad_input.rs b/examples/input/gamepad_input.rs index e07b43a5c2..4f3dfba4ae 100644 --- a/examples/input/gamepad_input.rs +++ b/examples/input/gamepad_input.rs @@ -9,35 +9,22 @@ fn main() { .run(); } -fn gamepad_system( - gamepads: Res, - button_inputs: Res>, - button_axes: Res>, - axes: Res>, -) { - for gamepad in gamepads.iter() { - if button_inputs.just_pressed(GamepadButton::new(gamepad, GamepadButtonType::South)) { - info!("{:?} just pressed South", gamepad); - } else if button_inputs.just_released(GamepadButton::new(gamepad, GamepadButtonType::South)) - { - info!("{:?} just released South", gamepad); +fn gamepad_system(gamepads: Query<(Entity, &Gamepad)>) { + for (entity, gamepad) in &gamepads { + if gamepad.just_pressed(GamepadButton::South) { + info!("{:?} just pressed South", entity); + } else if gamepad.just_released(GamepadButton::South) { + info!("{:?} just released South", entity); } - let right_trigger = button_axes - .get(GamepadButton::new( - gamepad, - GamepadButtonType::RightTrigger2, - )) - .unwrap(); + let right_trigger = gamepad.get(GamepadButton::RightTrigger2).unwrap(); if right_trigger.abs() > 0.01 { - info!("{:?} RightTrigger2 value is {}", gamepad, right_trigger); + info!("{:?} RightTrigger2 value is {}", entity, right_trigger); } - let left_stick_x = axes - .get(GamepadAxis::new(gamepad, GamepadAxisType::LeftStickX)) - .unwrap(); + let left_stick_x = gamepad.get(GamepadAxis::LeftStickX).unwrap(); if left_stick_x.abs() > 0.01 { - info!("{:?} LeftStickX value is {}", gamepad, left_stick_x); + info!("{:?} LeftStickX value is {}", entity, left_stick_x); } } } diff --git a/examples/input/gamepad_input_events.rs b/examples/input/gamepad_input_events.rs index db1e8fc04d..4986b98751 100644 --- a/examples/input/gamepad_input_events.rs +++ b/examples/input/gamepad_input_events.rs @@ -2,7 +2,7 @@ use bevy::{ input::gamepad::{ - GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadButtonInput, + GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadButtonStateChangedEvent, GamepadConnectionEvent, GamepadEvent, }, prelude::*, @@ -17,15 +17,14 @@ fn main() { fn gamepad_events( mut connection_events: EventReader, + // Handles the continuous measure of an axis, equivalent to GamepadAxes::get. mut axis_changed_events: EventReader, - // Handles the continuous measure of how far a button has been pressed down, as measured - // by `Axis`. Whenever that value changes, this event is emitted. + // Handles the continuous measure of how far a button has been pressed down, equivalent to `GamepadButtons::get`. mut button_changed_events: EventReader, // Handles the boolean measure of whether a button is considered pressed or unpressed, as - // defined by the thresholds in `GamepadSettings::button_settings` and measured by - // `Input`. When the threshold is crossed and the button state changes, - // this event is emitted. - mut button_input_events: EventReader, + // defined by the thresholds in `GamepadSettings::button_settings`. + // When the threshold is crossed and the button state changes, this event is emitted. + mut button_input_events: EventReader, ) { for connection_event in connection_events.read() { info!("{:?}", connection_event); @@ -33,15 +32,13 @@ fn gamepad_events( for axis_changed_event in axis_changed_events.read() { info!( "{:?} of {:?} is changed to {}", - axis_changed_event.axis_type, axis_changed_event.gamepad, axis_changed_event.value + axis_changed_event.axis, axis_changed_event.entity, axis_changed_event.value ); } for button_changed_event in button_changed_events.read() { info!( "{:?} of {:?} is changed to {}", - button_changed_event.button_type, - button_changed_event.gamepad, - button_changed_event.value + button_changed_event.button, button_changed_event.entity, button_changed_event.value ); } for button_input_event in button_input_events.read() { diff --git a/examples/input/gamepad_rumble.rs b/examples/input/gamepad_rumble.rs index a4d1ca614b..cdc5a9c85e 100644 --- a/examples/input/gamepad_rumble.rs +++ b/examples/input/gamepad_rumble.rs @@ -2,7 +2,7 @@ //! pressed. use bevy::{ - input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}, + input::gamepad::{Gamepad, GamepadRumbleIntensity, GamepadRumbleRequest}, prelude::*, utils::Duration, }; @@ -15,51 +15,43 @@ fn main() { } fn gamepad_system( - gamepads: Res, - button_inputs: Res>, + gamepads: Query<(Entity, &Gamepad)>, mut rumble_requests: EventWriter, ) { - for gamepad in gamepads.iter() { - let button_pressed = |button| { - button_inputs.just_pressed(GamepadButton { - gamepad, - button_type: button, - }) - }; - - if button_pressed(GamepadButtonType::North) { + for (entity, gamepad) in &gamepads { + if gamepad.just_pressed(GamepadButton::North) { info!( "North face button: strong (low-frequency) with low intensity for rumble for 5 seconds. Press multiple times to increase intensity." ); rumble_requests.send(GamepadRumbleRequest::Add { - gamepad, + gamepad: entity, intensity: GamepadRumbleIntensity::strong_motor(0.1), duration: Duration::from_secs(5), }); } - if button_pressed(GamepadButtonType::East) { + if gamepad.just_pressed(GamepadButton::East) { info!("East face button: maximum rumble on both motors for 5 seconds"); rumble_requests.send(GamepadRumbleRequest::Add { - gamepad, + gamepad: entity, duration: Duration::from_secs(5), intensity: GamepadRumbleIntensity::MAX, }); } - if button_pressed(GamepadButtonType::South) { + if gamepad.just_pressed(GamepadButton::South) { info!("South face button: low-intensity rumble on the weak motor for 0.5 seconds"); rumble_requests.send(GamepadRumbleRequest::Add { - gamepad, + gamepad: entity, duration: Duration::from_secs_f32(0.5), intensity: GamepadRumbleIntensity::weak_motor(0.25), }); } - if button_pressed(GamepadButtonType::West) { + if gamepad.just_pressed(GamepadButton::West) { info!("West face button: custom rumble intensity for 5 second"); rumble_requests.send(GamepadRumbleRequest::Add { - gamepad, + gamepad: entity, intensity: GamepadRumbleIntensity { // intensity low-frequency motor, usually on the left-hand side strong_motor: 0.5, @@ -70,9 +62,9 @@ fn gamepad_system( }); } - if button_pressed(GamepadButtonType::Start) { + if gamepad.just_pressed(GamepadButton::Start) { info!("Start button: Interrupt the current rumble"); - rumble_requests.send(GamepadRumbleRequest::Stop { gamepad }); + rumble_requests.send(GamepadRumbleRequest::Stop { gamepad: entity }); } } } diff --git a/examples/tools/gamepad_viewer.rs b/examples/tools/gamepad_viewer.rs index 7be76fe50a..2689195565 100644 --- a/examples/tools/gamepad_viewer.rs +++ b/examples/tools/gamepad_viewer.rs @@ -3,7 +3,7 @@ use std::f32::consts::PI; use bevy::{ - input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadSettings}, + input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnectionEvent}, prelude::*, sprite::{Anchor, MaterialMesh2dBundle, Mesh2dHandle}, }; @@ -25,20 +25,20 @@ const LIVE_COLOR: Color = Color::srgb(0.4, 0.4, 0.4); const DEAD_COLOR: Color = Color::srgb(0.13, 0.13, 0.13); #[derive(Component, Deref)] -struct ReactTo(GamepadButtonType); +struct ReactTo(GamepadButton); #[derive(Component)] struct MoveWithAxes { - x_axis: GamepadAxisType, - y_axis: GamepadAxisType, + x_axis: GamepadAxis, + y_axis: GamepadAxis, scale: f32, } #[derive(Component)] struct TextWithAxes { - x_axis: GamepadAxisType, - y_axis: GamepadAxisType, + x_axis: GamepadAxis, + y_axis: GamepadAxis, } #[derive(Component, Deref)] -struct TextWithButtonValue(GamepadButtonType); +struct TextWithButtonValue(GamepadButton); #[derive(Component)] struct ConnectedGamepadsText; @@ -84,7 +84,7 @@ struct GamepadButtonBundle { impl GamepadButtonBundle { pub fn new( - button_type: GamepadButtonType, + button_type: GamepadButton, mesh: Mesh2dHandle, material: Handle, x: f32, @@ -140,28 +140,28 @@ fn setup(mut commands: Commands, meshes: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, materials: Res, - gamepad_settings: Res, ) { + // NOTE: This stops making sense because in entities because there isn't a "global" default, + // instead each gamepad has its own default setting + let gamepad_settings = GamepadSettings::default(); let dead_upper = STICK_BOUNDS_SIZE * gamepad_settings.default_axis_settings.deadzone_upperbound(); let dead_lower = @@ -358,16 +360,16 @@ fn setup_sticks( spawn_stick( -STICKS_X, STICKS_Y, - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - GamepadButtonType::LeftThumb, + GamepadAxis::LeftStickX, + GamepadAxis::LeftStickY, + GamepadButton::LeftThumb, ); spawn_stick( STICKS_X, STICKS_Y, - GamepadAxisType::RightStickX, - GamepadAxisType::RightStickY, - GamepadButtonType::RightThumb, + GamepadAxis::RightStickX, + GamepadAxis::RightStickY, + GamepadButton::RightThumb, ); } @@ -403,16 +405,8 @@ fn setup_triggers( }); }; - spawn_trigger( - -BUTTONS_X, - BUTTONS_Y + 145., - GamepadButtonType::LeftTrigger2, - ); - spawn_trigger( - BUTTONS_X, - BUTTONS_Y + 145., - GamepadButtonType::RightTrigger2, - ); + spawn_trigger(-BUTTONS_X, BUTTONS_Y + 145., GamepadButton::LeftTrigger2); + spawn_trigger(BUTTONS_X, BUTTONS_Y + 145., GamepadButton::RightTrigger2); } fn setup_connected(mut commands: Commands) { @@ -443,30 +437,28 @@ fn setup_connected(mut commands: Commands) { } fn update_buttons( - gamepads: Res, - button_inputs: Res>, + gamepads: Query<&Gamepad>, materials: Res, mut query: Query<(&mut Handle, &ReactTo)>, ) { - for gamepad in gamepads.iter() { + for buttons in &gamepads { for (mut handle, react_to) in query.iter_mut() { - if button_inputs.just_pressed(GamepadButton::new(gamepad, **react_to)) { + if buttons.just_pressed(**react_to) { *handle = materials.active.clone(); } - if button_inputs.just_released(GamepadButton::new(gamepad, **react_to)) { + if buttons.just_released(**react_to) { *handle = materials.normal.clone(); } } } } - fn update_button_values( mut events: EventReader, mut query: Query<(&mut Text, &TextWithButtonValue)>, ) { for button_event in events.read() { for (mut text, text_with_button_value) in query.iter_mut() { - if button_event.button_type == **text_with_button_value { + if button_event.button == **text_with_button_value { text.sections[0].value = format!("{:.3}", button_event.value); } } @@ -479,7 +471,7 @@ fn update_axes( mut text_query: Query<(&mut Text, &TextWithAxes)>, ) { for axis_event in axis_events.read() { - let axis_type = axis_event.axis_type; + let axis_type = axis_event.axis; let value = axis_event.value; for (mut transform, move_with) in query.iter_mut() { if axis_type == move_with.x_axis { @@ -501,18 +493,19 @@ fn update_axes( } fn update_connected( - gamepads: Res, + mut connected: EventReader, + gamepads: Query<(Entity, &Gamepad)>, mut query: Query<&mut Text, With>, ) { - if !gamepads.is_changed() { + if connected.is_empty() { return; } - + connected.clear(); let mut text = query.single_mut(); let formatted = gamepads .iter() - .map(|g| format!("- {}", gamepads.name(g).unwrap())) + .map(|(entity, gamepad)| format!("{} - {}", entity, gamepad.name())) .collect::>() .join("\n");