-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Add gamepad rumble support to bevy_input #8398
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
Merged
Merged
Changes from 51 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
0707bdc
Add rumble support to bevy_gilrs
nicopap 8bfbfda
Rework error handling in rumble system
nicopap 1628d42
Merge remote-tracking branch 'origin/main' into rumble
johanhelsing a2f5fd9
Suggestions from code-review
johanhelsing 6ca8654
Remove gilrs types and re-exports from public API
johanhelsing 34e6420
Fix typo
johanhelsing 08e5690
Remove gilrs effect from rumble request
johanhelsing 7787e6a
Move gamepad rumble request to bevy_input
johanhelsing 55985d7
Rename RumbleRequest to GamepadRumbleRequest
johanhelsing 0b5615f
Move rumble above tests
johanhelsing 47dd5b3
refactor: Use retain instead of temporary Vec
johanhelsing dc75f63
refactor: Remove pointless is_empty check
johanhelsing d16c375
style: Use same logging style as rest of Bevy
johanhelsing bac6ddd
Add gamepad rumble example to Cargo.toml
johanhelsing c36af78
Add missing semis
johanhelsing 254be0f
docs: Finish renaming
johanhelsing 7fed666
fixup: fix example compile error
johanhelsing e54e6ef
fixup: Remove accidental code in docs
johanhelsing d092d2a
chore: build templated pages
johanhelsing 90079b9
Remove GamepadRumbleRequest::stop
johanhelsing 1f4e744
Expose strong and weak magnitudes, stop
johanhelsing 23668eb
Document weak and strong motor types
johanhelsing fbc5283
Add docs
johanhelsing e26fd82
Add WEAK_MAX and STRONG_MAX rumble constants
johanhelsing ef0e248
Document additive behavior
johanhelsing fc92a5b
Fix docs example
johanhelsing d455a2b
Make GamepadRumbleRequest an enum
johanhelsing 307eadc
Interrupt rumble with east button
johanhelsing 380764a
Make rumble duration a Duration
johanhelsing 71a09b6
Add GamepadRumbleIntensity::weak and strong constructors
johanhelsing 9a0a616
Fix clippy lints
johanhelsing 714f2af
fixup: Duration in doc test
johanhelsing 643b9a4
fixup: gamepad_rumble example compile error
johanhelsing 8d0ab03
Apply suggestions from code review
johanhelsing 52c7f4f
Add GamepadRumbleRequest::gamepad
johanhelsing fc21d2f
Fix issues with rumble durations
johanhelsing 677fd91
Use raw_elapsed
johanhelsing dcf8e2e
Document internal bevy_gilrs API
johanhelsing 58825df
Suffix weak and strong intensities with motor
johanhelsing f9748db
Clamp intensities to 0 to 1 range
johanhelsing ecd61ca
Add system label for rumble system
johanhelsing e07a55c
Use this_error for RumbleError
johanhelsing d83959a
Upgrade some gilrs rumble errors to warnings
johanhelsing 388ef33
Add aliases for GamepadRumbleRequest
johanhelsing 836218a
Change example button mapping
johanhelsing fa5ceb0
Update crates/bevy_input/src/gamepad.rs
johanhelsing f5ed292
refactor: Use values_mut
johanhelsing 7978fae
rename RumblesManager to RunningRumbleEffects
johanhelsing 438051f
fix docs issue
johanhelsing f250fb8
Add test for bevy to gilrs magnitude conversion
johanhelsing 4261821
Also test negative bevy magnitudes
johanhelsing 103fceb
Update crates/bevy_gilrs/src/lib.rs
johanhelsing 1829fc7
remove clamping in constructors
johanhelsing a87e486
Make constructors const
johanhelsing 673facd
Re-order example gamepad button order to NESW
johanhelsing 990a262
Add doc alias
alice-i-cecile 98b26f0
Remove unused dependency, per CI
alice-i-cecile File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
johanhelsing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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, | ||
johanhelsing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
#[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 { | ||
johanhelsing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(ratio * u16::MAX as f32) as u16 | ||
} | ||
|
||
fn get_base_effects( | ||
johanhelsing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)?; | ||
johanhelsing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.