Skip to content

Commit

Permalink
Implement gamepads as entities (#12770)
Browse files Browse the repository at this point in the history
# 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 <mcanders1@gmail.com>
  • Loading branch information
s-puig and cart authored Sep 27, 2024
1 parent 39d6a74 commit e788e3b
Show file tree
Hide file tree
Showing 12 changed files with 1,666 additions and 798 deletions.
60 changes: 28 additions & 32 deletions crates/bevy_gilrs/src/converter.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
use bevy_input::gamepad::{Gamepad, GamepadAxisType, GamepadButtonType};
use bevy_input::gamepad::{GamepadAxis, GamepadButton};

pub fn convert_gamepad_id(gamepad_id: gilrs::GamepadId) -> Gamepad {
Gamepad::new(gamepad_id.into())
}

pub fn convert_button(button: gilrs::Button) -> Option<GamepadButtonType> {
pub fn convert_button(button: gilrs::Button) -> Option<GamepadButton> {
match button {
gilrs::Button::South => Some(GamepadButtonType::South),
gilrs::Button::East => Some(GamepadButtonType::East),
gilrs::Button::North => Some(GamepadButtonType::North),
gilrs::Button::West => Some(GamepadButtonType::West),
gilrs::Button::C => Some(GamepadButtonType::C),
gilrs::Button::Z => Some(GamepadButtonType::Z),
gilrs::Button::LeftTrigger => Some(GamepadButtonType::LeftTrigger),
gilrs::Button::LeftTrigger2 => Some(GamepadButtonType::LeftTrigger2),
gilrs::Button::RightTrigger => Some(GamepadButtonType::RightTrigger),
gilrs::Button::RightTrigger2 => Some(GamepadButtonType::RightTrigger2),
gilrs::Button::Select => Some(GamepadButtonType::Select),
gilrs::Button::Start => Some(GamepadButtonType::Start),
gilrs::Button::Mode => Some(GamepadButtonType::Mode),
gilrs::Button::LeftThumb => Some(GamepadButtonType::LeftThumb),
gilrs::Button::RightThumb => Some(GamepadButtonType::RightThumb),
gilrs::Button::DPadUp => Some(GamepadButtonType::DPadUp),
gilrs::Button::DPadDown => Some(GamepadButtonType::DPadDown),
gilrs::Button::DPadLeft => Some(GamepadButtonType::DPadLeft),
gilrs::Button::DPadRight => Some(GamepadButtonType::DPadRight),
gilrs::Button::South => Some(GamepadButton::South),
gilrs::Button::East => Some(GamepadButton::East),
gilrs::Button::North => Some(GamepadButton::North),
gilrs::Button::West => Some(GamepadButton::West),
gilrs::Button::C => Some(GamepadButton::C),
gilrs::Button::Z => Some(GamepadButton::Z),
gilrs::Button::LeftTrigger => Some(GamepadButton::LeftTrigger),
gilrs::Button::LeftTrigger2 => Some(GamepadButton::LeftTrigger2),
gilrs::Button::RightTrigger => Some(GamepadButton::RightTrigger),
gilrs::Button::RightTrigger2 => Some(GamepadButton::RightTrigger2),
gilrs::Button::Select => Some(GamepadButton::Select),
gilrs::Button::Start => Some(GamepadButton::Start),
gilrs::Button::Mode => Some(GamepadButton::Mode),
gilrs::Button::LeftThumb => Some(GamepadButton::LeftThumb),
gilrs::Button::RightThumb => Some(GamepadButton::RightThumb),
gilrs::Button::DPadUp => Some(GamepadButton::DPadUp),
gilrs::Button::DPadDown => Some(GamepadButton::DPadDown),
gilrs::Button::DPadLeft => Some(GamepadButton::DPadLeft),
gilrs::Button::DPadRight => Some(GamepadButton::DPadRight),
gilrs::Button::Unknown => None,
}
}

pub fn convert_axis(axis: gilrs::Axis) -> Option<GamepadAxisType> {
pub fn convert_axis(axis: gilrs::Axis) -> Option<GamepadAxis> {
match axis {
gilrs::Axis::LeftStickX => Some(GamepadAxisType::LeftStickX),
gilrs::Axis::LeftStickY => Some(GamepadAxisType::LeftStickY),
gilrs::Axis::LeftZ => Some(GamepadAxisType::LeftZ),
gilrs::Axis::RightStickX => Some(GamepadAxisType::RightStickX),
gilrs::Axis::RightStickY => Some(GamepadAxisType::RightStickY),
gilrs::Axis::RightZ => Some(GamepadAxisType::RightZ),
gilrs::Axis::LeftStickX => Some(GamepadAxis::LeftStickX),
gilrs::Axis::LeftStickY => Some(GamepadAxis::LeftStickY),
gilrs::Axis::LeftZ => Some(GamepadAxis::LeftZ),
gilrs::Axis::RightStickX => Some(GamepadAxis::RightStickX),
gilrs::Axis::RightStickY => Some(GamepadAxis::RightStickY),
gilrs::Axis::RightZ => Some(GamepadAxis::RightZ),
// The `axis_dpad_to_button` gilrs filter should filter out all DPadX and DPadY events. If
// it doesn't then we probably need an entry added to the following repo and an update to
// GilRs to use the updated database: https://github.com/gabomdq/SDL_GameControllerDB
Expand Down
128 changes: 69 additions & 59 deletions crates/bevy_gilrs/src/gilrs_system.rs
Original file line number Diff line number Diff line change
@@ -1,103 +1,113 @@
use crate::{
converter::{convert_axis, convert_button, convert_gamepad_id},
Gilrs,
converter::{convert_axis, convert_button},
Gilrs, GilrsGamepads,
};
use bevy_ecs::event::EventWriter;
use bevy_ecs::prelude::Commands;
#[cfg(target_arch = "wasm32")]
use bevy_ecs::system::NonSendMut;
use bevy_ecs::{
event::EventWriter,
system::{Res, ResMut},
};
use bevy_input::{
gamepad::{
GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnection,
GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadSettings,
},
prelude::{GamepadAxis, GamepadButton},
Axis,
use bevy_ecs::system::ResMut;
use bevy_input::gamepad::{
GamepadConnection, GamepadConnectionEvent, GamepadInfo, RawGamepadAxisChangedEvent,
RawGamepadButtonChangedEvent, RawGamepadEvent,
};
use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter};

pub fn gilrs_event_startup_system(
mut commands: Commands,
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
mut events: EventWriter<GamepadEvent>,
mut gamepads: ResMut<GilrsGamepads>,
mut events: EventWriter<GamepadConnectionEvent>,
) {
for (id, gamepad) in gilrs.0.get().gamepads() {
// Create entity and add to mapping
let entity = commands.spawn_empty().id();
gamepads.id_to_entity.insert(id, entity);
gamepads.entity_to_id.insert(entity, id);

let info = GamepadInfo {
name: gamepad.name().into(),
};

events.send(
GamepadConnectionEvent {
gamepad: convert_gamepad_id(id),
connection: GamepadConnection::Connected(info),
}
.into(),
);
events.send(GamepadConnectionEvent {
gamepad: entity,
connection: GamepadConnection::Connected(info),
});
}
}

pub fn gilrs_event_system(
mut commands: Commands,
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
mut events: EventWriter<GamepadEvent>,
mut gamepad_buttons: ResMut<Axis<GamepadButton>>,
gamepad_axis: Res<Axis<GamepadAxis>>,
gamepad_settings: Res<GamepadSettings>,
mut gamepads: ResMut<GilrsGamepads>,
mut events: EventWriter<RawGamepadEvent>,
mut connection_events: EventWriter<GamepadConnectionEvent>,
mut button_events: EventWriter<RawGamepadButtonChangedEvent>,
mut axis_event: EventWriter<RawGamepadAxisChangedEvent>,
) {
let gilrs = gilrs.0.get();
while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) {
gilrs.update(&gilrs_event);

let gamepad = convert_gamepad_id(gilrs_event.id);
match gilrs_event.event {
EventType::Connected => {
let pad = gilrs.gamepad(gilrs_event.id);
let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| {
let entity = commands.spawn_empty().id();
gamepads.id_to_entity.insert(gilrs_event.id, entity);
gamepads.entity_to_id.insert(entity, gilrs_event.id);
entity
});

let info = GamepadInfo {
name: pad.name().into(),
};

events.send(
GamepadConnectionEvent::new(gamepad, GamepadConnection::Connected(info)).into(),
GamepadConnectionEvent::new(entity, GamepadConnection::Connected(info.clone()))
.into(),
);
connection_events.send(GamepadConnectionEvent::new(
entity,
GamepadConnection::Connected(info),
));
}
EventType::Disconnected => {
events.send(
GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected).into(),
);
let gamepad = gamepads
.id_to_entity
.get(&gilrs_event.id)
.copied()
.expect("mapping should exist from connection");
let event = GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected);
events.send(event.clone().into());
connection_events.send(event);
}
EventType::ButtonChanged(gilrs_button, raw_value, _) => {
if let Some(button_type) = convert_button(gilrs_button) {
let button = GamepadButton::new(gamepad, button_type);
let old_value = gamepad_buttons.get(button);
let button_settings = gamepad_settings.get_button_axis_settings(button);

// Only send events that pass the user-defined change threshold
if let Some(filtered_value) = button_settings.filter(raw_value, old_value) {
events.send(
GamepadButtonChangedEvent::new(gamepad, button_type, filtered_value)
.into(),
);
// Update the current value prematurely so that `old_value` is correct in
// future iterations of the loop.
gamepad_buttons.set(button, filtered_value);
}
}
let Some(button) = convert_button(gilrs_button) else {
continue;
};
let gamepad = gamepads
.id_to_entity
.get(&gilrs_event.id)
.copied()
.expect("mapping should exist from connection");
events.send(RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into());
button_events.send(RawGamepadButtonChangedEvent::new(
gamepad, button, raw_value,
));
}
EventType::AxisChanged(gilrs_axis, raw_value, _) => {
if let Some(axis_type) = convert_axis(gilrs_axis) {
let axis = GamepadAxis::new(gamepad, axis_type);
let old_value = gamepad_axis.get(axis);
let axis_settings = gamepad_settings.get_axis_settings(axis);

// Only send events that pass the user-defined change threshold
if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) {
events.send(
GamepadAxisChangedEvent::new(gamepad, axis_type, filtered_value).into(),
);
}
}
let Some(axis) = convert_axis(gilrs_axis) else {
continue;
};
let gamepad = gamepads
.id_to_entity
.get(&gilrs_event.id)
.copied()
.expect("mapping should exist from connection");
events.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into());
axis_event.send(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value));
}
_ => (),
};
Expand Down
26 changes: 24 additions & 2 deletions crates/bevy_gilrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,38 @@ mod gilrs_system;
mod rumble;

use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
use bevy_ecs::entity::EntityHashMap;
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_utils::{synccell::SyncCell, tracing::error};
use bevy_utils::{synccell::SyncCell, tracing::error, HashMap};
use gilrs::GilrsBuilder;
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
use rumble::{play_gilrs_rumble, RunningRumbleEffects};

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

/// A [`resource`](Resource) with the mapping of connected [`gilrs::GamepadId`] and their [`Entity`].
#[derive(Debug, Default, Resource)]
pub(crate) struct GilrsGamepads {
/// Mapping of [`Entity`] to [`gilrs::GamepadId`].
pub(crate) entity_to_id: EntityHashMap<gilrs::GamepadId>,
/// Mapping of [`gilrs::GamepadId`] to [`Entity`].
pub(crate) id_to_entity: HashMap<gilrs::GamepadId, Entity>,
}

impl GilrsGamepads {
/// Returns the [`Entity`] assigned to a connected [`gilrs::GamepadId`].
pub fn get_entity(&self, gamepad_id: gilrs::GamepadId) -> Option<Entity> {
self.id_to_entity.get(&gamepad_id).copied()
}

/// Returns the [`gilrs::GamepadId`] assigned to a gamepad [`Entity`].
pub fn get_gamepad_id(&self, entity: Entity) -> Option<gilrs::GamepadId> {
self.entity_to_id.get(&entity).copied()
}
}

/// Plugin that provides gamepad handling to an [`App`].
#[derive(Default)]
pub struct GilrsPlugin;
Expand All @@ -45,7 +67,7 @@ impl Plugin for GilrsPlugin {
app.insert_non_send_resource(Gilrs(SyncCell::new(gilrs)));
#[cfg(not(target_arch = "wasm32"))]
app.insert_resource(Gilrs(SyncCell::new(gilrs)));

app.init_resource::<GilrsGamepads>();
app.init_resource::<RunningRumbleEffects>()
.add_systems(PreStartup, gilrs_event_startup_system)
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
Expand Down
10 changes: 5 additions & 5 deletions crates/bevy_gilrs/src/rumble.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Handle user specified rumble request events.
use crate::Gilrs;
use crate::{Gilrs, GilrsGamepads};
use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource};
#[cfg(target_arch = "wasm32")]
use bevy_ecs::system::NonSendMut;
Expand All @@ -16,8 +16,6 @@ use 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
Expand Down Expand Up @@ -84,14 +82,15 @@ fn get_base_effects(
fn handle_rumble_request(
running_rumbles: &mut RunningRumbleEffects,
gilrs: &mut gilrs::Gilrs,
gamepads: &GilrsGamepads,
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)
.find(|(pad_id, _)| *pad_id == gamepads.get_gamepad_id(gamepad).unwrap())
.ok_or(RumbleError::GamepadNotFound)?;

match rumble {
Expand Down Expand Up @@ -129,6 +128,7 @@ pub(crate) fn play_gilrs_rumble(
time: Res<Time<Real>>,
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
gamepads: Res<GilrsGamepads>,
mut requests: EventReader<GamepadRumbleRequest>,
mut running_rumbles: ResMut<RunningRumbleEffects>,
) {
Expand All @@ -146,7 +146,7 @@ pub(crate) fn play_gilrs_rumble(
// Add new effects.
for rumble in requests.read().cloned() {
let gamepad = rumble.gamepad();
match handle_rumble_request(&mut running_rumbles, gilrs, rumble, current_time) {
match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) {
Ok(()) => {}
Err(RumbleError::GilrsError(err)) => {
if let ff::Error::FfNotSupported(_) = err {
Expand Down
Loading

0 comments on commit e788e3b

Please sign in to comment.