feat: return logical keys that does not works on every layout

This commit is contained in:
2026-09-04 14:10:08 +00:00
parent 22333c0c8c
commit b4e480954f
6 changed files with 110 additions and 37 deletions
+3 -2
View File
@@ -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"]
+3 -1
View File
@@ -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"
+18 -14
View File
@@ -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,
+57 -15
View File
@@ -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<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
#[derive(Resource, Default, Debug, Deref, DerefMut)]
struct TrigeredInputs(HashMap<InputType, InputValue>);
#[cfg(feature = "logical-keyboard")]
#[derive(Resource, Default, Debug, Deref, DerefMut)]
struct PressedLogicalKeys(HashSet<Key>);
#[cfg(not(feature = "reflect"))]
impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Plugin for ControlsPlugin<A, Ic, Gs> {
fn build(&self, app: &mut App) {
@@ -59,11 +69,15 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Plugin for ControlsPlugin
.insert_resource(self.controls.clone())
.init_resource::<Ic>()
.init_resource::<TrigeredInputs>()
.insert_state::<Gs>(Gs::default())
.add_systems(
PreUpdate,
Self::collect_inputs.pipe(Self::fill_players_inputs),
);
.insert_state::<Gs>(Gs::default());
#[cfg(feature = "logical-keyboard")]
app.init_resource::<PressedLogicalKeys>();
app.add_systems(
PreUpdate,
Self::collect_inputs.pipe(Self::fill_players_inputs),
);
}
}
@@ -80,11 +94,15 @@ where
.insert_resource(self.controls.clone())
.init_resource::<Ic>()
.init_resource::<TrigeredInputs>()
.insert_state::<Gs>(Gs::default())
.add_systems(
PreUpdate,
Self::collect_inputs.pipe(Self::fill_players_inputs),
);
.insert_state::<Gs>(Gs::default());
#[cfg(feature = "logical-keyboard")]
app.init_resource::<PressedLogicalKeys>();
app.add_systems(
PreUpdate,
Self::collect_inputs.pipe(Self::fill_players_inputs),
);
#[cfg(feature = "reflect")]
app.register_type::<Controls<A, Gs>>();
@@ -101,6 +119,8 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
///
fn collect_inputs(
keyboard_button: Res<ButtonInput<KeyCode>>,
#[cfg(feature = "logical-keyboard")] mut keyboard_evr: EventReader<KeyboardInput>,
#[cfg(feature = "logical-keyboard")] mut pressed_logical_keys: ResMut<PressedLogicalKeys>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut scroll_evr: EventReader<MouseWheel>,
mut motion_evr: EventReader<MouseMotion>,
@@ -108,9 +128,32 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
) -> 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<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
// 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
+28 -5
View File
@@ -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<A: Action> PlayerActions<A> {
// 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<A: Action, Gs: GameState> Controls<A, Gs> {
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);
}
}