Skip to content

Commit

Permalink
Replace axis-like input configuration with new input processors (#494)
Browse files Browse the repository at this point in the history
* Add input processors and input settings

* Replace all usage

* RELEASES.md

* Document

* Document

* Document

* Document

* Add more test cases

* Split out deadzones into another module

* Fix CI and examples

* add `with_settings`

* Update comment

* Split axis settings into separate modules

* Update the documentation

* typo

* RELEASES.md

* fix ci

* fix RELEASES.md

* fix documentation

* fix documentation

* Use new implementation

* Add comments

* Add an example

* Fix warnings in examples

* Remove compilation configuration

* Doc typo

* Doc typo

* Fix hashing

* Simplify

* Fix macros

* Improve docs

* Improve docs

* Improve

* Rename macros.rs

* Prefer import from bevy::prelude

* Improve macro

* Improve docs

* Improve docs

* Improve docs and fix CI

* Rename Square* to DualAxis*

* Split dual_axis.rs

* Update module docs

* Rename `with_processor` to `replace_processor`, add `with_processor`

* Improve docs

* Improve docs

* Rearrange the order of match arms

* Improve docs

* Typo

* Expand macros and refine the results

* Rearrange dual-axis input processors

* Rearrange processors

* Remove macros.rs

* Switch to `serde_flexitos`

* Add tests

* Minor

* Improve docs

* Inline doc alias

* Typo

* Add missing tests

* Cleanup

* Cleanup
  • Loading branch information
Shute052 authored Apr 12, 2024
1 parent 0115ca4 commit 7408157
Show file tree
Hide file tree
Showing 39 changed files with 4,585 additions and 654 deletions.
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)
.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);

/// 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

0 comments on commit 7408157

Please sign in to comment.