From b4e480954ffcd0d31436ef6006355f634c8ec422 Mon Sep 17 00:00:00 2001 From: yukkop Date: Fri, 4 Sep 2026 14:10:08 +0000 Subject: [PATCH] feat: return logical keys that does not works on every layout --- Cargo.lock | 1 + crate/bevy_controls/Cargo.toml | 5 +- crate/bevy_controls/example/basic/Cargo.toml | 4 +- crate/bevy_controls/example/basic/src/main.rs | 32 +++++---- crate/bevy_controls/src/plugin.rs | 72 +++++++++++++++---- crate/bevy_controls/src/resource.rs | 33 +++++++-- 6 files changed, 110 insertions(+), 37 deletions(-) mode change 100644 => 100755 crate/bevy_controls/example/basic/Cargo.toml mode change 100644 => 100755 crate/bevy_controls/src/resource.rs diff --git a/Cargo.lock b/Cargo.lock index 8510659..8d343c2 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,7 @@ dependencies = [ "bevy_derive", "bevy_ecs", "bevy_input", + "bevy_platform", "bevy_reflect", "bevy_state", "env_logger", diff --git a/crate/bevy_controls/Cargo.toml b/crate/bevy_controls/Cargo.toml index 916eac6..beb122c 100755 --- a/crate/bevy_controls/Cargo.toml +++ b/crate/bevy_controls/Cargo.toml @@ -3,8 +3,9 @@ name = "bevy_controls" version = "0.1.0" edition = "2021" -[features] -default = [] +[features] +default = [] +logical-keyboard = [] serialize = ["bevy_input/serialize", "serde"] reflect = ["dep:bevy_reflect"] inspector-egui = ["reflect", "dep:bevy-inspector-egui"] diff --git a/crate/bevy_controls/example/basic/Cargo.toml b/crate/bevy_controls/example/basic/Cargo.toml old mode 100644 new mode 100755 index 900270e..00c2081 --- a/crate/bevy_controls/example/basic/Cargo.toml +++ b/crate/bevy_controls/example/basic/Cargo.toml @@ -22,7 +22,9 @@ bevy = { version = "0.16.1", default-features = false, features = [ #"dynamic_linking", "default_font", ]} -bevy_controls = { path = "./../../" } +bevy_controls = { path = "./../../", features = [ + "logical-keyboard", +]} bevy_controls_derive = { path = "./../../../bevy_controls_derive" } strum_macros = "0.26.2" strum = "0.26.2" diff --git a/crate/bevy_controls/example/basic/src/main.rs b/crate/bevy_controls/example/basic/src/main.rs index 9111a55..c6fbfc9 100755 --- a/crate/bevy_controls/example/basic/src/main.rs +++ b/crate/bevy_controls/example/basic/src/main.rs @@ -8,12 +8,13 @@ // TODO strum redefine in bevy_controls use bevy::prelude::*; +use bevy::input::keyboard::Key; use bevy_controls::{ contract::InputsContainer, plugin::ControlsPlugin, resource::{ ActivationMode, ActivationOptions, AxisName, Binding, BindingCondition, BindingConfig, - Bindings, ButtonCombination, Controls, InputType, InputValue, MouseInput, OptionsMode, + Bindings, ButtonCombination, Controls, InputType, InputValue, Keyboard, MouseInput, OptionsMode, PlayerActions, }, }; @@ -81,13 +82,14 @@ fn main() { .with( MyAction::Left, BindingConfig::new(Bindings::new(vec![ - // this way input will work only on latin layout only on `A`, because it uses `Keyboard::KeyCode` + // `KeyCode` binds physical key location, so this is the US-layout A position. Binding::new(ButtonCombination::Single(InputType::Keyboard( - KeyCode::KeyA, + Keyboard::KeyCode(KeyCode::KeyA), ))) .with_condition(BindingCondition::InGameState(MyGameState::InGame)), + // `Key` binds logical text, so this activates when the current layout produces h. Binding::new(ButtonCombination::Single(InputType::Keyboard( - KeyCode::KeyH, + Keyboard::Key(Key::Character("h".into())), ))) .with_condition(BindingCondition::InGameState(MyGameState::InGame)), ])) @@ -99,27 +101,29 @@ fn main() { .with( MyAction::Back, BindingConfig::from_vec(vec![ - Binding::from_single(InputType::Keyboard(KeyCode::KeyS)), - Binding::from_single(InputType::Keyboard(KeyCode::KeyJ)), + Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(KeyCode::KeyS))), + Binding::from_single(InputType::Keyboard(Keyboard::Key(Key::Character("j".into())))), ]), ) .with( MyAction::Forward, BindingConfig::from_vec(vec![ - Binding::from_single(InputType::Keyboard(KeyCode::KeyW)), - Binding::from_single(InputType::Keyboard(KeyCode::KeyK)), + Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(KeyCode::KeyW))), + Binding::from_single(InputType::Keyboard(Keyboard::Key(Key::Character("k".into())))), ]), ) .with( MyAction::Right, BindingConfig::from_vec(vec![ - Binding::from_single(InputType::Keyboard(KeyCode::KeyD)), - Binding::from_single(InputType::Keyboard(KeyCode::KeyL)), + Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(KeyCode::KeyD))), + Binding::from_single(InputType::Keyboard(Keyboard::Key(Key::Character("l".into())))), ]), ) .with( MyAction::Up, - BindingConfig::from_bind(Binding::from_single(InputType::Keyboard(KeyCode::Space))), + BindingConfig::from_bind(Binding::from_single(InputType::Keyboard(Keyboard::KeyCode( + KeyCode::Space, + )))), ) .with( MyAction::MouseHorizontal, @@ -136,8 +140,8 @@ fn main() { .with( MyAction::Down, BindingConfig::from_bind(Binding::from_single( - InputType::Keyboard(KeyCode::ShiftLeft), - //InputType::Keyboard(KeyCode::Space), + InputType::Keyboard(Keyboard::KeyCode(KeyCode::ShiftLeft)), + //InputType::Keyboard(Keyboard::KeyCode(KeyCode::Space)), )), ) .build(), @@ -181,7 +185,7 @@ fn setup(mut commands: Commands) { commands .spawn(( Text::new( - "Info: this example is intended to show basic crate capabilities and limitations\nso WASD work only on US layout, but HJKL may work on any layout\nAction: ", + "Info: Important that WASD use physical key positions and HJKL use logical characters from current layout\nAction: ", ), TextFont { font_size: TEXT_SIZE, diff --git a/crate/bevy_controls/src/plugin.rs b/crate/bevy_controls/src/plugin.rs index 4481d4d..618e994 100755 --- a/crate/bevy_controls/src/plugin.rs +++ b/crate/bevy_controls/src/plugin.rs @@ -1,19 +1,25 @@ use bevy_app::{App, Plugin, PreUpdate}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::system::{In, IntoSystem}; +use bevy_ecs::event::EventReader; use bevy_ecs::{ - event::EventReader, prelude::Resource, - system::{Res, ResMut}, + system::{In, IntoSystem, Res, ResMut}, }; use bevy_input::{ keyboard::KeyCode, mouse::{MouseButton, MouseMotion, MouseScrollUnit, MouseWheel}, ButtonInput, }; +#[cfg(feature = "logical-keyboard")] +use bevy_input::{ + keyboard::{Key, KeyboardInput}, + ButtonState, +}; use bevy_state::app::AppExtStates; use bevy_state::state::State; use std::collections::HashMap; +#[cfg(feature = "logical-keyboard")] +use std::collections::HashSet; use crate::{ contract::{Action, GameState, InputsContainer}, @@ -52,6 +58,10 @@ impl, Gs: GameState> ControlsPlugin #[derive(Resource, Default, Debug, Deref, DerefMut)] struct TrigeredInputs(HashMap); +#[cfg(feature = "logical-keyboard")] +#[derive(Resource, Default, Debug, Deref, DerefMut)] +struct PressedLogicalKeys(HashSet); + #[cfg(not(feature = "reflect"))] impl, Gs: GameState> Plugin for ControlsPlugin { fn build(&self, app: &mut App) { @@ -59,11 +69,15 @@ impl, Gs: GameState> Plugin for ControlsPlugin .insert_resource(self.controls.clone()) .init_resource::() .init_resource::() - .insert_state::(Gs::default()) - .add_systems( - PreUpdate, - Self::collect_inputs.pipe(Self::fill_players_inputs), - ); + .insert_state::(Gs::default()); + + #[cfg(feature = "logical-keyboard")] + app.init_resource::(); + + app.add_systems( + PreUpdate, + Self::collect_inputs.pipe(Self::fill_players_inputs), + ); } } @@ -80,11 +94,15 @@ where .insert_resource(self.controls.clone()) .init_resource::() .init_resource::() - .insert_state::(Gs::default()) - .add_systems( - PreUpdate, - Self::collect_inputs.pipe(Self::fill_players_inputs), - ); + .insert_state::(Gs::default()); + + #[cfg(feature = "logical-keyboard")] + app.init_resource::(); + + app.add_systems( + PreUpdate, + Self::collect_inputs.pipe(Self::fill_players_inputs), + ); #[cfg(feature = "reflect")] app.register_type::>(); @@ -101,6 +119,8 @@ impl, Gs: GameState> ControlsPlugin /// fn collect_inputs( keyboard_button: Res>, + #[cfg(feature = "logical-keyboard")] mut keyboard_evr: EventReader, + #[cfg(feature = "logical-keyboard")] mut pressed_logical_keys: ResMut, mouse_buttons: Res>, mut scroll_evr: EventReader, mut motion_evr: EventReader, @@ -108,9 +128,32 @@ impl, Gs: GameState> ControlsPlugin ) -> TrigeredInputs { let mut collected_inputs = TrigeredInputs(HashMap::new()); + #[cfg(feature = "logical-keyboard")] + for event in keyboard_evr.read() { + match event.state { + ButtonState::Pressed => { + pressed_logical_keys.insert(event.logical_key.clone()); + } + ButtonState::Released => { + pressed_logical_keys.remove(&event.logical_key); + } + } + } + // Collect pressed keyboard buttons for key in keyboard_button.get_pressed() { - collected_inputs.insert(InputType::Keyboard(*key), InputValue::Boolean(true)); + collected_inputs.insert( + InputType::Keyboard(Keyboard::KeyCode(*key)), + InputValue::Boolean(true), + ); + } + + #[cfg(feature = "logical-keyboard")] + for key in pressed_logical_keys.iter() { + collected_inputs.insert( + InputType::Keyboard(Keyboard::Key(key.clone())), + InputValue::Boolean(true), + ); } // buttons.get_just_released() @@ -178,7 +221,6 @@ impl, Gs: GameState> ControlsPlugin // Iterate over the control bindings and collect all action-binding pairs for binding in config.bindings.iter() { - // Skip any binding with mismatched condition if binding .conditions diff --git a/crate/bevy_controls/src/resource.rs b/crate/bevy_controls/src/resource.rs old mode 100644 new mode 100755 index 4a6c7d8..5c5c233 --- a/crate/bevy_controls/src/resource.rs +++ b/crate/bevy_controls/src/resource.rs @@ -2,15 +2,17 @@ use std::error; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::Resource; +#[cfg(feature = "logical-keyboard")] +use bevy_input::keyboard::Key; use bevy_input::{keyboard::KeyCode, mouse::MouseButton}; #[cfg(feature = "inspector-egui")] use bevy_inspector_egui::prelude::*; +use bevy_platform::collections::HashMap; #[cfg(all(feature = "reflect", feature = "serialize"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use log::warn; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -use bevy_platform::collections::HashMap; use std::mem::discriminant; use strum::IntoEnumIterator; #[cfg(feature = "reflect")] @@ -194,10 +196,18 @@ impl PlayerActions { // TODO: add option on exlude or not exclude if used common_traits_conditions! { - #[derive(Debug, PartialEq, Clone, Eq, Hash)] - pub enum InputType { - Keyboard(KeyCode), - Mouse(MouseInput), + /// Keyboard input identified by physical location or logical meaning. + #[derive(Debug, PartialEq, Clone, Eq, Hash)] + pub enum Keyboard { + KeyCode(KeyCode), + #[cfg(feature = "logical-keyboard")] + Key(Key), + } + + #[derive(Debug, PartialEq, Clone, Eq, Hash)] + pub enum InputType { + Keyboard(Keyboard), + Mouse(MouseInput), // TODO: Gamepad(GamepadButtonType), // TODO: Touch screen // TODO: TouchPad inputs @@ -694,3 +704,16 @@ impl Controls { self.0.iter() } } + +#[cfg(all(test, feature = "logical-keyboard"))] +mod tests { + use super::*; + + #[test] + fn keyboard_variants_are_distinct() { + let physical = InputType::Keyboard(Keyboard::KeyCode(KeyCode::KeyH)); + let logical = InputType::Keyboard(Keyboard::Key(Key::Character("h".into()))); + + assert_ne!(physical, logical); + } +}