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 55 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Expand Up @@ -1247,6 +1247,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"
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_gilrs/Cargo.toml
Expand Up @@ -11,9 +11,13 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.11.0-dev" }
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
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"
12 changes: 10 additions & 2 deletions crates/bevy_gilrs/src/lib.rs
Expand Up @@ -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()
Expand All @@ -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),
}
Expand Down
177 changes: 177 additions & 0 deletions crates/bevy_gilrs/src/rumble.rs
@@ -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);
}
}
121 changes: 121 additions & 0 deletions crates/bevy_input/src/gamepad.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -1240,6 +1241,126 @@ 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 {
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
/// 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`]
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
///
/// # 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 = "force feedback")]
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
#[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};
Expand Down