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