diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86df8493c9..ab83dbfb36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,10 @@ jobs: run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev if: ${{ runner.os == 'Linux' }} + - name: Install udev + run: sudo apt-get update; sudo apt-get install --no-install-recommends libudev-dev + if: ${{ runner.os == 'Linux' }} + - name: Build run: cargo check env: @@ -65,6 +69,9 @@ jobs: - name: Install alsa run: sudo apt-get install --no-install-recommends libasound2-dev + - name: Install udev + run: sudo apt-get install --no-install-recommends libudev-dev + - name: Check the format run: cargo +nightly fmt --all -- --check diff --git a/Cargo.toml b/Cargo.toml index 8b205568e1..11aec63dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ exclude = ["assets/**/*", "tools/**/*", ".github/**/*", "crates/**/*"] [features] default = [ "bevy_audio", + "bevy_gilrs", "bevy_gltf", "bevy_wgpu", "bevy_winit", @@ -83,6 +84,7 @@ bevy_text = { path = "crates/bevy_text", optional = true, version = "0.1" } bevy_ui = { path = "crates/bevy_ui", optional = true, version = "0.1" } bevy_wgpu = { path = "crates/bevy_wgpu", optional = true, version = "0.1" } bevy_winit = { path = "crates/bevy_winit", optional = true, version = "0.1" } +bevy_gilrs = { path = "crates/bevy_gilrs", optional = true, version = "0.1" } [dev-dependencies] rand = "0.7.3" @@ -217,6 +219,10 @@ path = "examples/input/keyboard_input.rs" name = "keyboard_input_events" path = "examples/input/keyboard_input_events.rs" +[[example]] +name = "gamepad_input" +path = "examples/input/gamepad_input.rs" + [[example]] name = "scene" path = "examples/scene/scene.rs" diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml new file mode 100644 index 0000000000..3c6f74c235 --- /dev/null +++ b/crates/bevy_gilrs/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "bevy_gilrs" +version = "0.1.0" +edition = "2018" +authors = ["Bevy Contributors ", "Carter Anderson "] +description = "Gamepad system made using Gilrs for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.1" } +bevy_ecs = { path = "../bevy_ecs", version = "0.1" } +bevy_input = { path = "../bevy_input", version = "0.1" } + +# other +gilrs = "0.7.4" +log = { version = "0.4", features = ["release_max_level_info"] } \ No newline at end of file diff --git a/crates/bevy_gilrs/src/converter.rs b/crates/bevy_gilrs/src/converter.rs new file mode 100644 index 0000000000..a2e68b7db2 --- /dev/null +++ b/crates/bevy_gilrs/src/converter.rs @@ -0,0 +1,44 @@ +use bevy_input::gamepad::{Gamepad, GamepadAxisType, GamepadButtonType}; + +pub fn convert_gamepad_id(gamepad_id: gilrs::GamepadId) -> Gamepad { + Gamepad(gamepad_id.into()) +} + +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::Unknown => None, + } +} + +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::DPadX => Some(GamepadAxisType::DPadX), + gilrs::Axis::DPadY => Some(GamepadAxisType::DPadY), + gilrs::Axis::Unknown => None, + } +} diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs new file mode 100644 index 0000000000..0ee880d894 --- /dev/null +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -0,0 +1,176 @@ +use crate::converter::{convert_axis, convert_button, convert_gamepad_id}; +use bevy_app::Events; +use bevy_ecs::{Res, ResMut}; +use bevy_input::prelude::*; +use gilrs::{Button, EventType, Gilrs}; +use std::sync::{Arc, Mutex}; + +// TODO: remove this if/when bevy_ecs supports thread local resources +struct GilrsSendWrapper(Gilrs); + +unsafe impl Send for GilrsSendWrapper {} + +pub struct GilrsArcMutexWrapper(Arc>); + +impl GilrsArcMutexWrapper { + pub fn new(gilrs: Gilrs) -> GilrsArcMutexWrapper { + GilrsArcMutexWrapper(Arc::new(Mutex::new(GilrsSendWrapper(gilrs)))) + } +} + +pub fn gilrs_startup_system( + gilrs: Res, + mut gamepad_event: ResMut>, + mut inputs: ResMut>, + mut axes: ResMut>, +) { + gamepad_event.update(); + inputs.update(); + let gilrs = &gilrs.0.lock().unwrap().0; + for (gilrs_id, gilrs_gamepad) in gilrs.gamepads() { + connect_gamepad( + gilrs_gamepad, + convert_gamepad_id(gilrs_id), + &mut gamepad_event, + &mut inputs, + &mut axes, + ); + } +} + +pub fn gilrs_update_system( + gilrs: Res, + mut gamepad_event: ResMut>, + mut inputs: ResMut>, + mut axes: ResMut>, +) { + gamepad_event.update(); + inputs.update(); + let gilrs = &mut gilrs.0.lock().unwrap().0; + while let Some(gilrs_event) = gilrs.next_event() { + match gilrs_event.event { + EventType::Connected => { + connect_gamepad( + gilrs.gamepad(gilrs_event.id), + convert_gamepad_id(gilrs_event.id), + &mut gamepad_event, + &mut inputs, + &mut axes, + ); + } + EventType::Disconnected => { + disconnect_gamepad( + convert_gamepad_id(gilrs_event.id), + &mut gamepad_event, + &mut inputs, + &mut axes, + ); + } + EventType::ButtonPressed(gilrs_button, _) => { + if let Some(button_type) = convert_button(gilrs_button) { + inputs.press(GamepadButton( + convert_gamepad_id(gilrs_event.id), + button_type, + )); + } + } + EventType::ButtonReleased(gilrs_button, _) => { + if let Some(button_type) = convert_button(gilrs_button) { + inputs.release(GamepadButton( + convert_gamepad_id(gilrs_event.id), + button_type, + )); + } + } + EventType::AxisChanged(gilrs_axis, value, _) => { + if let Some(axis_type) = convert_axis(gilrs_axis) { + axes.set( + GamepadAxis(convert_gamepad_id(gilrs_event.id), axis_type), + value, + ); + } + } + _ => (), + }; + } + gilrs.inc(); +} + +const ALL_GILRS_BUTTONS: [Button; 19] = [ + Button::South, + Button::East, + Button::North, + Button::West, + Button::C, + Button::Z, + Button::LeftTrigger, + Button::LeftTrigger2, + Button::RightTrigger, + Button::RightTrigger2, + Button::Select, + Button::Start, + Button::Mode, + Button::LeftThumb, + Button::RightThumb, + Button::DPadUp, + Button::DPadDown, + Button::DPadLeft, + Button::DPadRight, +]; + +const ALL_GILRS_AXES: [gilrs::Axis; 8] = [ + gilrs::Axis::LeftStickX, + gilrs::Axis::LeftStickY, + gilrs::Axis::LeftZ, + gilrs::Axis::RightStickX, + gilrs::Axis::RightStickY, + gilrs::Axis::RightZ, + gilrs::Axis::DPadX, + gilrs::Axis::DPadY, +]; + +fn connect_gamepad( + gilrs_gamepad: gilrs::Gamepad, + gamepad: Gamepad, + events: &mut Events, + inputs: &mut Input, + axes: &mut Axis, +) { + for gilrs_button in ALL_GILRS_BUTTONS.iter() { + if let Some(button_type) = convert_button(*gilrs_button) { + let gamepad_button = GamepadButton(gamepad, button_type); + inputs.reset(gamepad_button); + if gilrs_gamepad.is_pressed(*gilrs_button) { + inputs.press(gamepad_button); + } + } + } + for gilrs_axis in ALL_GILRS_AXES.iter() { + if let Some(axis_type) = convert_axis(*gilrs_axis) { + let gamepad_axis = GamepadAxis(gamepad, axis_type); + axes.set(gamepad_axis, gilrs_gamepad.value(*gilrs_axis)); + } + } + events.send(GamepadEvent(gamepad, GamepadEventType::Connected)); +} + +fn disconnect_gamepad( + gamepad: Gamepad, + events: &mut Events, + inputs: &mut Input, + axes: &mut Axis, +) { + for gilrs_button in ALL_GILRS_BUTTONS.iter() { + if let Some(button_type) = convert_button(*gilrs_button) { + let gamepad_button = GamepadButton(gamepad, button_type); + inputs.reset(gamepad_button); + } + } + for gilrs_axis in ALL_GILRS_AXES.iter() { + if let Some(axis_type) = convert_axis(*gilrs_axis) { + let gamepad_axis = GamepadAxis(gamepad, axis_type); + axes.remove(&gamepad_axis); + } + } + events.send(GamepadEvent(gamepad, GamepadEventType::Disconnected)); +} diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs new file mode 100644 index 0000000000..af9657f2d9 --- /dev/null +++ b/crates/bevy_gilrs/src/lib.rs @@ -0,0 +1,22 @@ +mod converter; +mod gilrs_system; + +use bevy_app::prelude::*; +use bevy_ecs::IntoQuerySystem; +use gilrs_system::{gilrs_startup_system, gilrs_update_system, GilrsArcMutexWrapper}; + +#[derive(Default)] +pub struct GilrsPlugin; + +impl Plugin for GilrsPlugin { + fn build(&self, app: &mut AppBuilder) { + match gilrs::Gilrs::new() { + Ok(gilrs) => { + app.add_resource(GilrsArcMutexWrapper::new(gilrs)) + .add_startup_system(gilrs_startup_system.system()) + .add_system_to_stage(stage::EVENT_UPDATE, gilrs_update_system.system()); + } + Err(err) => log::error!("Failed to start Gilrs. {}", err), + } + } +} diff --git a/crates/bevy_input/src/axis.rs b/crates/bevy_input/src/axis.rs new file mode 100644 index 0000000000..6d8937cf7b --- /dev/null +++ b/crates/bevy_input/src/axis.rs @@ -0,0 +1,33 @@ +use std::{collections::HashMap, hash::Hash}; + +pub struct Axis { + axis_data: HashMap, +} + +impl Default for Axis +where + T: Copy + Eq + Hash, +{ + fn default() -> Self { + Axis { + axis_data: HashMap::new(), + } + } +} + +impl Axis +where + T: Copy + Eq + Hash, +{ + pub fn set(&mut self, axis: T, value: f32) -> Option { + self.axis_data.insert(axis, value) + } + + pub fn get(&self, axis: &T) -> Option { + self.axis_data.get(axis).copied() + } + + pub fn remove(&mut self, axis: &T) -> Option { + self.axis_data.remove(axis) + } +} diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs new file mode 100644 index 0000000000..1b37d64ccf --- /dev/null +++ b/crates/bevy_input/src/gamepad.rs @@ -0,0 +1,52 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Gamepad(pub usize); + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum GamepadEventType { + Connected, + Disconnected, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct GamepadEvent(pub Gamepad, pub GamepadEventType); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum GamepadButtonType { + South, + East, + North, + West, + C, + Z, + LeftTrigger, + LeftTrigger2, + RightTrigger, + RightTrigger2, + Select, + Start, + Mode, + LeftThumb, + RightThumb, + DPadUp, + DPadDown, + DPadLeft, + DPadRight, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct GamepadButton(pub Gamepad, pub GamepadButtonType); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum GamepadAxisType { + LeftStickX, + LeftStickY, + LeftZ, + RightStickX, + RightStickY, + RightZ, + DPadX, + DPadY, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct GamepadAxis(pub Gamepad, pub GamepadAxisType); diff --git a/crates/bevy_input/src/input.rs b/crates/bevy_input/src/input.rs index d73c6929e7..58583974f7 100644 --- a/crates/bevy_input/src/input.rs +++ b/crates/bevy_input/src/input.rs @@ -47,6 +47,12 @@ where self.just_released.contains(&input) } + pub fn reset(&mut self, input: T) { + self.pressed.remove(&input); + self.just_pressed.remove(&input); + self.just_released.remove(&input); + } + pub fn update(&mut self) { self.just_pressed.clear(); self.just_released.clear(); diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 1893d86b8a..278405c9f5 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -1,12 +1,23 @@ +mod axis; +pub mod gamepad; mod input; pub mod keyboard; pub mod mouse; pub mod system; +pub use axis::*; pub use input::*; pub mod prelude { - pub use crate::{keyboard::KeyCode, mouse::MouseButton, Input}; + pub use crate::{ + gamepad::{ + Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, GamepadEvent, + GamepadEventType, + }, + keyboard::KeyCode, + mouse::MouseButton, + Axis, Input, + }; } use bevy_app::prelude::*; @@ -14,6 +25,7 @@ use keyboard::{keyboard_input_system, KeyCode, KeyboardInput}; use mouse::{mouse_button_input_system, MouseButton, MouseButtonInput, MouseMotion, MouseWheel}; use bevy_ecs::IntoQuerySystem; +use gamepad::{GamepadAxis, GamepadButton, GamepadEvent}; /// Adds keyboard and mouse input to an App #[derive(Default)] @@ -34,6 +46,9 @@ impl Plugin for InputPlugin { .add_system_to_stage( bevy_app::stage::EVENT_UPDATE, mouse_button_input_system.system(), - ); + ) + .add_event::() + .init_resource::>() + .init_resource::>(); } } diff --git a/examples/input/gamepad_input.rs b/examples/input/gamepad_input.rs new file mode 100644 index 0000000000..d1c07699d1 --- /dev/null +++ b/examples/input/gamepad_input.rs @@ -0,0 +1,93 @@ +use bevy::prelude::*; +use bevy_input::gamepad::{Gamepad, GamepadButton, GamepadEvent, GamepadEventType}; +use std::collections::HashSet; + +fn main() { + App::build() + .add_default_plugins() + .add_startup_system(connection_system.system()) + .add_system(connection_system.system()) + .add_system(button_system.system()) + .add_system(axis_system.system()) + .add_resource(Lobby::default()) + .run(); +} + +#[derive(Default)] +struct Lobby { + gamepad: HashSet, + gamepad_event_reader: EventReader, +} + +fn connection_system(mut lobby: ResMut, gamepad_event: Res>) { + for event in lobby.gamepad_event_reader.iter(&gamepad_event) { + match &event { + GamepadEvent(gamepad, GamepadEventType::Connected) => { + lobby.gamepad.insert(*gamepad); + println!("Connected {:?}", gamepad); + } + GamepadEvent(gamepad, GamepadEventType::Disconnected) => { + lobby.gamepad.remove(gamepad); + println!("Disconnected {:?}", gamepad); + } + } + } +} + +fn button_system(manager: Res, inputs: Res>) { + let button_types = [ + 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, + ]; + for gamepad in manager.gamepad.iter() { + for button_type in button_types.iter() { + if inputs.just_pressed(GamepadButton(*gamepad, *button_type)) { + println!("Pressed {:?}", GamepadButton(*gamepad, *button_type)); + } else if inputs.just_released(GamepadButton(*gamepad, *button_type)) { + println!("Released {:?}", GamepadButton(*gamepad, *button_type)); + } + } + } +} + +fn axis_system(manager: Res, axes: Res>) { + let axis_types = [ + GamepadAxisType::LeftStickX, + GamepadAxisType::LeftStickY, + GamepadAxisType::LeftZ, + GamepadAxisType::RightStickX, + GamepadAxisType::RightStickY, + GamepadAxisType::RightZ, + GamepadAxisType::DPadX, + GamepadAxisType::DPadY, + ]; + for gamepad in manager.gamepad.iter() { + for axis_type in axis_types.iter() { + if let Some(value) = axes.get(&GamepadAxis(*gamepad, *axis_type)) { + if value.abs() > 0.01f32 + && (value - 1.0f32).abs() > 0.01f32 + && (value + 1.0f32).abs() > 0.01f32 + { + println!("Axis {:?} is {}", GamepadAxis(*gamepad, *axis_type), value); + } + } + } + } +} diff --git a/src/add_default_plugins.rs b/src/add_default_plugins.rs index 452494474d..ab3ecb789c 100644 --- a/src/add_default_plugins.rs +++ b/src/add_default_plugins.rs @@ -33,6 +33,9 @@ impl AddDefaultPlugins for AppBuilder { #[cfg(feature = "bevy_audio")] self.add_plugin(bevy_audio::AudioPlugin::default()); + #[cfg(feature = "bevy_gilrs")] + self.add_plugin(bevy_gilrs::GilrsPlugin::default()); + #[cfg(feature = "bevy_gltf")] self.add_plugin(bevy_gltf::GltfPlugin::default());