#leafwing-input-manager
1 messages Β· Page 3 of 1
AtLeast, AtMost, Range
just unify into range(min,max)
just enum variants help users to use
at least is (least, ?)
at most is (0, most)
you do want a timeout on all input sequences to avoid weird behaviours
you won't like write u32::MAX everywhere
you won't want max anywhere
there's also other conditions combos might break, like getting stunned or running out of resource
I personally think using AtLeast(200ms) is more straightforward and easier than Range(200ms, ?)
in this situation, you don't really need to worry about what the max might be, so it just makes things simpler
exactly! that's why having a fallthrough mechanism in ActionState or InputMap would be really beneficial.
might be more complex than that, i'll need to think about it...
short sequence that doesn't allow anything else is easy enough, but everything else seems to spawn more issues
and in fighters the sequence doesn't cancel at least move and block...
We should treat sequence as just another type of user input. However, the clashing check in LWIM currently prevents this.
So, if a chord or sequence includes a button that's also another single input, that single input will be canceled out.
thinking about warframe melee combo system, there's also the issue that sequence itself might need chords
but warframe has an actual action in between
ahhh, i forgot about that, clashing checks are implemented in UserInput
so it should be easy to avoid cancel
each single input is a separate move, but it's a more realistic use case for combos in general
sequence takes a vec of UserInput, and chords are UserInput
so, that won't be a problem
the main problem is the stuff in between
animations, resource costs, chance of getting stunned/interrupted, and actually calling the right move
so maybe sequence is overkill in general
so, ActionState really needs a cancel function
in warframe there's also the same case:
forward doesn't stop movement, block doesn't stop block, so (W+RMB)+E still moves and blocks until you press E
then the animation plays and prevents both move and block
we probably need someone that's actually working on a fighter to chip in
LWIM aims to be an input-action bindings manager, just a wrapper for Bevy's Input, not the game engine itself
so this might be beyond its scope
yeah
chords handle a huge majority of use cases, the rest might be easier to program manually
i'd really like the combo menu to be somehow able to display the correct sequence of real input without any chance of bugs though
a display for input stream could be useful in general too
the library can strive to meet most of the user's needs, but it's impossible to cover everything
yeah i don't think it should
just trying to make everything more useful and easier for everyone
which kind of display and input stream?
Just messing about with Cammy in training.
Just started using her :)
this thing on the left
ahhh, Bevy can definitely handle that. LWIM is more like an ActionManager
yep, it's just useful for debugging so i think it definitely should be built-in (or at least have a plugin in examples), alongside the rebind menu
see Bevy::input::gamepad
and you have ButtonInput<GamepadButton>, which collected all the pressing buttons in a frame
/// A "press-able" input of type `T`.
///
/// ## Usage
///
/// This type can be used as a resource to keep the current state of an input, by reacting to
/// events from the input. For a given input value:
///
/// * [`ButtonInput::pressed`] will return `true` between a press and a release event.
/// * [`ButtonInput::just_pressed`] will return `true` for one frame after a press event.
/// * [`ButtonInput::just_released`] will return `true` for one frame after a release event.
#[derive(Debug, Clone, Resource, Reflect)]
#[reflect(Default)]
pub struct ButtonInput<T: Copy + Eq + Hash + Send + Sync + 'static> {
/// A collection of every button that is currently being pressed.
pressed: HashSet<T>,
/// A collection of every button that has just been pressed.
just_pressed: HashSet<T>,
/// A collection of every button that has just been released.
just_released: HashSet<T>,
}
It's a resource, so you can use it directly in your system
fn button_stream(buttons: Res<ButtonInput<GamepadButton>>) {
let being_pressed_down_buttons = buttons.get_just_pressed();
}
all that's left is to convert them into the corresponding arrows and display them on the screen
but it's not buttons being pressed, otherwise it'd just show controller stick
Bevy is an ECS-based game engine, your systems (functions) will run every single frame
you could collect the buttons into your collections, and convert the collection into arrows on the screen
so it needs to convert dpad into input only if it's different, or does it already do that?
ahh, why not use Stack like collection to handle it?
or queue
just check the recent button with the newly pressing buttons?
stick isn't a button though
oh Shute052! I love your work on the LWIM repo!
stick needs to act as a dpad, and change in direction acting as input started
IIRC, Bevy has an event like axis value changed
what about LWIM?
LWIM just a wrapper for all Bevy's Input without touch input
also handle it, let me check the source code
axis_pair is LWIM's dual-axis value
and this
ok let my vscode finish fetching and i'll see how that works
LWIM has an Events<ActionDiffEvent> resource
In the example for twin_stick_controller it is suggested to use a player controller abstraction, would a resource be a good way to represent a player controller layer to communicate with a movement/camera system?
let me take a look. I haven't gone through the examples in detail yet. (I'm just a new contributor to LWIM, and initially, I was only fixing some bugs I came across π )
Aha π
alice has quite a few oversights (whisper
Also trying to figure out where I can find documentaion on methods for LWIM
for example, how to read gamepad axis magnitude
let rawInputDir = LWIM.axis_pair(&input::PlayerAction::Move).unwrap().xy();
rad, thanks π
let inputDir = rawInputDir.clamp_length_max(1.0);
Isn't ActionState::value(VirtualDpad) just returning the magnitude of the dual-axis?
It should be
and must be
Never! :p
I'm sure it is! I just started digging in the repo today
that or i just struggle navigating it
Just scroll down further π
Or use the modules / structs / traits link on the left
oh i see, i also had to click the documentatio button to get there
which took me here
that had the methods
to the left in the bottom
ahh that's true
BTW, my IDE's English text checker keeps showing errors in the code comments, but I'm not great at fixing them because I'm not skilled in English documentation
it's annoying when I have to keep dealing with text grammar errors while trying to check for code errors π¦
but most of the style errors are just because it thinks there shouldn't be informal writing in the doc comments, and the typos are mostly non-english words
Which one are you using? I can do a quick pass to see if they're real
JetBrains RustRover with plugins like Grazie Pro
and most of general's ananotators are just Long sentences (n words here) are harder to read, according to the research; consider splitting
Ah yeah. That's very much a matter of taste.
grammar errors, for example
These look more useful. Are you interested in submitting a PR to try and fix these? I can do an English editing pass on it π
It's okay if not, or if you just blindly follow the suggestions there and then we trim the back in review
if I were to just apply its suggestions, I'd only need to press a few buttons, much like cargo fix. However, many are matters of taste, and there are also some cases of simplifying long sentences. I'm currently doing a quick review to see if it breaks anything
Well, it's getting axed soon anyways
that's true indeed π
well, that's probably enough to make the input stream log working, just need to compare to previous to filter out repeats
and actually make the UI/icons working...
i guess this works
need to turn move into distinct directions...
@karmic monolith
not going to switch to Direction2d?
Ahaha, I can finally swap over!
I didn't bother to initially
@obtuse zinc can you glance over to see if anything there needs/is worth transfering over to Direction2d? i think north/east/west/south + ne/nw/sw/se might be worth adding for 2d
https://github.com/Leafwing-Studios/leafwing-input-manager/blob/main/src/orientation.rs
A straightforward stateful input manager for the Bevy game engine. - Leafwing-Studios/leafwing-input-manager
yeah there's actually a lot of fun stuff
@restive knoll FYI
So when pressing two directions at once in the twin_stick_controller example for the .axis_pair(&PlayerAction::Look).unwrap() portion I get "Looking at: [NaN, NaN]", is that supposed to happen?
actually pressing, or neutral?
if i press both arrow keys
let me check that...
it says [NaN, NaN], which gives me trouble when using .axis_pair().unwrap in my application
line 200?
yes
try changing normalize to normalize_or_zero
rad, obvious now that you say it
or clamp, which would allow subtle aiming/movement for controllers
Hmm, NW/NE/SW/SE could definitely be useful
i think an enum for the directions might be useful for tile-based games and fighters, not sure
And definitely the math ops like dir + dir = vec, f32 * dir = vec (we only have dir * f32 and only for 3D iirc), and so on
and a function to convert direction2d to 4 or 8 from cardinal direction enum and back
NE, SW and so on are ordinal, only N, E, S, W are cardinal
what's the cardinal+ordinal?
Compass::NE lol
that actually sounds good, yeah
there's no conversion to radians for direction2d yet, yeah?
probably
ask math-dev
or i guess you can just make both versions for conversion
Yeah
converting to radians like that would be a bit weird imo, you can already do angle_between
Like Direction2d::X.angle_between(*Direction2d::Y)
It's really useful with a Rotation2D component on your entities π
Since then you never screw up the reference angle
might be useful for shaders in some way too
probably something else we're not even considering
The LWIM code looks super clean btw
good luck upstreaming it.
Thank you both β€οΈ
Wait, you can make doc comments like this?
/**
Maps from raw inputs to an input-method agnostic representation
Multiple inputs can be mapped to the same action,
and each input can be mapped to multiple actions.
...
**/
pub struct InputMap<A: ActionLike> {
why didn't I know that... afaik block comments like this typically aren't considered good practise but this does look cleaner for this long-ish code example
yeah, it does
Yeah, it also makes things easier for copy-pasting tests in (since RA hates doc-tests)
but my main problem with docs is that when you're reading it through the file you need to scroll down to know what it's talking about
maybe add non-doc comments at top for titles just for the long ones?
in the twin_stick_controller example, the fn player_mouse_look is doing some wild stuff, I can't even make out where it captures mouse input data
might have to do something with new plane changes
it's supposed to create a plane from player towards camera, so that no matter what camera rotation is, it'd always have a flat plane relative to player position
screenspace cursor -> worldspace origin + direction -> check if it hits that plane ->
i think diff might fail depending on orientation, but it's supposed to be direction towards the player-relative worldspace cursor
and the final step is emulating controller's stick based on that ---------^
yeah, if it works then it's cool but definitely not beginner-friendly
ah
yeah bc it has dead zones now and feel inconsistent
hmmm, i might not be expressing myself correctly here, but there are places where it stops rotating the camera, and I thought the function just captured mouse x/y input for the action_state
because I got the gamepad equivalent to function with my camera system
But it might be a case of me behind the keyboard
so I'm using PlayerAction::Look in my own camera system to rotate the camera. Works fine with the gamepad, but player_mouse_look fn seems broken, but IDK where to begin bc the fn is doing magic to me π
also works fine with keyboard arrows
but as soon as I touch the mouse, there are places where rotation stops and feels inconsistent
3rd person, if that's what you are asking
yeah, then you need to switch to delta, you can scrap like half of that code
Ok, I'll try some of my own chops instead, thanks for helping me understand the fn a bit better β€οΈ
how do you paste it pretty like that?
My god, its beautiful in here.
thanks
You can put βrustβ after the ticks with no space to get syntax highlighting
anyways, FPS doesn't need you to distinguish between mouse and controller since mouse delta is similar enough to controller stick (because your cursor is locked to center, so you don't care about its' position on screen)
ah makes sense! I'm trying to get a hide cursor move camera mode and a cursor out when in UI mode kind of thing going
as well as the disable/enable of keyboard mouse/gamepad depending on what you use, which seemed straight forward following the example for twinstick controller
/*! inner doc comments */, /** outer doc commments */
using .normalize() on Vec2::ZERO will result in [NaN, NaN], and we need to fix that example
pub fn input_axis_pair(&self, input: &UserInput) -> Option<DualAxisData> {
match input {
UserInput::Chord(inputs) => {
if self.all_buttons_pressed(inputs) {
for input_kind in inputs.iter() {
// Return result of the first dual axis in the chord.
if let InputKind::DualAxis(dual_axis) = input_kind {
let data = self.extract_dual_axis_data(dual_axis).unwrap_or_default();
return Some(data);
}
}
}
None
}
UserInput::Single(InputKind::DualAxis(dual_axis)) => {
Some(self.extract_dual_axis_data(dual_axis).unwrap_or_default())
}
UserInput::VirtualDPad(dpad) => {
let x = self.extract_single_axis_data(&dpad.right, &dpad.left);
let y = self.extract_single_axis_data(&dpad.up, &dpad.down);
Some(DualAxisData::new(x, y))
}
_ => None,
}
}
source code for axis_pair
should it just be None when the values are [0.0, 0.0]?
this function hasn't been checking that since version 0.5 when alice added it
ah, so, None just means the data isn't available there
this should be fine
How does one map both arrow_keys and mouse_motion to one player action? Is it even possible?
how about the const arrow_keys() and mouse_motion() creations of VirtualDpad?
let input_map = InputMap::default()
.insert(Action, VirtualDpad::wasd())
.insert(Action, DualAxis::mouse_motion());
// or
let input_map = InputMap::new([
(Action, VirtualDpad::wasd()),
(Action, VirtualDpad::mouse_motion()),
]);
but VirtualDpad doesn't have value sensitivity
and deadzone
from the twin_stick_controller example: ```rs
impl PlayerAction {
fn default_kbm_binding(&self) -> UserInput {
// Match against the provided action to get the correct default gamepad input
match self {
Self::Move => UserInput::VirtualDPad(VirtualDPad::wasd()),
Self::Look => UserInput::VirtualDPad(VirtualDPad::arrow_keys()),
Self::Look => UserInput::Single(InputKind::DualAxis(DualAxis::mouse_motion())),
Self::Shoot => UserInput::Single(InputKind::Mouse(MouseButton::Left)),
}
}
}
ahh
fix them in PR later
but you could split it into two functions: default_keyboard_binding and default_mouse_binding
and I feel like this way would be better:
fn default_input_map() -> InputMap<PlayerAction> {
let mut input_map = InputMap::default();
// Default gamepad input bindings
input_map.insert(Self::Move, DualAxis::left_stick());
input_map.insert(Self::Look, DualAxis::right_stick());
input_map.insert(Self::Shoot, GamepadButtonType::RightTrigger);
// Default kbm input bindings
input_map.insert(Self::Move, VirtualDPad::wasd());
input_map.insert(Self::Look, VirtualDPad::arrow_keys());
input_map.insert(Self::Shoot, MouseButton::Left);
input_map
}
make it easy to add more stuff later on.
I added the extra 'Self::Look =>' just to illustrate my issue, sorry I see how I worded it poorly.
No need to fix!
This is a good idea
in PR, i added InputManagerBundle::with_map(InputMap) allowing creating the bundle with the given InputMap and default ActionState
// before
InputManagerBundle {
input_map,
..Default::default()
};
// after
InputManagerBundle::with_map(input_map);
@karmic monolithi thought it's pretty clearer. could we switch to this?
switch to clamp to 1 instead, better than normalize
vec2.length().clamp(0.0, 1.0)?
it's a direction in the example
so it should be normalized
direction2d
until we have Bevy's rotation2d
@karmic monolith would you like to create a new branch to avoid causing chaoes directly to the main branch until the new system has been fully tested?
many parts of the UserInput trait seem to be ready
.clamp_length_max(1.0);
wait, let me see the example...
yeah there's no real conflict there
clamping movement feels better when you can use full circle instead of snapping to max speed
normalizing look usually feels bad
could you provide any idea for
i've been digging through LWIM code yesterday to get a better idea of how it works in the first place
for now i think there's a much better immediate goal of changing direction to direction2d, upstreaming all useful functions and fixing LWIM to work with direction2d
i also want to work on my game a little more to make abusive LWIM stress test to make sure what i'd definitely want from it, and what rework might break
for now my goal is to make a crappy imitation of unity's input system binding menu
and the idea is that at runtime players should rebind whatever action they want to whatever they want, so e.g. if somebody wants to assign any button to be a chord of a stick at a certain angle plus a button, it should be possible
today i sidetracked because actions and etc would need to have translations, and i have no idea if bevy already has something built-in or planning to upstream anything
@candid vigil the twin stick example's stick emulation for mouse would be really good as built-in option in LWIM
i'm cleaning up my camera+reticle code right now, to see if it's a good idea or if it's better to just keep it as example
there's already a PR #488. Is that yours?
nope
i thought jondolf upstreaming half of the stuff there to dir2d would make it cleaner but i guess doing it like that is fine too
yeah LWIM has 8 direction constants, while Bevy only has 4
jondolf might add them, or the new compass enum. it just sounds really useful.
@karmic monolith could you take a look at #489, and then I'll go ahead and push a PR about the current progress of #483?
Just merged and releasing π
thank you!
@karmic monolith Couldn't find a thread for leafwing_abilities, so I'll just post it here.
whilst porting my personal project to bevy 0.13, I realised I need better input and abilty management, and decided that leafwing_abilities was good. but since it wasn't updated, I did it myself:
https://github.com/Leafwing-Studios/leafwing_abilities/pull/45
for the variants i just mixed controller and kbm into 1 map, is there even any benefit from splitting them?
no idea honestly
the only thing i'd need is knowing where kbm and controllers switch to change UI icons, and to prevent same action from being used from 2 devices, but i doubt input map helps with that...
i guess it makes sense when you don't want it to track touch/xr/controller when you don't even have one
Awesome I'll cut a release!
maybe it's not worth splitting them up.
InputMap itself is easy to expand, much like a HashMap.
Are there any practical uses for SingleAxis::negative_only and SingleAxis::positive_only?
I'm in the process of replacing the current SingleAxis, DualAxis, and dead zone settings with pre-processing, similar to Unity, for both single-axis and dual-axis
keep rebinding in mind
but yeah, in unity the action itself decided if it was button, axis, dual or 3d
and you could assign/emulate anything with that
but shouldn't handling specific directions be another input preprocessing, rather than the responsibility of the deadzone?
what do you even mean by preprocessing
I misread that
i know that bevy controller has deadzone but i'm not sure how it was supposed to be edited, if LWIM allowed that + remapped from deadzones to 0-1 it'd be good
well, actually, there is, but it's implemented in a bit of a weird way. I'm working on writing a new one.
src/axislike.rs in LWIM
but the core function/workflow of input manager was super simple
you make a "map" that you can easily switch between (e.g. walking, driving, flying)
then you make an action in that map, and decide if it's button/axis/dual/3d
the rest can be set up/rebound/added/removed by user
and the current one only has min, not max
the current one is implemented by https://github.com/Leafwing-Studios/leafwing-input-manager/pull/438
Closes #398
The issue above describes the changes made and why really well.
To-Do:
Fix tests
Finish updating docs
Turn back on missing_docs
Update RELEASES.md
i rewrite its handling in https://github.com/Leafwing-Studios/leafwing-input-manager/pull/466
but now i wanna replace it with input processors, like unity
mm...
normalize/clamp should be inside action, inaccessible to users
deadzone, scaling, and invert should be accessible to users
no idea what custom processors are used for
yeah, i wrote the processors for them
but i think the main thing is allowing users to bind anything to any action without having devs think about it
sure, but now i'm just working on rewriting the underlying architecture of LWIM, allowing more extensions
the current architecture is terrible
a lot of hardcoding
π€ i don't like the idea of solving problems ahead of time, but if you have a clear idea of what you want then why the hell not
yeah, that's why I'm trying to...
yesterday i reworked the camera + reticle for my game, changing from a camera-relative movement + aiming to plane-relative
controller stick (or dpad/arrows) takes input, clamps it (<-- this could be done for the action, can't do it right now because player-added input wouldn't be normalized),
then multiplies by max range and places reticle relative to plane and focus point
mouse is similar to twin stick example, takes cursor, converts to worldspace, intersects with plane...
and then clamps length by max range to make same thing as controller stick.
i wanted both mouse and controller to work at the same time since i've seen that some people play with controller in 1 hand and mouse or keyboard in the other
so to allow both of them to work i ended up comparing sq length and just picking the highest
this basically meant that mouse couldn't be used for the same input as controller, otherwise mouse delta conflicts with mouse position
i'm not sure if it was the best way to do it, but there's probably going to be another problem with VR/touch/ui later so i don't want to think about it right now
i still have to try it out with FPS and anti-gravity, but yeah...
LWIM doesn't really need to do much, and so far i didn't find anything new that i want that i didn't mention π€·ββοΈ
Hmm I don't think so?
I'm in the process of replacing the current SingleAxis, DualAxis, and dead zone settings with input processing, similar to Unity, for both single-axis and dual-axis
but shouldn't handling specific directions be another input processing, rather than the responsibility of the deadzone?
if you have a steering wheel and want steering right to be used as RMB and steering left to be used as LMB then it makes sense to add as processor, yeah
"takes cursor, converts to worldspace, intersects with plane", the new version should have a bulit-in function
yeah, i did it partially to understand if it should be
just because the current architecture is hard to add new extensions
that's because the current internal system has abstracted all supported input methods into three types: button-like, single-axis-like, and dual-axis-like.
plane-relative movement+reticle sounds like it could be built-in but i want to finish messing around with FPS, anti-gravity and using focused point to try lagging behind/looking ahead of player, and doing the cinematic sweeps
so it's about handling inputs based on these three abstractions, rather than dealing with each input type separately
it's required though, isn't it?
but that's why it's hard to add new extensions
float, vec2, vec3 need to be known ahead of time for making game
the real world not only these input types: button-like, single-axis-like, and dual-axis-like
yeah but you convert any input to these 3-4 types
if you want vec2 then you make a virtualdpad wasd
or a dualaxis controller stick
no idea if it supports using stick as a button by direction right now though
for example, XR inputs and gestures, triple-axis-like, multiple f32, multiple Vec2, multiple Vec3
and the current system is handling the input values in a single huge function
yeah gestures and triple-axis are necessary, don't know enough about the rest
and adding gestures to XR also implies you can add them for mouse
multiple finger gesture requires multiple f32 and multiple Vec2
for multi-fingered gloves XR, multiple Vec3s are also needed, as they deal with 3D input data
yeah, i have no idea how it's done so it's better to check how unity did that
they went pretty hard on XR support
yeah, i'm checking the unity documentation
maybe also ask around #xr
and in the future, XR might even support turning every joint of a person into an input point, so players can control characters just like controlling our own bodies, right?
even though it might be a while before we see that
there's probably a certain point where it's no longer a concern for LWIM though
yeah
it only needs to unify controls where it can, and give access to do it manually for the rest
i can see the thing i did being standardized though
i think all the input handlings should be able to upstream into Bevy
and LWIM just work as an input-action binding manager
cursor ray -> pointer ray
camera movement/looking at a point between focus and pointer -> head tracking
just separating gameplay, camera, "pointer", and reticle in general
yeah, my planned new user inputs, CursorWorldPosition and CursorScreenPosition
they're hard to add into the current LWIM
because LWIM obtains input values from the Resources currently
it cannot support get values from a Query right now
that's why i wanna split the existing terrible InputStreams
the problem with cursor is that you might not have it
like on console or mobile
yeah
so all the new user inputs will return their values wrapped in an Option
because sometimes the input devices are also unavailable
and ClickedWorldPosition and ClickedScreenPosition for both mice and mobiles
and ClickedEntity? like bevy_mod_picking
that'd be a bit annoying to work with
and if it's not available then it shouldn't be polled anyway
i'd rather have the default values
there's only 1 case where i think option would be useful and that's pausing the game when device is lost, but that should be handled by LWIM too
but shouldn't you consider adding support for multiple input devices in your game? Like keyboard and mouse, gamepad, and mobile?
if one kind of action only binds with a specific input device, like cursor edge panning
and the device is unavailable, it should return None instead of [0.0, 0.0]
or Result<ValueType, InputError>?
and just unwrap_or_default
ah, yeah
only one line would be added, or you wanna print the panic information
could you provide any idea for https://github.com/Leafwing-Studios/leafwing-input-manager/issues/491
and @karmic monolith
if this is user-configurable then clamp should be moved out of it, otherwise someone adds new input or removes a processor to gain ~30% more diagonal move speed
ahh, yeah
i don't understand for reject all values, is that like disabling device
just a thought for a possible configuration?
and .invert_ is already there in leafwing so it's not entirely new, clamp/normalize too, not sure what else
but the existing ones requires branch logic during the computation
/// The sensitivity and inversion of the input.
///
/// Using a single `f32` here for both sensitivity and inversion
/// improves performance by eliminating the need for separate fields and branching logic.
///
/// The absolution value determines the sensitivity of the input:
/// - `1.0` indicates no adjustment to the value
/// - `0.0` disregards input changes
/// - Values greater than `1.0` amplify input changes
/// - Values between `0.0` and `1.0` reduce input changes
///
/// The sign indicates the inversion:
/// - Positive values indicate no inversion.
/// - Negative values indicate inversion.
pub multiplier: f32,
there's my optimized ones
inner implementation
ohh, good idea
just multiply it
for FPS camera it makes sense, since it doesn't need to clamp/normalize...
for movement input it will need to be clamped afterwards...
if there's a case that movement needs to be circular but allowing sensitivity to be customizable, it'd need to be clamped beforehand
all of it is not for user to decide though
yeah, so the implemented one just a AxisInputProcessor, and the addition of all types of the processors just its functions
so basically the action will need an option to disable clamp(or normalize) / clamp(or normalize) before / after user-defined processor
if you add a new same one, the old processor will be replaced
maybe instead of processor, call it user settings?
that's what I used to call it, but I'm not sure which is the best name
user settings are more self-explanatory
processor would be better for dev side of it
yeah
validating input is the dev's responsibility imo
if it can be done by input manager then why not though
clamping doesnt make sense in all scenarios
sometimes diagonal input is supposed to be faster
yeah, that's why it should be an option
alright im down for that then
and if input manager takes care of user settings and you need clamping before it's inverted or multiplied then it'd become difficult unless clamping option is built-in
@candid vigil https://youtu.be/YglA08bgQtI?t=319
one more thing to consider π₯²
I have UI buttons hooked up to actions, and when I just_pressed those actions, it reports them pressed for every frame the UI button is held down. Is this a known issue?
Hmm, that's not ideal. I don't think that's a known issue
I'm looking through the examples, and it looks like the only ones that use just_pressed don't have UI hookups, so it could've slipped through. Do you need a minimal repro?
I know you've got more important things to do haha, I'm happy to submit a PR unless I get stumped
But I'd much rather present a solution with the problem, so I'm happy to take the first stab at this
That would be lovely π
i also noticed just_pressed doesn't work with me on 0.12.1 today
just doesn't work, even if i schedule the systems that reads inputs after the tick system
to be honest i really balk at using an individual system for every single action state, plus using the ECS for literally every problem in general-- thrashing the scheduler for no reason, to read a few inputs?
otherwise the API looks really nice but i am confused why bevy doesn't have anything other than an ECS, it is weird to me
hey I uhhhh was using the variants() method on Actionlike. but it's gone in the latest version and idk how to get the equivalent behavior
Take a look at strum: it's a crate dedicated to this sort of enum iteration
just combine the maps together into 1 if you can't find a reason to keep them split apart (and then combine them anyway)
ty for the reccomendation! i got the previous functionality working
but I think (from looking at recent examples) you guys have implemented the behavior I wanted so I might just need to update all my logic instead of the weird stuff I was doing before haha
I'll take a look at this in a bit
I'm still debugging all the new deadzone types
The current behavior of the Ellipse deadzone is actually incorrect because ideally, a circular deadzone should smoothly normalize all input outside the deadzone, but it's not doing that properly
So, I'm rewriting the implementation for all deadzones
The new circular deadzone looks fine after I've tested all xy values within the range [-1.0, 1.0] with a step of 0.01
However, there are still some issues with the new rounded square deadzone, especially around the corners
now more work is on Alice's plate π
1416 lines of code and 629 lines of comments
half of the code consists of unit tests
PR created, but the new implementation haven't been used to replace the old ones yet
Keeping up with the codebase standards I see π
"the trait bevy::prelude::FromReflect is not implemented for std::boxed::Box<input_settings::axislike_settings::SingleAxisSettings>"
@karmic monolith how do i implement Reflect manually?
Uh you really shouldn't
Very easy to screw up
Don't worry about boxing it for now: this will get cleaned up in the trait refactor anyways
clippy is really unhappy and threw a bunch of errors at me
this will prevent the CI tests from passing completely
Can you add an allow?
this PR isn't urgent. I might not be able to continue with the next stage of trait refactor for another two weeks. The holidays are over, and I'm busy with work for these two weeks as well
it is kind of weird imo that the allocation type of an object is tied to so much syntax in rust
in c# you add sealed, now the compiler can optimize away dynamic dispatch
in nim you add ref, now itβs on the heap instead of the stack
in rust, you add Box<T> and go refactor 200 lines of code, break all your clients
or swap from enum to trait which is also weeks of work

I am not rly certain rust got it right, I am actually confused how it got such a huge following with the sheer level of complexity and toil for absolutely minimal gains, I believe u can design a memory safe language that doesnβt have so much toil and explicitness without all this headache when u have to do.. anything
I just use it because I donβt wanna write c++ and nim doesnβt have a stable game dev ecosystem
but how are u guys not driven crazy by the limitations and toil you have to go through to accomplish simple things.. thereβs a reason these other languages got these features, to reduce toil
I had to create 4 types, all with confusing names/responsibilities, just so I could do some really basic stuff with dynamic dispatch
trait for clients to implement (which has associated cost, so not object safe), trait to be dynamic over it (DynX), newtype to specialize over it (BoxedX)

it doesnβt have to be like this.. no other lang is this insaneβ Iβve seen other ppl use these patterns all over big rust libs but nobody saying how awful they are
Also, sorry to derail your thread, I just think itβs an interesting talking point that was relevant and literally nobody will engage this kind of talk in the rust community in an unbiased way
u should just delete all that but itβs worth a read to see how the rest of the world works if you are annoyed at your epic Box/Trait refactor
@tiny fern seems like it's great for discouraging people from using heap though
premature optimization at the cost of everything
well not everything, but half the dev worlds sanity
true, but at the same time you know that people will always try to use the easy way to get shit done and it's always a disaster
I feel like rust could be even more popular if it wasnβt so allergic to trad patterns for nooooo reason
the majority of ppl I know in the industry literally thinks rust devs are insane
they also include me, and I understand why
they like their features and I also hate not having em
so for now i'm just taking it as rust nudging me to write better code
but yeah, your point is definitely valid
there are features in the pipeline to reduce toil too even in the specific β4 typesβ example I gave so itβs rly not ultra fair but
still painful right now
I can't remember the last time I had to add Box to a type. Like, I generally know when I am going to need dynamic dispatch when I am writing the code the first time. You got me with the Enum/Struct refactoring through, that's always a pain. Really wish they just only had algebraic data-types types with one or more constructors.
is it still "okay" to add a LeafwingActionState<T> directly to an entity and later access and directly modify it within a query? I'm trying to get the underlying ActionData by using .action_data_mut but I keep getting a None value back and I'm not sure why? Does it not come initialized and I need to handle the ActionData not existing initially?
I believe it no longer comes initialized
welp that explains a thing or two haha.
you could use action_data_mut_or_default()
this function will add a default value to the internal hashmap when the action data is missing, and return a &mut of the newly added one
could you explain the problem with this is what exactly? If this can be implemented, I can then add a user-defined input processor
There was concern that it would lead to silent recursive nesting of boxes
I'm pretty sure that this has been proven to not be the case, but validation of that and a concrete use case would be helpful to drive it forward
(I really want this too)
that's a pity
but are there any tutorials on how to implement FromReflect? I think it should be fine if it's just to implement a &dyn InputProcessor
which just a trait with a fn process(f32) -> f32
Check the macro definition π shouldn't be too bad here
yup I just found this last night and my game is officially fully migrated to 0.13
Alright, I'm totally lost. This seems to be what's pressing the buttons:
And this seems to be what's releasing it (specifically the ButtonState::Released match arm):
(This is for UI buttons triggering just_pressed repeatedly btw)
Near as I can figure, this is because which_pressed goes through the list of Actions that need to have their inputs updated and finds that "whoopsie, nobody's pressing any of the key bindings for this Action, let's release it"
Then update runs on that information and releases the button. Unfortunately that means that it's released next time the UI interaction system runs, and the button gets newly pressed again
At a high level, do you think it's a good idea to maybe add a UI interaction field to InputStreams? Then which_pressed could treat it as just another way to trigger an action
Hmmm, actually that would require adding a new InputKind. And that would require reworking how UI buttons even interact with actions. Right now you can wire up a button to an action, but what I'm proposing would end up with wiring actions to buttons, just like how keyboard works now:
InputMap::new([
(Action::Left, UiInteraction(left_button)),
...
])
instead of:
left_button.insert(ActionStateDriver {
action: Action::Left,
targets: player.into(),
})
...
Before I even go down this road, is this counter-productive/even what we want? We could keep the current API with some hack or other in update_action_state instead. What's the preference here?
Yes and no! I want to have better first class support for that, but I really want to rip apart InputStreams and InputKind in favor of a more flexible and principled trait-based approach.
@candid vigil has been making good progress towards that, which is great because there's a dozen Bevy intitiatives that want attention from me π
Awesome, I'll check out those PRs and see how they affect my current problem
@karmic monolith I'm going to open an issue for this specific problem so I have something to link and discuss with @candid vigil, but it doesn't need any extra attention π
Yes please!
Do action states read properly in fixed update schedule? Or just in update?
Hmm. It used to be broken with FixedUpdate, but there have been changes in Bevy there. I haven't tried recently
this might also mean changing how InputMap and ActionState get updated, like which input operation takes priority, and we'll need to register new input types
but it looks like we won't need ActionStateDriver anymore
@untold thorn I think you might be hitting https://github.com/Leafwing-Studios/leafwing-input-manager/issues/496
Thanks for the ping! Iβll look into it
@karmic monolith I have looked into it. My specific issue is not about the repeated just_pressed inputs but it is indeed connected. Considering Interactions as input types may allow to fix it (like mentioned here: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/483#issuecomment-1972125204 by the same issue author).
What is happening in my case is that:
- Thereβs the βghostβ UI issue in Bevy since 0.13: https://github.com/bevyengine/bevy/issues/12007, which manifests itself (pun intended) in my game because I do toggle visibility on and off of certain UI nodes.
- This ghost UI is being detected by the
ui_focus_systemin Bevy, which marks the ghost UI withInteraction::Hovered. - Leafwing Input Manager in the
update_action_statesystem, filters outButtonInput<MouseButton>inputs because there are some UIInteractions that are notNone(those on the ghost UI)https://github.com/Leafwing-Studios/leafwing-input-manager/blob/fff223b72deb58364457b7241918d822a21ed48e/src/systems.rs#L96
I can work around this by setting the "no_ui_priority" feature for Leafwing Input Manager in my game until #12007 in Bevy is solved.
Oh boy. Okay, thanks a ton for the investigation
Question -
How would you architect different 'sensitivity' configurations for different inputs driving the same action?
Usecase - I want to support panning the camera in a 2d game with a virtual DPad or with a 'click + drag' mouse action.
This is really easy to configure, the mouse motion is buttery smooth and great, and the virtual Dpad is slow as molasses in Jan.
(Also just as a random aside, this isn't exactly a surprising problem but I -really- would've assumed it'd go the other way and I'm sort of confused as to how it doesn't)
Do you mean you're looking for .with_sensitivity()? π€
iirc there was an issue with the speed of non-mouse inputs being FPS bound tho ... Idk if that's been fixed yet
guys there are the way to get combined action state for every registered actionlike? or i need manually create "global" actionlike component with all actions i need and then query it?
I ended up creating a generic system to handle this situation
then you can just register a system per action you have
I want to move the camera by dragging with the mouse so I found this example here
https://github.com/Leafwing-Studios/leafwing-input-manager/blob/main/examples/mouse_motion.rs
which is basically what I need I just want to hold the left mb as well so I read the docs and found chords and changed the code to this
let mut input_map = InputMap::default();
input_map.insert_chord(CameraMovement::Pan, [DualAxis::mouse_motion(), MouseButton::Left]);
which doesnt work because those inputs are different types
is there a canonical way to make this work?
I don't think so currently: chords only really work with button like inputs
Instead I would check the logic within one of your own systems, and use an action to abstract over the non-drag part of the input
alright thank you
Why doesn't input_map.insert_chord(CameraMovement::Pan, [DualAxis::mouse_motion(), MouseButton::Left]) work?
IIRC [VirtualDPad::mouse_motion(), MouseButton::Left] working
error[E0308]: mismatched types
--> src/main.rs:20:76
|
20 | input_map.insert_chord(CameraMovement::Pan, [DualAxis::mouse_motion(), MouseButton::Left]);
| ^^^^^^^^^^^^^^^^^ expected `DualAxis`, found `MouseButton`
ohhh, add .into()
to what?
InputKind or UserInput
[DualAxis::mouse_motion().into(), MouseButton::Left.into()]
this should be fine
|
209 | pub fn insert_chord(
| ------------ required by a bound in this associated function
...
209 | pub fn insert_chord(
| ------------ required by a bound in this associated function
...
212 | buttons: impl IntoIterator<Item = impl Into<InputKind>>,
| ^^^^^^^^^^^^^^^ required by this bound in `InputMap::<A>::insert_chord`
help: try using a fully qualified path to specify the expected types
|
20 | input_map.insert_chord(CameraMovement::Pan, [<leafwing_input_manager::axislike::DualAxis as Into<T>>::into(DualAxis::mouse_motion()), MouseButton::Left.into()]
| ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ~
tried the suggested fix for UserInput doesnt work cause From isnt implemented for that and for InputKind it crashes at runtime
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(InputManagerPlugin::<CameraMovement>::default())
.add_systems(Startup, setup)
.add_systems(Update, pan_camera)
.run()
}
#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
enum CameraMovement {
Pan,
}
fn setup(mut commands: Commands) {
let mut input_map = InputMap::default();
input_map.insert_chord(CameraMovement::Pan,
[<leafwing_input_manager::axislike::DualAxis as Into<InputKind>>::into(DualAxis::mouse_motion()), MouseButton::Left.into()]
);
commands
.spawn(Camera2dBundle::default())
.insert(InputManagerBundle::with_map(input_map));
commands.spawn(SpriteBundle {
transform: Transform::from_scale(Vec3::new(100., 100., 1.)),
..default()
});
}
fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), With<Camera2d>>) {
const CAMERA_PAN_RATE: f32 = 0.5;
let (mut camera_transform, action_state) = query.single_mut();
let camera_pan_vector = action_state.axis_pair(&CameraMovement::Pan).unwrap();
// Because we're moving the camera, not the object, we want to pan in the opposite direction.
// However, UI coordinates are inverted on the y-axis, so we need to flip y a second time.
camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x();
camera_transform.translation.y += CAMERA_PAN_RATE * camera_pan_vector.y();
}
Heres the full code now
I see why it doesn't work. It's a bit inconvenient, but I understand it's because of the two layer nesting of InputKind and UserInput
I mostly use InputMap::new() for different types of inputs, and I rarely use InputMap::insert_chord()
fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), With<Camera2d>>) {
const CAMERA_PAN_RATE: f32 = 0.5;
let (mut camera_transform, action_state) = query.single_mut();
if let Some(camera_pan_vector) = action_state.axis_pair(&CameraMovement::Pan)
{
// Because we're moving the camera, not the object, we want to pan in the opposite direction.
// However, UI coordinates are inverted on the y-axis, so we need to flip y a second time.
camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x();
camera_transform.translation.y += CAMERA_PAN_RATE * camera_pan_vector.y();
}
}
that fixed it
dont get how it could be None tho
A chord is only active when all of its inside buttons are pressed
so if the left mouse button isn't pressed or there's no mouse movement, it returns none
returning none means either this action has no dual-axis input bound to it, or the input is not active like above
ahh and I guess in the original example mouse_motion is always active so it can never be None
yeah
makes sense thank you
pub fn input_axis_pair(&self, input: &UserInput) -> Option<DualAxisData> {
match input {
UserInput::Chord(inputs) => {
if self.all_buttons_pressed(inputs) {
for input_kind in inputs.iter() {
// Return result of the first dual axis in the chord.
if let InputKind::DualAxis(dual_axis) = input_kind {
let data = self.extract_dual_axis_data(dual_axis);
return Some(data.unwrap_or_default());
}
}
}
None
}
UserInput::Single(InputKind::DualAxis(dual_axis)) => {
Some(self.extract_dual_axis_data(dual_axis).unwrap_or_default())
}
UserInput::VirtualDPad(dpad) => {
let x = self.extract_single_axis_data(&dpad.right, &dpad.left);
let y = self.extract_single_axis_data(&dpad.up, &dpad.down);
Some(DualAxisData::new(x, y))
}
_ => None,
}
}
the source code
how do i toggle an action/state with the same key? e.g. for a pause menu i have this, but obviously i want to be able to unpause the game as well:
fn pause_game(
mut time: ResMut<Time<Virtual>>,
pause_action: Query<&ActionState<GameAction>>,
mut menu_state: ResMut<NextState<MenuState>>,
mut game_state: ResMut<NextState<GameState>>,
) {
let pause_action = pause_action.single();
if pause_action.just_pressed(&GameAction::Pause) {
time.pause();
menu_state.set(MenuState::Paused);
game_state.set(GameState::Paused);
}
}
why would you split pause from unpause
because i'm clearly doing it wrong. how would you do it
if state is paused then unpause, else pause?
error[E0204]: the trait `std::marker::Copy` cannot be implemented for this type
--> src\input_settings\single_axis_settings.rs:45:24
|
45 | #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
| ^^^^
...
88 | processors: Vec<Box<dyn SingleAxisInputProcessor>>,
| --------------------------------------------- this field does not implement `std::marker::Copy`
|
should I temporarily remove the Copy for UserInput and InputKind in the PR #494?
I'm also not sure how to name the input processors. For example, should I call it SingleAxisInputProcessor or SingleAxisProcessor? InputClamp or InputLimit? Deadzone2d or DualAxisDeadzone?
Ugh, that's a pain. I would really prefer not to
SingleAxisProcessor and DualAxisDeadzone
Why does the preprocessor need to get stored on UserInput? Can't we configure this elsewhere, rather than cloning it and other settings all over the place?
should not InputKind::SingleAxis and DualAxis store a corresponding settings?
After the trait refactoring is completed, I expect it to look like this: (CameraAction::Rotate, MouseMotion::new().sensitivity(2.0).deadzone_symmetric(0.1).normalize_into(-1.0, 1.0))
I'm definitely second guessing this decision π
I personally think that InputKind will be replaced by UserInput implementors, and only InputKind has the Copy trait, but not UserInput
and internally, InputMap actually uses UserInput which only has the Clone trait, not InputKind which has the Copy trait
In addition, InputStreams will become a place to simply store input data instead of being responsible for processing data. The processing part will be done by the new UserInput implementors
Okay yeah, let's proceed down this path for now
And unblock the rest of the refactoring
I'm not sure if my idea can be perfectly implemented, but I can't find a good way to integrate the existing Vec<Box<dyn Processor>> and InputKind for now
or temporarily replace InputKind::SingleAxis and peers with UserInput::SingleAxis?
This should minimize the changes and avoid removing the existing Copy derive for InputKind
Yeah try this!
and which name sounds better, SingleAxisProcessingChain or SingleAxisProcessingPipeline?
uhhh I ran into some problems that I caused myself π
The main problem is that I implemented a UserInputIter to iterate over the internal InputKinds during the previous refactoring. This simplified the internal processing of Clashing Check, N-Matching, RawInputs conversion, and InputStreams
However, now if it is UserInput::SingleAxis, it cannot get an InputKind that holds a ProcessingPipeline. Maybe I still have to remove Copy
Maybe this is just my brain being rather syrup-y today but the documentation of Rotation seems rather conflicting (compared to its implementation)? Both from_degrees and from_degrees_int say that they construct a rotation measured counterclockwise from the positive x-axis...but then new and micro_degrees say that they construct a rotation/return microdegrees measured clockwise from midnight/the positive y-axis, and the math of those functions does not seem to be doing anything to resolve the supposed difference
LWIM's Rotation may be replaced by Bevy's newly added Rotation2d in its 0.14 version
Yep that's the hope
Most of the code and documentation for PR #494 are done (haven't push yet), but do I need to separate the new Deadzones, like the diamond and rounded square, into a new PR?
Also, I'm currently considering adding a visual example like the one in this video
Configs:
YouTube doesn't allow me to make the config links clickable but you can find them in this reddit comment: https://www.reddit.com/r/RocketLeague/comments/6j3pby/i_made_a_webapp_and_a_video_to_show_how_the/djb8uyu/
The deadzone webapp:
https://halfwaydead.gitlab.io/rl-deadzone/
------------------------------------------------------------...
in this way, we can provide an intuitive way for users to understand the differences between various Deadzones
And in this way, we can keep #494 focused mainly on refactoring the existing input handling system
yeah that would be useful for settings menu, which reminds me that i got completely carried away from rebind menu i wanted to make
@karmic monolith should I include them in the PR or create a new PR?
All the code, documentation, and document examples of processors are done, except for debugging the macro rules and missing visual examples
New PR please!
Are you ready for me to review and merge #494 now?
I've been working on defining some macro rules that can help us simplify our code and improve our efficiency, but I want to make sure they're thoroughly tested before I share them with you
I'm expecting to finish testing by tonight in CN time or tomorrow morning in US time
please don't feel rushed π 3000+ lines updated but the source code just about 800 lines, most of others are documentation
That's the sort of ratio we like around here π
let move_delta = action_state
.clamped_axis_pair(&input::PlayerAction::Move)
.unwrap()
.xy()
* time.delta_seconds();
// used with bevy_rapier2d's KinematicCharacterController.
controller.translation = Some(move_delta * 200.0);
This seems to be nicely normalised for gamepads, but using the VirtualDPad::wasd() has the diagonals moving faster. I tried .normalize(), keyboard works as expected, but now the gamepad stopped being analogue and just instantly goes between 0 and max.
It's probably something silly on my side, but I've been scratching my head a while.
Was going to say something, but tested my own code and realized that I have the exact same issue when I went back to try the gamepad after fixing the strafe bonus with normalizing π
@signal quiver are you using a state to switch between gamepad/keyboard input? I just added to my movement function so that if it is in keyboard state, it will normalize the input to get strafe bonus out of the game
No, not yet, I can try that, though. I just didn't get there as I thought I was doing something wrong. π
That works, but results in a funny situation where if you hold diagonal on a keyboard and then touch anything on a controller, the character speeds up. π
The new parts (haven't been pushed) of #494 will fix this
I've added four types of normalizers, each handling circular deadzones or square deadzones normalized to circular input bounds or square input bounds
the circular input ones will remove the diagonal acceleration
ohhh, #494 don't fix that for VirtualDPad now
but VirtualAxis and VirtualDPad should also be able to add input processors
When I wake up, I'll push #494, just got a bit of work left to finish
Update the description for #494 and the related issue, but the code hasn't pushed since a strange bug for reflection
I'm unsure about the naming convention; some games and Unreal use terms like AxialDeadzone and RadialDeadzone, while others prefer SquareDeadzone and CircleDeadzone
yeah, I use the terms in the documentation but call them as the latter
how about other names in #491?
should we need to add some processors like abs and those commonly used in machine learning, such as ReLU, sigmoid, and tanh?
Yeah let's wait to see what users need
#494 is ready for review
I'm not sure if these new processors need to be registered with 'register_type'. I didn't see any issues in my tests even without registering them
Maybe adding AxisInspection processors is better for debugging? But I'm also not sure whether they should be used with dbg! or println! or some other approach.
This is needed for serializing them into scenes
Good to do
Although I guess it might be less necessary now that we have recursive type registration
Personal life is very busy right now so I don't have much time for Bevy, but this is number 3 on my list of priorities β€οΈ
No worries! Life comes first. As long as we can get it sorted before LWIM 0.14 rolls out, that'll be perfect.
Do these processors need to implement Display? Because in my implementation, many fields in constructors are replaced with precomputed internal fields used to eliminate redundant calculations
Might be nice, but likely not required
I have a problem with running both client and server plugins in the same app: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/503
What problem does this solve? I'm working on a networking library: https://github.com/cBournhonesque/lightyear and I use leafwing-input-manager to network inputs. I'm working on a feature w...
I wonder what you think about it
Left feedback in the issue π
How can I add mouse position as a action? I saw a example "mouse_position" but my code is giving a error at action_state.action_data_mut(&driver.action).unwrap()
Do we still need DualAxisData? The only difference currently is its built-in function to convert to Rotation. However, we should also switch to Bevy's Rotation2d in 0.14
Do you have a reproducible code snippet?
Yes, I'll upload
Ohh I see, this part was updated in my codebase last month, but it hasn't been merged (#494).
The functions action_data and action_data_mut are quite confusing
I think it would be better to refactor their implementation based on action_data_mut_or_default
Or rename them to *_unchecked
π Alternatively, reintroduce Actionlike::iter and reimplement old behaviors.
If we keep using or_default for every query like this, it might affect performance a bit. In the old version, because all action states were initialized, we avoided an extra layer of Option
@karmic monolith I'm curious about your thoughts on this
ohh..
Before LWIM 0.12, the internal ActionState was a Vec with initialized states of all Actionlikes
However, in the new version, Actionlike::iter was removed, and ActionState now uses a HashMap. This means that if an Action has never been updated by the internal system, its State will always be None if you just fetch it from action_data or action_data_mut
I think we should use the _or_default behavior everywhere
Issue is fixed now, thanks you so much
Do we need to rename action_data and action_data_mut, or improve their documentation? Adding just "if populated" doesn't seem very clear
@karmic monolith And this
I'm unsure whether to use Vec2 or DualAxisData in #491
in trait refactoring
We should be able to cut and simplify this yeah
There's one more thing I'm not quite sure about, and if it's convenient (since it's probably still early morning on your end)
Is it allowed to use &World in Bevy like this?
impl UserInput for KeyCode {
fn input_kind(&self) -> InputKind {
InputKind::Button
}
fn is_active(&self, world: &World) -> bool {
world.resource::<ButtonInput<KeyCode>>().pressed(*self)
}
}
Because currently, it seems that without rust nightly, it's not able to implement Deserialize for boxed generic trait objects (See https://github.com/dtolnay/typetag/issues/1).
If possible, I'd like to integrate some commonly used ActionStateDrivers, such as mouse position on the screen and in the world, into another version of UserInput that uses &mut World. Perhaps it could be called UserInputDriver?
Currently, I haven't found a better solution because generic traits can't be used, and associated type bounds will likely become stable in Rust 1.79, but that's a bit late
This is definitely possible π€ Not super clean though
Oh, then it's not a problem because I haven't used &World much in systems; mostly &mut World
Any progress on the review for #494? I've gathered most of the processor descriptions in the input_processing.rs documentation
But before moving forward with #490, I want to introduce a few new processors to get rid of some unnecessary VirtualDPads
/// Emulates an eight-way D-pad behavior, prioritizing stronger directional input.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct EightWayDPad;
impl DualAxisProcessor for EightWayDPad {
/// Returns a processed [`Vec2`] value representing one of the eight main compass directions.
fn process(&self, input_value: Vec2) -> Vec2 {
let Vec2 { x, y } = input_value;
let x_abs = x.abs();
let y_abs = y.abs();
if x_abs > 2.0 * y_abs {
Vec2::new(x.signum(), 0.0)
} else if y_abs > 2.0 * x_abs {
Vec2::new(0.0, y.signum())
} else {
input_value.signum()
}
}
}
This would help remove things like VirtualDPad::mouse_motion and simply add DualAxis::mouse_motion().with_processor(EightWayDPad)
VirtualDPad needs to read input for all four directions separately, calculate them individually, and then combine them
This leads to a lot of redundant calculations for axis inputs coming from the same event
After adding the new processors, the dual_axis_processor.rs file has become too large. Should we split it in #494 first?
With the new processors added, the entire file is 1960 lines long
The deadzone section comprises 360 lines of source code and 280 lines of test code, totaling 640 lines
The bounds section comprises 340 lines of source code and 180 lines of test code, totaling 520 lines
Yes please to the split
I'll give you a review today
I don't know what's been going on lately, but every time I move items to another location, the cargo cache goes invalid and I can't compile. I have to clean it up each time
pushed #494 and updated the description
I've seen a 6-button D-pad variant in Unity recently, which corresponds to Vec3 inputs. I think we can introduce this in the future, but I'm not sure how to name it
Most input providers in Unity are strings in the code, which is very untyped and lacks type safety π
I think I should rename the current 'with_processor' to 'replace_processor'
'with_processor' should be used to create a pipeline by adding the current processor and the given new processor
Oh that's neat. It's like a 3D dpad
I'll chew on names...
Reviewed! Sorry for not being more proactive on this; I should have been following it more closely to avoid wasted work. Overall really well-made and a lovely abstraction, but there's a few architectural issues (pipelines, macros, typetag) that need to be addressed
Since the implementations generated by define_dual_axis_processor are enum variants (All, Separate(x, y), OnlyX, OnlyY), I feel like repeating code such as DualAxisDeadzone::All(AxisDeadzone::magnitude(0.2)) and DualAxisDeadzone::Separate(AxisDeadzone::magnitude(0.2), AxisDeadzone::magnitude(0.3)) would be more cumbersome
Sorry, what's this in response to?
to extend_dual
Ah, I see
Yeah, looking at the actual implementation I think I like it π
We can bikeshed the names maybe, but overall I think it makes sense
I can add a impl From<AxisProcessor> for DualAxisProcessor into All
Yeah, I think that's probably still useful
But won't allow us to cut the methods
Since there's more than one sensible way to upcast it
(heading to sleep now fyi)
yeah, good night
When using structs with float fields, we cannot directly derive Eq
Here is a library created by a contributor to Bevy
But it's not as convenient as typetag, which only requires adding #[typetag::serde] above impl Trait for Type, whereas this one requires writing a lot of repetitive code manually
Another library that seems usable, but since almost no one uses it, it's still unclear whether it's viable
produces Vec3 values
bindings for up, down, left, right, forward, backward
This naming is a bit odd because D-pad stands for Directional Pad, so it's mostly referred to as a 4-way D-pad or an 8-way D-pad
up, down, left, right, up-left, up-right, down-left, down-right
LWIM's dpad is the 4-way version
but it produces an 8-way Vec2: -1, 0, 1 along each axis
I think DPad3D sounds better than 3Dpad because it should be a three-dimensional directional pad rather than a three-direction pad
I added a Digital processor that quantizes values into -1, 0, 1 for single-axis and dual-axis inputs in an upcoming PR
So the current VirtualDPad for axial inputs like mouse motion and sticks can be replaced with DualAxis::mouse_motion().with_processor(DualAxisDigital)
After trait refactoring, there will have MouseMotion::default() and MouseMotion::digital()
Inputs are only used in the InputMap, in actual logic, we deal with Actions and ActionState<Action>
For example, in a game I'm working on, we use Edge Pan and WASD to scroll the map
However, Edge Pan requires multiplying by 2.0 in its implementation to match the default Vec2 input values of VirtualDPad::WASD
This is because Unity seems to use this approach, even using strings in the code
Ah, this generic approach is also okay, but it can be quite complex
We'll need a Context to store InputTypeRegistry, then store all input values, and retrieve them by type. This replicates the implementation of Bevy's Query<Param>
Rust isn't as straightforward as C#. Both Bevy and Axum have to do it this way
Axum calls it the Extractor pattern. I haven't seen how Bevy refers to it in its implementation yet, but the approach is similar. It's mentioned in this release log
Tokio is a runtime for writing reliable asynchronous applications with Rust. It provides async I/O, networking, scheduling, timers, and more.
This library appears to be a simple implementation
Implemented on a provider, it extracts a future that resolves to Ok(T) on success or Err(Self::Err) on error.
However, because LWIM will eventually be upstreamed into Bevy itself, external dependencies need to be carefully considered
Moreover, Bevy has similar implementations internally, so perhaps they can be reused
i can't even begin to imagine how to do it all, so it's all in your hands anyway
Ah, theoretically, it should be one way to implement Dependency Injection, and there should be many places to reference for guidance
π€·ββοΈ take your time to think it over, it should just be simple and failproof from both player and dev sides besides the initial setup
yeah, but it may not catch up with LWIM 0.14, as Bevy 0.14 might be released by the end of May
I think the api could be fn value<T: InputValueType>(&self) -> Option<T>
or Result
how about: if let Some(value) = action_state.value<Vec2>(&Move) { let dir: Vec2 = value; }
dir is just used to hint the type
I think zeros don't need to be handled, right? How should we handle them? For movement, it should simply mean not moving
Or input.Default.Move.performed += action_state.value<Vec2>().unwrap_or_default()
It's a bit tricky, but take a look at these examples
For continuous input values, you need to deal with events
LWIM currently handles them like this
CheatBook is outdated, but this part hasn't changed much
That's why LWIM exists; you only need to
let input_map = InputMap::new([
(Move, MouseMotion),
(Move, LeftStick),
])
let direction: Vec2 = action_state.axis_pair(&Move).unwrap().xy();
Right now, we need to use .xy() because Bevy 0.12 doesn't have Direction2d and Rotation2d yet, so LWIM added them. But with Direction2d in 0.13 and Rotation2d in 0.14, LWIM 0.14 can make this simpler
I'm on board
Oh yeah, I forgot
But in 0.13, it's still called Direction2/3d
directions are just normalized vectors
Some people might find it useful, but I'm not sure. LWIM also doesn't provide direct Dir2 input data, so if they need it, they'll have to manually convert it
LWIM's directions and rotations are only used in there
And this structure, DualAxisData, is just a wrapped Vec2, so my plan is to remove it in the next step
Rotation now is only used in orientation.rs (the module itself), examples, and this struct
it merged
LWIM's Direction has eight compass direction consts, but his only four
Rotation is clockwise/counterclockwise vs only counterclockwise
Other than that, there doesn't seem to be much difference, and it doesn't affect usage
Ah got it
Yeah this seems sensible
infallible action_data seems like impossible
Because it only takes &self, inserting a default ActionData needs a mutable reference
But if we're just using unwrap_or_default() inside, we don't really need to make it mutable
Yeah we can just do this π
Serde trait objects on WASM have too many problems
On the one hand, using libs like serde_tagged is much lighter than typetag, but it also requires the use of #[ctor] to add code segments that run before main() because #[wasm_bingen(start)] can only appear once in entire applicaiton
This makes it impossible to pre-register all possible deserialization functions on WASM
On the other hand, it is possible to use App::register_typetag like bevy_serde_project
But this lib currently only supports deserialization of Box<dyn Trait>, while Option<Box<dyn Trait>> and Vec<Box<dyn Trait>> used in #494 are not supported
Ugh π¦
This works, but I'm not sure if register_processor() should be a normal pub fn or an extension function for App
App extension method is the style π
You may also be able to get away with reusing Bevy's own type registry
I had an issue with the actions not updating when using a custom schedule (like at all, and only intermittently), but I managed to fix it by adding in a update_action_state before the system I read actions in, and it seems to be working quite well now.
Is there something I should be aware of when doing this? I don't care about the just_* functionality, as I post-process that myself, so I'm not calling tick_action_state, but I'm not sure if that's all it does or not.
Do you think should work okay?
Deserializers need a static registry to function correctly, but it's possible to use Bevy's TypeRegistration like that?
pub type TypeRegistry = BTreeMap<&'static str, BoxFnSeed<Box<dyn AxisProcessor>>>;
static mut PROCESSOR_REGISTRY: RwLock<TypeRegistry> = RwLock::new(TypeRegistry::new());
fn register_processor(
typetag: &'static str,
func: impl FnSeed<Box<dyn AxisProcessor>> + Sync + 'static,
) {
let mut registry = unsafe { PROCESSOR_REGISTRY.write().unwrap() };
registry.insert(typetag, BoxFnSeed::new(func));
}
impl<'de> serde::Deserialize<'de> for Box<dyn AxisProcessor> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let registry = unsafe { PROCESSOR_REGISTRY.read().unwrap() };
serde_tagged::de::external::deserialize(deserializer, &*registry)
}
}
BoxFnSeed is the deserializer function for boxed dyn trait objects using the actual implementer's deserializer
impl Foo {
fn deserialize_erased(
de: &mut dyn erased_serde::Deserializer<'_>,
) -> Result<Box<dyn AxisProcessor>, erased_serde::Error> {
Ok(Box::new(Self::deserialize(de)?))
}
}
register_processor("Foo", Foo::deserialize_erased);
Simplified
trait Register<'de, T: serde::Deserialize<'de>> {
fn register(registry: &mut RwLockWriteGuard<TypeRegistry<T>>);
}
impl<'de> Register<'de, Box<dyn AxisProcessor>> for Foo {
fn register(registry: &mut RwLockWriteGuard<TypeRegistry<Box<dyn AxisProcessor>>>) {
registry.insert("Foo", BoxFnSeed::new(Foo::deserialize_erased));
}
}
fn register_processor<'de, T: Register<'de, Box<dyn AxisProcessor>>>() {
let mut registry = unsafe { PROCESSOR_REGISTRY.write().unwrap() };
T::register(&mut registry);
}
register_processor::<Foo>();
May I write a procedural macro to simplify the trait impls?
Yep for the serde code I think that's fine
Code seems like ready, but I should add tests for them
Pushed
I'll be quite busy next week
I can complete the works for #494 promptly, but other tasks like #490 may have to wait until two weeks from now
Sounds good, thanks for the heads up π
Typetag-like stuffs are just serialization and deserializations for trait objects
Even if we don't use them in the processors and switch back to the original single struct implementation, the refactoring of UserInput still needs them
#346 Take this as an example, it can implement Trait Object Serialization, but Trait Object Deserialization can only be done in this way.
Ugh. Right: we need to store a Box<dyn Buttonlike> or whatever in our input maps
And serialization and deserialization of input maps is non-negotiable
Okay. I think let's keep it, remove processors and get this merged?
All processors and Box<dyn Processor> are serde now
Yep, my apologies
Pipelines appear to just be a performance optimization, correct?
I'd rather split that into its own PR
The old macro-optimized versions have been deleted in the PR. Now there is only the version that wraps Vec<Box<dyn Processor>>
Ah okay; I'll take a closer look
But I think the processing steps should not exceed five: Inversion, Sensitivity, Digital (which converts the input to -1,0,1, and will be implemented in a new PR), ValueBounds, and DeadZone
Therefore, there should not be any significant performance issues with the non-inline version either
Recently, I've implemented a new approach for handling UserInput.
pub enum InputKind {
Complex,
Button,
Axis,
DualAxis,
}
pub struct UserInputData {
pub(crate) kind: InputKind,
pub(crate) is_active: bool,
pub(crate) value: f32,
pub(crate) axis_pair: Option<Vec2>,
}
impl UserInputData {
pub fn button(pressed: bool) -> Self {
Self {
kind: InputKind::Button,
is_active: pressed,
value: f32::from(pressed),
axis_pair: None,
}
}
pub fn axis(value: f32) -> Self {
Self {
kind: InputKind::Axis,
is_active: value != 0.0,
value,
axis_pair: None,
}
}
pub fn dual_axis(value: Vec2) -> Self {
Self {
kind: InputKind::DualAxis,
is_active: value != Vec2::ZERO,
value: value.length(),
axis_pair: Some(value),
}
}
}
/// A trait for defining a specific user input.
pub trait UserInput {
fn fetch(&self, input_streams: &InputStreams) -> UserInputData;
}
For example
impl UserInput for KeyCode {
fn fetch(&self, input_streams: &InputStreams) -> UserInputData {
let pressed = input_streams.keycodes.is_some_and(|kb| kb.pressed(*self));
UserInputData::button(pressed)
}
}
This should significantly reduce the number of redundant retrievals for input values
pub struct MouseMotionInput(Option<Box<dyn DualAxisProcessor>>);
impl UserInput for MouseMotionInput {
fn fetch(&self, input_streams: &InputStreams) -> UserInputData {
let mut value = Vec2::ZERO;
for event in &input_streams.mouse_motion {
value += event.delta;
}
if let Some(processor) = &self.0 {
value = processor.process(value);
}
UserInputData::dual_axis(value)
}
}
For the refactorings in #490, I'm not quite sure how to name the new input types
Many good names have been taken by Bevy, such as MouseMotion and Axis
So my current naming scheme is to add an *Input suffix, in line with ButtonInput and KeyboardInput
But, for single-axis inputs, such as the vertical mouse wheel, should we use MouseMotionInputVertical or MouseMotionInputY?
For #494, the current serde implementation is probably the simplest one, as the added dependencies are specifically for dyn trait objects
I've tried using InputProcessor<T> where T = f32 | Vec2 | Vec3, but there's currently no way in Rust to handle the deserialization of generic trait objects
This is because we can implement different traits, and deserialization cannot determine which specific trait to use, so I have to split it into concrete traits
Alternatively, we could create TypeTagRegistries for all possible input value types. However, this would be quite cumbersome, and it would also introduce potential issues with AsAny.
I think the latter is a bit better
Let me explain why TypeTag. It converts all trait objects into Rust's tagged enum format for Serde
For instance, given Foo(Vec2::new[1.0, 2.0]). After type tagging, it becomes equivalent to JSON {"Foo": [1.0, 2.0]}
Right, I gathered that much. Does it work with user-supplied types that implement the trait?
yeah, I added register_*_processor as extenstion methods for App
It seems surprising to me that you could generate an enum after those had been added to the code, rather than solely in the first-party library level π€
I definitely trust that this works, just trying to build up a better intuition
How can calling runtime methods like register_processor change the generated code?
Does it parse the AST and check for invocations?
nope, just add the Processor's type tag and a corresponding deserializer into the TypeTagRegistry, and call register_type for them
e.g. register_type::<AxisInverted>()
But it is common to customize input processing methods in games, and I feel that we can provide a unified interface to limit their use in LWIM
For example, many games have completely different dead zone implementations
Yep, totally agree!
This is one of my core goals here
If you're interested, check out this resource. It gathers deadzone settings for over 300 games, encompassing over ten deadzone types
The gray area is the input switching zone, which is what's required in #505
I wonder if we could procedurally generate these diagrams π€
Yeah, I suggested at the beginning of the month that we could create a visual example
@midnight bramble also said that he could add these to the GUI input manager he's working on
The graphs feel flawed ... They don't tell you how the green area is mapped π€
I feel like it needs those lines you see in 2D SDF examples so you can see how they scale ... Say make every other 0.1 value a differen color
Yeah
this original post seems to have been around for a long time, and many old games don't have scaled dead zones
I'm also quite unsure about the purpose of those oddly-shaped deadzone shapes
For now, CrossDeadzone and CircleDeadzone seem to suffice for most cases
One limits the value range along an axis, while the other limits the value's magnitude
On the other hand, I currently call those dual-axis ranges 'DualAxis* ' because the dual-axis version of AxisBounds is clearly a rectangle, while the dual-axis version of AxisDeadzone is a cross
So, for consistency, I call them DualAxis* ranges, since they're just the dual-axis versions of Axis* and I've added doc aliases
The spikes pointing towards the center makes some sense, it just snaps the angle if its close
A lot of them also just look like poorly implemented circle math π
Ah, it's mainly because I don't often check for information in English game communities (I'm a Chinese player). I couldn't find the specific details about them this month
Deadzones are generally very overlooked by players, it's the kind of thing that makes people say "these controls suck" but not understand why 
@candid vigil 494 is merged!
Thank you so much for your hard work and patience here π
I came across a term that refers to it as 'BowTieDeadzone'
Wow, that was incredibly fast! Are you sure there's nothing that needs further refinement or adjustments?
It's this shape that I'm most confused about π
I'm fundamentally sold on the core architectural decisions at this point, and cleanup will be easier to do in seperate smaller PRs as we notice issues
It's also much easier to iterate on some of the other work with this merged π
Since the diff is so large
How about this thin sliver of green between the red and blue? 
The main reason is that I split them into smaller modules after the last review. This is because there might be additional features to add in the future, and having a single large module would become too unwieldy at that point
Many of the settings are truly bizarre and incredibly fun, but I can't fathom how the code for them was written
Yep, definitely. I was really pleased with the organization of the code!
Ahh, I found an unused dependency that I forgot to removeπ
I'll create a cleanup PR to remove it and this
Hmmm ... I wonder if disabling components could at some point be useful as a more obvious version of having a component-level ToggleActions π€
Or should I split them into two separate PRs?
But out-of-date
Awesome, thank you
Ah this moves the toggles to the component/resource itself instead of the fairly hard to find (and also not per-entity) ToggleActions ... Why isn't this how it always worked π
As per the changelog: Version 0.3: InputManagerPlugin::run_in_state has been replaced with the ToggleActions<A: Actionlike> resource, which determines whether or not [ActionState] / [InputMap] pairs of type A are active
Alice might still remember the reason π
I mean at least ToggleActions is an improvement over passing a state in which to run the systems π
But this approach also introduces some problems https://github.com/Leafwing-Studios/leafwing-input-manager/issues/446
Oh, I was mistaken. once_cell was used, so the new PR is clearer
I have more issues than just that with ToggleActions atm tho I haven't been able to figure out what exactly is happening so far, luckily I only toggle actions in one specific case currently
I haven't used it in a real-world environment yet, so I haven't encountered issues π
I made action_state::ActionData and timing::Timing implement Copy, and I made action_data() return a copy of ActionData instead of &ActionData. That should be fine, right?
It seems there's no way to change this position from the internal HashMap::get() -> Option<&ActionData> to return &ActionData. unwrap_or_default() doesn't work for Option<&>
Yep, that seems very reasonable
Could we use the HashMap Entry API to check and then insert?
But action_data() takes a &self
I made it like this
or rename the latter to get_action_data
Right π€
Yeah this seems fine
Although, I guess we could just leave it as is, and then tell people to unwrap_or_default it themselves :/
That's probably the most idiomatic
Revert everything and only improve the documentation?
But how should this documentation be written? The current "if populated" is definitely unclear
Yes please. This is fundamentally how hash maps work, and doing something different is weird and unexpected.
Something like
This value may be
Noneif this action has never been pressed. Callingunwrap_or_defaulton the returnedOptionwill give you the appropriateActionDatafor a never-pressed action.
Pushed the PR and off to dreamland
I'm porting my game to bevy 0.13 and leafwing-input-manager (from 0.11 to 0.13.3) and getting this error:
error[E0277]: the trait bound `bevy::prelude::KeyCode: leafwing_input_manager::Actionlike` is not satisfied
--> crates/ui/src/lib.rs:115:19
|
115 | let input_map = InputMap::new([(KeyCode::Escape, UIAction::Esc)]).build();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `leafwing_input_manager::Actionlike` is not implemented for `bevy::prelude::KeyCode`
how do I solve this? is there Actionline implementation for KeyCode?
InputMap::new([(UIAction::Esc, KeyCode::Escape)])
InputMap is a HashMap<Action, Vec<Input>> now
Is there a way to use an InputMap directly without the Action abstraction? e.g. feed it an Res<ButtonInput<T>> and check if the input is currently pressed.
Yes, but you have to use InputStreams currently, and call which_pressed
Although maybe I don't understand what you're after: that's not very useful at all without any actions
and if you just want to check if a ButtonInput is pressed, just call pressed on it directly
I think I miswrote what I needed there, but I wanted to see if I can re-use the UserInput abstraction without Action (I like the key modifier abstraction). I sometimes just want to do something quick and dirty where Action may be a bit too much.
use std::any::{TypeId, Any};
#[derive(Clone, Copy, Debug)]
pub struct Vec2 {
x: f32,
y: f32,
}
pub struct RawInputData {
active: bool,
value: f32,
value2d: Option<Vec2>,
}
impl RawInputData {
pub const fn is_active(&self) -> bool {
self.active
}
#[inline(never)]
pub fn get_value<V: Copy + 'static>(&self) -> Option<V> {
let type_id = TypeId::of::<V>();
if type_id == TypeId::of::<f32>() {
let any: &dyn Any = &self.value;
return any.downcast_ref::<V>().copied()
}
if let Some(value2d) = self.value2d {
if type_id == TypeId::of::<Vec2>() {
let any: &dyn Any = &value2d;
return any.downcast_ref::<V>().copied()
}
}
None
}
}
pub fn foo(data: RawInputData) {
println!("{:?}", data.get_value::<f32>());
println!("{:?}", data.get_value::<Vec2>());
}
fn main() {
foo(RawInputData {
active: false,
value: 2.5,
value2d: Some(Vec2 {
x: 2.8,
y: 3.4,
}),
})
}
this works
use std::any::{TypeId, Any};
#[derive(Clone, Copy, Debug)]
pub struct Vec2 {
x: f32,
y: f32,
}
pub struct RawInputData {
active: bool,
value: f32,
value2d: Option<Vec2>,
}
impl RawInputData {
pub const fn is_active(&self) -> bool {
self.active
}
#[inline(never)]
pub fn get_value<V: Copy + 'static>(...
And the compiled code is very well optimized.
just a context that stores input data from a single user input
But this is only for internal data transfer and not for public use. I think this implementation is the simplest, it can handle all possible types of input data (f32, Vec2, Vec3, Entity, etc.) in just a few lines. In the future, it will still be ActionState::get_value::<T>()
An enum is a closed set of types, with an arbitrary number of related properties.
A trait is a closed set of properties, with an arbitrary number of related types.
The current god enum UserInput prevents users from designing and using their own input types
For example, what about clicking on an object with the mouse or touching it and getting its EntityID?
The ultimate goal is to deduplicate the common input handling for Actions
fn update_cursor_state_from_window(
window_query: Query<(&Window, &ActionStateDriver<BoxMovement>)>,
mut action_state_query: Query<&mut ActionState<BoxMovement>>,
) {
// Update each action state with the mouse position from the window
// by using the referenced entities in ActionStateDriver and the stored action as
// a key into the action data
for (window, driver) in window_query.iter() {
for entity in driver.targets.iter() {
let mut action_state = action_state_query
.get_mut(*entity)
.expect("Entity does not exist, or does not have an `ActionState` component");
if let Some(val) = window.cursor_position() {
action_state
.action_data_mut_or_default(&driver.action)
.axis_pair = Some(DualAxisData::from_xy(val));
}
}
}
}
This is how current LWIM users (not the built-in LWIM functionality) handle mouse clicks on the screen and trigger corresponding Actions
When interacting with other components in the game, users need to repeat a lot of these code. I think that many common methods can be extracted and converted into built-in methods
I think UI should also be a common way to trigger Actions. This will lead to many custom UserInputs
pub trait InputProcessor {
type ValueType;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType;
}
#[derive(Clone, Copy)]
pub struct AxisInverted;
impl InputProcessor for AxisInverted {
type ValueType = f32;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType {
-input_value
}
}
#[derive(Clone, Copy)]
pub struct AxisSensitivity(pub f32);
impl InputProcessor for AxisSensitivity {
type ValueType = f32;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType {
self.0 * input_value
}
}
impl <T, P1: InputProcessor<ValueType = T>, P2: InputProcessor<ValueType = T>> InputProcessor for (P1, P2) {
type ValueType = T;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType {
self.1.process(self.0.process(input_value))
}
}
pub fn main() {
let processor = (AxisInverted, AxisSensitivity(1.5));
let processor = (processor, AxisSensitivity(2.5));
let processor: Box<dyn InputProcessor<ValueType = f32>> = Box::new(processor);
println!("{}", processor.process(3.5));
}
This should perform better than the current Vec<Box<dyn Processor>>-based ProcessingPipelines
because it can be inlined
pub trait InputProcessor {
type ValueType;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType;
}
#[derive(Clone, Copy)]
pub struct AxisInverted;
impl InputProcessor for AxisInverted {
type ValueType = f32;
fn process(&self, input_value: Self::ValueType) -> Self::ValueType {
-input_value
}
}
#[deriv...
Then, implementing the UserInput trait for <U: UserInput, P: InputProcessor> (U, P) is all that's needed
@karmic monolith
I love this: ship it?
but there is an issue when integrating into the current main branch
because it is also an InputProcessor
then there is no way to distinguish whether the processor stored in the current UserInput is a regular processor (such as sensitivity and dead zone) or a Sequence
This is because the latter uses generics, so TypeId and Any::downcast cannot be used
DualAxis::mouse_motion().processor(
// The first processor is a circular deadzone.
// The next processor doubles inputs normalized by the deadzone.
DualAxisSequentialProcessor::new(CircleDeadZone::new(0.1), DualAxisSensitivity::all(2.0))
// The next processor inverts inputs along both axes.
.with(DualAxisInverted::ALL),
),
It's a bit counterintuitive now
Can we do .processor(a).processor(b) instead?
this fine
But now there is a new issue, I forgot that generic trait objects cannot be deserialized.
that is, we cannot register_axis_processor::<AxisSequentialProcessor<_, _>>()
pub enum AxisProcessor {
Pipeline(Vec<AxisProcessor>),
Inverted,
Sensitivity(f32),
Custom(Box<dyn DynAxisProcessor>),
}
impl AxisProcessor {
pub fn process(&self, input_value: f32) -> f32 {
match self {
Self::Pipeline(sequence) => {
sequence.iter().fold(input_value, |value, next| next.process(value))
},
Self::Inverted => -input_value,
Self::Sensitivity(sensitivity) => sensitivity * input_value,
Self::Custom(processor) => processor.process(input_value),
}
}
}
pub trait DynAxisProcessor {
fn process(&self, input_value: f32) -> f32;
}
I guess this is the only way
#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub enum DualAxisProcessor {
/// A wrapper around [`DualAxisInverted`] to represent inversion.
Inverted(DualAxisInverted),
/// A wrapper around [`DualAxisSensitivity`] to represent sensitivity.
Sensitivity(DualAxisSensitivity),
/// A wrapper around [`DualAxisBounds`] to represent value bounds.
ValueBounds(DualAxisBounds),
/// A wrapper around [`DualAxisExclusion`] to represent unscaled deadzone.
Exclusion(DualAxisExclusion),
/// A wrapper around [`DualAxisDeadZone`] to represent scaled deadzone.
DeadZone(DualAxisDeadZone),
/// A wrapper around [`CircleBounds`] to represent circular value bounds.
CircleBounds(CircleBounds),
/// A wrapper around [`CircleExclusion`] to represent unscaled deadzone.
CircleExclusion(CircleExclusion),
/// A wrapper around [`CircleDeadZone`] to represent scaled deadzone.
CircleDeadZone(CircleDeadZone),
/// Processes input values sequentially through a [`Vec`] containing [`DualAxisProcessor`]s.
Pipeline(Vec<DualAxisProcessor>),
/// A user-defined processor that implements [`CustomDualAxisProcessor`].
Custom(Box<dyn CustomDualAxisProcessor>),
}
error[E0275]: overflow evaluating the requirement `std::vec::Vec<input_processing::dual_axis::DualAxisProcessor>: bevy::prelude::FromReflect`
--> src\user_input.rs:232:45
|
232 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
| ^^^^^^^
|
note: required for `input_processing::dual_axis::DualAxisProcessor` to implement `bevy::prelude::FromReflect`
--> src\input_processing\dual_axis\mod.rs:20:45
|
20 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
| ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro
21 | pub enum DualAxisProcessor {
| ^^^^^^^^^^^^^^^^^
= note: 2 redundant requirements hidden
= note: required for `axislike::DualAxis` to implement `bevy::prelude::FromReflect`
= help: see issue #48214
= note: this error originates in the derive macro `Reflect` (in Nightly builds, run with -Z macro-backtrace for more info)
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
--> src/lib.rs:5:1
|
5 + #![feature(trivial_bounds)]
|
For more information about this error, try `rustc --explain E0275`.
It's a bit odd, but I can only change the implementation to a form like ((First, Second), Third)
This approach is slightly faster than using a Vec; on my computer, it's approximately 1.2x to 1.3x faster when comparing the speed of 2 to 10 processors
use std::hint::black_box;
const N: usize = 300;
#[derive(Clone)]
pub enum AxisProcessor {
Inverted,
Sensitivity(f32),
Sequential(Box<AxisProcessor>, Box<AxisProcessor>),
Sequence(Vec<AxisProcessor>),
}
impl AxisProcessor {
/// Computes the result by processing the `input_value`.
#[must_use]
#[inline]
pub fn process(&self, input_value: f32) -> f32 {
match self {
Self::Inverted => -input_value,
Self::Sensitivity(sensitivity) => sensitivity * input_value,
Self::Sequential(current, next) => next.process(current.process(input_value)),
Self::Sequence(sequence) => sequence
.iter()
.fold(input_value, |value, next| next.process(value)),
}
}
/// Appends the given `next_processor` as the next processing step.
#[inline]
pub fn with_processor(self, next_processor: impl Into<AxisProcessor>) -> Self {
Self::Sequential(Box::new(self), Box::new(next_processor.into()))
}
}
fn main() {
let processor = AxisProcessor::Inverted
.with_processor(AxisProcessor::Sensitivity(1.5))
.with_processor(AxisProcessor::Sensitivity(5.0))
.with_processor(AxisProcessor::Sensitivity(-3.0));
println!(
"With: {}",
easybench::bench(|| {
let mut sum = 0.0;
for i in 0..N {
sum += black_box(processor.process(i as f32));
}
sum
})
);
let processor = AxisProcessor::Sequence(vec![
AxisProcessor::Inverted,
AxisProcessor::Sensitivity(1.5),
AxisProcessor::Sensitivity(5.0),
AxisProcessor::Sensitivity(-3.0),
]);
println!(
"Vec: {}",
easybench::bench(|| {
let mut sum = 0.0;
for i in 0..N {
sum += black_box(processor.process(i as f32));
}
sum
})
);
}
pushed #511 PR
Pipeline(Vec<Arc<AxisProcessor>>) resolved [E0275]
Unfortunately, due to compilation error E0117, it's not possible to impl serde for Arc<dyn T>
Otherwise, it might have been possible to eliminate the manual impl of reflection for Arc<dyn T>
I feel like there are many areas that could be rewritten better, but it would require too many breaking changes
For example, using a HashMap<Box<dyn Actionlike>, Vec<Box<dyn UserInput>>> in InputMap and a similar approach in ActionState could potentially eliminate all generics and require only one addition for InputManagerPlugin into App
Additionally, I plan to streamline the functions within InputStream so that it only has something like a get_data method
pub struct UserInputData {
pub(crate) is_active: bool,
pub(crate) value: f32,
pub(crate) axis_pair: Option<Vec2>,
}
I'll probably start a new repo next week, write a rough draft of the completely new version, and then figure out how to gradually transition to LWIM
Hmm, I worry that this loses the nice type-safety a bit, especially on Actionlike π€
Yes please
I still find the behavior of Chord somewhat inconsistent
Its value is the sum of all single-axis inputs and ignores the magnitude of dual-axis inputs
However, its axis_pair only retrieves the value of the first dual-axis input
That was an oversight on my part, sorry. I did not see it was already implemented for axis_pair and should've followed that. For value, the issue I was looking to fix was #380. I believe the reasoning I had for adding together the values was by asking "what if there was more than one SingleAxis value in the chord?" So, if there was a chord with two SingleAxis inputs one with -1.0 and another 1.0, I thought the chord taking both into account made more sense.
Just noticed that the docs on value wasn't updated to explain this either.
Yeah, I understand this part
I'm currently breaking down UserInput and InputKind into trait implementations, so I'm thinking about what the behavior of Chord should be
Because I still need to implement Sequence after that
Chord's axis_pair doesn't consider the values of multiple dual-axis inputs, only taking the first one, and value also ignores the magnitude of all dual-axis inputs
Hi, I have a test where I do:
- BevyInput's KeyCode Press
- run Update() -> check that ActionState is JustPressed
- BevyInput's KeyCode Releaes
- run Update() -> check that ActionState is JustReleased
but sometimes I notice that I need to runupdate()twice for my KeyCode::release to cause a ActionState::JustReleased
it's probably me, but i wonder if that's something that was seen before
Ah this might be because i'm running this in a test; and leafwing uses Time<Real> in tick_actions, which could be causing issues? not sure
@karmic monolith Now on my end, all the variants of enums like UserInput and InputKind have been converted into the UserInput trait impls
but I have a few questions
For example, do I also need to implement some of the methods that come with the old UserInput enum, such as len, is_empty, n_matching, and raw_inputs?
No, it's okay if we have breaking changes here
We can re-evaluate the API after and see what people need and what makes sense
yeah, only testing and documentation are left, so it should be completed in a day or two
Awesome β€οΈ
I'll review promptly: bandwidth is doing much better and we're looking good to make the next Bevy release
I think we'll actually ship two versions of LWIM for that: a trivial "just upgrade Bevy" release, and the refactor release
To make it easier for people to migrate
agree
and if time allows, I may also introduce some breaking changes
For example
to implement InputSequence, we would need the InputStreams to track input with ticks
but before that, I think we can break down InputStreams into smaller Resources similar to Bevy's ButtonInput and Axis
And their internal data can be fetched on-demand, and if a device (like mouse) is not in the InputMap, we can skip fetching its data
Even ActionData can also become lazily fetched because even if I only need the input's value or axis_pair, it still calculates and stores whether it is_pressed every frame
Oh nice
Is there any recommended way by Bevy to implement a Resource that contains lazy data?
Right, as in "it's not computed until requested"?
Or as in "added after startup"
the former
So, for simple and light things I would store a copy of needed the data in the resource, and then just have getter methods that do the crunchy calculation
But if you don't want to clone it and instead rely on a different source of truth, a custom system param that fetches the required data (including any local cache in a private resource) is better
but some inputs can impose a significant computational burden
such as converting mouse coordinates in world space and screen
I want to implement these as UserInput as well, but it's clear that collecting this data not on demand would impact performance
Using something like once_cell::Lazy makes it simple to achieve that; just stores a Lazy::new(|| heavy_computation())
but I'm curious if Bevy has any officially recommended approach for this
Hmm, nothing beyond what I recommended above
This isn't a pattern I've seen in the engine itself yet
You could also do a handle-style reference counting approach
To count the number of users that care
And then compute it if at least one user cares
Okay, I'll explore if there's any simple way later
Yep π
Like always, working first, then benchmarks, then try and speed things up :p
I'm unsure about the clash check for mouse input
For instance, why doesn't MouseMotion (the dual-axis version) clash with MouseMotionAxisType (the single-axis version) and MouseMotionDirection (the button-like version)?
However, for mouse input, I didn't implement VirtualAxis and VirtualDPad
Instead, I used an AxisInputMode internally in MouseMotion, MouseMotionAxis, MouseWheel, and MouseWheelAxis to indicate whether they should be continuous analog inputs or discrete digital inputs (-1, 0, 1)
So, MouseMotionDirection and MouseWheelDirection seem less useful now
Does anyone actually use a specific MouseDirection instead of MouseAxis corresponding to an action?
And does anyone actually use MouseButtons in VirtualAxis and VirtualDPad?
I agree with this choice