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 21 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
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
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
3 changes: 3 additions & 0 deletions crates/bevy_gilrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ 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"
8 changes: 6 additions & 2 deletions crates/bevy_gilrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

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, RumblesManager};

#[derive(Default)]
pub struct GilrsPlugin;
Expand All @@ -22,8 +24,10 @@ impl Plugin for GilrsPlugin {
{
Ok(gilrs) => {
app.insert_non_send_resource(gilrs)
.init_non_send_resource::<RumblesManager>()
.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);
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
}
Err(err) => error!("Failed to start Gilrs. {}", err),
}
Expand Down
133 changes: 133 additions & 0 deletions crates/bevy_gilrs/src/rumble.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! 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::HashMap;
use gilrs::{
ff::{self, BaseEffect, BaseEffectType},
GamepadId, Gilrs,
};

use crate::converter::convert_gamepad_id;

struct RunningRumble {
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
deadline: f32,
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
// We use `effect.drop()` to interact with this, but rustc can't know
// gilrs uses Drop as an API feature.
#[allow(dead_code)]
effect: ff::Effect,
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
}

enum RumbleError {
GamepadNotFound,
GilrsError(ff::Error),
}
impl From<ff::Error> for RumbleError {
fn from(err: ff::Error) -> Self {
RumbleError::GilrsError(err)
}
}
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Default)]
pub(crate) struct RumblesManager {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc strings please. This is used as a non-send resource below, but it's hard to figure out the intent of this at first glance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed it to RunningRumbleEffects, which should hopefully make its purpose more clear, and added a little bit for explanation.

rumbles: HashMap<GamepadId, Vec<RunningRumble>>,
}

fn is_stop_request(request: &GamepadRumbleRequest) -> bool {
!request.additive && request.intensity == GamepadRumbleIntensity::ZERO
}

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, strong }: GamepadRumbleIntensity,
) -> Vec<ff::BaseEffect> {
let mut effects = Vec::new();
if strong > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(strong),
},
..Default::default()
});
}
if weak > 0. {
effects.push(BaseEffect {
kind: BaseEffectType::Strong {
magnitude: to_gilrs_magnitude(weak),
},
..Default::default()
});
}
effects
}

fn add_rumble(
manager: &mut RumblesManager,
gilrs: &mut Gilrs,
rumble: GamepadRumbleRequest,
current_time: f32,
) -> Result<(), RumbleError> {
let (gamepad_id, _) = gilrs
.gamepads()
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == rumble.gamepad)
.ok_or(RumbleError::GamepadNotFound)?;

if !rumble.additive {
// `ff::Effect` uses RAII, dropping = deactivating
manager.rumbles.remove(&gamepad_id);
}

if !is_stop_request(&rumble) {
let deadline = current_time + rumble.duration_seconds;

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

for effect in get_base_effects(rumble.intensity) {
effect_builder.add_effect(effect);
}

let effect = effect_builder.gamepads(&[gamepad_id]).finish(gilrs)?;
effect.play()?;

let gamepad_rumbles = manager.rumbles.entry(gamepad_id).or_default();
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 manager: NonSendMut<RumblesManager>,
) {
let current_time = time.elapsed_seconds();
// Remove outdated rumble effects.
for (_gamepad, rumbles) in manager.rumbles.iter_mut() {
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
// `ff::Effect` uses RAII, dropping = deactivating
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
}
manager
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
.rumbles
.retain(|_gamepad, rumbles| !rumbles.is_empty());

// Add new effects.
for rumble in requests.iter().cloned() {
let pad = rumble.gamepad;
match add_rumble(&mut manager, &mut gilrs, rumble, current_time) {
Ok(()) => {}
Err(RumbleError::GilrsError(err)) => {
debug!("Tried to rumble {pad:?} but an error occurred: {err}");
}
Err(RumbleError::GamepadNotFound) => {
warn!("Tried to rumble {pad:?} but it doesn't exist!");
}
};
}
}
77 changes: 77 additions & 0 deletions crates/bevy_input/src/gamepad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,83 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
GamepadAxisType::RightZ,
];

#[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
pub strong: f32,
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
/// The rumble intensity of the weak gamepad motor
///
/// Ranges from 0.0 to 1.0
pub weak: f32,
}

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

/// Don't rumble at all, only makes sense when `additive` is `false`
pub const ZERO: Self = GamepadRumbleIntensity {
strong: 0.0,
weak: 0.0,
};
}

/// Request that `gamepad` should rumble with `intensity` for `duration_seconds`
///
/// # Notes
///
/// * Does nothing if the `gamepad` does not support rumble
/// * If a new `GamepadRumbleRequest` is sent while another one is still executing, it
/// replaces the old one.
///
/// # Example
///
/// ```
/// # use bevy_gilrs::{GamepadRumbleRequest, GamepadRumbleIntensity};
/// # use bevy_input::gamepad::Gamepad;
/// # use bevy_app::EventWriter;
/// fn rumble_pad_system(mut rumble_requests: EventWriter<GamepadRumbleRequest>) {
/// let request = GamepadRumbleRequest {
/// intensity: GamepadRumbleIntensity::Strong,
/// duration_seconds: 10.0,
/// gamepad: Gamepad(0),
/// additive: true,
/// };
/// rumble_requests.send(request);
/// }
/// ```
#[derive(Clone)]
pub struct GamepadRumbleRequest {
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
/// The duration in seconds of the rumble
pub duration_seconds: f32,
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
/// How intense the rumble should be
pub intensity: GamepadRumbleIntensity,
/// The gamepad to rumble
pub gamepad: Gamepad,
/// Whether the rumble effects should add up, or replace any existing effects
pub additive: bool,
}

impl GamepadRumbleRequest {
/// Stop all running rumbles on the given `Gamepad`
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
pub fn stop(gamepad: Gamepad) -> Self {
Self {
duration_seconds: 0.0,
intensity: GamepadRumbleIntensity {
strong: 0.0,
weak: 0.0,
},
gamepad,
additive: false,
}
}
}

#[cfg(test)]
mod tests {
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_input/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ use gamepad::{
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
Gamepads,
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
GamepadRumbleRequest, GamepadSettings, Gamepads,
};

#[cfg(feature = "serialize")]
Expand Down Expand Up @@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
.add_event::<GamepadButtonChangedEvent>()
.add_event::<GamepadAxisChangedEvent>()
.add_event::<GamepadEvent>()
.add_event::<GamepadRumbleRequest>()
.init_resource::<GamepadSettings>()
.init_resource::<Gamepads>()
.init_resource::<Input<GamepadButton>>()
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ Example | Description
[Char Input Events](../examples/input/char_input_events.rs) | Prints out all chars as they are inputted
[Gamepad Input](../examples/input/gamepad_input.rs) | Shows handling of gamepad input, connections, and disconnections
[Gamepad Input Events](../examples/input/gamepad_input_events.rs) | Iterates and prints gamepad input and connection events
[Gamepad Rumble](../examples/input/gamepad_rumble.rs) | Shows how to rumble a gamepad using force feedback
[Keyboard Input](../examples/input/keyboard_input.rs) | Demonstrates handling a key press/release
[Keyboard Input Events](../examples/input/keyboard_input_events.rs) | Prints out all keyboard events
[Keyboard Modifiers](../examples/input/keyboard_modifiers.rs) | Demonstrates using key modifiers (ctrl, shift)
Expand Down
56 changes: 56 additions & 0 deletions examples/input/gamepad_rumble.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Shows how to trigger force-feedback, making gamepads rumble when buttons are
//! pressed.

use bevy::{
input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest},
prelude::*,
};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, gamepad_system)
.run();
}

fn gamepad_system(
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
gamepads: Res<Gamepads>,
button_inputs: Res<Input<GamepadButton>>,
mut rumble_requests: EventWriter<GamepadRumbleRequest>,
) {
for gamepad in gamepads.iter() {
let button_pressed = |button| {
button_inputs.just_pressed(GamepadButton {
gamepad,
button_type: button,
})
};
if button_pressed(GamepadButtonType::South) {
johanhelsing marked this conversation as resolved.
Show resolved Hide resolved
info!("(S) South face button: weak rumble for 1 second");
// Use the simplified API provided by Bevy
rumble_requests.send(GamepadRumbleRequest {
gamepad,
duration_seconds: 1.0,
intensity: GamepadRumbleIntensity {
strong: 0.0,
weak: 0.25,
},
additive: true,
});
} else if button_pressed(GamepadButtonType::West) {
info!("(W) West face button: maximum rumble for 5 second");
rumble_requests.send(GamepadRumbleRequest {
gamepad,
intensity: GamepadRumbleIntensity {
strong: 1.0,
weak: 1.0,
},
duration_seconds: 5.0,
additive: true,
});
} else if button_pressed(GamepadButtonType::North) {
info!("(N) North face button: Interrupt the current rumble");
rumble_requests.send(GamepadRumbleRequest::stop(gamepad));
}
}
}