#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] //! Systems and type definitions for gamepad handling in Bevy. //! //! This crate is built on top of [GilRs](gilrs), a library //! that handles abstracting over platform-specific gamepad APIs. mod converter; mod gilrs_system; mod rumble; #[cfg(not(target_arch = "wasm32"))] use bevy_utils::synccell::SyncCell; #[cfg(target_arch = "wasm32")] use core::cell::RefCell; use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate}; use bevy_ecs::entity::hash_map::EntityHashMap; use bevy_ecs::prelude::*; use bevy_input::InputSystem; use bevy_platform_support::collections::HashMap; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; use rumble::{play_gilrs_rumble, RunningRumbleEffects}; use tracing::error; #[cfg(target_arch = "wasm32")] thread_local! { /// Temporary storage of gilrs data to replace usage of `!Send` resources. This will be replaced with proper /// storage of `!Send` data after issue #17667 is complete. /// /// Using a `thread_local!` here relies on the fact that wasm32 can only be single threaded. Previously, we used a /// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send` /// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so /// we need to rely on the platform to make such a guarantee. static GILRS: RefCell> = const { RefCell::new(None) }; } #[derive(Resource)] pub(crate) struct Gilrs { #[cfg(not(target_arch = "wasm32"))] cell: SyncCell, } impl Gilrs { #[inline] pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) { #[cfg(target_arch = "wasm32")] GILRS.with(|g| f(g.borrow_mut().as_mut().expect("GILRS was not initialized"))); #[cfg(not(target_arch = "wasm32"))] f(self.cell.get()); } } /// 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, /// Mapping of [`gilrs::GamepadId`] to [`Entity`]. pub(crate) id_to_entity: HashMap, } impl GilrsGamepads { /// Returns the [`Entity`] assigned to a connected [`gilrs::GamepadId`]. pub fn get_entity(&self, gamepad_id: gilrs::GamepadId) -> Option { 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 { self.entity_to_id.get(&entity).copied() } } /// Plugin that provides gamepad handling to an [`App`]. #[derive(Default)] pub struct GilrsPlugin; /// Updates the running gamepad rumble effects. #[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)] pub struct RumbleSystem; impl Plugin for GilrsPlugin { fn build(&self, app: &mut App) { match GilrsBuilder::new() .with_default_filters(false) .set_update_state(false) .build() { Ok(gilrs) => { let g = Gilrs { #[cfg(not(target_arch = "wasm32"))] cell: SyncCell::new(gilrs), }; #[cfg(target_arch = "wasm32")] GILRS.with(|g| { g.replace(Some(gilrs)); }); app.insert_resource(g); app.init_resource::(); app.init_resource::() .add_systems(PreStartup, gilrs_event_startup_system) .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), } } }