Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Question Using last key pressed in a composite 2D vector

Discussion in 'Input System' started by Baste, May 14, 2021.

  1. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,198
    We want to build differently behaving 2D composite vectors than what's included, and I've kinda run into a wall.

    Right now, if you build a standard 2D composite vector from WASD, and use it for movement, holding A and D at the same time will give you 0 on the x-axis.

    We instead want the player's last input to get priority. So if you're running towards the left with A, and then start holding down D as well, you should be running to the right. If you're running towards the right with D and then start holding down A, you should be running towards the left.

    This is because players generally don't let go of one key and start pressing the other key on the same frame, but instead have an overlap of a frame or two where they hold both. During this overlap, the player character stops moving, which both feels and looks bad.

    I'm not quite sure how to go about doing this. The behaviour we want depends on state - we need to know which button was pressed last. So processors can't be used, they have no way to store state, and ones on composites don't even get access to the InputControl (it's null).

    I could build a processor, but that does seem like a bit much. Since a processor essentially bypasses the entire default processing of input, I kinda have to copy the content of
    InputActionState.ProcessDefaultInteraction
    and make modifications, which means also copying a ton of methods.

    I really don't want to do something hacky like manually implementing composite from wasd, because that in turn would cause us to have to manually implement rebinds, and ugh.

    Any ideas?
     
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,198
    Solved it!

    The solution was to make my own composite type. I assumed it'd be super-hard, but it was actually very easy. I just copied stuff from Vector2Composite and rewrote ReadValue.

    Here's the implementation if anyone needs it:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.InputSystem;
    4. using UnityEngine.InputSystem.Layouts;
    5. using UnityEngine.InputSystem.Utilities;
    6. using UnityEngine.Scripting;
    7.  
    8. #if UNITY_EDITOR
    9. [UnityEditor.InitializeOnLoad]
    10. #endif
    11. [Preserve]
    12. [DisplayStringFormat("{up}/{left}/{down}/{right}")]
    13. public class WASDComposite : InputBindingComposite<Vector2> {
    14.  
    15.     // NOTE: This is a modified copy of Vector2Composite
    16.  
    17.     [InputControl(layout = "Button")]
    18.     public int up = 0;
    19.     [InputControl(layout = "Button")]
    20.     public int down = 0;
    21.     [InputControl(layout = "Button")]
    22.     public int left = 0;
    23.     [InputControl(layout = "Button")]
    24.     public int right = 0;
    25.  
    26.     private bool upPressedLastFrame;
    27.     private bool downPressedLastFrame;
    28.     private bool leftPressedLastFrame;
    29.     private bool rightPressedLastFrame;
    30.     private float upPressTimestamp;
    31.     private float downPressTimestamp;
    32.     private float leftPressTimestamp;
    33.     private float rightPressTimestamp;
    34.  
    35.     public override Vector2 ReadValue(ref InputBindingCompositeContext context) {
    36.         var upPressed    = context.ReadValueAsButton(up);
    37.         var downPressed  = context.ReadValueAsButton(down);
    38.         var leftPressed  = context.ReadValueAsButton(left);
    39.         var rightPressed = context.ReadValueAsButton(right);
    40.  
    41.         if (upPressed    && !upPressedLastFrame)    upPressTimestamp    = Time.time;
    42.         if (downPressed  && !downPressedLastFrame)  downPressTimestamp  = Time.time;
    43.         if (leftPressed  && !leftPressedLastFrame)  leftPressTimestamp  = Time.time;
    44.         if (rightPressed && !rightPressedLastFrame) rightPressTimestamp = Time.time;
    45.  
    46.         float x = (leftPressed, rightPressed) switch {
    47.             (false, false)                                              =>  0f,
    48.             (true,  false)                                              => -1f,
    49.             (false, true)                                               =>  1f,
    50.             (true,  true) when rightPressTimestamp > leftPressTimestamp =>  1f,
    51.             (true,  true) when rightPressTimestamp < leftPressTimestamp => -1f,
    52.             (true,  true)                                               =>  0f
    53.         };
    54.  
    55.         float y = (downPressed, upPressed) switch {
    56.             (false, false)                                           =>  0f,
    57.             (true,  false)                                           => -1f,
    58.             (false, true)                                            =>  1f,
    59.             (true,  true) when upPressTimestamp > downPressTimestamp =>  1f,
    60.             (true,  true) when upPressTimestamp < downPressTimestamp => -1f,
    61.             (true,  true)                                            =>  0f
    62.         };
    63.  
    64.         const float diagonal = 0.707107f;
    65.         if (x != 0f && y != 0f) {
    66.             x *= diagonal;
    67.             y *= diagonal;
    68.         }
    69.  
    70.         upPressedLastFrame    = upPressed;
    71.         downPressedLastFrame  = downPressed;
    72.         leftPressedLastFrame  = leftPressed;
    73.         rightPressedLastFrame = rightPressed;
    74.  
    75.         return new Vector2(x, y);
    76.     }
    77.  
    78.     public override float EvaluateMagnitude(ref InputBindingCompositeContext context) {
    79.         var value = ReadValue(ref context);
    80.         return value.magnitude;
    81.     }
    82.  
    83. #if UNITY_EDITOR
    84.     static WASDComposite() {
    85.         Initialize();
    86.     }
    87. #endif
    88.  
    89.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    90.     static void Initialize() {
    91.         InputSystem.RegisterBindingComposite<WASDComposite>();
    92.     }
    93. }
    94.  
     
    metageist and Lurking-Ninja like this.
  3. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,198
    @Rene-Damm, two pieces of feedback:

    - I believe this implementation is what games should do in most cases. Could this be a toggle in the default Vector2Composite?
    - Implementing a InputBindingComposite was pretty straight fowards, so that's nice. Initially, I did not implement EvaluateMagnitude, and that has the side-effect of the action using my composite never getting cancelled. I can't really see not implementing the method being common. Could it perhaps be abstract instead of virtual? If we really want magnitudes to not be a thing, we could return -1 on our side.
     
    Franc3spo and metageist like this.
  4. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,923
    + 1D composite too. As a fourth option besides the current Negative/Neither/Positive wins.
     
  5. Franc3spo

    Franc3spo

    Joined:
    Jul 14, 2021
    Posts:
    3
    Hi, I wrote a modified version of the default Unity's vector2 composite, based on @Baste 's script, maybe it can be useful to you guys coming in the future.

    Code (CSharp):
    1. using System.ComponentModel;
    2. using UnityEngine;
    3. using UnityEngine.InputSystem;
    4. using UnityEngine.InputSystem.Controls;
    5. using UnityEngine.InputSystem.Layouts;
    6. using UnityEngine.InputSystem.Utilities;
    7. using UnityEngine.Scripting;
    8.  
    9. #if UNITY_EDITOR
    10. [UnityEditor.InitializeOnLoad]
    11. #endif
    12. [Preserve]
    13. [DisplayStringFormat("{up}/{left}/{down}/{right}")]
    14. [DisplayName("Up/Down/Left/Right Composite")]
    15. public class EnhancedVector2Composite : InputBindingComposite<Vector2>
    16. {
    17.     /// <summary>
    18.     /// Binding for the button that represents the up (that is, <c>(0,1)</c>) direction of the vector.
    19.     /// </summary>
    20.     /// <remarks>
    21.     /// This property is automatically assigned by the input system.
    22.     /// </remarks>
    23.     // ReSharper disable once MemberCanBePrivate.Global
    24.     // ReSharper disable once FieldCanBeMadeReadOnly.Global
    25.     [InputControl(layout = "Axis")] public int up = 0;
    26.  
    27.     /// <summary>
    28.     /// Binding for the button represents the down (that is, <c>(0,-1)</c>) direction of the vector.
    29.     /// </summary>
    30.     /// <remarks>
    31.     /// This property is automatically assigned by the input system.
    32.     /// </remarks>
    33.     // ReSharper disable once MemberCanBePrivate.Global
    34.     // ReSharper disable once FieldCanBeMadeReadOnly.Global
    35.     [InputControl(layout = "Axis")] public int down = 0;
    36.  
    37.     /// <summary>
    38.     /// Binding for the button represents the left (that is, <c>(-1,0)</c>) direction of the vector.
    39.     /// </summary>
    40.     /// <remarks>
    41.     /// This property is automatically assigned by the input system.
    42.     /// </remarks>
    43.     // ReSharper disable once MemberCanBePrivate.Global
    44.     // ReSharper disable once FieldCanBeMadeReadOnly.Global
    45.     [InputControl(layout = "Axis")] public int left = 0;
    46.  
    47.     /// <summary>
    48.     /// Binding for the button that represents the right (that is, <c>(1,0)</c>) direction of the vector.
    49.     /// </summary>
    50.     /// <remarks>
    51.     /// This property is automatically assigned by the input system.
    52.     /// </remarks>
    53.     [InputControl(layout = "Axis")] public int right = 0;
    54.  
    55.     /// <summary>
    56.     /// How to synthesize a <c>Vector2</c> from the values read from <see cref="up"/>, <see cref="down"/>,
    57.     /// <see cref="left"/>, and <see cref="right"/>.
    58.     /// </summary>
    59.     /// <value>Determines how X and Y of the resulting <c>Vector2</c> are formed from input values.</value>
    60.     /// <remarks>
    61.     /// <example>
    62.     /// <code>
    63.     /// var action = new InputAction();
    64.     ///
    65.     /// // DigitalNormalized composite (the default). Turns gamepad left stick into
    66.     /// // control equivalent to the D-Pad.
    67.     /// action.AddCompositeBinding("2DVector(mode=0)")
    68.     ///     .With("up", "&lt;Gamepad&gt;/leftStick/up")
    69.     ///     .With("down", "&lt;Gamepad&gt;/leftStick/down")
    70.     ///     .With("left", "&lt;Gamepad&gt;/leftStick/left")
    71.     ///     .With("right", "&lt;Gamepad&gt;/leftStick/right");
    72.     ///
    73.     /// // Digital composite. Turns gamepad left stick into control equivalent
    74.     /// // to the D-Pad except that diagonals will not be normalized.
    75.     /// action.AddCompositeBinding("2DVector(mode=1)")
    76.     ///     .With("up", "&lt;Gamepad&gt;/leftStick/up")
    77.     ///     .With("down", "&lt;Gamepad&gt;/leftStick/down")
    78.     ///     .With("left", "&lt;Gamepad&gt;/leftStick/left")
    79.     ///     .With("right", "&lt;Gamepad&gt;/leftStick/right");
    80.     ///
    81.     /// // Analog composite. In this case results in setup that behaves exactly
    82.     /// // the same as leftStick already does. But you could use it, for example,
    83.     /// // to swap directions by binding "up" to leftStick/down and "down" to
    84.     /// // leftStick/up.
    85.     /// action.AddCompositeBinding("2DVector(mode=2)")
    86.     ///     .With("up", "&lt;Gamepad&gt;/leftStick/up")
    87.     ///     .With("down", "&lt;Gamepad&gt;/leftStick/down")
    88.     ///     .With("left", "&lt;Gamepad&gt;/leftStick/left")
    89.     ///     .With("right", "&lt;Gamepad&gt;/leftStick/right");
    90.     /// </code>
    91.     /// </example>
    92.     /// </remarks>
    93.     public Mode mode;
    94.  
    95.     [Tooltip("ONLY WORKS IF MODE IS NOT SET TO ANALOG. If both the positive and negative side are actuated, decides what value to return. 'Neither' (default) means that " +
    96.     "the resulting value is 0. 'Positive' means that 1 will be returned. 'Negative' means that " +
    97.     "-1 will be returned. 'LastPressed' means that 1 or -1 will be returned based on which button was pressed last")]
    98.     public WhichSideWins xAxisWhichSideWins;
    99.     [Tooltip("ONLY WORKS IF MODE IS NOT SET TO ANALOG. If both the positive and negative side are actuated, decides what value to return. 'Neither' (default) means that " +
    100. "the resulting value is 0. 'Positive' means that 1 will be returned. 'Negative' means that " +
    101. "-1 will be returned. 'LastPressed' means that 1 or -1 will be returned based on which button was pressed last")]
    102.     public WhichSideWins yAxisWhichSideWins;
    103.  
    104.     private bool upPressedLastFrame;
    105.     private bool downPressedLastFrame;
    106.     private bool leftPressedLastFrame;
    107.     private bool rightPressedLastFrame;
    108.     private float upPressTimestamp;
    109.     private float downPressTimestamp;
    110.     private float leftPressTimestamp;
    111.     private float rightPressTimestamp;
    112.  
    113.     /// <inheritdoc />
    114.     public override Vector2 ReadValue(ref InputBindingCompositeContext context)
    115.     {
    116.         Mode mode = this.mode;
    117.  
    118.         if (mode == Mode.Analog)
    119.         {
    120.             float upValue = context.ReadValue<float>(up);
    121.             float downValue = context.ReadValue<float>(down);
    122.             float leftValue = context.ReadValue<float>(left);
    123.             float rightValue = context.ReadValue<float>(right);
    124.  
    125.             return DpadControl.MakeDpadVector(upValue, downValue, leftValue, rightValue);
    126.         }
    127.  
    128.         bool upIsPressed = context.ReadValueAsButton(up);
    129.         bool downIsPressed = context.ReadValueAsButton(down);
    130.         bool leftIsPressed = context.ReadValueAsButton(left);
    131.         bool rightIsPressed = context.ReadValueAsButton(right);
    132.  
    133.         if (upIsPressed && !upPressedLastFrame) upPressTimestamp = Time.time;
    134.         if (downIsPressed && !downPressedLastFrame) downPressTimestamp = Time.time;
    135.         if (leftIsPressed && !leftPressedLastFrame) leftPressTimestamp = Time.time;
    136.         if (rightIsPressed && !rightPressedLastFrame) rightPressTimestamp = Time.time;
    137.  
    138.         upPressedLastFrame = upIsPressed;
    139.         downPressedLastFrame = downIsPressed;
    140.         leftPressedLastFrame = leftIsPressed;
    141.         rightPressedLastFrame = rightIsPressed;
    142.  
    143.         if (upIsPressed && downIsPressed)
    144.             switch (yAxisWhichSideWins)
    145.             {
    146.                 case WhichSideWins.LeftOrUp:
    147.                     downIsPressed = false;
    148.                     break;
    149.                 case WhichSideWins.RightOrDown:
    150.                     upIsPressed = false;
    151.                     break;
    152.                 case WhichSideWins.Neither:
    153.                     downIsPressed = false;
    154.                     upIsPressed = false;
    155.                     break;
    156.                 case WhichSideWins.LastPressed:
    157.                     if (upPressTimestamp > downPressTimestamp)
    158.                         downIsPressed = false;
    159.                     else
    160.                         upIsPressed = false;
    161.                     break;
    162.             }
    163.         if (leftIsPressed && rightIsPressed)
    164.             switch (xAxisWhichSideWins)
    165.             {
    166.                 case WhichSideWins.LeftOrUp:
    167.                     rightIsPressed = false;
    168.                     break;
    169.                 case WhichSideWins.RightOrDown:
    170.                     leftIsPressed = false;
    171.                     break;
    172.                 case WhichSideWins.Neither:
    173.                     rightIsPressed = false;
    174.                     leftIsPressed = false;
    175.                     break;
    176.                 case WhichSideWins.LastPressed:
    177.                     if (leftPressTimestamp > rightPressTimestamp)
    178.                         rightIsPressed = false;
    179.                     else
    180.                         leftIsPressed = false;
    181.                     break;
    182.             }
    183.  
    184.         return DpadControl.MakeDpadVector(upIsPressed, downIsPressed, leftIsPressed, rightIsPressed, mode == Mode.DigitalNormalized);
    185.     }
    186.  
    187.     /// <inheritdoc />
    188.     public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    189.     {
    190.         Vector2 value = ReadValue(ref context);
    191.         return value.magnitude;
    192.     }
    193.  
    194. #if UNITY_EDITOR
    195.     static EnhancedVector2Composite() => Initialize();
    196. #endif
    197.  
    198.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    199.     static void Initialize()
    200.     {
    201.         InputSystem.RegisterBindingComposite<EnhancedVector2Composite>();
    202.     }
    203.  
    204.     /// <summary>
    205.     /// Determines how a <c>Vector2</c> is synthesized from part controls.
    206.     /// </summary>
    207.     public enum Mode
    208.     {
    209.         /// <summary>
    210.         /// Part controls are treated as analog meaning that the floating-point values read from controls
    211.         /// will come through as is (minus the fact that the down and left direction values are negated).
    212.         /// </summary>
    213.         Analog = 2,
    214.  
    215.         /// <summary>
    216.         /// Part controls are treated as buttons (on/off) and the resulting vector is normalized. This means
    217.         /// that if, for example, both left and up are pressed, instead of returning a vector (-1,1), a vector
    218.         /// of roughly (-0.7,0.7) (that is, corresponding to <c>new Vector2(-1,1).normalized</c>) is returned instead.
    219.         /// The resulting 2D area is diamond-shaped.
    220.         /// </summary>
    221.         DigitalNormalized = 0,
    222.  
    223.         /// <summary>
    224.         /// Part controls are treated as buttons (on/off) and the resulting vector is not normalized. This means
    225.         /// that if, for example, both left and up are pressed, the resulting vector is (-1,1) and has a length
    226.         /// greater than 1. The resulting 2D area is box-shaped.
    227.         /// </summary>
    228.         Digital = 1
    229.     }
    230.  
    231.     public enum WhichSideWins
    232.     {
    233.         LeftOrUp,
    234.         RightOrDown,
    235.         Neither,
    236.         LastPressed
    237.     }
    238. }
    239.  
    240.  
     

    Attached Files: