Skip to content

Commit a1e442c

Browse files
johanhelsingnicopapalice-i-cecileB-Reif
authored
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 <[email protected]> Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Nicola Papale <[email protected]> Co-authored-by: Bruce Reif (Buswolley) <[email protected]>
1 parent 288009a commit a1e442c

File tree

8 files changed

+404
-4
lines changed

8 files changed

+404
-4
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,16 @@ description = "Iterates and prints gamepad input and connection events"
12741274
category = "Input"
12751275
wasm = false
12761276

1277+
[[example]]
1278+
name = "gamepad_rumble"
1279+
path = "examples/input/gamepad_rumble.rs"
1280+
1281+
[package.metadata.example.gamepad_rumble]
1282+
name = "Gamepad Rumble"
1283+
description = "Shows how to rumble a gamepad using force feedback"
1284+
category = "Input"
1285+
wasm = false
1286+
12771287
[[example]]
12781288
name = "keyboard_input"
12791289
path = "examples/input/keyboard_input.rs"

crates/bevy_gilrs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ keywords = ["bevy"]
1313
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
1414
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
1515
bevy_input = { path = "../bevy_input", version = "0.11.0-dev" }
16+
bevy_log = { path = "../bevy_log", version = "0.11.0-dev" }
1617
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
18+
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
1719

1820
# other
1921
gilrs = "0.10.1"
22+
thiserror = "1.0"

crates/bevy_gilrs/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
mod converter;
44
mod gilrs_system;
5+
mod rumble;
56

6-
use bevy_app::{App, Plugin, PreStartup, PreUpdate};
7+
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
78
use bevy_ecs::prelude::*;
89
use bevy_input::InputSystem;
910
use bevy_utils::tracing::error;
1011
use gilrs::GilrsBuilder;
1112
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
13+
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
1214

1315
#[derive(Default)]
1416
pub struct GilrsPlugin;
1517

18+
/// Updates the running gamepad rumble effects.
19+
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
20+
pub struct RumbleSystem;
21+
1622
impl Plugin for GilrsPlugin {
1723
fn build(&self, app: &mut App) {
1824
match GilrsBuilder::new()
@@ -22,8 +28,10 @@ impl Plugin for GilrsPlugin {
2228
{
2329
Ok(gilrs) => {
2430
app.insert_non_send_resource(gilrs)
31+
.init_non_send_resource::<RunningRumbleEffects>()
2532
.add_systems(PreStartup, gilrs_event_startup_system)
26-
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem));
33+
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
34+
.add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem));
2735
}
2836
Err(err) => error!("Failed to start Gilrs. {}", err),
2937
}

crates/bevy_gilrs/src/rumble.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! Handle user specified rumble request events.
2+
use bevy_ecs::{
3+
prelude::{EventReader, Res},
4+
system::NonSendMut,
5+
};
6+
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
7+
use bevy_log::{debug, warn};
8+
use bevy_time::Time;
9+
use bevy_utils::{Duration, HashMap};
10+
use gilrs::{
11+
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
12+
GamepadId, Gilrs,
13+
};
14+
use thiserror::Error;
15+
16+
use crate::converter::convert_gamepad_id;
17+
18+
/// A rumble effect that is currently in effect.
19+
struct RunningRumble {
20+
/// Duration from app startup when this effect will be finished
21+
deadline: Duration,
22+
/// A ref-counted handle to the specific force-feedback effect
23+
///
24+
/// Dropping it will cause the effect to stop
25+
#[allow(dead_code)]
26+
effect: ff::Effect,
27+
}
28+
29+
#[derive(Error, Debug)]
30+
enum RumbleError {
31+
#[error("gamepad not found")]
32+
GamepadNotFound,
33+
#[error("gilrs error while rumbling gamepad: {0}")]
34+
GilrsError(#[from] ff::Error),
35+
}
36+
37+
/// Contains the gilrs rumble effects that are currently running for each gamepad
38+
#[derive(Default)]
39+
pub(crate) struct RunningRumbleEffects {
40+
/// If multiple rumbles are running at the same time, their resulting rumble
41+
/// will be the saturated sum of their strengths up until [`u16::MAX`]
42+
rumbles: HashMap<GamepadId, Vec<RunningRumble>>,
43+
}
44+
45+
/// gilrs uses magnitudes from 0 to [`u16::MAX`], while ours go from `0.0` to `1.0` ([`f32`])
46+
fn to_gilrs_magnitude(ratio: f32) -> u16 {
47+
(ratio * u16::MAX as f32) as u16
48+
}
49+
50+
fn get_base_effects(
51+
GamepadRumbleIntensity {
52+
weak_motor,
53+
strong_motor,
54+
}: GamepadRumbleIntensity,
55+
duration: Duration,
56+
) -> Vec<ff::BaseEffect> {
57+
let mut effects = Vec::new();
58+
if strong_motor > 0. {
59+
effects.push(BaseEffect {
60+
kind: BaseEffectType::Strong {
61+
magnitude: to_gilrs_magnitude(strong_motor),
62+
},
63+
scheduling: Replay {
64+
play_for: duration.into(),
65+
..Default::default()
66+
},
67+
..Default::default()
68+
});
69+
}
70+
if weak_motor > 0. {
71+
effects.push(BaseEffect {
72+
kind: BaseEffectType::Strong {
73+
magnitude: to_gilrs_magnitude(weak_motor),
74+
},
75+
..Default::default()
76+
});
77+
}
78+
effects
79+
}
80+
81+
fn handle_rumble_request(
82+
running_rumbles: &mut RunningRumbleEffects,
83+
gilrs: &mut Gilrs,
84+
rumble: GamepadRumbleRequest,
85+
current_time: Duration,
86+
) -> Result<(), RumbleError> {
87+
let gamepad = rumble.gamepad();
88+
89+
let (gamepad_id, _) = gilrs
90+
.gamepads()
91+
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
92+
.ok_or(RumbleError::GamepadNotFound)?;
93+
94+
match rumble {
95+
GamepadRumbleRequest::Stop { .. } => {
96+
// `ff::Effect` uses RAII, dropping = deactivating
97+
running_rumbles.rumbles.remove(&gamepad_id);
98+
}
99+
GamepadRumbleRequest::Add {
100+
duration,
101+
intensity,
102+
..
103+
} => {
104+
let mut effect_builder = ff::EffectBuilder::new();
105+
106+
for effect in get_base_effects(intensity, duration) {
107+
effect_builder.add_effect(effect);
108+
effect_builder.repeat(Repeat::For(duration.into()));
109+
}
110+
111+
let effect = effect_builder.gamepads(&[gamepad_id]).finish(gilrs)?;
112+
effect.play()?;
113+
114+
let gamepad_rumbles = running_rumbles.rumbles.entry(gamepad_id).or_default();
115+
let deadline = current_time + duration;
116+
gamepad_rumbles.push(RunningRumble { deadline, effect });
117+
}
118+
}
119+
120+
Ok(())
121+
}
122+
pub(crate) fn play_gilrs_rumble(
123+
time: Res<Time>,
124+
mut gilrs: NonSendMut<Gilrs>,
125+
mut requests: EventReader<GamepadRumbleRequest>,
126+
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
127+
) {
128+
let current_time = time.raw_elapsed();
129+
// Remove outdated rumble effects.
130+
for rumbles in running_rumbles.rumbles.values_mut() {
131+
// `ff::Effect` uses RAII, dropping = deactivating
132+
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
133+
}
134+
running_rumbles
135+
.rumbles
136+
.retain(|_gamepad, rumbles| !rumbles.is_empty());
137+
138+
// Add new effects.
139+
for rumble in requests.iter().cloned() {
140+
let gamepad = rumble.gamepad();
141+
match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) {
142+
Ok(()) => {}
143+
Err(RumbleError::GilrsError(err)) => {
144+
if let ff::Error::FfNotSupported(_) = err {
145+
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
146+
} else {
147+
warn!(
148+
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
149+
);
150+
}
151+
}
152+
Err(RumbleError::GamepadNotFound) => {
153+
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
154+
}
155+
};
156+
}
157+
}
158+
159+
#[cfg(test)]
160+
mod tests {
161+
use super::to_gilrs_magnitude;
162+
163+
#[test]
164+
fn magnitude_conversion() {
165+
assert_eq!(to_gilrs_magnitude(1.0), u16::MAX);
166+
assert_eq!(to_gilrs_magnitude(0.0), 0);
167+
168+
// bevy magnitudes of 2.0 don't really make sense, but just make sure
169+
// they convert to something sensible in gilrs anyway.
170+
assert_eq!(to_gilrs_magnitude(2.0), u16::MAX);
171+
172+
// negative bevy magnitudes don't really make sense, but just make sure
173+
// they convert to something sensible in gilrs anyway.
174+
assert_eq!(to_gilrs_magnitude(-1.0), 0);
175+
assert_eq!(to_gilrs_magnitude(-0.1), 0);
176+
}
177+
}

crates/bevy_input/src/gamepad.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use bevy_ecs::{
55
system::{Res, ResMut, Resource},
66
};
77
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
8+
use bevy_utils::Duration;
89
use bevy_utils::{tracing::info, HashMap};
910
use thiserror::Error;
1011

@@ -1240,6 +1241,127 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
12401241
GamepadAxisType::RightZ,
12411242
];
12421243

1244+
/// The intensity at which a gamepad's force-feedback motors may rumble.
1245+
#[derive(Clone, Copy, Debug, PartialEq)]
1246+
pub struct GamepadRumbleIntensity {
1247+
/// The rumble intensity of the strong gamepad motor
1248+
///
1249+
/// Ranges from 0.0 to 1.0
1250+
///
1251+
/// By convention, this is usually a low-frequency motor on the left-hand
1252+
/// side of the gamepad, though it may vary across platforms and hardware.
1253+
pub strong_motor: f32,
1254+
/// The rumble intensity of the weak gamepad motor
1255+
///
1256+
/// Ranges from 0.0 to 1.0
1257+
///
1258+
/// By convention, this is usually a high-frequency motor on the right-hand
1259+
/// side of the gamepad, though it may vary across platforms and hardware.
1260+
pub weak_motor: f32,
1261+
}
1262+
1263+
impl GamepadRumbleIntensity {
1264+
/// Rumble both gamepad motors at maximum intensity
1265+
pub const MAX: Self = GamepadRumbleIntensity {
1266+
strong_motor: 1.0,
1267+
weak_motor: 1.0,
1268+
};
1269+
1270+
/// Rumble the weak motor at maximum intensity
1271+
pub const WEAK_MAX: Self = GamepadRumbleIntensity {
1272+
strong_motor: 0.0,
1273+
weak_motor: 1.0,
1274+
};
1275+
1276+
/// Rumble the strong motor at maximum intensity
1277+
pub const STRONG_MAX: Self = GamepadRumbleIntensity {
1278+
strong_motor: 1.0,
1279+
weak_motor: 0.0,
1280+
};
1281+
1282+
/// Creates a new rumble intensity with weak motor intensity set to the given value
1283+
///
1284+
/// Clamped within the 0 to 1 range
1285+
pub const fn weak_motor(intensity: f32) -> Self {
1286+
Self {
1287+
weak_motor: intensity,
1288+
strong_motor: 0.0,
1289+
}
1290+
}
1291+
1292+
/// Creates a new rumble intensity with strong motor intensity set to the given value
1293+
///
1294+
/// Clamped within the 0 to 1 range
1295+
pub const fn strong_motor(intensity: f32) -> Self {
1296+
Self {
1297+
strong_motor: intensity,
1298+
weak_motor: 0.0,
1299+
}
1300+
}
1301+
}
1302+
1303+
/// An event that controls force-feedback rumbling of a [`Gamepad`]
1304+
///
1305+
/// # Notes
1306+
///
1307+
/// Does nothing if the gamepad or platform does not support rumble.
1308+
///
1309+
/// # Example
1310+
///
1311+
/// ```
1312+
/// # use bevy_input::gamepad::{Gamepad, Gamepads, GamepadRumbleRequest, GamepadRumbleIntensity};
1313+
/// # use bevy_ecs::prelude::{EventWriter, Res};
1314+
/// # use bevy_utils::Duration;
1315+
/// fn rumble_gamepad_system(
1316+
/// mut rumble_requests: EventWriter<GamepadRumbleRequest>,
1317+
/// gamepads: Res<Gamepads>
1318+
/// ) {
1319+
/// for gamepad in gamepads.iter() {
1320+
/// rumble_requests.send(GamepadRumbleRequest::Add {
1321+
/// gamepad,
1322+
/// intensity: GamepadRumbleIntensity::MAX,
1323+
/// duration: Duration::from_secs_f32(0.5),
1324+
/// });
1325+
/// }
1326+
/// }
1327+
/// ```
1328+
#[doc(alias = "haptic feedback")]
1329+
#[doc(alias = "force feedback")]
1330+
#[doc(alias = "vibration")]
1331+
#[doc(alias = "vibrate")]
1332+
#[derive(Clone)]
1333+
pub enum GamepadRumbleRequest {
1334+
/// Add a rumble to the given gamepad.
1335+
///
1336+
/// Simultaneous rumble effects add up to the sum of their strengths.
1337+
///
1338+
/// Consequently, if two rumbles at half intensity are added at the same
1339+
/// time, their intensities will be added up, and the controller will rumble
1340+
/// at full intensity until one of the rumbles finishes, then the rumble
1341+
/// will continue at the intensity of the remaining event.
1342+
///
1343+
/// To replace an existing rumble, send a [`GamepadRumbleRequest::Stop`] event first.
1344+
Add {
1345+
/// How long the gamepad should rumble
1346+
duration: Duration,
1347+
/// How intense the rumble should be
1348+
intensity: GamepadRumbleIntensity,
1349+
/// The gamepad to rumble
1350+
gamepad: Gamepad,
1351+
},
1352+
/// Stop all running rumbles on the given [`Gamepad`]
1353+
Stop { gamepad: Gamepad },
1354+
}
1355+
1356+
impl GamepadRumbleRequest {
1357+
/// Get the [`Gamepad`] associated with this request
1358+
pub fn gamepad(&self) -> Gamepad {
1359+
match self {
1360+
Self::Add { gamepad, .. } | Self::Stop { gamepad } => *gamepad,
1361+
}
1362+
}
1363+
}
1364+
12431365
#[cfg(test)]
12441366
mod tests {
12451367
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};

crates/bevy_input/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ use gamepad::{
3939
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
4040
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
4141
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
42-
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
43-
Gamepads,
42+
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
43+
GamepadRumbleRequest, GamepadSettings, Gamepads,
4444
};
4545

4646
#[cfg(feature = "serialize")]
@@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
7272
.add_event::<GamepadButtonChangedEvent>()
7373
.add_event::<GamepadAxisChangedEvent>()
7474
.add_event::<GamepadEvent>()
75+
.add_event::<GamepadRumbleRequest>()
7576
.init_resource::<GamepadSettings>()
7677
.init_resource::<Gamepads>()
7778
.init_resource::<Input<GamepadButton>>()

0 commit comments

Comments
 (0)