Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add gamepad rumble support to bevy_input #8398

Merged
merged 57 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0707bdc
Add rumble support to bevy_gilrs
nicopap Feb 5, 2022
8bfbfda
Rework error handling in rumble system
nicopap Feb 5, 2022
1628d42
Merge remote-tracking branch 'origin/main' into rumble
johanhelsing Apr 16, 2023
a2f5fd9
Suggestions from code-review
johanhelsing Apr 16, 2023
6ca8654
Remove gilrs types and re-exports from public API
johanhelsing Apr 16, 2023
34e6420
Fix typo
johanhelsing Apr 16, 2023
08e5690
Remove gilrs effect from rumble request
johanhelsing Apr 16, 2023
7787e6a
Move gamepad rumble request to bevy_input
johanhelsing Apr 16, 2023
55985d7
Rename RumbleRequest to GamepadRumbleRequest
johanhelsing Apr 16, 2023
0b5615f
Move rumble above tests
johanhelsing Apr 16, 2023
47dd5b3
refactor: Use retain instead of temporary Vec
johanhelsing Apr 16, 2023
dc75f63
refactor: Remove pointless is_empty check
johanhelsing Apr 16, 2023
d16c375
style: Use same logging style as rest of Bevy
johanhelsing Apr 16, 2023
bac6ddd
Add gamepad rumble example to Cargo.toml
johanhelsing Apr 16, 2023
c36af78
Add missing semis
johanhelsing Apr 16, 2023
254be0f
docs: Finish renaming
johanhelsing Apr 16, 2023
7fed666
fixup: fix example compile error
johanhelsing Apr 16, 2023
e54e6ef
fixup: Remove accidental code in docs
johanhelsing Apr 16, 2023
d092d2a
chore: build templated pages
johanhelsing Apr 16, 2023
90079b9
Remove GamepadRumbleRequest::stop
johanhelsing Apr 16, 2023
1f4e744
Expose strong and weak magnitudes, stop
johanhelsing Apr 16, 2023
23668eb
Document weak and strong motor types
johanhelsing Apr 16, 2023
fbc5283
Add docs
johanhelsing Apr 16, 2023
e26fd82
Add WEAK_MAX and STRONG_MAX rumble constants
johanhelsing Apr 16, 2023
ef0e248
Document additive behavior
johanhelsing Apr 16, 2023
fc92a5b
Fix docs example
johanhelsing Apr 16, 2023
d455a2b
Make GamepadRumbleRequest an enum
johanhelsing Apr 18, 2023
307eadc
Interrupt rumble with east button
johanhelsing Apr 18, 2023
380764a
Make rumble duration a Duration
johanhelsing Apr 18, 2023
71a09b6
Add GamepadRumbleIntensity::weak and strong constructors
johanhelsing Apr 18, 2023
9a0a616
Fix clippy lints
johanhelsing Apr 18, 2023
714f2af
fixup: Duration in doc test
johanhelsing Apr 18, 2023
643b9a4
fixup: gamepad_rumble example compile error
johanhelsing Apr 18, 2023
8d0ab03
Apply suggestions from code review
johanhelsing Apr 19, 2023
52c7f4f
Add GamepadRumbleRequest::gamepad
johanhelsing Apr 20, 2023
fc21d2f
Fix issues with rumble durations
johanhelsing Apr 20, 2023
677fd91
Use raw_elapsed
johanhelsing Apr 20, 2023
dcf8e2e
Document internal bevy_gilrs API
johanhelsing Apr 20, 2023
58825df
Suffix weak and strong intensities with motor
johanhelsing Apr 20, 2023
f9748db
Clamp intensities to 0 to 1 range
johanhelsing Apr 20, 2023
ecd61ca
Add system label for rumble system
johanhelsing Apr 20, 2023
e07a55c
Use this_error for RumbleError
johanhelsing Apr 20, 2023
d83959a
Upgrade some gilrs rumble errors to warnings
johanhelsing Apr 20, 2023
388ef33
Add aliases for GamepadRumbleRequest
johanhelsing Apr 20, 2023
836218a
Change example button mapping
johanhelsing Apr 20, 2023
fa5ceb0
Update crates/bevy_input/src/gamepad.rs
johanhelsing Apr 20, 2023
f5ed292
refactor: Use values_mut
johanhelsing Apr 20, 2023
7978fae
rename RumblesManager to RunningRumbleEffects
johanhelsing Apr 20, 2023
438051f
fix docs issue
johanhelsing Apr 20, 2023
f250fb8
Add test for bevy to gilrs magnitude conversion
johanhelsing Apr 20, 2023
4261821
Also test negative bevy magnitudes
johanhelsing Apr 20, 2023
103fceb
Update crates/bevy_gilrs/src/lib.rs
johanhelsing Apr 20, 2023
1829fc7
remove clamping in constructors
johanhelsing Apr 20, 2023
a87e486
Make constructors const
johanhelsing Apr 20, 2023
673facd
Re-order example gamepad button order to NESW
johanhelsing Apr 20, 2023
990a262
Add doc alias
alice-i-cecile Apr 23, 2023
98b26f0
Remove unused dependency, per CI
alice-i-cecile Apr 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_gilrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }

# other
gilrs = "0.10.1"
thiserror = "1.0"
10 changes: 7 additions & 3 deletions crates/bevy_gilrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ 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, RumblesManager};
use rumble::{play_gilrs_rumble, RunningRumbleEffects};

#[derive(Default)]
pub struct GilrsPlugin;

#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
/// Updates the running gamepad rumble effects.
pub struct RumbleSystem;

impl Plugin for GilrsPlugin {
fn build(&self, app: &mut App) {
match GilrsBuilder::new()
Expand All @@ -24,10 +28,10 @@ impl Plugin for GilrsPlugin {
{
Ok(gilrs) => {
app.insert_non_send_resource(gilrs)
.init_non_send_resource::<RumblesManager>()
.init_non_send_resource::<RunningRumbleEffects>()
.add_systems(PreStartup, gilrs_event_startup_system)
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
.add_systems(PostUpdate, play_gilrs_rumble);
.add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem));
}
Err(err) => error!("Failed to start Gilrs. {}", err),
}
Expand Down
113 changes: 72 additions & 41 deletions crates/bevy_gilrs/src/rumble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,71 @@ use bevy_ecs::{
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy_log::{debug, warn};
use bevy_time::Time;
use bevy_utils::HashMap;
use bevy_utils::{Duration, HashMap};
use gilrs::{
ff::{self, BaseEffect, BaseEffectType},
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 {
deadline: f32,
// We use `effect.drop()` to interact with this, but rustc can't know
// gilrs uses Drop as an API feature.
/// 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,
GilrsError(ff::Error),
}
impl From<ff::Error> for RumbleError {
fn from(err: ff::Error) -> Self {
RumbleError::GilrsError(err)
}
#[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 RumblesManager {
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, strong }: GamepadRumbleIntensity,
GamepadRumbleIntensity {
weak_motor,
strong_motor,
}: GamepadRumbleIntensity,
duration: Duration,
) -> Vec<ff::BaseEffect> {
let mut effects = Vec::new();
if strong > 0. {
if strong_motor > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(strong),
magnitude: to_gilrs_magnitude(strong_motor),
},
scheduling: Replay {
play_for: duration.into(),
..Default::default()
},
..Default::default()
});
}
if weak > 0. {
if weak_motor > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(weak),
magnitude: to_gilrs_magnitude(weak_motor),
},
..Default::default()
});
Expand All @@ -65,16 +79,13 @@ fn get_base_effects(
}

fn handle_rumble_request(
manager: &mut RumblesManager,
running_rumbles: &mut RunningRumbleEffects,
gilrs: &mut Gilrs,
rumble: GamepadRumbleRequest,
current_time: f32,
current_time: Duration,
) -> Result<(), RumbleError> {
let gamepad = match rumble {
GamepadRumbleRequest::Add { gamepad, .. } | GamepadRumbleRequest::Stop { gamepad } => {
gamepad
}
};
let gamepad = rumble.gamepad();

let (gamepad_id, _) = gilrs
.gamepads()
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
Expand All @@ -83,25 +94,25 @@ fn handle_rumble_request(
match rumble {
GamepadRumbleRequest::Stop { .. } => {
// `ff::Effect` uses RAII, dropping = deactivating
manager.rumbles.remove(&gamepad_id);
running_rumbles.rumbles.remove(&gamepad_id);
}
GamepadRumbleRequest::Add {
duration,
intensity,
..
} => {
let deadline = current_time + duration.as_secs_f32();

let mut effect_builder = ff::EffectBuilder::new();

for effect in get_base_effects(intensity) {
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 = manager.rumbles.entry(gamepad_id).or_default();
let gamepad_rumbles = running_rumbles.rumbles.entry(gamepad_id).or_default();
let deadline = current_time + duration;
gamepad_rumbles.push(RunningRumble { deadline, effect });
}
}
Expand All @@ -112,35 +123,55 @@ pub(crate) fn play_gilrs_rumble(
time: Res<Time>,
mut gilrs: NonSendMut<Gilrs>,
mut requests: EventReader<GamepadRumbleRequest>,
mut manager: NonSendMut<RumblesManager>,
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
) {
let current_time = time.elapsed_seconds();
let current_time = time.raw_elapsed();
// Remove outdated rumble effects.
for (_gamepad, rumbles) in manager.rumbles.iter_mut() {
for rumbles in running_rumbles.rumbles.values_mut() {
// `ff::Effect` uses RAII, dropping = deactivating
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
}
manager
running_rumbles
.rumbles
.retain(|_gamepad, rumbles| !rumbles.is_empty());

// Add new effects.
for rumble in requests.iter().cloned() {
let gamepad = match rumble {
GamepadRumbleRequest::Add { gamepad, .. } | GamepadRumbleRequest::Stop { gamepad } => {
gamepad
}
};
match handle_rumble_request(&mut manager, &mut gilrs, rumble, current_time) {
let gamepad = rumble.gamepad();
match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) {
Ok(()) => {}
Err(RumbleError::GilrsError(err)) => {
debug!(
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);
}
}
48 changes: 35 additions & 13 deletions crates/bevy_input/src/gamepad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1250,43 +1250,53 @@ pub struct GamepadRumbleIntensity {
///
/// 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: f32,
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: f32,
pub weak_motor: f32,
}

impl GamepadRumbleIntensity {
/// Rumble both gamepad motors at maximum intensity
pub const MAX: Self = GamepadRumbleIntensity {
strong: 1.0,
weak: 1.0,
strong_motor: 1.0,
weak_motor: 1.0,
};

/// Rumble the weak motor at maximum intensity
pub const WEAK_MAX: Self = GamepadRumbleIntensity {
strong: 0.0,
weak: 1.0,
strong_motor: 0.0,
weak_motor: 1.0,
};

/// Rumble the strong motor at maximum intensity
pub const STRONG_MAX: Self = GamepadRumbleIntensity {
strong: 1.0,
weak: 0.0,
strong_motor: 1.0,
weak_motor: 0.0,
};

/// Creates a new rumble intensity with weak motor intensity set to the given value
pub fn weak(weak: f32) -> Self {
Self { weak, strong: 0.0 }
///
/// Clamped within the 0 to 1 range
pub fn weak_motor(intensity: f32) -> Self {
Self {
weak_motor: f32::clamp(intensity, 0.0, 1.0),
strong_motor: 0.0,
}
}

/// Creates a new rumble intensity with strong motor intensity set to the given value
pub fn strong(strong: f32) -> Self {
Self { strong, weak: 0.0 }
///
/// Clamped within the 0 to 1 range
pub fn strong_motor(intensity: f32) -> Self {
Self {
strong_motor: f32::clamp(intensity, 0.0, 1.0),
weak_motor: 0.0,
}
}
}

Expand Down Expand Up @@ -1315,6 +1325,9 @@ impl GamepadRumbleIntensity {
/// }
/// }
/// ```
#[doc(alias = "force feedback")]
#[doc(alias = "vibration")]
#[doc(alias = "vibrate")]
#[derive(Clone)]
pub enum GamepadRumbleRequest {
/// Add a rumble to the given gamepad.
Expand All @@ -1335,10 +1348,19 @@ pub enum GamepadRumbleRequest {
/// The gamepad to rumble
gamepad: Gamepad,
},
/// Stop all running rumbles on the given `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};
Expand Down
Loading