diff --git a/Cargo.toml b/Cargo.toml index 22e4005cb2c8b..0b8f9fcd8985a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -389,6 +389,10 @@ path = "examples/input/gamepad_input.rs" name = "gamepad_input_events" path = "examples/input/gamepad_input_events.rs" +[[example]] +name = "gamepad_rumble" +path = "examples/input/gamepad_rumble.rs" + [[example]] name = "keyboard_input" path = "examples/input/keyboard_input.rs" diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 20e2f83451c87..a89afb62d3b4a 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -11,8 +11,10 @@ keywords = ["bevy"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.6.0" } +bevy_core = { path = "../bevy_core", version = "0.6.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.6.0" } bevy_input = { path = "../bevy_input", version = "0.6.0" } +bevy_log = { path = "../bevy_log", version = "0.6.0" } bevy_utils = { path = "../bevy_utils", version = "0.6.0" } # other diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index 37fa70a83316a..a139ef5ef1ab4 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -1,12 +1,15 @@ mod converter; mod gilrs_system; +mod rumble; use bevy_app::{App, CoreStage, Plugin, StartupStage}; use bevy_ecs::schedule::ParallelSystemDescriptorCoercion; use bevy_input::InputSystem; use bevy_utils::tracing::error; +pub use gilrs::ff; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; +pub use rumble::{RumbleIntensity, RumbleRequest}; #[derive(Default)] pub struct GilrsPlugin; @@ -20,10 +23,13 @@ impl Plugin for GilrsPlugin { { Ok(gilrs) => { app.insert_non_send_resource(gilrs) + .add_event::() + .init_non_send_resource::() .add_startup_system_to_stage( StartupStage::PreStartup, gilrs_event_startup_system, ) + .add_system_to_stage(CoreStage::PostUpdate, rumble::gilrs_rumble_system) .add_system_to_stage( CoreStage::PreUpdate, gilrs_event_system.before(InputSystem), diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs new file mode 100644 index 0000000000000..9c6c3f864747b --- /dev/null +++ b/crates/bevy_gilrs/src/rumble.rs @@ -0,0 +1,160 @@ +//! Handle user specified Rumble request events. +use crate::converter::convert_gamepad_id; +use bevy_app::EventReader; +use bevy_core::Time; +use bevy_ecs::{prelude::Res, system::NonSendMut}; +use bevy_input::gamepad::Gamepad; +use bevy_log as log; +use bevy_utils::HashMap; +use gilrs::{ff, GamepadId, Gilrs}; + +pub enum RumbleIntensity { + Strong, + Medium, + Weak, +} +impl RumbleIntensity { + fn effect_type(&self) -> ff::BaseEffectType { + use RumbleIntensity::*; + match self { + Strong => ff::BaseEffectType::Strong { magnitude: 63_000 }, + Medium => ff::BaseEffectType::Strong { magnitude: 40_000 }, + Weak => ff::BaseEffectType::Weak { magnitude: 40_000 }, + } + } +} + +/// Request `pad` rumble in `gilrs_effect` pattern for `duration_seconds` +/// +/// # Notes +/// +/// * Does nothing if `pad` does not support rumble +/// * If a new `RumbleRequest` is sent while another one is still executing, it +/// replaces the old one. +/// +/// # Example +/// +/// ``` +/// # use bevy_gilrs::{RumbleRequest, RumbleIntensity}; +/// # use bevy_input::gamepad::Gamepad; +/// # use bevy_app::EventWriter; +/// fn rumble_pad_system(mut rumble_requests: EventWriter) { +/// let request = RumbleRequest::with_intensity( +/// RumbleIntensity::Strong, +/// 10.0, +/// Gamepad(0), +/// ); +/// rumble_requests.send(request); +/// } +/// ``` +#[derive(Clone)] +pub struct RumbleRequest { + /// The duration in seconds of the rumble + pub duration_seconds: f32, + /// The gilrs descriptor, use [`RumbleRequest::with_intensity`] if you want + /// a simpler API. + pub gilrs_effect: ff::EffectBuilder, + /// The gamepad to rumble + pub pad: Gamepad, +} +impl RumbleRequest { + /// Causes `pad` to rumble for `duration_seconds` at given `intensity`. + pub fn with_intensity(intensity: RumbleIntensity, duration_seconds: f32, pad: Gamepad) -> Self { + let kind = intensity.effect_type(); + let effect = ff::BaseEffect { + kind, + ..Default::default() + }; + let mut gilrs_effect = ff::EffectBuilder::new(); + gilrs_effect.add_effect(effect); + RumbleRequest { + duration_seconds, + gilrs_effect, + pad, + } + } + /// Stops provided `pad` rumbling. + pub fn stop(pad: Gamepad) -> Self { + RumbleRequest { + duration_seconds: 0.0, + gilrs_effect: ff::EffectBuilder::new(), + pad, + } + } +} + +struct RunningRumble { + deadline: f32, + // We use `effect.drop()` to interact with this, but rustc can't know + // gilrs uses Drop as an API feature. + #[allow(dead_code)] + effect: ff::Effect, +} + +enum RumbleError { + GamepadNotFound, + GilrsError(ff::Error), +} +impl From for RumbleError { + fn from(err: ff::Error) -> Self { + RumbleError::GilrsError(err) + } +} + +#[derive(Default)] +pub(crate) struct RumblesManager { + rumbles: HashMap, +} + +fn add_rumble( + manager: &mut RumblesManager, + gilrs: &mut Gilrs, + mut rumble: RumbleRequest, + current_time: f32, +) -> Result<(), RumbleError> { + let (pad_id, _) = gilrs + .gamepads() + .find(|(pad_id, _)| convert_gamepad_id(*pad_id) == rumble.pad) + .ok_or(RumbleError::GamepadNotFound)?; + let deadline = current_time + rumble.duration_seconds; + let effect = rumble.gilrs_effect.gamepads(&[pad_id]).finish(gilrs)?; + effect.play()?; + manager + .rumbles + .insert(pad_id, RunningRumble { deadline, effect }); + Ok(()) +} +pub(crate) fn gilrs_rumble_system( + time: Res