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 <nico@nicopap.ch> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com> Co-authored-by: Bruce Reif (Buswolley) <bruce.reif@dynata.com>
This commit is contained in:
parent
288009ab8b
commit
a1e442cd2a
10
Cargo.toml
10
Cargo.toml
@ -1274,6 +1274,16 @@ description = "Iterates and prints gamepad input and connection events"
|
|||||||
category = "Input"
|
category = "Input"
|
||||||
wasm = false
|
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]]
|
[[example]]
|
||||||
name = "keyboard_input"
|
name = "keyboard_input"
|
||||||
path = "examples/input/keyboard_input.rs"
|
path = "examples/input/keyboard_input.rs"
|
||||||
|
|||||||
@ -13,7 +13,10 @@ keywords = ["bevy"]
|
|||||||
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
|
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
|
||||||
bevy_input = { path = "../bevy_input", 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_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
||||||
|
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
gilrs = "0.10.1"
|
gilrs = "0.10.1"
|
||||||
|
thiserror = "1.0"
|
||||||
@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
mod converter;
|
mod converter;
|
||||||
mod gilrs_system;
|
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_ecs::prelude::*;
|
||||||
use bevy_input::InputSystem;
|
use bevy_input::InputSystem;
|
||||||
use bevy_utils::tracing::error;
|
use bevy_utils::tracing::error;
|
||||||
use gilrs::GilrsBuilder;
|
use gilrs::GilrsBuilder;
|
||||||
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
|
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
|
||||||
|
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GilrsPlugin;
|
pub struct GilrsPlugin;
|
||||||
|
|
||||||
|
/// Updates the running gamepad rumble effects.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
|
||||||
|
pub struct RumbleSystem;
|
||||||
|
|
||||||
impl Plugin for GilrsPlugin {
|
impl Plugin for GilrsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
match GilrsBuilder::new()
|
match GilrsBuilder::new()
|
||||||
@ -22,8 +28,10 @@ impl Plugin for GilrsPlugin {
|
|||||||
{
|
{
|
||||||
Ok(gilrs) => {
|
Ok(gilrs) => {
|
||||||
app.insert_non_send_resource(gilrs)
|
app.insert_non_send_resource(gilrs)
|
||||||
|
.init_non_send_resource::<RunningRumbleEffects>()
|
||||||
.add_systems(PreStartup, gilrs_event_startup_system)
|
.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),
|
Err(err) => error!("Failed to start Gilrs. {}", err),
|
||||||
}
|
}
|
||||||
|
|||||||
177
crates/bevy_gilrs/src/rumble.rs
Normal file
177
crates/bevy_gilrs/src/rumble.rs
Normal file
@ -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 {
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
(ratio * u16::MAX as f32) as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_base_effects(
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ use bevy_ecs::{
|
|||||||
system::{Res, ResMut, Resource},
|
system::{Res, ResMut, Resource},
|
||||||
};
|
};
|
||||||
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
|
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
|
||||||
|
use bevy_utils::Duration;
|
||||||
use bevy_utils::{tracing::info, HashMap};
|
use bevy_utils::{tracing::info, HashMap};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@ -1240,6 +1241,127 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
|
|||||||
GamepadAxisType::RightZ,
|
GamepadAxisType::RightZ,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The intensity at which a gamepad's force-feedback motors may rumble.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct GamepadRumbleIntensity {
|
||||||
|
/// 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`]
|
||||||
|
///
|
||||||
|
/// # 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 = "haptic feedback")]
|
||||||
|
#[doc(alias = "force feedback")]
|
||||||
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};
|
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};
|
||||||
|
|||||||
@ -39,8 +39,8 @@ use gamepad::{
|
|||||||
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
|
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
|
||||||
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
|
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
|
||||||
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
|
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
|
||||||
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
|
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
|
||||||
Gamepads,
|
GamepadRumbleRequest, GamepadSettings, Gamepads,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
|
|||||||
.add_event::<GamepadButtonChangedEvent>()
|
.add_event::<GamepadButtonChangedEvent>()
|
||||||
.add_event::<GamepadAxisChangedEvent>()
|
.add_event::<GamepadAxisChangedEvent>()
|
||||||
.add_event::<GamepadEvent>()
|
.add_event::<GamepadEvent>()
|
||||||
|
.add_event::<GamepadRumbleRequest>()
|
||||||
.init_resource::<GamepadSettings>()
|
.init_resource::<GamepadSettings>()
|
||||||
.init_resource::<Gamepads>()
|
.init_resource::<Gamepads>()
|
||||||
.init_resource::<Input<GamepadButton>>()
|
.init_resource::<Input<GamepadButton>>()
|
||||||
|
|||||||
@ -239,6 +239,7 @@ Example | Description
|
|||||||
[Char Input Events](../examples/input/char_input_events.rs) | Prints out all chars as they are inputted
|
[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](../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 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](../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 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)
|
[Keyboard Modifiers](../examples/input/keyboard_modifiers.rs) | Demonstrates using key modifiers (ctrl, shift)
|
||||||
|
|||||||
78
examples/input/gamepad_rumble.rs
Normal file
78
examples/input/gamepad_rumble.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//! Shows how to trigger force-feedback, making gamepads rumble when buttons are
|
||||||
|
//! pressed.
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest},
|
||||||
|
prelude::*,
|
||||||
|
utils::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Update, gamepad_system)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gamepad_system(
|
||||||
|
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::North) {
|
||||||
|
info!(
|
||||||
|
"North face button: strong (low-frequency) with low intensity for rumble for 5 seconds. Press multiple times to increase intensity."
|
||||||
|
);
|
||||||
|
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||||
|
gamepad,
|
||||||
|
intensity: GamepadRumbleIntensity::strong_motor(0.1),
|
||||||
|
duration: Duration::from_secs(5),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_pressed(GamepadButtonType::East) {
|
||||||
|
info!("East face button: maximum rumble on both motors for 5 seconds");
|
||||||
|
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||||
|
gamepad,
|
||||||
|
duration: Duration::from_secs(5),
|
||||||
|
intensity: GamepadRumbleIntensity::MAX,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_pressed(GamepadButtonType::South) {
|
||||||
|
info!("South face button: low-intensity rumble on the weak motor for 0.5 seconds");
|
||||||
|
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||||
|
gamepad,
|
||||||
|
duration: Duration::from_secs_f32(0.5),
|
||||||
|
intensity: GamepadRumbleIntensity::weak_motor(0.25),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_pressed(GamepadButtonType::West) {
|
||||||
|
info!("West face button: custom rumble intensity for 5 second");
|
||||||
|
rumble_requests.send(GamepadRumbleRequest::Add {
|
||||||
|
gamepad,
|
||||||
|
intensity: GamepadRumbleIntensity {
|
||||||
|
// intensity low-frequency motor, usually on the left-hand side
|
||||||
|
strong_motor: 0.5,
|
||||||
|
// intensity of high-frequency motor, usually on the right-hand side
|
||||||
|
weak_motor: 0.25,
|
||||||
|
},
|
||||||
|
duration: Duration::from_secs(5),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if button_pressed(GamepadButtonType::Start) {
|
||||||
|
info!("Start button: Interrupt the current rumble");
|
||||||
|
rumble_requests.send(GamepadRumbleRequest::Stop { gamepad });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user