Compare commits

31 Commits

Author SHA1 Message Date
yukkop d80bd8b03a feat: bindings deduplication 2026-09-05 04:13:44 +00:00
yukkop a75fb650e3 build(bevy): updade to 0.19.5 2026-09-04 21:59:20 +00:00
yukkop a156490d4d feat: rebinding example 2026-09-04 19:37:52 +00:00
yukkop f4268d7bef fix: bindings manipulations 2026-09-04 18:14:21 +00:00
yukkop b4e480954f feat: return logical keys that does not works on every layout 2026-09-04 14:10:45 +00:00
yukkop 22333c0c8c fix(examples): multiple action bindings in exmaple 2026-09-04 10:24:59 +00:00
yukkop 9a32598eb5 fix(controls): combine multiple action bindings 2026-09-04 10:14:08 +00:00
yukkop 11c5ff2aff refactor: update bevy version 2026-09-03 22:57:39 +00:00
yukkop 98d7b52546 I can build it now again 2026-09-03 20:25:11 +00:00
yukkop f063db3568 dev: nix environment 2026-09-03 20:12:49 +00:00
yukkop e6c9e65417 refactor: more coments 2024-09-13 08:01:38 +00:00
yukkop 815891ed33 feat: update example to bevy 14.1 2024-08-12 23:45:25 +00:00
yukkop 1defbfc1c9 feat: update to bevy 14.1, some day it will see release 2024-08-12 22:52:42 +00:00
yukkop 366d11453b refactor: remove warnings 2024-06-22 10:17:42 +02:00
yukkop 9cd99b898b fix: get_just_pressed 2024-04-06 02:06:45 +02:00
yukkop b332347f85 fix: InputValue handling 2024-04-06 01:55:16 +02:00
yukkop 58573dbc1c feat: InputValue::to_float() 2024-04-06 01:29:03 +02:00
yukkop 0f094a0f8c feat: update strum version 2024-04-05 13:22:57 +02:00
yukkop 8aae841930 feat: update strum version 2024-04-05 13:19:17 +02:00
yukkop 7e00ce3fda feat: update to bevy 13.2; rework input collection system 2024-04-05 12:56:32 +02:00
yukkop 1812be6ae3 ! 2024-04-05 01:42:17 +02:00
yukkop 234e086a77 feat: possibility define BindConfig in plugin setup 2024-03-12 10:17:34 +01:00
yukkop e1e762395f feat: controlls builder 2024-03-12 07:56:53 +01:00
yukkop c651f37759 docs: setup todo list 2024-03-12 07:00:53 +01:00
yukkop 6246ec0dd8 docs(basic): setup readme.md 2024-03-11 01:37:52 +01:00
nativerv 4888e60ccf refactor: Fixed a bug in NoteLineCount... not seriously... 2024-02-19 04:02:17 +03:00
nativerv 55feb6e67f refactor: trying to do something right! 2024-02-19 03:44:00 +03:00
nativerv 43be3da536 refactor: hardcoded proc macro for GameState supertraits 2024-02-19 03:18:58 +03:00
yukkop 25b3580384 build: examples target directory 2024-02-19 03:18:58 +03:00
nativerv 05b9c0b9cd refactor(examples): use bevy_controls_derive::GameState 2024-02-17 05:22:04 +03:00
nativerv e833546669 feat(derive): implement derive(GameState) 2024-02-17 05:21:17 +03:00
32 changed files with 12733 additions and 3939 deletions
Generated Regular → Executable
+1899 -1730
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
# v0.1.0
- [ ] debug log for trigered actions
- [ ] core
- [x] single keys
- [ ] chords
- [x] contracts
- [x] loby
- [x] actions
- [x] game state
- [ ] serialization & deserialization with ignoring imutable values
- [x] way to define default controlls
- [ ] layers for controlls: if on (shift + space) action enable, ignore on (space) action
- [ ] warnings on actions collision
- [ ] examples
- [ ] basic example
- [ ] serialization example
- [ ] action options work
- [ ] deley
- [ ] tap / hold
# v0.2.0
- [ ] layers for controlls: user posibility give priority to controls
- [ ] controller support
- [ ] touch pad support
- [ ] posibility support more that one game state
Regular → Executable
+16 -14
View File
@@ -3,24 +3,26 @@ name = "bevy_controls"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[features] [features]
default = [] default = []
serialize = ["bevy_input/serialize", "serde"] logical-keyboard = []
reflect = ["dep:bevy_reflect"] serialize = ["bevy_input/serialize", "serde"]
inspector-egui = ["reflect", "dep:bevy-inspector-egui"] reflect = ["dep:bevy_reflect"]
inspector-egui = ["reflect", "dep:bevy-inspector-egui"]
[dependencies] [dependencies]
bevy-inspector-egui = { version = "0.22.1", optional = true } bevy-inspector-egui = { version = "0.37", optional = true, default-features = false }
bevy_app = "0.12.1" bevy_app = "0.19.1"
bevy_derive = "0.12.1" bevy_derive = "0.19.1"
bevy_ecs = "0.12.1" bevy_ecs = "0.19.1"
bevy_input = "0.12.1" bevy_input = { version = "0.19.1", features = ["keyboard", "mouse"] }
bevy_reflect = { version = "0.12.1", optional = true } bevy_state = "0.19.1"
bevy_utils = "0.12.1" bevy_platform = "0.19.1"
bevy_reflect = { version = "0.19.1", features = ["auto_register_static"], optional = true }
log = "0.4.20" log = "0.4.20"
serde = { version = "1.0.194", optional = true } serde = { version = "1.0.194", optional = true }
strum = "0.25.0" strum = "0.26.2"
strum_macros = "0.25.3" strum_macros = "0.26.2"
[dev-dependencies] [dev-dependencies]
env_logger = "0.10.1" env_logger = "0.10.1"
+3145 -1429
View File
File diff suppressed because it is too large Load Diff
+18 -7
View File
@@ -7,19 +7,30 @@ edition = "2021"
target-dir = "../../../../target" target-dir = "../../../../target"
[features] [features]
x11 = ["bevy/x11", "bevy/bevy_winit"] x11 = ["bevy/x11"]
wayland = ["bevy/wayland", "bevy/bevy_winit"] wayland = ["bevy/wayland"]
windows = ["bevy/bevy_winit"] windows = ["bevy/bevy_winit"]
[dependencies] [dependencies]
bevy = { version = "0.12.1", default-features = false, features = [ bevy = { version = "0.19.1", default-features = false, features = [
"std",
"default_app",
"bevy_asset", "bevy_asset",
"bevy_text", "bevy_text",
"bevy_state",
"bevy_ui", "bevy_ui",
"dynamic_linking", "bevy_ui_render",
"bevy_window",
"bevy_winit",
"default_font", "default_font",
]} ]}
bevy_controls = { path = "./../../" } bevy_reflect = { version = "0.19.1", features = ["auto_register_static"] }
bevy_controls = { path = "./../../", features = [
"logical-keyboard",
]}
bevy_controls_derive = { path = "./../../../bevy_controls_derive" } bevy_controls_derive = { path = "./../../../bevy_controls_derive" }
strum = "0.25.0" strum_macros = "0.26.2"
strum_macros = "0.25.3" strum = "0.26.2"
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
@@ -0,0 +1,6 @@
Basic example
to run
```bash
cargo run --features '<x11|wayland|windows>'
```
+9 -12
View File
@@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1705309234, "lastModified": 1710146030,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1707092692, "lastModified": 1788316716,
"narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=", "narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "faf912b086576fd1a15fca610166c98d47bc667e", "rev": "3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -43,19 +43,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1707358215, "lastModified": 1788456718,
"narHash": "sha256-Nuhi8KEJ2e+2nTimSyEIPqN5eh7ECVWd+AnPXG6L+SY=", "narHash": "sha256-TxHC0sBjV2pmsBLKAX02JTXlye/EOLt6Do+WNtGofGw=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "dd917bb1b67fc049fd56fe6de70266a9ab74a4aa", "rev": "0b20a18e84d9164464739189c2b2a1b00f6f0e4b",
"type": "github" "type": "github"
}, },
"original": { "original": {
+4 -5
View File
@@ -6,7 +6,6 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs = { inputs = {
nixpkgs.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
}; };
}; };
}; };
@@ -27,10 +26,10 @@
vulkan-loader vulkan-loader
xorg.libX11 libx11
xorg.libXrandr libxrandr
xorg.libXcursor libxcursor
xorg.libXi libxi
wayland wayland
libxkbcommon libxkbcommon
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = '1.72.0' channel = '1.95.0'
+193 -165
View File
@@ -2,55 +2,60 @@
//! //!
//! It displays //! It displays
//! //!
//! Yeah I know, now it's a bit ugly //! Yeah I know, that's kind of weird that it displays
//! But it does.........
use bevy::{ecs::schedule::States, prelude::*}; // TODO strum redefine in bevy_controls
use bevy::prelude::*;
use bevy::input::keyboard::Key;
use bevy_controls::{ use bevy_controls::{
contract::{ contract::InputsContainer,
Action, ActionInner, GameState, GameStateInner, InputsContainer, InputsContainerInner,
},
plugin::ControlsPlugin, plugin::ControlsPlugin,
resource::{Binding, ButtonCombination, Controls, InputType, InputValue, Keyboard, PlayerInputs}, resource::{
ActivationMode, ActivationOptions, AxisName, Binding, BindingCondition, BindingConfig,
Bindings, ButtonCombination, Controls, InputType, InputValue, Keyboard, MouseInput, OptionsMode,
PlayerActions,
},
}; };
use bevy_controls_derive::{Action, GameState};
use strum_macros::EnumIter; use strum_macros::EnumIter;
#[derive(PartialEq, Eq, Hash, EnumIter, Clone, Copy, Debug, bevy_controls_derive::Action)] #[derive(PartialEq, Eq, Hash, EnumIter, Clone, Copy, Debug, Action)]
enum MyAction { enum MyAction {
Forward, Forward,
Back, Back,
Left, Left,
Right, Right,
Up, Up,
Down,
MouseHorizontal,
MouseVertical,
} }
#[derive(States, PartialEq, Eq, Clone, Hash, Debug, Default)] #[derive(States, PartialEq, Eq, Clone, Hash, Debug, Default, GameState)]
enum MyGameState { enum MyGameState {
#[default] #[default]
InGame, InGame,
Menu,
} }
impl GameState for MyGameState {}
impl GameStateInner for MyGameState {}
#[derive(Resource, Default, Clone, Debug)] #[derive(Resource, Default, Clone, Debug)]
struct MyInputsContainer { struct MyInputsContainer {
// When the game does not provide multiplayer, one field is enough // When the game does not provide multiplayer, one field is enough
player_inputs: PlayerInputs<MyAction>, player_inputs: PlayerActions<MyAction>,
} }
impl InputsContainer<MyAction> for MyInputsContainer {} impl InputsContainer<MyAction> for MyInputsContainer {
fn iter_inputs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a PlayerActions<MyAction>> + 'a> {
impl InputsContainerInner<MyAction> for MyInputsContainer {
fn iter_inputs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a PlayerInputs<MyAction>> + 'a> {
todo!() todo!()
} }
fn me<'a>(&'a self) -> Option<&'a PlayerInputs<MyAction>> { fn me<'a>(&'a self) -> Option<&'a PlayerActions<MyAction>> {
Some(&self.player_inputs) Some(&self.player_inputs)
} }
fn me_mut<'a>(&'a mut self) -> Option<&'a mut PlayerInputs<MyAction>> { fn me_mut<'a>(&'a mut self) -> Option<&'a mut PlayerActions<MyAction>> {
Some(&mut self.player_inputs) Some(&mut self.player_inputs)
} }
} }
@@ -61,175 +66,198 @@ struct JumpsCount(i32);
fn main() { fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins, DefaultPlugins.set(WindowPlugin {
ControlsPlugin::<MyAction, MyInputsContainer, MyGameState>::default(), primary_window: Some(Window {
transparent: true,
decorations: false,
#[cfg(target_os = "macos")]
composite_alpha_mode: CompositeAlphaMode::PostMultiplied,
..default()
}),
..default()
}),
ControlsPlugin::<MyAction, MyInputsContainer, MyGameState>::new(
Controls::<MyAction, MyGameState>::new()
// the first control instance I will describe in detail
.with(
MyAction::Left,
BindingConfig::new(Bindings::new(vec![
// `KeyCode` binds physical key location, so this is the US-layout A position.
Binding::new(ButtonCombination::Single(InputType::Keyboard(
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(
Keyboard::Key(Key::Character("h".into())),
)))
.with_condition(BindingCondition::InGameState(MyGameState::InGame)),
]))
.with_activation_options(
ActivationOptions::new(ActivationMode::Tap, OptionsMode::Immutable).with_delay(0.05),
),
)
// the rest will be simplified
.with(
MyAction::Back,
BindingConfig::from_vec(vec![
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(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(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(Keyboard::KeyCode(
KeyCode::Space,
)))),
)
.with(
MyAction::MouseHorizontal,
BindingConfig::from_bind(Binding::from_single(InputType::Mouse(MouseInput::Axis(
AxisName::Horizontal,
)))),
)
.with(
MyAction::MouseVertical,
BindingConfig::from_bind(Binding::from_single(InputType::Mouse(MouseInput::Axis(
AxisName::Vertical,
)))),
)
.with(
MyAction::Down,
BindingConfig::from_bind(Binding::from_single(
InputType::Keyboard(Keyboard::KeyCode(KeyCode::ShiftLeft)),
//InputType::Keyboard(Keyboard::KeyCode(KeyCode::Space)),
)),
)
.build(),
),
)) ))
.insert_resource(ClearColor(Color::NONE))
.init_resource::<JumpsCount>() .init_resource::<JumpsCount>()
.add_systems(Startup, (add_bindings, setup)) .add_systems(Startup, setup)
.add_systems(Update, text_update_system) .add_systems(Update, text_update_system)
.run(); .run();
} }
// TODO: default bingings... #[derive(Component, Clone, Copy)]
fn add_bindings(mut controls: ResMut<Controls<MyAction, MyGameState>>) { enum TextSpanKind {
controls.force_push( MoveLeft,
MyAction::Left, MoveBack,
// this way input will work only on latin layout only on `A` MoveForward,
Binding::new(ButtonCombination::Single(InputType::Keyboard( MoveRight,
Keyboard::KeyCode(KeyCode::A), Jump,
))), DeltaX,
); DeltaY,
controls.force_push(
MyAction::Left,
// this way input will work only on any layout on same button, 35 is `H` for `us` layout or
// `О` for `ru` layout
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::ScanCode(ScanCode(35)),
))),
);
controls.force_push(
MyAction::Back,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::KeyCode(KeyCode::S),
))),
);
controls.force_push(
MyAction::Back,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::ScanCode(ScanCode(36)),
))),
);
controls.force_push(
MyAction::Forward,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::KeyCode(KeyCode::W),
))),
);
controls.force_push(
MyAction::Forward,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::ScanCode(ScanCode(37)),
))),
);
controls.force_push(
MyAction::Right,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::KeyCode(KeyCode::D),
))),
);
controls.force_push(
MyAction::Right,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::ScanCode(ScanCode(38)),
))),
);
controls.force_push(
MyAction::Up,
Binding::new(ButtonCombination::Single(InputType::Keyboard(
Keyboard::KeyCode(KeyCode::Space),
))),
);
} }
#[derive(Component)]
struct TextBlock;
const TEXT_SIZE: f32 = 30.; const TEXT_SIZE: f32 = 30.;
fn setup(mut commands: Commands) { fn text_span(text: &str, kind: TextSpanKind) -> (TextSpan, TextFont, TextColor, TextSpanKind) {
commands.spawn(Camera2dBundle::default()); (
TextSpan::new(text),
commands.spawn(( TextFont {
TextBundle::from_sections([ font_size: FontSize::Px(TEXT_SIZE),
TextSection::new( ..default()
"\nAction: ", },
TextStyle { TextColor(Color::WHITE),
font_size: TEXT_SIZE, kind,
..default() )
},
),
TextSection::new(
"Left (H/A) ",
TextStyle {
font_size: TEXT_SIZE,
..default()
},
),
TextSection::new(
"Back (J/S) ",
TextStyle {
font_size: TEXT_SIZE,
..default()
},
),
TextSection::new(
"Forward (K/W) ",
TextStyle {
font_size: TEXT_SIZE,
..default()
},
),
TextSection::new(
"Right (L/D) ",
TextStyle {
font_size: TEXT_SIZE,
..default()
},
),
TextSection::new(
"Up (Space)",
TextStyle {
font_size: TEXT_SIZE,
..default()
},
),
]),
TextBlock,
));
} }
// text sections indexes fn setup(mut commands: Commands) {
const MOVE_LEFT_INDEX: usize = 1; commands.spawn(Camera2d);
const MOVE_BACK_INDEX: usize = 2;
const MOVE_FORWARD_INDEX: usize = 3; commands
const MOVE_RIGHT_INDEX: usize = 4; .spawn((
const JUMP_INDEX: usize = 5; Text::new(
"Info: Important that WASD use physical key positions and HJKL use logical characters from current layout\nAction: ",
),
TextFont {
font_size: FontSize::Px(TEXT_SIZE),
..default()
},
TextColor(Color::WHITE),
BackgroundColor(Color::BLACK),
))
.with_children(|parent| {
parent.spawn(text_span("Left (H/A) ", TextSpanKind::MoveLeft));
parent.spawn(text_span("Back (J/S) ", TextSpanKind::MoveBack));
parent.spawn(text_span("Forward (K/W) ", TextSpanKind::MoveForward));
parent.spawn(text_span("Right (L/D) ", TextSpanKind::MoveRight));
parent.spawn(text_span("Up (Space)", TextSpanKind::Jump));
parent.spawn(text_span("(MouseDeltaX)", TextSpanKind::DeltaX));
parent.spawn(text_span("(MouseDeltaY)", TextSpanKind::DeltaY));
});
}
fn text_update_system( fn text_update_system(
mut query: Query<&mut Text, With<TextBlock>>, mut query: Query<(&TextSpanKind, &mut TextSpan, &mut TextColor)>,
inputs_container: Res<MyInputsContainer>, inputs_container: Res<MyInputsContainer>,
mut jumps_count: ResMut<JumpsCount>, mut jumps_count: ResMut<JumpsCount>,
) { ) {
for text in query.iter_mut() { for (text_span_kind, mut text_span, mut text_color) in &mut query {
let player_inputs = inputs_container.me().expect("This is bad"); let player_inputs = inputs_container.me().expect("This is bad");
let mut text = update_text(text, player_inputs.get(MyAction::Left), MOVE_LEFT_INDEX); match *text_span_kind {
text = update_text(text, player_inputs.get(MyAction::Back), MOVE_BACK_INDEX); TextSpanKind::MoveLeft => {
text = update_text( update_text_color(&mut text_color, player_inputs.get(MyAction::Left))
text, }
player_inputs.get(MyAction::Forward), TextSpanKind::MoveBack => {
MOVE_FORWARD_INDEX, update_text_color(&mut text_color, player_inputs.get(MyAction::Back))
); }
text = update_text(text, player_inputs.get(MyAction::Right), MOVE_RIGHT_INDEX); TextSpanKind::MoveForward => {
if player_inputs update_text_color(&mut text_color, player_inputs.get(MyAction::Forward));
.get_just_pressed(MyAction::Up) }
.unwrap_or(false) TextSpanKind::MoveRight => {
{ update_text_color(&mut text_color, player_inputs.get(MyAction::Right));
**jumps_count += 1; }
text.sections[JUMP_INDEX].value = format!("Up (Space) - {}", **jumps_count); TextSpanKind::Jump => {
if player_inputs
.get_just_pressed(MyAction::Up)
.unwrap_or(false)
{
**jumps_count += 1;
text_span.0 = format!("Up (Space) - {}", **jumps_count);
}
if player_inputs
.get_just_pressed(MyAction::Down)
.unwrap_or(false)
{
**jumps_count -= 1;
text_span.0 = format!("Up (Space) - {}", **jumps_count);
}
}
TextSpanKind::DeltaX => {
let x = player_inputs.get(MyAction::MouseHorizontal).to_float();
text_span.0 = format!("(MouseDeltaX) - {}", x);
}
TextSpanKind::DeltaY => {
let y = player_inputs.get(MyAction::MouseVertical).to_float();
text_span.0 = format!("(MouseDeltaY) - {}", y);
}
} }
} }
} }
fn update_text(mut text: Mut<Text>, input_value: InputValue, index: usize) -> Mut<Text> { fn update_text_color(text_color: &mut TextColor, input_value: InputValue) {
if input_value.is_boolean() && input_value.to_boolean() { if input_value.is_boolean() && input_value.to_boolean() {
text.sections[index].style.color = Color::GOLD; text_color.0 = Color::linear_rgb(0.855, 0.647, 0.125);
} else { } else {
text.sections[index].style.color = Color::WHITE; text_color.0 = Color::WHITE;
} }
text
} }
+5128
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "rebinding"
version = "0.1.0"
edition = "2021"
[build]
target-dir = "../../../../target"
[features]
x11 = ["bevy/x11"]
wayland = ["bevy/wayland"]
windows = ["bevy/bevy_winit"]
[dependencies]
bevy = { version = "0.19.1", default-features = false, features = [
"std",
"default_app",
"bevy_asset",
"bevy_text",
"bevy_state",
"bevy_ui",
"bevy_ui_render",
"bevy_window",
"bevy_winit",
"default_font",
] }
bevy_reflect = { version = "0.19.1", features = ["auto_register_static"] }
bevy_controls = { path = "./../../" }
bevy_controls_derive = { path = "./../../../bevy_controls_derive" }
strum = "0.26.2"
strum_macros = "0.26.2"
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
+18
View File
@@ -0,0 +1,18 @@
# Rebinding example
Run from this directory with one platform feature:
```bash
cargo run --features x11
cargo run --features wayland
cargo run --features windows
```
Controls:
- `1`-`4`: select Left, Back, Forward, or Right.
- `R`: capture next key and add it to selected action.
- `C`: clear selected action bindings.
- `Escape`: cancel key capture.
Default WASD bindings and captured keys use physical `KeyCode` positions.
+208
View File
@@ -0,0 +1,208 @@
use bevy::{
ecs::message::MessageReader,
input::{keyboard::KeyboardInput, ButtonState},
prelude::*,
};
use bevy_controls::{
contract::InputsContainer,
plugin::ControlsPlugin,
resource::{Binding, BindingConfig, Bindings, Controls, InputType, Keyboard, PlayerActions},
};
use bevy_controls_derive::{Action, GameState};
use strum_macros::EnumIter;
#[derive(PartialEq, Eq, Hash, EnumIter, Clone, Copy, Debug, Action)]
enum MovementAction {
Left,
Back,
Forward,
Right,
}
impl MovementAction {
const fn name(self) -> &'static str {
match self {
Self::Left => "Left",
Self::Back => "Back",
Self::Forward => "Forward",
Self::Right => "Right",
}
}
}
#[derive(States, PartialEq, Eq, Clone, Hash, Debug, Default, GameState)]
enum DemoState {
#[default]
Playing,
}
#[derive(Resource, Default, Clone, Debug)]
struct MovementInputs(PlayerActions<MovementAction>);
impl InputsContainer<MovementAction> for MovementInputs {
fn iter_inputs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a PlayerActions<MovementAction>> + 'a> {
Box::new(std::iter::once(&self.0))
}
fn me<'a>(&'a self) -> Option<&'a PlayerActions<MovementAction>> {
Some(&self.0)
}
fn me_mut<'a>(&'a mut self) -> Option<&'a mut PlayerActions<MovementAction>> {
Some(&mut self.0)
}
}
#[derive(Resource)]
struct RebindingUi {
selected: MovementAction,
capturing: bool,
}
impl Default for RebindingUi {
fn default() -> Self {
Self {
selected: MovementAction::Left,
capturing: false,
}
}
}
#[derive(Component)]
struct RebindingText;
const TEXT_SIZE: f32 = 30.;
fn physical_binding(key_code: KeyCode) -> Binding<DemoState> {
Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(key_code)))
}
fn controls() -> Controls<MovementAction, DemoState> {
Controls::new()
.with(
MovementAction::Left,
BindingConfig::new(Bindings::new(vec![physical_binding(KeyCode::KeyA)]).customizable()),
)
.with(
MovementAction::Back,
BindingConfig::new(Bindings::new(vec![physical_binding(KeyCode::KeyS)]).customizable()),
)
.with(
MovementAction::Forward,
BindingConfig::new(Bindings::new(vec![physical_binding(KeyCode::KeyW)]).customizable()),
)
.with(
MovementAction::Right,
BindingConfig::new(Bindings::new(vec![physical_binding(KeyCode::KeyD)]).customizable()),
)
.build()
}
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Controls Rebinding".into(),
..default()
}),
..default()
}),
ControlsPlugin::<MovementAction, MovementInputs, DemoState>::new(controls()),
))
.init_resource::<RebindingUi>()
.add_systems(Startup, setup)
.add_systems(Update, (handle_rebinding, update_text))
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn((
Text::new(""),
TextFont {
font_size: FontSize::Px(TEXT_SIZE),
..default()
},
TextColor(Color::WHITE),
BackgroundColor(Color::BLACK),
RebindingText,
));
}
fn handle_rebinding(
mut controls: ResMut<Controls<MovementAction, DemoState>>,
mut ui: ResMut<RebindingUi>,
keys: Res<ButtonInput<KeyCode>>,
mut keyboard_events: MessageReader<KeyboardInput>,
) {
if ui.capturing {
for event in keyboard_events.read() {
if event.state != ButtonState::Pressed {
continue;
}
if event.key_code == KeyCode::Escape {
ui.capturing = false;
return;
}
let binding = Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(event.key_code)));
controls.push_binding(ui.selected, binding);
ui.capturing = false;
return;
}
return;
}
for _ in keyboard_events.read() {}
if keys.just_pressed(KeyCode::Digit1) {
ui.selected = MovementAction::Left;
}
if keys.just_pressed(KeyCode::Digit2) {
ui.selected = MovementAction::Back;
}
if keys.just_pressed(KeyCode::Digit3) {
ui.selected = MovementAction::Forward;
}
if keys.just_pressed(KeyCode::Digit4) {
ui.selected = MovementAction::Right;
}
if keys.just_pressed(KeyCode::KeyC) {
controls.clear_bindings(ui.selected);
}
if keys.just_pressed(KeyCode::KeyR) {
ui.capturing = true;
}
}
fn update_text(
mut text: Single<&mut Text, With<RebindingText>>,
ui: Res<RebindingUi>,
controls: Res<Controls<MovementAction, DemoState>>,
) {
let bindings = controls.get(ui.selected).map_or_else(
|| String::from(" (none)"),
|config| {
config
.bindings
.iter()
.map(|binding| format!(" {binding:?}"))
.collect::<Vec<_>>()
.join("\n")
},
);
let capture_status = if ui.capturing {
"Waiting for a key. Escape cancels."
} else {
"Ready. Press R to add a binding."
};
text.0 = format!(
"Keyboard rebinding demo\n\nSelected action: {}\n{}\n\nBindings:\n{}\n\n1-4 select action | R capture | C clear | Escape cancel",
ui.selected.name(),
capture_status,
bindings,
);
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "bevy_controls_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
#bevy_controls = { path = "./../bevy_controls" }
syn = { version = "1.0", features = [ "full", "extra-traits" ] }
quote = "1.0"
[dev-dependencies]
env_logger = "0.10.1"
+114
View File
@@ -0,0 +1,114 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use std::collections::HashMap;
use quote::quote;
use syn::{
parse::{Parse, ParseStream, Result},
parse_macro_input,
punctuated::Punctuated,
ItemTrait, LitStr, Token, TypeParamBound,
};
// #[cfg_feature_trait_bounds {
// "serialize" => Serialize + DeserializeOwned,
// "reflect" => Reflect + TypePath + FromReflect,
// "inspector-egui" => InspectorOptionsType,
// }]
type RuleKey = LitStr;
#[derive(Debug)]
struct FeatureTraitBoundMap(HashMap<RuleKey, Punctuated<TypeParamBound, Token![+]>>);
impl Parse for FeatureTraitBoundMap {
fn parse(input: ParseStream) -> Result<Self> {
let mut rules = HashMap::new();
loop {
let condition: RuleKey = input.parse()?;
let _arrow = input.parse::<Token![=>]>()?;
let supertraits = {
let mut supertraits = Punctuated::new();
loop {
supertraits.push_value(input.parse()?);
if !input.peek(Token![+]) {
break;
}
supertraits.push_punct(input.parse()?);
if input.peek(Token![,]) {
break;
}
}
supertraits
};
let _comma = input.parse::<Token![,]>();
rules.insert(condition, supertraits);
if input.is_empty() {
break;
}
}
Ok(FeatureTraitBoundMap(rules))
}
}
#[proc_macro_attribute]
pub fn cfg_feature_trait_bounds(attr: TokenStream, input: TokenStream) -> TokenStream {
let _rules = parse_macro_input!(attr as FeatureTraitBoundMap);
let _input = parse_macro_input!(input as ItemTrait);
dbg!(std::env::vars().map(|(key, _)| key).collect::<Vec<_>>());
// let code = quote! {
// trait #name: #rules {}
// };
//
// dbg!(code);
TokenStream::new()
}
#[proc_macro_attribute]
pub fn supertraits(_attrs: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemTrait);
let mut bounds = Vec::new();
if cfg!(feature = "serialize") {
bounds.push(quote! { serde::Serialize });
bounds.push(quote! { serde::de::DeserializeOwned });
};
if cfg!(feature = "reflect") {
bounds.push(quote! { bevy_reflect::Reflect });
bounds.push(quote! { bevy_reflect::TypePath });
bounds.push(quote! { bevy_reflect::FromReflect });
};
if cfg!(feature = "inspector-egui") {
bounds.push(quote! { bevy_inspector_egui::inspector_options::InspectorOptionsType });
};
let ItemTrait {
ident,
vis,
items,
generics,
supertraits: bounds_old,
..
} = input;
let bounds_old = bounds_old.iter().collect::<Vec<_>>();
let out = quote! {
#vis trait #ident #generics: #(#bounds_old)+* + #(#bounds)+* { #(#items)* }
};
// dbg!(&out);
// dbg!(out.to_string());
TokenStream::from(out)
}
+1 -1
View File
@@ -7,7 +7,7 @@ log "enable logging"
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case $1 in case $1 in
-h|-?|--help|help) -h|--help|help)
HELP=1 HELP=1
;; ;;
-*) -*)
+60 -226
View File
@@ -1,248 +1,82 @@
use std::hash::Hash; use std::hash::Hash;
use bevy_ecs::{schedule::States, system::Resource}; use bevy_ecs::{component::Mutable, prelude::Resource};
#[cfg(feature = "inspector-egui")] #[cfg(feature = "inspector-egui")]
use bevy_inspector_egui::inspector_options::InspectorOptionsType; use bevy_inspector_egui::inspector_options::InspectorOptionsType;
use strum::IntoEnumIterator;
// #[cfg(all(feature = "reflect", feature = "serialize"))]
// use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[cfg(feature = "reflect")] #[cfg(feature = "reflect")]
use bevy_reflect::{FromReflect, Reflect, TypePath}; use bevy_reflect::{FromReflect, Reflect, TypePath};
use bevy_state::state::{FreelyMutableState, States};
#[cfg(feature = "serialize")] #[cfg(feature = "serialize")]
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use strum::IntoEnumIterator;
use crate::resource::PlayerInputs; use crate::resource::PlayerActions;
pub trait ActionInner: macro_rules! define_contracts {
'static ($($feature_bounds:tt)*) => {
+ std::marker::Sync pub trait Action:
+ std::marker::Send 'static
+ PartialEq + std::marker::Sync
+ Eq + std::marker::Send
+ Hash + PartialEq
+ IntoEnumIterator + Eq
+ Clone + Hash
+ Copy + IntoEnumIterator
+ std::fmt::Debug + Clone
{ + Copy
+ std::fmt::Debug
$($feature_bounds)*
{
}
pub trait GameState:
'static
+ std::marker::Sync
+ std::marker::Send
+ States
+ Default
+ std::fmt::Debug
+ FreelyMutableState
$($feature_bounds)*
{
}
pub trait InputsContainer<A: Action>:
'static
+ std::marker::Sync
+ std::marker::Send
+ Resource<Mutability = Mutable>
+ Default
+ std::fmt::Debug
$($feature_bounds)*
{
/// The method returning an iterator over references to the PlayerInputs.
fn iter_inputs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a PlayerActions<A>> + 'a>;
/// The method returning an player inputs of this client.
fn me<'a>(&'a self) -> Option<&'a PlayerActions<A>>;
/// The method returning an mutable player inputs of this client.
fn me_mut<'a>(&'a mut self) -> Option<&'a mut PlayerActions<A>>;
}
};
} }
pub trait GameStateInner: #[cfg(all(not(feature = "serialize"), not(feature = "reflect")))]
'static + std::marker::Sync + std::marker::Send + States + Default + std::fmt::Debug define_contracts!();
{ #[cfg(all(feature = "serialize", not(feature = "reflect")))]
} define_contracts!(+ Serialize + DeserializeOwned);
pub trait InputsContainerInner<A: Action>:
'static + std::marker::Sync + std::marker::Send + Resource + Default + std::fmt::Debug
{
/// The method returning an iterator over references to the PlayerInputs.
/// Note: the use of 'a for both the method's lifetime and the returned Iterator's lifetime.
fn iter_inputs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a PlayerInputs<A>> + 'a>;
/// The method returning an player inputs of this client.
fn me<'a>(&'a self) -> Option<&'a PlayerInputs<A>>;
/// The method returning an mutable player inputs of this client.
fn me_mut<'a>(&'a mut self) -> Option<&'a mut PlayerInputs<A>>;
}
#[cfg(feature = "serialize")]
pub trait SerializeImpl: Serialize + DeserializeOwned {}
#[cfg(feature = "reflect")]
pub trait ReflectImpl: Reflect + TypePath + FromReflect {}
#[cfg(feature = "inspector-egui")]
pub trait InspectorEguiImpl: InspectorOptionsType {}
// ===== Action =====
// SAFETY: inspector-egui includes reflect
#[cfg(all( #[cfg(all(
not(feature = "serialize"),
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
pub trait Action: ActionInner {}
#[cfg(all(
feature = "serialize",
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
pub trait Action: ActionInner + SerializeImpl {}
#[cfg(all(
not(feature = "serialize"),
feature = "reflect", feature = "reflect",
not(feature = "inspector-egui")
))]
pub trait Action: ActionInner + ReflectImpl {}
#[cfg(all(
feature = "serialize",
feature = "reflect",
not(feature = "inspector-egui")
))]
pub trait Action: ActionInner + SerializeImpl + ReflectImpl {}
#[cfg(all(not(feature = "serialize"), feature = "inspector-egui"))]
pub trait Action: ActionInner + ReflectImpl + InspectorEguiImpl {}
#[cfg(all(feature = "serialize", feature = "inspector-egui"))]
pub trait Action: ActionInner + SerializeImpl + ReflectImpl + InspectorEguiImpl {}
// ===== GameState =====
// SAFETY: inspector-egui includes reflect
#[cfg(all(
not(feature = "serialize"), not(feature = "serialize"),
not(feature = "reflect"),
not(feature = "inspector-egui") not(feature = "inspector-egui")
))] ))]
pub trait GameState: GameStateInner {} define_contracts!(+ Reflect + TypePath + FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed);
#[cfg(all( #[cfg(all(
feature = "serialize",
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
pub trait GameState: GameStateInner + SerializeImpl {}
#[cfg(all(
not(feature = "serialize"),
feature = "reflect", feature = "reflect",
not(feature = "inspector-egui")
))]
pub trait GameState: GameStateInner + ReflectImpl {}
#[cfg(all(
feature = "serialize", feature = "serialize",
feature = "reflect",
not(feature = "inspector-egui") not(feature = "inspector-egui")
))] ))]
pub trait GameState: GameStateInner + SerializeImpl + ReflectImpl {} define_contracts!(+ Serialize + DeserializeOwned + Reflect + TypePath + FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed);
#[cfg(all(feature = "inspector-egui", not(feature = "serialize")))]
#[cfg(all(not(feature = "serialize"), feature = "inspector-egui"))] define_contracts!(+ Reflect + TypePath + FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed + InspectorOptionsType);
pub trait GameState: GameStateInner + ReflectImpl + InspectorEguiImpl {} #[cfg(all(feature = "inspector-egui", feature = "serialize"))]
define_contracts!(+ Serialize + DeserializeOwned + Reflect + TypePath + FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed + InspectorOptionsType);
#[cfg(all(feature = "serialize", feature = "inspector-egui"))]
pub trait GameState: GameStateInner + SerializeImpl + ReflectImpl + InspectorEguiImpl {}
// ===== InputsContainer =====
// SAFETY: inspector-egui includes reflect
#[cfg(all(
not(feature = "serialize"),
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
pub trait InputsContainer<A: Action>: InputsContainerInner<A> {}
#[cfg(all(
feature = "serialize",
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
pub trait InputsContainer<A: Action>: InputsContainerInner<A> + SerializeImpl {}
#[cfg(all(
not(feature = "serialize"),
feature = "reflect",
not(feature = "inspector-egui")
))]
pub trait InputsContainer<A: Action>: InputsContainerInner<A> + ReflectImpl {}
#[cfg(all(
feature = "serialize",
feature = "reflect",
not(feature = "inspector-egui")
))]
pub trait InputsContainer<A: Action>:
InputsContainerInner<A> + SerializeImpl + ReflectImpl
{
}
#[cfg(all(not(feature = "serialize"), feature = "inspector-egui"))]
pub trait InputsContainer<A: Action>:
InputsContainerInner<A> + ReflectImpl + InspectorEguiImpl
{
}
#[cfg(all(feature = "serialize", feature = "inspector-egui"))]
pub trait InputsContainer<A: Action>:
InputsContainerInner<A> + SerializeImpl + ReflectImpl + InspectorEguiImpl
{
}
// #[cfg(test)]
// mod test {
// use bevy_ecs::{entity::Entity, schedule::States};
// use bevy_utils::{HashMap, prelude::default};
// use strum_macros::EnumIter;
// #[cfg(feature = "serialize")]
// use serde::{Serialize, Deserialize};
// use crate::{hashmap, resource::PlayerInputs, common_traits_conditions};
// common_traits_conditions! {
// #[derive(Debug, Clone, PartialEq)]
// #[cfg_attr(feature = "reflect", reflect(Resource))]
// pub struct Lobby {
// pub me: PlayerId,
// pub players: HashMap<PlayerId, Player>,
// }
// }
// impl Default for Lobby {
// fn default() -> Self {
// Self {
// me: PlayerId::Host,
// players: hashmap! {
// PlayerId::Host => Player::default()
// },
// }
// }
// }
// common_traits_conditions! {
// #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
// pub enum PlayerId {
// /// Host or alone
// Host,
// /// Client
// Client(()),
// }
// #[derive(Default, Debug, PartialEq, Clone)]
// pub struct Player {
// /// Client do not need to know about other clients
// #[cfg_attr(feature = "serialize", serde(skip))]
// pub inputs: PlayerInputs<Action>,
// #[cfg_attr(feature = "serialize", serde(skip))]
// pub entity: Option<Entity>,
// }
// }
// #[derive(Debug, PartialEq, Clone, States, Default, Hash, Eq)]
// pub enum GameState {
// #[default]
// LevelEditor,
// InGame,
// }
// impl super::GameState for GameState {}
// // TODO: it is just plug, it must be a trait
// /// Actions that can be performed by player
// #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, EnumIter)]
// pub enum Action {
// /// Move forward
// LeverEditorForward,
// /// Move backward
// LevelEditorBackward,
// /// Move left
// LevelEditorLeft,
// /// Move right
// LevelEditorRight,
// LevelEditorFly,
// }
// impl super::Action for Action {}
// }
-2
View File
@@ -2,5 +2,3 @@ pub mod contract;
pub mod plugin; pub mod plugin;
pub mod resource; pub mod resource;
mod util; mod util;
pub use util::*;
+197 -211
View File
@@ -1,38 +1,108 @@
use bevy_app::{App, Plugin, Update}; use bevy_app::{App, Plugin, PreUpdate};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
schedule::State, message::MessageReader,
system::{Res, ResMut}, prelude::Resource,
system::{In, IntoSystem, Res, ResMut},
}; };
use bevy_input::{keyboard::{KeyCode, ScanCode}, mouse::MouseButton, Input}; 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::{ use crate::{
contract::{Action, GameState, InputsContainer}, contract::{Action, GameState, InputsContainer},
resource::*, resource::*,
}; };
pub struct ControlsPlugin<A, Ic, Gs> { pub struct ControlsPlugin<A: Action, Ic: InputsContainer<A>, Gs: GameState> {
_action: std::marker::PhantomData<A>, _action: std::marker::PhantomData<A>,
_inputs_container: std::marker::PhantomData<Ic>, _inputs_container: std::marker::PhantomData<Ic>,
_game_state: std::marker::PhantomData<Gs>, _game_state: std::marker::PhantomData<Gs>,
controls: Controls<A, Gs>,
} }
impl<A, Ic, Gs> Default for ControlsPlugin<A, Ic, Gs> { impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Default for ControlsPlugin<A, Ic, Gs> {
fn default() -> Self { fn default() -> Self {
Self { Self {
_action: std::marker::PhantomData, _action: std::marker::PhantomData,
_inputs_container: std::marker::PhantomData, _inputs_container: std::marker::PhantomData,
_game_state: std::marker::PhantomData, _game_state: std::marker::PhantomData,
controls: Controls::<A, Gs>::default(),
} }
} }
} }
impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs> {
pub fn new(controls: Controls<A, Gs>) -> Self {
Self {
_action: std::marker::PhantomData,
_inputs_container: std::marker::PhantomData,
_game_state: std::marker::PhantomData,
controls,
}
}
}
#[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> { impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Plugin for ControlsPlugin<A, Ic, Gs> {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app app
.init_resource::<Controls<A, Gs>>() .insert_resource(self.controls.clone())
.init_resource::<Ic>() .init_resource::<Ic>()
.add_state::<Gs>() .init_resource::<TrigeredInputs>()
.add_systems(Update, Self::save_input); .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")]
impl<A, Ic, Gs> Plugin for ControlsPlugin<A, Ic, Gs>
where
A: Action + bevy_reflect::FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed,
Ic: InputsContainer<A>,
Gs:
GameState + bevy_reflect::FromReflect + bevy_reflect::GetTypeRegistration + bevy_reflect::Typed,
{
fn build(&self, app: &mut App) {
app
.insert_resource(self.controls.clone())
.init_resource::<Ic>()
.init_resource::<TrigeredInputs>()
.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")] #[cfg(feature = "reflect")]
app.register_type::<Controls<A, Gs>>(); app.register_type::<Controls<A, Gs>>();
@@ -40,223 +110,139 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Plugin for ControlsPlugin
} }
impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs> { impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs> {
/// Process all hard inputs and bindings to update [`PlayerInputs`] /// Collects input events from various input devices
fn save_input( /// (keyboard, mouse buttons, mouse motion, and scroll)
keyboard_input_keycode: Res<Input<KeyCode>>, /// and stores them in a `TrigeredInputs` structure.
keyboard_input_scancode: Res<Input<ScanCode>>, ///
mouse_input: Res<Input<MouseButton>>, /// This function is separated from filling inputs to allow for more complex input handling
/// in the future, particularly in scenarios where the order of input events becomes significant.
///
fn collect_inputs(
keyboard_button: Res<ButtonInput<KeyCode>>,
#[cfg(feature = "logical-keyboard")] mut keyboard_evr: MessageReader<KeyboardInput>,
#[cfg(feature = "logical-keyboard")] mut pressed_logical_keys: ResMut<PressedLogicalKeys>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut scroll_evr: MessageReader<MouseWheel>,
mut motion_evr: MessageReader<MouseMotion>,
// mut collected_inputs: ResMut<TrigeredInputs>,
) -> 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(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()
// Collect pressed mouse buttons
for button in mouse_buttons.get_pressed() {
collected_inputs.insert(
InputType::Mouse(MouseInput::Button(*button)),
InputValue::Boolean(true),
);
}
// Collect mouse motion events
for ev in motion_evr.read() {
collected_inputs.insert(
InputType::Mouse(MouseInput::Axis(AxisName::Horizontal)),
InputValue::Float(ev.delta.x),
);
collected_inputs.insert(
InputType::Mouse(MouseInput::Axis(AxisName::Vertical)),
InputValue::Float(ev.delta.y),
);
}
// Collect mouse scroll events
for ev in scroll_evr.read() {
match ev.unit {
// Handle scroll in line units
MouseScrollUnit::Line => {
collected_inputs.insert(
InputType::Mouse(MouseInput::Wheel(AxisName::Horizontal)),
InputValue::Float(ev.x),
);
collected_inputs.insert(
InputType::Mouse(MouseInput::Wheel(AxisName::Vertical)),
InputValue::Float(ev.y),
);
}
// Handle scroll in pixel units (not accepted in this game)
MouseScrollUnit::Pixel => {
panic!("I'm do not accept you to use touch pad in my game");
}
}
}
collected_inputs
}
/// Fills the player's inputs based on the collected inputs and the current control bindings.
///
/// This function takes in the current inputs, updates them according to the specified control
/// bindings, and then stores the updated inputs in the input container. It only updates the
/// inputs if they satisfy the binding conditions, such as being in a specific game state.
///
fn fill_players_inputs(
In(collected_inputs): In<TrigeredInputs>,
mut inputs_container: ResMut<Ic>, mut inputs_container: ResMut<Ic>,
controls: Res<Controls<A, Gs>>, controls: Res<Controls<A, Gs>>,
game_state: Res<State<Gs>>, game_state: Res<State<Gs>>,
) { ) {
// Check if the input container is accessible
if let Some(inputs) = inputs_container.me_mut() { if let Some(inputs) = inputs_container.me_mut() {
for (action, config) in controls.iter() { for (action, config) in controls.iter() {
'bindings_loop: for binding in config.bindings.iter() { let mut action_value = InputValue::Empty;
for condition in &binding.conditions {
match condition { // Iterate over the control bindings and collect all action-binding pairs
BindingCondition::InGameState(state) => { for binding in config.bindings.iter() {
if *state != *game_state.get() { // Skip any binding with mismatched condition
continue 'bindings_loop; if binding
} .conditions
} .iter()
} .any(|condition| matches!(condition, BindingCondition::InGameState(state) if *state != *game_state.get()))
{
continue;
} }
log::trace!("action binding {:?} {:?} in condition", action, binding);
match &binding.input { match &binding.input {
ButtonCombination::Single(button) => match button { ButtonCombination::Single(input_type) => {
InputType::Keyboard(Keyboard::KeyCode(key)) => { if let Some(&value) = collected_inputs.get(&*input_type) {
log::trace!("presed?: {:?}", keyboard_input_keycode.pressed(*key)); action_value = action_value.merge(value);
log::trace!("active");
if keyboard_input_keycode.pressed(*key) {
inputs.forced_set(*action, true);
break; // when on binding trigered no sence check another
}
inputs.forced_set(*action, false); // FIXME: this is happening on every pass so too frequently
} }
InputType::Keyboard(Keyboard::ScanCode(code)) => {
log::trace!("presed?: {:?}", keyboard_input_scancode.pressed(*code));
log::trace!("active");
if keyboard_input_scancode.pressed(*code) {
inputs.forced_set(*action, true);
break; // when on binding trigered no sence check another
}
inputs.forced_set(*action, false); // FIXME: this is happening on every pass so too frequently
}
InputType::Mouse(input) => match input {
MouseInput::Axis(_axis) => {
todo!();
}
MouseInput::Button(button) => {
if !mouse_input.get_pressed().any(|b| b == button) {
continue 'bindings_loop;
}
}
MouseInput::Wheel(_axis) => {
todo!();
}
},
},
ButtonCombination::Chord(buttons) => {
// If any button in chord is not pressed we skip this `binding`
for button in buttons {
match button {
InputType::Keyboard(Keyboard::KeyCode(key)) => {
if !keyboard_input_keycode.pressed(*key) {
continue 'bindings_loop;
}
}
InputType::Keyboard(Keyboard::ScanCode(code)) => {
if !keyboard_input_scancode.pressed(*code) {
continue 'bindings_loop;
}
}
InputType::Mouse(input) => match input {
MouseInput::Axis(_axis) => {
todo!();
}
MouseInput::Button(button) => {
if !mouse_input.get_pressed().any(|b| b == button) {
continue 'bindings_loop;
}
}
MouseInput::Wheel(_axis) => {
todo!();
}
},
}
}
log::trace!("active");
// TODO: should be [`Chord`](ButtonCombination::Chord) only [`Boolean`](InputValue::Boolean) type
inputs.forced_set(*action, true);
break; // when on binding trigered no sence check another
} }
} }
} }
inputs.forced_set(*action, action_value);
} }
} else { } else {
log::error!("cannot find me in inputs container") log::warn!("cannot find me in inputs container");
} }
} }
} }
// #[cfg(test)]
// mod performance_test {
// use crate::{ util::test::{enable_loggings, measure_time, Times}, resource::PlayerInputs};
// use super::Controls;
// use std::time::Duration;
// /// Test for execution time for [`Controls`] get
// ///
// /// Example:
// /// ```
// /// cargo test --package pih-pah-app --lib --features "dev, ui_egui" -- controls::test::controls_get --exact --nocapture
// /// ```
// #[test]
// fn controls_get() {
// enable_loggings();
// let controls = Controls::default();
// let duration = measure_time(
// || {
// controls.get(Action::LeverEditorForward);
// controls.get(Action::LevelEditorBackward);
// controls.get(Action::LevelEditorLeft);
// controls.get(Action::LevelEditorRight);
// },
// Times::default(),
// );
// log::info!("time: {:?}", duration);
// }
// /// Test for execution speed for [`PlayerInputs`] get
// fn player_inputs_get() -> Duration {
// enable_loggings();
// let inputs = PlayerInputs::default();
// let duration = measure_time(
// || {
// inputs.get(Action::LeverEditorForward);
// inputs.get(Action::LevelEditorBackward);
// inputs.get(Action::LevelEditorLeft);
// inputs.get(Action::LevelEditorRight);
// },
// Times::default(),
// );
// duration
// }
// /// Test for execution speed for [`PlayerInputs`] get_many
// fn player_inputs_get_many() -> Duration {
// enable_loggings();
// let inputs = PlayerInputs::default();
// let duration = measure_time(
// || {
// inputs.get_many(vec![
// Action::LeverEditorForward,
// Action::LevelEditorBackward,
// Action::LevelEditorLeft,
// Action::LevelEditorRight,
// ]);
// },
// Times::default(),
// );
// duration
// }
// /// Test for execution speed for [`PlayerInputs`] get and get_many
// ///
// /// Example:
// /// ```
// /// cargo test --package pih-pah-app --lib --features "dev, ui_egui" -- controls::test::compare_player_inputs_get_and_get_many --exact --nocapture
// /// ```
// #[test]
// fn compare_player_inputs_get_and_get_many() {
// let get = player_inputs_get();
// let get_many = player_inputs_get_many();
// log::info!("get: {:?}", get);
// log::info!("get_many: {:?}", get_many);
// }
// /// Test for execution speed for [`InputValue`] casting
// #[test]
// fn action_casting() {
// enable_loggings();
// let inputs = PlayerInputs::default();
// let duration = measure_time(
// || {
// let _ = (inputs.get(Action::LeverEditorForward).as_boolean() as i8
// - inputs.get(Action::LevelEditorBackward).as_boolean() as i8)
// as f32;
// },
// 10000000.into(),
// );
// log::info!("with casting: {:?}", duration);
// let inputs = PlayerInputs::default();
// let duration = measure_time(
// || {
// let _ = inputs.get(Action::LeverEditorForward);
// let _ = inputs.get(Action::LevelEditorBackward);
// },
// 10000000.into(),
// );
// log::info!("without casting: {:?}", duration);
// }
// }
+240 -56
View File
@@ -1,13 +1,15 @@
use std::error; use std::error;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::Resource; use bevy_ecs::prelude::Resource;
use bevy_input::{keyboard::{KeyCode, ScanCode}, mouse::MouseButton}; #[cfg(feature = "logical-keyboard")]
use bevy_input::keyboard::Key;
use bevy_input::{keyboard::KeyCode, mouse::MouseButton};
#[cfg(feature = "inspector-egui")] #[cfg(feature = "inspector-egui")]
use bevy_inspector_egui::prelude::*; use bevy_inspector_egui::prelude::*;
use bevy_platform::collections::HashMap;
#[cfg(all(feature = "reflect", feature = "serialize"))] #[cfg(all(feature = "reflect", feature = "serialize"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use bevy_utils::HashMap;
use log::warn; use log::warn;
#[cfg(feature = "serialize")] #[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -44,27 +46,27 @@ where
common_traits_conditions! { common_traits_conditions! {
/// Struct that contains user's inputs corresponding to actions /// Struct that contains user's inputs corresponding to actions
#[derive(Debug, PartialEq, Clone, Deref, DerefMut)] #[derive(Debug, PartialEq, Clone, Deref, DerefMut)]
pub struct Inputs<A: Action>(#[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))] HashMap<A, InputValue>); pub struct Actions<A: Action>(#[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))] HashMap<A, InputValue>);
/// Resource that contains current user inputs /// Resource that contains current user inputs
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct PlayerInputs<A: Action> { pub struct PlayerActions<A: Action> {
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))]
current: Inputs<A>, current: Actions<A>,
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))]
previous: Inputs<A>, previous: Actions<A>,
} }
} }
impl<A: Action> Default for PlayerInputs<A> { impl<A: Action> Default for PlayerActions<A> {
fn default() -> Self { fn default() -> Self {
PlayerInputs { PlayerActions {
current: Inputs( current: Actions(
A::iter() A::iter()
.map(|action| (action, InputValue::Empty)) .map(|action| (action, InputValue::Empty))
.collect(), .collect(),
), ),
previous: Inputs( previous: Actions(
A::iter() A::iter()
.map(|action| (action, InputValue::Empty)) .map(|action| (action, InputValue::Empty))
.collect(), .collect(),
@@ -73,7 +75,7 @@ impl<A: Action> Default for PlayerInputs<A> {
} }
} }
impl<A: Action> PlayerInputs<A> { impl<A: Action> PlayerActions<A> {
/// Returns current input value for action /// Returns current input value for action
/// ///
/// in any case faster that `get_many` method /// in any case faster that `get_many` method
@@ -119,15 +121,11 @@ impl<A: Action> PlayerInputs<A> {
/// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame /// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame
/// If input has not same type as `InputValue` on previous frame return `Error()` /// If input has not same type as `InputValue` on previous frame return `Error()`
pub fn get_just_pressed(&self, action: A) -> Result<bool, Box<dyn error::Error>> { pub fn get_just_pressed(&self, action: A) -> Result<bool, Box<dyn error::Error>> {
// SAFETY: action is always valid return Ok(
// because we iterate over all actions in `default` method self.current.get(&action).unwrap().to_boolean()
if let InputValue::Boolean(current) = *self.current.get(&action).unwrap() { && !self.previous.get(&action).unwrap().to_boolean(),
if let InputValue::Boolean(previous) = *self.previous.get(&action).unwrap() { );
return Ok(current && !previous); //Err("This input is not boolean".into())
}
return Err("Previous input is not boolean".into());
}
Err("This input is not boolean".into())
} }
/// Set input value to new /// Set input value to new
@@ -168,7 +166,7 @@ impl<A: Action> PlayerInputs<A> {
/// Update current inputs /// Update current inputs
/// ///
/// notice: not recomended to use if you do not know what do you do /// notice: not recomended to use if you do not know what do you do
pub fn forced_update(&mut self, inputs: Inputs<A>) { pub fn forced_update(&mut self, inputs: Actions<A>) {
*self.previous = self.current.clone().0; *self.previous = self.current.clone().0;
*self.current = inputs.0; *self.current = inputs.0;
} }
@@ -178,7 +176,7 @@ impl<A: Action> PlayerInputs<A> {
/// ! remember that it work safely but slow /// ! remember that it work safely but slow
/// ///
/// notice: not recomended to use if you do not know what do you do /// notice: not recomended to use if you do not know what do you do
pub fn update(&mut self, inputs: Inputs<A>) -> Result<(), Box<dyn error::Error>> { pub fn update(&mut self, inputs: Actions<A>) -> Result<(), Box<dyn error::Error>> {
// SAFETY: action is always valid // SAFETY: action is always valid
// because we iterate over all actions in `default` method // because we iterate over all actions in `default` method
for (action, value) in inputs.0.iter() { for (action, value) in inputs.0.iter() {
@@ -196,48 +194,56 @@ impl<A: Action> PlayerInputs<A> {
} }
} }
// TODO: add option on exlude or not exclude if used
common_traits_conditions! { common_traits_conditions! {
#[derive(Debug, PartialEq, Clone)] /// 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 { pub enum InputType {
Keyboard(Keyboard), Keyboard(Keyboard),
Mouse(MouseInput), Mouse(MouseInput),
// TODO: Gamepad(GamepadButtonType), // TODO: Gamepad(GamepadButtonType),
// TODO: Touch screen // TODO: Touch screen
} // TODO: TouchPad inputs
}
}
#[derive(Debug, PartialEq, Clone)] common_traits_conditions! {
pub enum Keyboard { #[derive(Debug, PartialEq, Clone, Eq, Hash)]
KeyCode(KeyCode),
ScanCode(ScanCode),
}
#[derive(Debug, PartialEq, Clone)]
pub enum MouseInput { pub enum MouseInput {
Button(MouseButton), Button(MouseButton),
Axis(AxisName), Axis(AxisName),
Wheel(AxisName) Wheel(AxisName)
// TODO: CursorPosition // TODO: CursorPosition
// TODO: TouchPad inputs
} }
/// Name of axis /// Name of axis
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub enum AxisName { pub enum AxisName {
Horizontal, Horizontal,
Vertical, Vertical,
} }
/// Represents a binding that can be changed by the player /// Represents a binding that can be changed by the player
#[derive(Debug, PartialEq, Clone)] /// default: [`Immutable`](OptionsMode::Immutable)
#[derive(Debug, PartialEq, Clone, Default)]
pub enum OptionsMode { pub enum OptionsMode {
/// Represents a binding that cannot be changed as it is crucial to the game logic /// Represents a binding that cannot be changed as it is crucial to the game logic
#[default] // I think this is more safe
Immutable, Immutable,
/// Represents a binding that the player can customize /// Represents a binding that the player can customize
Customizable, Customizable,
} }
/// Represents the manner in which input is registered /// Represents the manner in which input is registered
#[derive(Debug, PartialEq, Clone)] /// default: [`Tap`](ActivationMode::Tap)
#[derive(Debug, PartialEq, Clone, Default)]
pub enum ActivationMode { pub enum ActivationMode {
/// for<'a> Action<'a> is triggered when the button is pressed /// for<'a> Action<'a> is triggered when the button is pressed
Hold, Hold,
@@ -245,6 +251,7 @@ common_traits_conditions! {
/// and cannot be triggered again until the button is released /// and cannot be triggered again until the button is released
/// you don't need to release all buttons in chord /// you don't need to release all buttons in chord
/// you can release only last pressed button /// you can release only last pressed button
#[default] // I think this is more safe
Tap, Tap,
} }
@@ -262,8 +269,8 @@ common_traits_conditions! {
impl Default for ActivationOptions { impl Default for ActivationOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
mode: ActivationMode::Tap, // Because it is more safe mode: ActivationMode::default(),
option: OptionsMode::Immutable, option: OptionsMode::default(),
delay: 0.05, // 20 times per second; 14 - world record? delay: 0.05, // 20 times per second; 14 - world record?
} }
} }
@@ -290,10 +297,21 @@ common_traits_conditions! {
pub enum ButtonCombination { pub enum ButtonCombination {
/// Single button press /// Single button press
Single(InputType), Single(InputType),
/// Chord is a combination of buttons that must be pressed at the same time // /// Chord is a combination of buttons that must be pressed at the same time
Chord(Vec<InputType>), // Chord(Vec<InputType>),
} }
}
impl ButtonCombination {
pub fn len(&self) -> usize {
use crate::resource::ButtonCombination::*;
match self {
Single(_) => 1, // TODO: Chord
}
}
}
common_traits_conditions! {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum BindingCondition<Gs: GameState> { pub enum BindingCondition<Gs: GameState> {
/// During specific game state /// During specific game state
@@ -328,6 +346,20 @@ impl<Gs: GameState> Binding<Gs> {
} }
} }
pub fn from_single(input: InputType) -> Self {
Self {
input: ButtonCombination::Single(input),
conditions: Vec::new(),
}
}
// pub fn from_chord(input: Vec<InputType>) -> Self {
// Self {
// input: ButtonCombination::Chord(input),
// conditions: Vec::new(),
// }
// }
pub fn with_condition(mut self, condition: BindingCondition<Gs>) -> Self { pub fn with_condition(mut self, condition: BindingCondition<Gs>) -> Self {
self.conditions.push(condition); self.conditions.push(condition);
self self
@@ -395,6 +427,50 @@ impl InputValue {
InputValue::Empty => false, InputValue::Empty => false,
} }
} }
pub fn to_float(&self) -> f32 {
match self {
InputValue::Boolean(value) => {
if *value {
1.
} else {
0.
}
}
InputValue::Float(value) => *value,
InputValue::Empty => 0.,
}
}
/// Merges another input value into this one.
///
/// `Empty` acts as an identity value. Boolean inputs are combined using
/// logical OR. For other non-empty values, the existing value is preserved.
///
/// This is used when multiple control bindings contribute to the same action.
pub(crate) fn merge(self, other: Self) -> Self {
match (self, other) {
// Empty doesn't affect an existing value
(Self::Empty, rhs) => rhs,
(lhs, Self::Empty) => lhs,
// Multiple boolean bindings combine with OR
(Self::Boolean(lhs), Self::Boolean(rhs)) => Self::Boolean(lhs || rhs),
// For floats choose max
// TODO: maybe user want to chose multiply same inputs of chose max
(Self::Float(lhs), Self::Float(rhs)) => {
if rhs.abs() > lhs.abs() {
Self::Float(rhs)
} else {
Self::Float(lhs)
}
}
(lhs, _) if !lhs.is_empty() => lhs,
(_, rhs) => rhs,
}
}
} }
common_traits_conditions! { common_traits_conditions! {
@@ -412,20 +488,44 @@ impl<Gs: GameState> Default for Bindings<Gs> {
fn default() -> Self { fn default() -> Self {
Self { Self {
list: Vec::new(), list: Vec::new(),
options: OptionsMode::Immutable, options: OptionsMode::default(),
} }
} }
} }
impl<Gs: GameState> Bindings<Gs> { impl<Gs: GameState> Bindings<Gs> {
pub fn new(bindings: Vec<Binding<Gs>>, options: OptionsMode) -> Self { pub fn new(bindings: Vec<Binding<Gs>>) -> Self {
Self { let mut result = Self {
list: bindings, list: Vec::with_capacity(bindings.len()),
options, ..Default::default()
};
// To avoid duplicates
for binding in bindings {
result.force_push(binding);
} }
result
}
pub fn customizable(mut self) -> Self {
self.options = OptionsMode::Customizable;
self
}
pub fn immutable(mut self) -> Self {
self.options = OptionsMode::Immutable;
self
}
pub fn with_option_mode(mut self, options: OptionsMode) -> Self {
self.options = options;
self
} }
pub fn force_push(&mut self, binding: Binding<Gs>) { pub fn force_push(&mut self, binding: Binding<Gs>) {
if self.list.contains(&binding) {
return;
}
self.list.push(binding); self.list.push(binding);
} }
@@ -434,7 +534,7 @@ impl<Gs: GameState> Bindings<Gs> {
warn!("You try to push binding to immutable bindings"); warn!("You try to push binding to immutable bindings");
return; return;
} }
self.list.push(binding); self.force_push(binding);
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@@ -492,22 +592,60 @@ common_traits_conditions! {
} }
impl<Gs: GameState> BindingConfig<Gs> { impl<Gs: GameState> BindingConfig<Gs> {
pub fn new(bindings: Bindings<Gs>, activation: ActivationOptions) -> Self { pub fn new(bindings: Bindings<Gs>) -> Self {
Self { Self {
bindings, bindings,
activation, ..Default::default()
} }
} }
pub fn from_vec(bindings: Vec<Binding<Gs>>) -> Self {
Self {
bindings: Bindings::new(bindings),
..Default::default()
}
}
pub fn from_bind(binding: Binding<Gs>) -> Self {
Self {
bindings: Bindings::new(vec![binding]),
..Default::default()
}
}
pub fn with_activation_options(mut self, activation: ActivationOptions) -> Self {
self.activation = activation;
self
}
}
#[derive(Deref, DerefMut)]
pub struct ControlsBuilder<A: Action, Gs: GameState>(Controls<A, Gs>);
impl<A: Action, Gs: GameState> ControlsBuilder<A, Gs> {
///
pub fn with(mut self, action: A, config: BindingConfig<Gs>) -> Self {
self.insert(action, config);
self
}
///
pub fn build(mut self) -> Controls<A, Gs> {
// If you see this error, you may add new action in menu_actions
// or make sure that you have only one Menufor<'a> Action<'a> with the same name in the Menufor<'a> Action<'a>s
assert!(validate_hash_map(&**self));
std::mem::take(&mut self) // TODO
}
} }
common_traits_conditions! { common_traits_conditions! {
/// Contains all bindings for actions /// Contains all bindings for actions
#[derive(Resource, Clone)] #[derive(Resource, Clone, Deref, DerefMut)]
#[cfg_attr(feature = "reflect", reflect(Resource))] #[cfg_attr(feature = "reflect", reflect(Resource))]
// TODO: Add delay for input
pub struct Controls<A: Action, Gs: GameState>( pub struct Controls<A: Action, Gs: GameState>(
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "")))]
// TODO: change HashMap to Vec for faster iteration // FIXME: change HashMap to Vec for faster iteration
HashMap<A, BindingConfig<Gs>> HashMap<A, BindingConfig<Gs>>
); );
} }
@@ -530,6 +668,11 @@ impl<A: Action, Gs: GameState> Default for Controls<A, Gs> {
} }
impl<A: Action, Gs: GameState> Controls<A, Gs> { impl<A: Action, Gs: GameState> Controls<A, Gs> {
/// New [`Controls`] instance
pub fn new() -> ControlsBuilder<A, Gs> {
ControlsBuilder(Controls(HashMap::new()))
}
/// Returns bindings for action /// Returns bindings for action
pub fn get(&self, action: A) -> Option<&BindingConfig<Gs>> { pub fn get(&self, action: A) -> Option<&BindingConfig<Gs>> {
self.0.get(&action) self.0.get(&action)
@@ -548,7 +691,7 @@ impl<A: Action, Gs: GameState> Controls<A, Gs> {
} }
/// Push new binding for action /// Push new binding for action
pub fn push(&mut self, action: A, binding: Binding<Gs>) { pub fn push_binding(&mut self, action: A, binding: Binding<Gs>) {
// TODO: warning if config is not exist yet // TODO: warning if config is not exist yet
// TODO: interface for [`ActivationOptions`] // TODO: interface for [`ActivationOptions`]
self self
@@ -560,7 +703,7 @@ impl<A: Action, Gs: GameState> Controls<A, Gs> {
} }
/// Remove all bindings for action /// Remove all bindings for action
pub fn remove(&mut self, action: A) { pub fn clear_bindings(&mut self, action: A) {
// TODO: warning if config is not exist yet // TODO: warning if config is not exist yet
self self
.0 .0
@@ -570,9 +713,10 @@ impl<A: Action, Gs: GameState> Controls<A, Gs> {
.clear(); .clear();
} }
/// Remove binding for action
pub fn remove_binding(&mut self, action: A, binding: Binding<Gs>) { pub fn remove_binding(&mut self, action: A, binding: Binding<Gs>) {
if let Some(config) = self.0.get_mut(&action) { if let Some(config) = self.0.get_mut(&action) {
config.bindings.retain(|b| *b == binding); config.bindings.retain(|b| *b != binding);
} }
} }
@@ -580,3 +724,43 @@ impl<A: Action, Gs: GameState> Controls<A, Gs> {
self.0.iter() self.0.iter()
} }
} }
#[cfg(all(
test,
feature = "logical-keyboard",
not(feature = "serialize"),
not(feature = "reflect"),
not(feature = "inspector-egui")
))]
mod tests {
use super::*;
use bevy_state::state::States;
#[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);
}
#[derive(States, PartialEq, Eq, Clone, Hash, Debug, Default)]
enum TestState {
#[default]
Playing,
}
impl crate::contract::GameState for TestState {}
#[test]
fn bindings_reject_duplicates() {
let binding: Binding<TestState> =
Binding::from_single(InputType::Keyboard(Keyboard::KeyCode(KeyCode::KeyH)));
let mut bindings = Bindings::new(vec![binding.clone(), binding.clone()]).customizable();
bindings.push(binding.clone());
bindings.force_push(binding);
assert_eq!(bindings.iter().count(), 1);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
/// Creates a [`HashMap`](bevy_utils::HashMap) from a list of key-value pairs. /// Creates a [`HashMap`](std::collections::HashMap) from a list of key-value pairs.
/// ///
/// Example: /// Example:
/// ```ignore /// ```ignore
@@ -9,7 +9,7 @@
#[macro_export] #[macro_export]
macro_rules! hashmap { macro_rules! hashmap {
($( $key: expr => $val: expr ),*) => {{ ($( $key: expr => $val: expr ),*) => {{
let mut map = HashMap::new(); let mut map = ::std::collections::HashMap::new();
$( $(
map.insert($key, $val); map.insert($key, $val);
)* )*
-17
View File
@@ -6,24 +6,7 @@ edition = "2021"
[lib] [lib]
proc-macro = true proc-macro = true
# [features]
# default = []
# serialize = ["bevy_input/serialize", "serde"]
# reflect = ["dep:bevy_reflect"]
# inspector-egui = ["reflect", "dep:bevy-inspector-egui"]
[dependencies] [dependencies]
# bevy-inspector-egui = { version = "0.22.1", optional = true }
# bevy_app = "0.12.1"
# bevy_derive = "0.12.1"
# bevy_ecs = "0.12.1"
# bevy_input = "0.12.1"
# bevy_reflect = { version = "0.12.1", optional = true }
# bevy_utils = "0.12.1"
# log = "0.4.20"
# serde = { version = "1.0.194", optional = true }
# strum = "0.25.0"
# strum_macros = "0.25.3"
bevy_controls = { path = "./../bevy_controls" } bevy_controls = { path = "./../bevy_controls" }
syn = "1.0" syn = "1.0"
quote = "1.0" quote = "1.0"
+13 -11
View File
@@ -1,25 +1,27 @@
extern crate proc_macro; extern crate proc_macro;
use bevy_controls::contract::{Action, ActionInner};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn; use syn;
#[proc_macro_derive(Action)] #[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream { pub fn action(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; let name = &ast.ident;
let gen = quote! { let gen = quote! {
impl Action for #name {} impl bevy_controls::contract::Action for #name {}
impl ActionInner for #name {} };
gen.into()
}
#[proc_macro_derive(GameState)]
pub fn game_state(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl bevy_controls::contract::GameState for #name {}
}; };
gen.into() gen.into()
} }
+47
View File
@@ -0,0 +1,47 @@
{ inputs, flake, self, pkgs }:
let
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
runtimeLibraries = with pkgs; [
alsa-lib
vulkan-loader
udev
libx11
libxcursor
libxi
libxrandr
libxkbcommon
wayland
];
in {
default = pkgs.mkShell {
buildInputs = with pkgs; [
rustToolchain
stdenv.cc
git
pkg-config
shellcheck
file
openssl
] ++ runtimeLibraries;
shellHook = ''
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"
if [ -d /run/opengl-driver/lib ]; then
export LD_LIBRARY_PATH="/run/opengl-driver/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibraries}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
if git_root=$(${pkgs.git}/bin/git rev-parse --show-toplevel 2>/dev/null); then
export LOCAL_DIR="$git_root"
else
export LOCAL_DIR="$PWD"
fi
printf '%s\n' "bevy_controls dev shell"
printf '%s\n' "Library: cargo check && cargo test"
printf '%s\n' "Example: cd crate/bevy_controls/example/basic && cargo run --features x11"
'';
};
}
Generated
+1238 -20
View File
File diff suppressed because it is too large Load Diff
+19 -29
View File
@@ -1,38 +1,28 @@
{ {
description = "Bevy controls library and examples";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; hearth = {
url = "git+https://gitea.hectic-lab.com/hinterland/hearth.git";
inputs.nixpkgs.follows = "nixpkgs";
};
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs = { inputs.nixpkgs.follows = "nixpkgs";
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
}; };
}; };
outputs = { self, nixpkgs, flake-utils, rust-overlay }: outputs = { self, hearth, ... }@inputs:
flake-utils.lib.eachDefaultSystem let
(system: flake = builtins.warn "Using the `flake` variable may produce a large source closure." ./.;
let lib = import ./lib { inherit inputs flake self; };
overlays = [ (import rust-overlay) ]; in
pkgs = import nixpkgs { hearth.lib.forAllSystemsWithPkgs lib.composedOverlays ({ system, pkgs }: {
inherit system overlays; legacyPackages.${system} = pkgs;
}; packages.${system} = import ./package { inherit self inputs flake pkgs; };
rustToolchain = (pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml); devShells.${system} = import ./devshell { inherit self inputs flake pkgs; };
nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ]; }) // {
buildInputs = with pkgs; [ inherit lib;
openssl };
shellcheck
file
];
in
with pkgs;
{
devShells.default = mkShell {
inherit buildInputs nativeBuildInputs;
};
}
);
} }
+28
View File
@@ -0,0 +1,28 @@
<h1 id="v0.1.0">v0.1.0</h1>
<ul class="task-list">
<li><label><input type="checkbox" checked="" />contracts</label>
<ul class="task-list">
<li><label><input type="checkbox" checked="" />loby<br />
</label></li>
<li><label><input type="checkbox" checked="" />actions<br />
</label></li>
<li><label><input type="checkbox" checked="" />game state<br />
</label></li>
</ul></li>
<li><label><input type="checkbox" checked="" />way to define default
controlls</label></li>
<li><label><input type="checkbox" />layers for controlls: if on (shift +
space) action enable, ignore on (space) action</label></li>
<li><label><input type="checkbox" />warnings on actions
collision</label></li>
<li><label><input type="checkbox" />basic example</label></li>
</ul>
<h1 id="v0.2.0">v0.2.0</h1>
<ul class="task-list">
<li><label><input type="checkbox" />layers for controlls: user
posibility give priority to controls</label></li>
<li><label><input type="checkbox" />controller support</label></li>
<li><label><input type="checkbox" />touch pad support</label></li>
<li><label><input type="checkbox" />posibility support more that one
game state</label></li>
</ul>
+10
View File
@@ -0,0 +1,10 @@
{ self, flake, inputs }:
let
overlays = with inputs; [
hearth.overlays.default
(import rust-overlay)
];
in {
nixpkgs-lib = inputs.nixpkgs.lib;
composedOverlays = overlays;
}
+38
View File
@@ -0,0 +1,38 @@
{ pkgs }:
let
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;
rustPlatform = pkgs.makeRustPlatform {
rustc = rustToolchain;
cargo = rustToolchain;
};
in rustPlatform.buildRustPackage {
pname = "bevy-controls";
version = "0.1.0";
src = ../..;
cargoLock.lockFile = ../../Cargo.lock;
cargoBuildFlags = [ "--workspace" ];
cargoTestFlags = [ "--workspace" ];
nativeBuildInputs = with pkgs; [
pkg-config
];
buildInputs = with pkgs; [
openssl
alsa-lib
vulkan-loader
udev
libx11
libxcursor
libxi
libxrandr
libxkbcommon
wayland
];
installPhase = ''
mkdir -p "$out"
touch "$out/workspace-built"
'';
}
+5
View File
@@ -0,0 +1,5 @@
{ pkgs, ... }:
{
bevy-controls = import ./bevy-controls { inherit pkgs; };
default = import ./bevy-controls { inherit pkgs; };
}
Regular → Executable
+2 -1
View File
@@ -1,2 +1,3 @@
[toolchain] [toolchain]
channel = '1.72.0' channel = '1.95.0'
components = ["rustfmt", "clippy", "rust-src"]