Skip to content

Commit e788e3b

Browse files
s-puigcart
andauthored
Implement gamepads as entities (#12770)
# Objective - Significantly improve the ergonomics of gamepads and allow new features Gamepads are a bit unergonomic to work with, they use resources but unlike other inputs, they are not limited to a single gamepad, to get around this it uses an identifier (Gamepad) to interact with anything causing all sorts of issues. 1. There are too many: Gamepads, GamepadSettings, GamepadInfo, ButtonInput<T>, 2 Axis<T>. 2. ButtonInput/Axis generic methods become really inconvenient to use e.g. any_pressed() 3. GamepadButton/Axis structs are unnecessary boilerplate: ```rust for gamepad in gamepads.iter() { if button_inputs.just_pressed(GamepadButton::new(gamepad, GamepadButtonType::South)) { info!("{:?} just pressed South", gamepad); } else if button_inputs.just_released(GamepadButton::new(gamepad, GamepadButtonType::South)) { info!("{:?} just released South", gamepad); } } ``` 4. Projects often need to create resources to store the selected gamepad and have to manually check if their gamepad is still valid anyways. - Previously attempted by #3419 and #12674 ## Solution - Implement gamepads as entities. Using entities solves all the problems above and opens new possibilities. 1. Reduce boilerplate and allows iteration ```rust let is_pressed = gamepads_buttons.iter().any(|buttons| buttons.pressed(GamepadButtonType::South)) ``` 2. ButtonInput/Axis generic methods become ergonomic again ```rust gamepad_buttons.any_just_pressed([GamepadButtonType::Start, GamepadButtonType::Select]) ``` 3. Reduces the number of public components significantly (Gamepad, GamepadSettings, GamepadButtons, GamepadAxes) 4. Components are highly convenient. Gamepad optional features could now be expressed naturally (`Option<Rumble> or Option<Gyro>`), allows devs to attach their own components and filter them, so code like this becomes possible: ```rust fn move_player<const T: usize>( player: Query<&Transform, With<Player<T>>>, gamepads_buttons: Query<&GamepadButtons, With<Player<T>>>, ) { if let Ok(gamepad_buttons) = gamepads_buttons.get_single() { if gamepad_buttons.pressed(GamepadButtonType::South) { // move player } } } ``` --- ## Follow-up - [ ] Run conditions? - [ ] Rumble component # Changelog ## Added TODO ## Changed TODO ## Removed TODO ## Migration Guide TODO --------- Co-authored-by: Carter Anderson <[email protected]>
1 parent 39d6a74 commit e788e3b

File tree

12 files changed

+1666
-798
lines changed

12 files changed

+1666
-798
lines changed

crates/bevy_gilrs/src/converter.rs

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,38 @@
1-
use bevy_input::gamepad::{Gamepad, GamepadAxisType, GamepadButtonType};
1+
use bevy_input::gamepad::{GamepadAxis, GamepadButton};
22

3-
pub fn convert_gamepad_id(gamepad_id: gilrs::GamepadId) -> Gamepad {
4-
Gamepad::new(gamepad_id.into())
5-
}
6-
7-
pub fn convert_button(button: gilrs::Button) -> Option<GamepadButtonType> {
3+
pub fn convert_button(button: gilrs::Button) -> Option<GamepadButton> {
84
match button {
9-
gilrs::Button::South => Some(GamepadButtonType::South),
10-
gilrs::Button::East => Some(GamepadButtonType::East),
11-
gilrs::Button::North => Some(GamepadButtonType::North),
12-
gilrs::Button::West => Some(GamepadButtonType::West),
13-
gilrs::Button::C => Some(GamepadButtonType::C),
14-
gilrs::Button::Z => Some(GamepadButtonType::Z),
15-
gilrs::Button::LeftTrigger => Some(GamepadButtonType::LeftTrigger),
16-
gilrs::Button::LeftTrigger2 => Some(GamepadButtonType::LeftTrigger2),
17-
gilrs::Button::RightTrigger => Some(GamepadButtonType::RightTrigger),
18-
gilrs::Button::RightTrigger2 => Some(GamepadButtonType::RightTrigger2),
19-
gilrs::Button::Select => Some(GamepadButtonType::Select),
20-
gilrs::Button::Start => Some(GamepadButtonType::Start),
21-
gilrs::Button::Mode => Some(GamepadButtonType::Mode),
22-
gilrs::Button::LeftThumb => Some(GamepadButtonType::LeftThumb),
23-
gilrs::Button::RightThumb => Some(GamepadButtonType::RightThumb),
24-
gilrs::Button::DPadUp => Some(GamepadButtonType::DPadUp),
25-
gilrs::Button::DPadDown => Some(GamepadButtonType::DPadDown),
26-
gilrs::Button::DPadLeft => Some(GamepadButtonType::DPadLeft),
27-
gilrs::Button::DPadRight => Some(GamepadButtonType::DPadRight),
5+
gilrs::Button::South => Some(GamepadButton::South),
6+
gilrs::Button::East => Some(GamepadButton::East),
7+
gilrs::Button::North => Some(GamepadButton::North),
8+
gilrs::Button::West => Some(GamepadButton::West),
9+
gilrs::Button::C => Some(GamepadButton::C),
10+
gilrs::Button::Z => Some(GamepadButton::Z),
11+
gilrs::Button::LeftTrigger => Some(GamepadButton::LeftTrigger),
12+
gilrs::Button::LeftTrigger2 => Some(GamepadButton::LeftTrigger2),
13+
gilrs::Button::RightTrigger => Some(GamepadButton::RightTrigger),
14+
gilrs::Button::RightTrigger2 => Some(GamepadButton::RightTrigger2),
15+
gilrs::Button::Select => Some(GamepadButton::Select),
16+
gilrs::Button::Start => Some(GamepadButton::Start),
17+
gilrs::Button::Mode => Some(GamepadButton::Mode),
18+
gilrs::Button::LeftThumb => Some(GamepadButton::LeftThumb),
19+
gilrs::Button::RightThumb => Some(GamepadButton::RightThumb),
20+
gilrs::Button::DPadUp => Some(GamepadButton::DPadUp),
21+
gilrs::Button::DPadDown => Some(GamepadButton::DPadDown),
22+
gilrs::Button::DPadLeft => Some(GamepadButton::DPadLeft),
23+
gilrs::Button::DPadRight => Some(GamepadButton::DPadRight),
2824
gilrs::Button::Unknown => None,
2925
}
3026
}
3127

32-
pub fn convert_axis(axis: gilrs::Axis) -> Option<GamepadAxisType> {
28+
pub fn convert_axis(axis: gilrs::Axis) -> Option<GamepadAxis> {
3329
match axis {
34-
gilrs::Axis::LeftStickX => Some(GamepadAxisType::LeftStickX),
35-
gilrs::Axis::LeftStickY => Some(GamepadAxisType::LeftStickY),
36-
gilrs::Axis::LeftZ => Some(GamepadAxisType::LeftZ),
37-
gilrs::Axis::RightStickX => Some(GamepadAxisType::RightStickX),
38-
gilrs::Axis::RightStickY => Some(GamepadAxisType::RightStickY),
39-
gilrs::Axis::RightZ => Some(GamepadAxisType::RightZ),
30+
gilrs::Axis::LeftStickX => Some(GamepadAxis::LeftStickX),
31+
gilrs::Axis::LeftStickY => Some(GamepadAxis::LeftStickY),
32+
gilrs::Axis::LeftZ => Some(GamepadAxis::LeftZ),
33+
gilrs::Axis::RightStickX => Some(GamepadAxis::RightStickX),
34+
gilrs::Axis::RightStickY => Some(GamepadAxis::RightStickY),
35+
gilrs::Axis::RightZ => Some(GamepadAxis::RightZ),
4036
// The `axis_dpad_to_button` gilrs filter should filter out all DPadX and DPadY events. If
4137
// it doesn't then we probably need an entry added to the following repo and an update to
4238
// GilRs to use the updated database: https://github.com/gabomdq/SDL_GameControllerDB

crates/bevy_gilrs/src/gilrs_system.rs

Lines changed: 69 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,113 @@
11
use crate::{
2-
converter::{convert_axis, convert_button, convert_gamepad_id},
3-
Gilrs,
2+
converter::{convert_axis, convert_button},
3+
Gilrs, GilrsGamepads,
44
};
5+
use bevy_ecs::event::EventWriter;
6+
use bevy_ecs::prelude::Commands;
57
#[cfg(target_arch = "wasm32")]
68
use bevy_ecs::system::NonSendMut;
7-
use bevy_ecs::{
8-
event::EventWriter,
9-
system::{Res, ResMut},
10-
};
11-
use bevy_input::{
12-
gamepad::{
13-
GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnection,
14-
GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadSettings,
15-
},
16-
prelude::{GamepadAxis, GamepadButton},
17-
Axis,
9+
use bevy_ecs::system::ResMut;
10+
use bevy_input::gamepad::{
11+
GamepadConnection, GamepadConnectionEvent, GamepadInfo, RawGamepadAxisChangedEvent,
12+
RawGamepadButtonChangedEvent, RawGamepadEvent,
1813
};
1914
use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter};
2015

2116
pub fn gilrs_event_startup_system(
17+
mut commands: Commands,
2218
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
2319
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
24-
mut events: EventWriter<GamepadEvent>,
20+
mut gamepads: ResMut<GilrsGamepads>,
21+
mut events: EventWriter<GamepadConnectionEvent>,
2522
) {
2623
for (id, gamepad) in gilrs.0.get().gamepads() {
24+
// Create entity and add to mapping
25+
let entity = commands.spawn_empty().id();
26+
gamepads.id_to_entity.insert(id, entity);
27+
gamepads.entity_to_id.insert(entity, id);
28+
2729
let info = GamepadInfo {
2830
name: gamepad.name().into(),
2931
};
3032

31-
events.send(
32-
GamepadConnectionEvent {
33-
gamepad: convert_gamepad_id(id),
34-
connection: GamepadConnection::Connected(info),
35-
}
36-
.into(),
37-
);
33+
events.send(GamepadConnectionEvent {
34+
gamepad: entity,
35+
connection: GamepadConnection::Connected(info),
36+
});
3837
}
3938
}
4039

4140
pub fn gilrs_event_system(
41+
mut commands: Commands,
4242
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
4343
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
44-
mut events: EventWriter<GamepadEvent>,
45-
mut gamepad_buttons: ResMut<Axis<GamepadButton>>,
46-
gamepad_axis: Res<Axis<GamepadAxis>>,
47-
gamepad_settings: Res<GamepadSettings>,
44+
mut gamepads: ResMut<GilrsGamepads>,
45+
mut events: EventWriter<RawGamepadEvent>,
46+
mut connection_events: EventWriter<GamepadConnectionEvent>,
47+
mut button_events: EventWriter<RawGamepadButtonChangedEvent>,
48+
mut axis_event: EventWriter<RawGamepadAxisChangedEvent>,
4849
) {
4950
let gilrs = gilrs.0.get();
5051
while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) {
5152
gilrs.update(&gilrs_event);
52-
53-
let gamepad = convert_gamepad_id(gilrs_event.id);
5453
match gilrs_event.event {
5554
EventType::Connected => {
5655
let pad = gilrs.gamepad(gilrs_event.id);
56+
let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| {
57+
let entity = commands.spawn_empty().id();
58+
gamepads.id_to_entity.insert(gilrs_event.id, entity);
59+
gamepads.entity_to_id.insert(entity, gilrs_event.id);
60+
entity
61+
});
62+
5763
let info = GamepadInfo {
5864
name: pad.name().into(),
5965
};
6066

6167
events.send(
62-
GamepadConnectionEvent::new(gamepad, GamepadConnection::Connected(info)).into(),
68+
GamepadConnectionEvent::new(entity, GamepadConnection::Connected(info.clone()))
69+
.into(),
6370
);
71+
connection_events.send(GamepadConnectionEvent::new(
72+
entity,
73+
GamepadConnection::Connected(info),
74+
));
6475
}
6576
EventType::Disconnected => {
66-
events.send(
67-
GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected).into(),
68-
);
77+
let gamepad = gamepads
78+
.id_to_entity
79+
.get(&gilrs_event.id)
80+
.copied()
81+
.expect("mapping should exist from connection");
82+
let event = GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected);
83+
events.send(event.clone().into());
84+
connection_events.send(event);
6985
}
7086
EventType::ButtonChanged(gilrs_button, raw_value, _) => {
71-
if let Some(button_type) = convert_button(gilrs_button) {
72-
let button = GamepadButton::new(gamepad, button_type);
73-
let old_value = gamepad_buttons.get(button);
74-
let button_settings = gamepad_settings.get_button_axis_settings(button);
75-
76-
// Only send events that pass the user-defined change threshold
77-
if let Some(filtered_value) = button_settings.filter(raw_value, old_value) {
78-
events.send(
79-
GamepadButtonChangedEvent::new(gamepad, button_type, filtered_value)
80-
.into(),
81-
);
82-
// Update the current value prematurely so that `old_value` is correct in
83-
// future iterations of the loop.
84-
gamepad_buttons.set(button, filtered_value);
85-
}
86-
}
87+
let Some(button) = convert_button(gilrs_button) else {
88+
continue;
89+
};
90+
let gamepad = gamepads
91+
.id_to_entity
92+
.get(&gilrs_event.id)
93+
.copied()
94+
.expect("mapping should exist from connection");
95+
events.send(RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into());
96+
button_events.send(RawGamepadButtonChangedEvent::new(
97+
gamepad, button, raw_value,
98+
));
8799
}
88100
EventType::AxisChanged(gilrs_axis, raw_value, _) => {
89-
if let Some(axis_type) = convert_axis(gilrs_axis) {
90-
let axis = GamepadAxis::new(gamepad, axis_type);
91-
let old_value = gamepad_axis.get(axis);
92-
let axis_settings = gamepad_settings.get_axis_settings(axis);
93-
94-
// Only send events that pass the user-defined change threshold
95-
if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) {
96-
events.send(
97-
GamepadAxisChangedEvent::new(gamepad, axis_type, filtered_value).into(),
98-
);
99-
}
100-
}
101+
let Some(axis) = convert_axis(gilrs_axis) else {
102+
continue;
103+
};
104+
let gamepad = gamepads
105+
.id_to_entity
106+
.get(&gilrs_event.id)
107+
.copied()
108+
.expect("mapping should exist from connection");
109+
events.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into());
110+
axis_event.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value));
101111
}
102112
_ => (),
103113
};

crates/bevy_gilrs/src/lib.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,38 @@ mod gilrs_system;
1515
mod rumble;
1616

1717
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
18+
use bevy_ecs::entity::EntityHashMap;
1819
use bevy_ecs::prelude::*;
1920
use bevy_input::InputSystem;
20-
use bevy_utils::{synccell::SyncCell, tracing::error};
21+
use bevy_utils::{synccell::SyncCell, tracing::error, HashMap};
2122
use gilrs::GilrsBuilder;
2223
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
2324
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
2425

2526
#[cfg_attr(not(target_arch = "wasm32"), derive(Resource))]
2627
pub(crate) struct Gilrs(pub SyncCell<gilrs::Gilrs>);
2728

29+
/// A [`resource`](Resource) with the mapping of connected [`gilrs::GamepadId`] and their [`Entity`].
30+
#[derive(Debug, Default, Resource)]
31+
pub(crate) struct GilrsGamepads {
32+
/// Mapping of [`Entity`] to [`gilrs::GamepadId`].
33+
pub(crate) entity_to_id: EntityHashMap<gilrs::GamepadId>,
34+
/// Mapping of [`gilrs::GamepadId`] to [`Entity`].
35+
pub(crate) id_to_entity: HashMap<gilrs::GamepadId, Entity>,
36+
}
37+
38+
impl GilrsGamepads {
39+
/// Returns the [`Entity`] assigned to a connected [`gilrs::GamepadId`].
40+
pub fn get_entity(&self, gamepad_id: gilrs::GamepadId) -> Option<Entity> {
41+
self.id_to_entity.get(&gamepad_id).copied()
42+
}
43+
44+
/// Returns the [`gilrs::GamepadId`] assigned to a gamepad [`Entity`].
45+
pub fn get_gamepad_id(&self, entity: Entity) -> Option<gilrs::GamepadId> {
46+
self.entity_to_id.get(&entity).copied()
47+
}
48+
}
49+
2850
/// Plugin that provides gamepad handling to an [`App`].
2951
#[derive(Default)]
3052
pub struct GilrsPlugin;
@@ -45,7 +67,7 @@ impl Plugin for GilrsPlugin {
4567
app.insert_non_send_resource(Gilrs(SyncCell::new(gilrs)));
4668
#[cfg(not(target_arch = "wasm32"))]
4769
app.insert_resource(Gilrs(SyncCell::new(gilrs)));
48-
70+
app.init_resource::<GilrsGamepads>();
4971
app.init_resource::<RunningRumbleEffects>()
5072
.add_systems(PreStartup, gilrs_event_startup_system)
5173
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))

crates/bevy_gilrs/src/rumble.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! Handle user specified rumble request events.
2-
use crate::Gilrs;
2+
use crate::{Gilrs, GilrsGamepads};
33
use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource};
44
#[cfg(target_arch = "wasm32")]
55
use bevy_ecs::system::NonSendMut;
@@ -16,8 +16,6 @@ use gilrs::{
1616
};
1717
use thiserror::Error;
1818

19-
use crate::converter::convert_gamepad_id;
20-
2119
/// A rumble effect that is currently in effect.
2220
struct RunningRumble {
2321
/// Duration from app startup when this effect will be finished
@@ -84,14 +82,15 @@ fn get_base_effects(
8482
fn handle_rumble_request(
8583
running_rumbles: &mut RunningRumbleEffects,
8684
gilrs: &mut gilrs::Gilrs,
85+
gamepads: &GilrsGamepads,
8786
rumble: GamepadRumbleRequest,
8887
current_time: Duration,
8988
) -> Result<(), RumbleError> {
9089
let gamepad = rumble.gamepad();
9190

9291
let (gamepad_id, _) = gilrs
9392
.gamepads()
94-
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
93+
.find(|(pad_id, _)| *pad_id == gamepads.get_gamepad_id(gamepad).unwrap())
9594
.ok_or(RumbleError::GamepadNotFound)?;
9695

9796
match rumble {
@@ -129,6 +128,7 @@ pub(crate) fn play_gilrs_rumble(
129128
time: Res<Time<Real>>,
130129
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
131130
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
131+
gamepads: Res<GilrsGamepads>,
132132
mut requests: EventReader<GamepadRumbleRequest>,
133133
mut running_rumbles: ResMut<RunningRumbleEffects>,
134134
) {
@@ -146,7 +146,7 @@ pub(crate) fn play_gilrs_rumble(
146146
// Add new effects.
147147
for rumble in requests.read().cloned() {
148148
let gamepad = rumble.gamepad();
149-
match handle_rumble_request(&mut running_rumbles, gilrs, rumble, current_time) {
149+
match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) {
150150
Ok(()) => {}
151151
Err(RumbleError::GilrsError(err)) => {
152152
if let ff::Error::FfNotSupported(_) = err {

0 commit comments

Comments
 (0)