Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor UserInput and InputKind variants into separate implementations #490

Closed
wants to merge 27 commits into from

Conversation

Shute052
Copy link
Collaborator

@Shute052 Shute052 commented Feb 23, 2024

Objective

The current UserInput and InputKind enums are overly large and challenging to extend with new input types.

Solution

Introduce a new trait called UserInput trait.

Refactor the UserInput and InputKind variants into separate impls of the new UserInput trait, each focusing on a single responsibility.

TODO

  • Fix broken clashing check.
  • Documentation and doc examples.

Breacking Changes

  • renamed Modifier enum to ModifierKey, avoiding potential naming conflicts with other libraries or parts of your codebase.
  • refactored InputKind variants for representing user inputs, it now represents the kind of data an input can provide (e.g., button-like input, axis-like input).
  • removed UserInput enum in favor of the new UserInput trait and its impls.
  • by default, all inputs are unprocessed now, using With*ProcessingPipelineExt methods to define your preferred processing steps.
  • applied clashing check to continuous inputs, for example:
    • MouseScrollAxis::Y will clash with MouseScrollDirection::UP and MouseScrollDirection::DOWN.
    • MouseMove will clash with all individual axes and all directional events.

Changelog

  • added UserInput trait.
  • added UserInput impls for gamepad input events:
    • implemented UserInput for Bevy’s GamepadAxisType-related inputs.
      • GamepadStick: Continuous or discrete movement events of the left or right gamepad stick along both X and Y axes.
      • GamepadControlAxis: Continuous or discrete movement events of a GamepadAxisType.
      • GamepadControlDirection: Discrete movement direction events of a GamepadAxisType, treated as a button press.
    • implemented UserInput for Bevy’s GamepadButtonType directly.
    • added GamepadVirtualAxis, similar to the old UserInput::VirtualAxis using two GamepadButtonTypes.
    • added GamepadVirtualDPad, similar to the old UserInput::VirtualDPad using four GamepadButtonTypes.
  • added UserInput impls for keyboard inputs:
    • implemented UserInput for Bevy’s KeyCode directly.
    • implemented UserInput for ModifierKey.
    • added KeyboardVirtualAxis, similar to the old UserInput::VirtualAxis using two KeyCodes.
    • added KeyboardVirtualDPad, similar to the old UserInput::VirtualDPad using four KeyCodes.
  • added UserInput impls for mouse inputs:
    • implemented UserInput for movement-related inputs.
      • MouseMove: Continuous or discrete movement events of the mouse both X and Y axes.
      • MouseMoveAxis: Continuous or discrete movement events of the mouse on an axis, similar to the old MouseMotionAxisType.
      • MouseMoveDirection: Discrete movement direction events of the mouse on an axis, similar to the old MouseMotionDirection.
    • implemented UserInput for wheel-related inputs.
      • MouseScroll: Continuous or discrete movement events of the mouse wheel both X and Y axes.
      • MouseScrollAxis: Continuous or discrete movement events of the mouse wheel on an axis, similar to the old MouseWheelAxisType.
      • MouseScrollDirection: Discrete movement direction events of the mouse wheel on an axis, similar to the old MouseWheelDirection.
  • added InputChord for combining multiple inputs, similar to the old UserInput::Chord.

Migration Guide

  • The old SingleAxis is now:
    • GamepadControlAxis for gamepad axes.
    • MouseMoveAxis::X and MouseMoveAxis::Y for continuous mouse movement.
    • MouseScrollAxis::X and MouseScrollAxis::Y for continuous mouse wheel movement.
  • The old DualAxis is now:
    • GamepadStick for gamepad sticks.
    • MouseMove::default() for continuous mouse movement.
    • MouseScroll::default() for continuous mouse wheel movement.
  • The old Modifier is now ModifierKey.
  • The old MouseMotionDirection is now MouseMoveDirection.
  • The old MouseWheelDirection is now MouseScrollDirection.
  • The old UserInput::Chord is now InputChord.
  • The old UserInput::VirtualAxis is now:
    • GamepadVirtualAxis for four gamepad buttons.
    • KeyboardVirtualAxis for four keys.
    • MouseMoveAxis::X.digital() and MouseMoveAxis::Y.digital() for discrete mouse movement.
    • MouseScrollAxis::X.digital() and MouseScrollAxis::Y.digital() for discrete mouse wheel movement.
  • The old UserInput::VirtualDPad is now:
    • GamepadVirtualDPad for four gamepad buttons.
    • KeyboardVirtualDPad for four keys.
    • MouseMove::default().digital() for discrete mouse movement.
    • MouseScroll::default().digital() for discrete mouse wheel movement.

Next Steps

  • Refactor InputStreams into separate implementations.
  • Find alternative approaches for input mocking that provide more flexibility for future input type extensions, replacing RawInputs

@Shute052 Shute052 added code-quality Make the code faster or prettier breaking-change A breaking change that requires a new major version controversial Requires a heightened standard of review labels Feb 23, 2024
@Shute052
Copy link
Collaborator Author

Shute052 commented Feb 23, 2024

If there are any parts of the doc comments where examples should be added, please let me know directly.
Because I am the author of the document, I'm not sure where examples should be added to ensure clarity for the readers.
However, adding examples to every part would be too much work, and my holiday is ending soon.
I can only do as much as I can before then

@Shute052 Shute052 added the enhancement New feature or request label Feb 23, 2024
@Shute052 Shute052 marked this pull request as draft February 23, 2024 18:29
Shute052 added 7 commits May 4, 2024 12:12
# Conflicts:
#	examples/action_state_resource.rs
#	examples/axis_inputs.rs
#	examples/input_processing.rs
#	examples/mouse_wheel.rs
#	examples/virtual_dpad.rs
#	src/axislike.rs
#	src/buttonlike.rs
#	src/input_map.rs
#	src/input_mocking.rs
#	src/input_streams.rs
#	src/user_input.rs
#	tests/gamepad_axis.rs
#	tests/mouse_motion.rs
#	tests/mouse_wheel.rs
# Conflicts:
#	RELEASES.md
#	examples/input_processing.rs
#	src/axislike.rs
#	tests/gamepad_axis.rs
@Shute052 Shute052 marked this pull request as ready for review May 4, 2024 14:04
@Shute052 Shute052 marked this pull request as draft May 4, 2024 14:43
@Shute052
Copy link
Collaborator Author

Shute052 commented May 7, 2024

@Shute052 Shute052 marked this pull request as ready for review May 7, 2024 05:21
Shute052 added 2 commits May 7, 2024 13:23
# Conflicts:
#	RELEASES.md
#	src/input_mocking.rs
///
/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad
///
/// ```rust,ignore
Copy link
Collaborator Author

@Shute052 Shute052 May 7, 2024

Choose a reason for hiding this comment

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

Doc tests for this module are ignored, since they required to add usable testing controllers for each example.
I feel that they should be focusing on describing input behavior

Shute052 added 6 commits May 9, 2024 05:45
# Conflicts:
#	RELEASES.md
#	src/plugin.rs
# Conflicts:
#	RELEASES.md
#	examples/input_processing.rs
#	src/axislike.rs
#	src/clashing_inputs.rs
#	tests/gamepad_axis.rs
@alice-i-cecile
Copy link
Contributor

@Shute052 I'd like to review and merge this this week: please resolve merge conflicts if you get a chance and let me know if there's anything else that remains to be done.

/// In most cases, the input simply holds itself.
Simple(Box<dyn UserInput>),

/// The input consists of multiple independent [`UserInput`]s.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand the distinction between Composite and Group.

Copy link
Collaborator Author

@Shute052 Shute052 Jun 9, 2024

Choose a reason for hiding this comment

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

This is about handling input conflicts. If I understand correctly, when Input A and Input B conflict, one of them is a proper subset of the other.

Group is used for Chords and other similar input combinations (like Sequences in future), and these are made up of one or more inputs.
Composite is for inputs that are composed of at least two inputs, like VirtualAxis and VirtualDPad, which are made up of two and four buttons, respectively.

But maybe it could be changed to return a Vec and just check the lengths and basic inputs of each.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, got it.

pub fn len(&self) -> usize {
match self {
Self::Simple(_) => 1,
Self::Composite(_) => 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's quite surprising that this is 1. If it's not a bug, it needs a doc comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the old UserInput::len

/// Typically obtained by calling [`UserInput::raw_inputs`](crate::user_input::UserInput::raw_inputs).
#[derive(Default, Debug, Clone, PartialEq)]
#[must_use]
pub struct RawInputs {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. I don't love this design. Doesn't this just get us back to the same place, where UserInput can't be reasonably extended?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This struct has just been moved out from the original user_input.rs file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It should be able to replace with a new trait for implementing input mocking

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I see!

Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize
{
/// Defines the kind of behavior that the input should be.
fn kind(&self) -> InputControlKind;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the better design here is to have three separate traits ButtonlikeUserInput etc that all rely on a core UserInput trait that captures shared behavior. I think that's likely to be easier to do in a follow-up though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had previously considered splitting it into different input kinds: Button, Axis, DualAxis, etc.; and there are corresponding data types for each. Then, modify the ActionState API, something like this:

if let ButtonState(state) = action_state.get(ButtonAction) {
   let pressed = state.pressed;
   let pressure = state.pressure; // Gamepad buttons are ranging from 0 to 1, and others are 0 or 1
}
if let AxisValue(value: f32) = action_state.get(AxisAction) {
}

Copy link
Contributor

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Okay so I've finally had a chance to review this in earnest. Overall, I'm very impressed with the quality of both the engineering and documentation: thank you very very much.

The net effect of this refactor is great: much more flexible and principled, with some of the code that I hated most (InputStream) gone.

I have a small concern, which should be addressed now, and then two larger ones, which may make sense to wait on:

  1. I would like to avoid breaking the end user API when possible during this: can we keep UserInput::chord and UserInput::modified?
  2. I don't like the BasicInput struct. I understand why it's needed for clash handling, but I think it's the wrong design because it means that custom implemented user input type won't get clashes resolved properly (and we're back to hardcoding the set).
  3. The UserInput trait is too monolithic, and requires the implementation of methods that don't make sense for all input kinds. I think this should be split into buttonlike/single-axis/dual-axis.

@Shute052
Copy link
Collaborator Author

Shute052 commented Jun 9, 2024

  1. I would like to avoid breaking the end user API when possible during this: can we keep UserInput::chord and UserInput::modified?

If UserInput is a trait, I'm not sure how to add static methods to it when using its implementations as trait objects. However, I have added some builder methods.

  • Chord:
    • pub fn new<U: UserInput>(inputs: impl IntoIterator<Item = U>) -> Self
    • pub fn from_single(input: impl UserInput) -> Self
    • pub fn with(mut self, input: impl UserInput) -> Self
    • pub fn with_multiple<U: UserInput>(mut self, inputs: impl IntoIterator<Item = U>) -> Self
  • Modifier:
    • pub fn with(&self, other: impl UserInput) -> InputChord

@Shute052 Shute052 closed this Jun 9, 2024
@Shute052 Shute052 reopened this Jun 9, 2024
@alice-i-cecile
Copy link
Contributor

Alright, I can get behind that. Let's get this merged and then we can start experimenting on further refinement.

@alice-i-cecile
Copy link
Contributor

Adopted!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change A breaking change that requires a new major version code-quality Make the code faster or prettier controversial Requires a heightened standard of review enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Represent user inputs via traits, rather than a large enum
2 participants