mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
Add gamepad rumble support to bevy_input (#8398)
# Objective Provide the ability to trigger controller rumbling (force-feedback) with a cross-platform API. ## Solution This adds the `GamepadRumbleRequest` event to `bevy_input` and adds a system in `bevy_gilrs` to read them and rumble controllers accordingly. It's a relatively primitive API with a `duration` in seconds and `GamepadRumbleIntensity` with values for the weak and strong gamepad motors. It's is an almost 1-to-1 mapping to platform APIs. Some platforms refer to these motors as left and right, and low frequency and high frequency, but by convention, they're usually the same. I used #3868 as a starting point, updated to main, removed the low-level gilrs effect API, and moved the requests to `bevy_input` and exposed the strong and weak intensities. I intend this to hopefully be a non-controversial cross-platform starting point we can build upon to eventually support more fine-grained control (closer to the gilrs effect API) --- ## Changelog ### Added - Gamepads can now be rumbled by sending the `GamepadRumbleRequest` event. --------- Co-authored-by: Nicola Papale <nico@nicopap.ch> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com> Co-authored-by: Bruce Reif (Buswolley) <bruce.reif@dynata.com>
This commit is contained in:
parent
288009ab8b
commit
a1e442cd2a
8 changed files with 404 additions and 4 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1274,6 +1274,16 @@ description = "Iterates and prints gamepad input and connection events"
|
|||
category = "Input"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "gamepad_rumble"
|
||||
path = "examples/input/gamepad_rumble.rs"
|
||||
|
||||
[package.metadata.example.gamepad_rumble]
|
||||
name = "Gamepad Rumble"
|
||||
description = "Shows how to rumble a gamepad using force feedback"
|
||||
category = "Input"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "keyboard_input"
|
||||
path = "examples/input/keyboard_input.rs"
|
||||
|
|
|
@ -13,7 +13,10 @@ keywords = ["bevy"]
|
|||
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.11.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.11.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
|
||||
|
||||
# other
|
||||
gilrs = "0.10.1"
|
||||
thiserror = "1.0"
|
|
@ -2,17 +2,23 @@
|
|||
|
||||
mod converter;
|
||||
mod gilrs_system;
|
||||
mod rumble;
|
||||
|
||||
use bevy_app::{App, Plugin, PreStartup, PreUpdate};
|
||||
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_input::InputSystem;
|
||||
use bevy_utils::tracing::error;
|
||||
use gilrs::GilrsBuilder;
|
||||
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
|
||||
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GilrsPlugin;
|
||||
|
||||
/// Updates the running gamepad rumble effects.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
|
||||
pub struct RumbleSystem;
|
||||
|
||||
impl Plugin for GilrsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
match GilrsBuilder::new()
|
||||
|
@ -22,8 +28,10 @@ impl Plugin for GilrsPlugin {
|
|||
{
|
||||
Ok(gilrs) => {
|
||||
app.insert_non_send_resource(gilrs)
|
||||
.init_non_send_resource::<RunningRumbleEffects>()
|
||||
.add_systems(PreStartup, gilrs_event_startup_system)
|
||||
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem));
|
||||
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
|
||||
.add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem));
|
||||
}
|
||||
Err(err) => error!("Failed to start Gilrs. {}", err),
|
||||
}
|
||||
|
|
177
crates/bevy_gilrs/src/rumble.rs
Normal file
177
crates/bevy_gilrs/src/rumble.rs
Normal file
|
@ -0,0 +1,177 @@
|
|||
//! Handle user specified rumble request events.
|
||||
use bevy_ecs::{
|
||||
prelude::{EventReader, Res},
|
||||
system::NonSendMut,
|
||||
};
|
||||
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
|
||||
use bevy_log::{debug, warn};
|
||||
use bevy_time::Time;
|
||||
use bevy_utils::{Duration, HashMap};
|
||||
use gilrs::{
|
||||
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
|
||||
GamepadId, 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
|
||||
deadline: Duration,
|
||||
/// A ref-counted handle to the specific force-feedback effect
|
||||
///
|
||||
/// Dropping it will cause the effect to stop
|
||||
#[allow(dead_code)]
|
||||
effect: ff::Effect,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum RumbleError {
|
||||
#[error("gamepad not found")]
|
||||
GamepadNotFound,
|
||||
#[error("gilrs error while rumbling gamepad: {0}")]
|
||||
GilrsError(#[from] ff::Error),
|
||||
}
|
||||
|
||||
/// Contains the gilrs rumble effects that are currently running for each gamepad
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RunningRumbleEffects {
|
||||
/// If multiple rumbles are running at the same time, their resulting rumble
|
||||
/// will be the saturated sum of their strengths up until [`u16::MAX`]
|
||||
rumbles: HashMap<GamepadId, Vec<RunningRumble>>,
|
||||
}
|
||||
|
||||
/// gilrs uses magnitudes from 0 to [`u16::MAX`], while ours go from `0.0` to `1.0` ([`f32`])
|
||||
fn to_gilrs_magnitude(ratio: f32) -> u16 {
|
||||
(ratio * u16::MAX as f32) as u16
|
||||
}
|
||||
|
||||
fn get_base_effects(
|
||||
GamepadRumbleIntensity {
|
||||
weak_motor,
|
||||
strong_motor,
|
||||
}: GamepadRumbleIntensity,
|
||||
duration: Duration,
|
||||
) -> Vec<ff::BaseEffect> {
|
||||
let mut effects = Vec::new();
|
||||
if strong_motor > 0. {
|
||||
effects.push(BaseEffect {
|
||||
kind: BaseEffectType::Strong {
|
||||
magnitude: to_gilrs_magnitude(strong_motor),
|
||||
},
|
||||
scheduling: Replay {
|
||||
play_for: duration.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if weak_motor > 0. {
|
||||
effects.push(BaseEffect {
|
||||
kind: BaseEffectType::Strong {
|
||||
magnitude: to_gilrs_magnitude(weak_motor),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
effects
|
||||
}
|
||||
|
||||
fn handle_rumble_request(
|
||||
running_rumbles: &mut RunningRumbleEffects,
|
||||
gilrs: &mut Gilrs,
|
||||
rumble: GamepadRumbleRequest,
|
||||
current_time: Duration,
|
||||
) -> Result<(), RumbleError> {
|
||||
let gamepad = rumble.gamepad();
|
||||
|
||||
let (gamepad_id, _) = gilrs
|
||||
.gamepads()
|
||||
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
|
||||
.ok_or(RumbleError::GamepadNotFound)?;
|
||||
|
||||
match rumble {
|
||||
GamepadRumbleRequest::Stop { .. } => {
|
||||
// `ff::Effect` uses RAII, dropping = deactivating
|
||||
running_rumbles.rumbles.remove(&gamepad_id);
|
||||
}
|
||||
GamepadRumbleRequest::Add {
|
||||
duration,
|
||||
intensity,
|
||||
..
|
||||
} => {
|
||||
let mut effect_builder = ff::EffectBuilder::new();
|
||||
|
||||
for effect in get_base_effects(intensity, duration) {
|
||||
effect_builder.add_effect(effect);
|
||||
effect_builder.repeat(Repeat::For(duration.into()));
|
||||
}
|
||||
|
||||
let effect = effect_builder.gamepads(&[gamepad_id]).finish(gilrs)?;
|
||||
effect.play()?;
|
||||
|
||||
let gamepad_rumbles = running_rumbles.rumbles.entry(gamepad_id).or_default();
|
||||
let deadline = current_time + duration;
|
||||
gamepad_rumbles.push(RunningRumble { deadline, effect });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn play_gilrs_rumble(
|
||||
time: Res<Time>,
|
||||
mut gilrs: NonSendMut<Gilrs>,
|
||||
mut requests: EventReader<GamepadRumbleRequest>,
|
||||
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
|
||||
) {
|
||||
let current_time = time.raw_elapsed();
|
||||
// Remove outdated rumble effects.
|
||||
for rumbles in running_rumbles.rumbles.values_mut() {
|
||||
// `ff::Effect` uses RAII, dropping = deactivating
|
||||
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
|
||||
}
|
||||
running_rumbles
|
||||
.rumbles
|
||||
.retain(|_gamepad, rumbles| !rumbles.is_empty());
|
||||
|
||||
// Add new effects.
|
||||
for rumble in requests.iter().cloned() {
|
||||
let gamepad = rumble.gamepad();
|
||||
match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) {
|
||||
Ok(()) => {}
|
||||
Err(RumbleError::GilrsError(err)) => {
|
||||
if let ff::Error::FfNotSupported(_) = err {
|
||||
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
|
||||
} else {
|
||||
warn!(
|
||||
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(RumbleError::GamepadNotFound) => {
|
||||
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::to_gilrs_magnitude;
|
||||
|
||||
#[test]
|
||||
fn magnitude_conversion() {
|
||||
assert_eq!(to_gilrs_magnitude(1.0), u16::MAX);
|
||||
assert_eq!(to_gilrs_magnitude(0.0), 0);
|
||||
|
||||
// bevy magnitudes of 2.0 don't really make sense, but just make sure
|
||||
// they convert to something sensible in gilrs anyway.
|
||||
assert_eq!(to_gilrs_magnitude(2.0), u16::MAX);
|
||||
|
||||
// negative bevy magnitudes don't really make sense, but just make sure
|
||||
// they convert to something sensible in gilrs anyway.
|
||||
assert_eq!(to_gilrs_magnitude(-1.0), 0);
|
||||
assert_eq!(to_gilrs_magnitude(-0.1), 0);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ use bevy_ecs::{
|
|||
system::{Res, ResMut, Resource},
|
||||
};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
|
||||
use bevy_utils::Duration;
|
||||
use bevy_utils::{tracing::info, HashMap};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -1240,6 +1241,127 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
|
|||
GamepadAxisType::RightZ,
|
||||
];
|
||||
|
||||
/// The intensity at which a gamepad's force-feedback motors may rumble.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct GamepadRumbleIntensity {
|
||||
/// The rumble intensity of the strong gamepad motor
|
||||
///
|
||||
/// Ranges from 0.0 to 1.0
|
||||
///
|
||||
/// By convention, this is usually a low-frequency motor on the left-hand
|
||||
/// side of the gamepad, though it may vary across platforms and hardware.
|
||||
pub strong_motor: f32,
|
||||
/// The rumble intensity of the weak gamepad motor
|
||||
///
|
||||
/// Ranges from 0.0 to 1.0
|
||||
///
|
||||
/// By convention, this is usually a high-frequency motor on the right-hand
|
||||
/// side of the gamepad, though it may vary across platforms and hardware.
|
||||
pub weak_motor: f32,
|
||||
}
|
||||
|
||||
impl GamepadRumbleIntensity {
|
||||
/// Rumble both gamepad motors at maximum intensity
|
||||
pub const MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 1.0,
|
||||
weak_motor: 1.0,
|
||||
};
|
||||
|
||||
/// Rumble the weak motor at maximum intensity
|
||||
pub const WEAK_MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 0.0,
|
||||
weak_motor: 1.0,
|
||||
};
|
||||
|
||||
/// Rumble the strong motor at maximum intensity
|
||||
pub const STRONG_MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 1.0,
|
||||
weak_motor: 0.0,
|
||||
};
|
||||
|
||||
/// Creates a new rumble intensity with weak motor intensity set to the given value
|
||||
///
|
||||
/// Clamped within the 0 to 1 range
|
||||
pub const fn weak_motor(intensity: f32) -> Self {
|
||||
Self {
|
||||
weak_motor: intensity,
|
||||
strong_motor: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new rumble intensity with strong motor intensity set to the given value
|
||||
///
|
||||
/// Clamped within the 0 to 1 range
|
||||
pub const fn strong_motor(intensity: f32) -> Self {
|
||||
Self {
|
||||
strong_motor: intensity,
|
||||
weak_motor: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event that controls force-feedback rumbling of a [`Gamepad`]
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// Does nothing if the gamepad or platform does not support rumble.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_input::gamepad::{Gamepad, Gamepads, GamepadRumbleRequest, GamepadRumbleIntensity};
|
||||
/// # use bevy_ecs::prelude::{EventWriter, Res};
|
||||
/// # use bevy_utils::Duration;
|
||||
/// fn rumble_gamepad_system(
|
||||
/// mut rumble_requests: EventWriter<GamepadRumbleRequest>,
|
||||
/// gamepads: Res<Gamepads>
|
||||
/// ) {
|
||||
/// for gamepad in gamepads.iter() {
|
||||
/// rumble_requests.send(GamepadRumbleRequest::Add {
|
||||
/// gamepad,
|
||||
/// intensity: GamepadRumbleIntensity::MAX,
|
||||
/// duration: Duration::from_secs_f32(0.5),
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[doc(alias = "haptic feedback")]
|
||||
#[doc(alias = "force feedback")]
|
||||
#[doc(alias = "vibration")]
|
||||
#[doc(alias = "vibrate")]
|
||||
#[derive(Clone)]
|
||||
pub enum GamepadRumbleRequest {
|
||||
/// Add a rumble to the given gamepad.
|
||||
///
|
||||
/// Simultaneous rumble effects add up to the sum of their strengths.
|
||||
///
|
||||
/// Consequently, if two rumbles at half intensity are added at the same
|
||||
/// time, their intensities will be added up, and the controller will rumble
|
||||
/// at full intensity until one of the rumbles finishes, then the rumble
|
||||
/// will continue at the intensity of the remaining event.
|
||||
///
|
||||
/// To replace an existing rumble, send a [`GamepadRumbleRequest::Stop`] event first.
|
||||
Add {
|
||||
/// How long the gamepad should rumble
|
||||
duration: Duration,
|
||||
/// How intense the rumble should be
|
||||
intensity: GamepadRumbleIntensity,
|
||||
/// The gamepad to rumble
|
||||
gamepad: Gamepad,
|
||||
},
|
||||
/// Stop all running rumbles on the given [`Gamepad`]
|
||||
Stop { gamepad: Gamepad },
|
||||
}
|
||||
|
||||
impl GamepadRumbleRequest {
|
||||
/// Get the [`Gamepad`] associated with this request
|
||||
pub fn gamepad(&self) -> Gamepad {
|
||||
match self {
|
||||
Self::Add { gamepad, .. } | Self::Stop { gamepad } => *gamepad,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};
|
||||
|
|
|
@ -39,8 +39,8 @@ use gamepad::{
|
|||
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
|
||||
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
|
||||
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
|
||||
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
|
||||
Gamepads,
|
||||
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
|
||||
GamepadRumbleRequest, GamepadSettings, Gamepads,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
|
@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
|
|||
.add_event::<GamepadButtonChangedEvent>()
|
||||
.add_event::<GamepadAxisChangedEvent>()
|
||||
.add_event::<GamepadEvent>()
|
||||
.add_event::<GamepadRumbleRequest>()
|
||||
.init_resource::<GamepadSettings>()
|
||||
.init_resource::<Gamepads>()
|
||||
.init_resource::<Input<GamepadButton>>()
|
||||
|
|
|
@ -239,6 +239,7 @@ Example | Description
|
|||
[Char Input Events](../examples/input/char_input_events.rs) | Prints out all chars as they are inputted
|
||||
[Gamepad Input](../examples/input/gamepad_input.rs) | Shows handling of gamepad input, connections, and disconnections
|
||||
[Gamepad Input Events](../examples/input/gamepad_input_events.rs) | Iterates and prints gamepad input and connection events
|
||||
[Gamepad Rumble](../examples/input/gamepad_rumble.rs) | Shows how to rumble a gamepad using force feedback
|
||||
[Keyboard Input](../examples/input/keyboard_input.rs) | Demonstrates handling a key press/release
|
||||
[Keyboard Input Events](../examples/input/keyboard_input_events.rs) | Prints out all keyboard events
|
||||
[Keyboard Modifiers](../examples/input/keyboard_modifiers.rs) | Demonstrates using key modifiers (ctrl, shift)
|
||||
|
|
78
examples/input/gamepad_rumble.rs
Normal file
78
examples/input/gamepad_rumble.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
//! Shows how to trigger force-feedback, making gamepads rumble when buttons are
|
||||
//! pressed.
|
||||
|
||||
use bevy::{
|
||||
input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest},
|
||||
prelude::*,
|
||||
utils::Duration,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Update, gamepad_system)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn gamepad_system(
|
||||
gamepads: Res<Gamepads>,
|
||||
button_inputs: Res<Input<GamepadButton>>,
|
||||
mut rumble_requests: EventWriter<GamepadRumbleRequest>,
|
||||
) {
|
||||
for gamepad in gamepads.iter() {
|
||||
let button_pressed = |button| {
|
||||
button_inputs.just_pressed(GamepadButton {
|
||||
gamepad,
|
||||
button_type: button,
|
||||
})
|
||||
};
|
||||
|
||||
if button_pressed(GamepadButtonType::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,
|
||||
intensity: GamepadRumbleIntensity::strong_motor(0.1),
|
||||
duration: Duration::from_secs(5),
|
||||
});
|
||||
}
|
||||
|
||||
if button_pressed(GamepadButtonType::East) {
|
||||
info!("East face button: maximum rumble on both motors for 5 seconds");
|
||||
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||
gamepad,
|
||||
duration: Duration::from_secs(5),
|
||||
intensity: GamepadRumbleIntensity::MAX,
|
||||
});
|
||||
}
|
||||
|
||||
if button_pressed(GamepadButtonType::South) {
|
||||
info!("South face button: low-intensity rumble on the weak motor for 0.5 seconds");
|
||||
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||
gamepad,
|
||||
duration: Duration::from_secs_f32(0.5),
|
||||
intensity: GamepadRumbleIntensity::weak_motor(0.25),
|
||||
});
|
||||
}
|
||||
|
||||
if button_pressed(GamepadButtonType::West) {
|
||||
info!("West face button: custom rumble intensity for 5 second");
|
||||
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||
gamepad,
|
||||
intensity: GamepadRumbleIntensity {
|
||||
// intensity low-frequency motor, usually on the left-hand side
|
||||
strong_motor: 0.5,
|
||||
// intensity of high-frequency motor, usually on the right-hand side
|
||||
weak_motor: 0.25,
|
||||
},
|
||||
duration: Duration::from_secs(5),
|
||||
});
|
||||
}
|
||||
|
||||
if button_pressed(GamepadButtonType::Start) {
|
||||
info!("Start button: Interrupt the current rumble");
|
||||
rumble_requests.send(GamepadRumbleRequest::Stop { gamepad });
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue