Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace axis-like input configuration with new input processors #494

Merged
merged 64 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6b5b344
Add input processors and input settings
Shute052 Feb 27, 2024
88f7a7a
Merge branch 'main' into input-processors
Shute052 Feb 28, 2024
fd18dcb
Replace all usage
Shute052 Feb 28, 2024
91f6e4e
RELEASES.md
Shute052 Feb 28, 2024
08a6acf
Document
Shute052 Feb 28, 2024
d7ac626
Document
Shute052 Feb 28, 2024
03e54fa
Document
Shute052 Feb 28, 2024
714c296
Document
Shute052 Feb 28, 2024
09579bb
Add more test cases
Shute052 Feb 28, 2024
f58953d
Split out deadzones into another module
Shute052 Feb 29, 2024
491c38f
Fix CI and examples
Shute052 Feb 29, 2024
789017c
add `with_settings`
Shute052 Feb 29, 2024
cfd4afd
Update comment
Shute052 Feb 29, 2024
4c44577
Split axis settings into separate modules
Shute052 Feb 29, 2024
a3ccf29
Update the documentation
Shute052 Mar 1, 2024
f8a3d4d
typo
Shute052 Mar 1, 2024
f4468cc
RELEASES.md
Shute052 Mar 1, 2024
324e76e
fix ci
Shute052 Mar 1, 2024
ed9b72e
fix RELEASES.md
Shute052 Mar 2, 2024
6b8d445
fix documentation
Shute052 Mar 2, 2024
1eb50b2
fix documentation
Shute052 Mar 2, 2024
34de561
Use new implementation
Shute052 Mar 22, 2024
9066278
Add comments
Shute052 Mar 28, 2024
cf860d7
Merge branch 'main' into input-processors
Shute052 Mar 28, 2024
fed7811
Add an example
Shute052 Mar 28, 2024
e9810ef
Fix warnings in examples
Shute052 Mar 28, 2024
f572244
Remove compilation configuration
Shute052 Mar 28, 2024
3513305
Doc typo
Shute052 Mar 28, 2024
b181a30
Doc typo
Shute052 Mar 28, 2024
d4a4f35
Fix hashing
Shute052 Mar 28, 2024
070e815
Simplify
Shute052 Mar 30, 2024
a191dd4
Fix macros
Shute052 Apr 4, 2024
3623571
Merge branch 'main' into input-processors
Shute052 Apr 4, 2024
3d8d7ad
Improve docs
Shute052 Apr 4, 2024
84ad6ef
Improve docs
Shute052 Apr 4, 2024
ee10149
Improve
Shute052 Apr 4, 2024
b130c8a
Rename macros.rs
Shute052 Apr 4, 2024
64f23ce
Prefer import from bevy::prelude
Shute052 Apr 4, 2024
feefdf1
Improve macro
Shute052 Apr 4, 2024
25011c8
Improve docs
Shute052 Apr 5, 2024
f7808aa
Improve docs
Shute052 Apr 5, 2024
6b08029
Improve docs and fix CI
Shute052 Apr 5, 2024
bc63f18
Rename Square* to DualAxis*
Shute052 Apr 5, 2024
729948f
Split dual_axis.rs
Shute052 Apr 5, 2024
a10d675
Update module docs
Shute052 Apr 5, 2024
c700dae
Rename `with_processor` to `replace_processor`, add `with_processor`
Shute052 Apr 5, 2024
e79f5d2
Improve docs
Shute052 Apr 5, 2024
48a8929
Improve docs
Shute052 Apr 5, 2024
5a9dc5a
Rearrange the order of match arms
Shute052 Apr 5, 2024
2bacf98
Improve docs
Shute052 Apr 5, 2024
953a935
Typo
Shute052 Apr 5, 2024
f2d8cd3
Expand macros and refine the results
Shute052 Apr 9, 2024
a444164
Rearrange dual-axis input processors
Shute052 Apr 9, 2024
4a8afa0
Rearrange processors
Shute052 Apr 12, 2024
de7ca2b
Remove macros.rs
Shute052 Apr 12, 2024
531cf51
Switch to `serde_flexitos`
Shute052 Apr 12, 2024
ff18aa4
Add tests
Shute052 Apr 12, 2024
d614f7c
Minor
Shute052 Apr 12, 2024
5c1821c
Improve docs
Shute052 Apr 12, 2024
5a4d4de
Inline doc alias
Shute052 Apr 12, 2024
1f35022
Typo
Shute052 Apr 12, 2024
c846c38
Add missing tests
Shute052 Apr 12, 2024
8a9c864
Cleanup
Shute052 Apr 12, 2024
9fe0a6d
Cleanup
Shute052 Apr 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ derive_more = { version = "0.99", default-features = false, features = [
] }
itertools = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_flexitos = "0.2"
dyn-clone = "1.0"
dyn-eq = "0.1"
dyn-hash = "0.2"
once_cell = "1.19"

[dev-dependencies]
bevy = { version = "0.13", default-features = false, features = [
Expand Down
34 changes: 33 additions & 1 deletion RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,39 @@

### Breaking Changes

- Removed `Direction` type. Use `bevy::math::primitives::Direction2d`.
- removed `Direction` type in favor of `bevy::math::primitives::Direction2d`.
- added input processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad` to refine input values:
- added processor traits:
- `AxisProcessor`: Handles single-axis values.
- `DualAxisProcessor`: Handles dual-axis values.
- added built-in processors:
- Pipelines: Combine multiple processors into a pipeline.
- `AxisProcessingPipeline`: Chain processors for single-axis values.
- `DualAxisProcessingPipeline`: Chain processors for dual-axis values.
- Inversion: Reverses control (positive becomes negative, etc.)
- `AxisInverted`: Single-axis inversion.
- `DualAxisInverted`: Dual-axis inversion.
- Sensitivity: Adjusts control responsiveness (doubling, halving, etc.).
- `AxisSensitivity`: Single-axis scaling.
- `DualAxisSensitivity`: Dual-axis scaling.
- Value Bounds: Define the boundaries for constraining input values.
- `AxisBounds`: Restricts single-axis values to a range.
- `DualAxisBounds`: Restricts single-axis values to a range along each axis.
- `CircleBounds`: Limits dual-axis values to a maximum magnitude.
- Deadzones: Ignores near-zero values, treating them as zero.
- Unscaled versions:
- `AxisExclusion`: Excludes small single-axis values.
- `DualAxisExclusion`: Excludes small dual-axis values along each axis.
- `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold.
- Scaled versions:
- `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`.
- `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`.
- `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`.
- removed `DeadZoneShape`.
- removed functions for inverting, adjusting sensitivity, and creating deadzones from `SingleAxis` and `DualAxis`.
- added `with_processor`, `replace_processor`, and `no_processor` to manage processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad`.
- added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors.
- added `serde_typetag` procedural macro attribute for processor type tagging.

### Bugs

Expand Down
2 changes: 1 addition & 1 deletion examples/action_state_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! and include it as a resource in a bevy app.

use bevy::prelude::*;
use leafwing_input_manager::{prelude::*, user_input::InputKind};
use leafwing_input_manager::prelude::*;

fn main() {
App::new()
Expand Down
6 changes: 3 additions & 3 deletions examples/arpg_indirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,19 @@ fn spawn_player(mut commands: Commands) {

fn copy_action_state(
mut query: Query<(
&ActionState<Slot>,
&mut ActionState<Slot>,
&mut ActionState<Ability>,
&AbilitySlotMap,
)>,
) {
for (slot_state, mut ability_state, ability_slot_map) in query.iter_mut() {
for (mut slot_state, mut ability_state, ability_slot_map) in query.iter_mut() {
for slot in Slot::variants() {
if let Some(matching_ability) = ability_slot_map.get(&slot) {
// This copies the `ActionData` between the ActionStates,
// including information about how long the buttons have been pressed or released
ability_state.set_action_data(
*matching_ability,
slot_state.action_data(&slot).unwrap().clone(),
slot_state.action_data_mut_or_default(&slot).clone(),
);
}
}
Expand Down
4 changes: 3 additions & 1 deletion examples/axis_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ fn spawn_player(mut commands: Commands) {
.insert(Action::Throttle, GamepadButtonType::RightTrigger2)
// And we'll use the right stick's x-axis as a rudder control
.insert(
// Add an AxisDeadzone to process horizontal values of the right stick.
// This will trigger if the axis is moved 10% or more in either direction.
Action::Rudder,
SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1),
SingleAxis::new(GamepadAxisType::RightStickX)
.with_processor(AxisDeadZone::magnitude(0.1)),
)
.build();
commands
Expand Down
1 change: 0 additions & 1 deletion examples/clash_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//! See [`ClashStrategy`] for more details.

use bevy::prelude::*;
use leafwing_input_manager::clashing_inputs::ClashStrategy;
use leafwing_input_manager::prelude::*;

fn main() {
Expand Down
1 change: 0 additions & 1 deletion examples/consuming_actions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Demonstrates how to "consume" actions, so they can only be responded to by a single system

use bevy::ecs::system::Resource;
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;

Expand Down
2 changes: 1 addition & 1 deletion examples/default_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum PlayerAction {
}

impl PlayerAction {
/// Define the default binding to the input
/// Define the default bindings to the input
fn default_input_map() -> InputMap<Self> {
let mut input_map = InputMap::default();

Expand Down
70 changes: 70 additions & 0 deletions examples/input_processing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(InputManagerPlugin::<Action>::default())
.add_systems(Startup, spawn_player)
.add_systems(Update, check_data)
.run();
}

#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
enum Action {
Move,
LookAround,
}

#[derive(Component)]
struct Player;

fn spawn_player(mut commands: Commands) {
let mut input_map = InputMap::default();
input_map
.insert(
Action::Move,
VirtualDPad::wasd()
// You can add a processor to handle axis-like user inputs by using the `with_processor`.
//
// This processor is a circular deadzone that normalizes input values
// by clamping their magnitude to a maximum of 1.0,
// excluding those with a magnitude less than 0.1,
// and scaling other values linearly in between.
.with_processor(CircleDeadZone::new(0.1))
// Followed by appending Y-axis inversion for the next processing step.
.with_processor(DualAxisInverted::ONLY_Y),
)
.insert(
Action::Move,
DualAxis::left_stick()
// You can replace the currently used processor with another processor.
.replace_processor(CircleDeadZone::default())
// Or remove the processor directly, leaving no processor applied.
.no_processor(),
)
.insert(
Action::LookAround,
// You can also add a pipeline to handle axis-like user inputs.
DualAxis::mouse_motion().with_processor(
DualAxisProcessingPipeline::default()
// The first processor is a circular deadzone.
.with(CircleDeadZone::new(0.1))
// The next processor doubles inputs normalized by the deadzone.
.with(DualAxisSensitivity::all(2.0)),
),
);
commands
.spawn(InputManagerBundle::with_map(input_map))
.insert(Player);
}

fn check_data(query: Query<&ActionState<Action>, With<Player>>) {
let action_state = query.single();
for action in action_state.get_pressed() {
println!(
"Pressed {action:?}! Its data: {:?}",
action_state.axis_pair(&action)
);
}
}
3 changes: 1 addition & 2 deletions examples/mouse_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ fn update_cursor_state_from_window(

if let Some(val) = window.cursor_position() {
action_state
.action_data_mut(&driver.action)
.unwrap()
.action_data_mut_or_default(&driver.action)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make action_data and action_data_mut infallible again, and remove this. Seperate PR though; I don't care whether it's before or after this PR is merged.

.axis_pair = Some(DualAxisData::from_xy(val));
}
}
Expand Down
6 changes: 1 addition & 5 deletions examples/send_actions_over_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! Note that [`ActionState`] can also be serialized and sent directly.
//! This approach will be less bandwidth efficient, but involve less complexity and CPU work.

use bevy::ecs::event::{Events, ManualEventReader};
use bevy::ecs::event::ManualEventReader;
use bevy::input::InputPlugin;
use bevy::prelude::*;
use leafwing_input_manager::action_diff::ActionDiffEvent;
Expand All @@ -23,10 +23,6 @@ enum FpsAction {
Shoot,
}

/// This identifier uniquely identifies entities across the network
#[derive(Component, Clone, PartialEq, Eq, Hash, Debug)]
struct StableId(u64);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cleanup is good, but not particularly related. Whatever, it can stay.


/// Processes an [`Events`] stream of [`ActionDiff`] to update an [`ActionState`]
///
/// In a real scenario, you would have to map the entities between the server and client world.
Expand Down
2 changes: 1 addition & 1 deletion examples/twin_stick_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub enum PlayerAction {
}

impl PlayerAction {
/// Define the default binding to the input
/// Define the default bindings to the input
fn default_input_map() -> InputMap<Self> {
let mut input_map = InputMap::default();

Expand Down
1 change: 1 addition & 0 deletions examples/virtual_dpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn spawn_player(mut commands: Commands) {
down: KeyCode::KeyS.into(),
left: KeyCode::KeyA.into(),
right: KeyCode::KeyD.into(),
processor: None,
},
)]);
commands
Expand Down
26 changes: 3 additions & 23 deletions macros/src/actionlike.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use proc_macro2::Span;
use crate::utils;
use proc_macro2::TokenStream;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{DeriveInput, Ident};
use syn::DeriveInput;

/// This approach and implementation is inspired by the `strum` crate,
/// Copyright (c) 2019 Peter Glotfelty
Expand All @@ -13,26 +12,7 @@ pub(crate) fn actionlike_inner(ast: &DeriveInput) -> TokenStream {
let enum_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

let crate_path = if let Ok(found_crate) = crate_name("leafwing_input_manager") {
// The crate was found in the Cargo.toml
match found_crate {
FoundCrate::Itself => quote!(leafwing_input_manager),
FoundCrate::Name(name) => {
let ident = Ident::new(&name, Span::call_site());
quote!(#ident)
}
}
} else {
// The crate was not found in the Cargo.toml,
// so we assume that we are in the owning_crate itself
//
// In order for this to play nicely with unit tests within the crate itself,
// `use crate as leafwing_input_manager` at the top of each test module
//
// Note that doc tests, integration tests and examples want the full standard import,
// as they are evaluated as if they were external
quote!(leafwing_input_manager)
};
let crate_path = utils::crate_path();

quote! {
impl #impl_generics #crate_path::Actionlike for #enum_name #type_generics #where_clause {}
Expand Down
16 changes: 14 additions & 2 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@
//! Copyright (c) 2019 Peter Glotfelty under the MIT License

extern crate proc_macro;
mod actionlike;

use proc_macro::TokenStream;
use syn::DeriveInput;
use syn::{DeriveInput, ItemImpl};

mod actionlike;
mod typetag;

mod utils;

#[proc_macro_derive(Actionlike)]
pub fn actionlike(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as DeriveInput);

crate::actionlike::actionlike_inner(&ast).into()
}

#[proc_macro_attribute]
pub fn serde_typetag(_: TokenStream, input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as ItemImpl);

crate::typetag::expand_serde_typetag(&ast).into()
}
68 changes: 68 additions & 0 deletions macros/src/typetag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Error, ItemImpl, Type, TypePath};

use crate::utils;

/// This approach and implementation is inspired by the `typetag` crate,
/// Copyright (c) 2019 David Tolnay
/// available under either of `Apache License, Version 2.0` or `MIT` license
/// at <https://github.com/dtolnay/typetag>
pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> TokenStream {
let Some(trait_) = &input.trait_ else {
let impl_token = input.impl_token;
let ty = &input.self_ty;
let span = quote!(#impl_token, #ty);
let msg = "expected impl Trait for Type";
return Error::new_spanned(span, msg).to_compile_error();
};

let trait_path = &trait_.1;

let self_ty = input.self_ty.clone();

let ident = match type_name(&self_ty) {
Some(name) => quote!(#name),
None => {
let impl_token = input.impl_token;
let ty = &input.self_ty;
let span = quote!(#impl_token, #ty);
let msg = "expected explicit name for Type";
return Error::new_spanned(span, msg).to_compile_error();
}
};

let crate_path = utils::crate_path();

quote! {
#input

impl<'de> #crate_path::typetag::RegisterTypeTag<'de, dyn #trait_path> for #self_ty {
fn register_typetag(
registry: &mut #crate_path::typetag::MapRegistry<dyn #trait_path>,
) {
#crate_path::typetag::Registry::register(
registry,
#ident,
|de| Ok(::std::boxed::Box::new(
::bevy::reflect::erased_serde::deserialize::<#self_ty>(de)?,
)),
)
}
}
}
}

fn type_name(mut ty: &Type) -> Option<String> {
loop {
match ty {
Type::Group(group) => {
ty = &group.elem;
}
Type::Path(TypePath { qself, path }) if qself.is_none() => {
return Some(path.segments.last().unwrap().ident.to_string())
}
_ => return None,
}
}
}
Loading
Loading