Using last key pressed in a composite 2D vector

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?

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

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;

#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
#endif
[Preserve]
[DisplayStringFormat("{up}/{left}/{down}/{right}")]
public class WASDComposite : InputBindingComposite<Vector2> {

    // NOTE: This is a modified copy of Vector2Composite

    [InputControl(layout = "Button")]
    public int up = 0;
    [InputControl(layout = "Button")]
    public int down = 0;
    [InputControl(layout = "Button")]
    public int left = 0;
    [InputControl(layout = "Button")]
    public int right = 0;

    private bool upPressedLastFrame;
    private bool downPressedLastFrame;
    private bool leftPressedLastFrame;
    private bool rightPressedLastFrame;
    private float upPressTimestamp;
    private float downPressTimestamp;
    private float leftPressTimestamp;
    private float rightPressTimestamp;

    public override Vector2 ReadValue(ref InputBindingCompositeContext context) {
        var upPressed    = context.ReadValueAsButton(up);
        var downPressed  = context.ReadValueAsButton(down);
        var leftPressed  = context.ReadValueAsButton(left);
        var rightPressed = context.ReadValueAsButton(right);

        if (upPressed    && !upPressedLastFrame)    upPressTimestamp    = Time.time;
        if (downPressed  && !downPressedLastFrame)  downPressTimestamp  = Time.time;
        if (leftPressed  && !leftPressedLastFrame)  leftPressTimestamp  = Time.time;
        if (rightPressed && !rightPressedLastFrame) rightPressTimestamp = Time.time;

        float x = (leftPressed, rightPressed) switch {
            (false, false)                                              =>  0f,
            (true,  false)                                              => -1f,
            (false, true)                                               =>  1f,
            (true,  true) when rightPressTimestamp > leftPressTimestamp =>  1f,
            (true,  true) when rightPressTimestamp < leftPressTimestamp => -1f,
            (true,  true)                                               =>  0f
        };

        float y = (downPressed, upPressed) switch {
            (false, false)                                           =>  0f,
            (true,  false)                                           => -1f,
            (false, true)                                            =>  1f,
            (true,  true) when upPressTimestamp > downPressTimestamp =>  1f,
            (true,  true) when upPressTimestamp < downPressTimestamp => -1f,
            (true,  true)                                            =>  0f
        };

        const float diagonal = 0.707107f;
        if (x != 0f && y != 0f) {
            x *= diagonal;
            y *= diagonal;
        }

        upPressedLastFrame    = upPressed;
        downPressedLastFrame  = downPressed;
        leftPressedLastFrame  = leftPressed;
        rightPressedLastFrame = rightPressed;

        return new Vector2(x, y);
    }

    public override float EvaluateMagnitude(ref InputBindingCompositeContext context) {
        var value = ReadValue(ref context);
        return value.magnitude;
    }

#if UNITY_EDITOR
    static WASDComposite() {
        Initialize();
    }
#endif

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Initialize() {
        InputSystem.RegisterBindingComposite<WASDComposite>();
    }
}
2 Likes

@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.
2 Likes
  • 1D composite too. As a fourth option besides the current Negative/Neither/Positive wins.

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.

using System.ComponentModel;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;

#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
#endif
[Preserve]
[DisplayStringFormat("{up}/{left}/{down}/{right}")]
[DisplayName("Up/Down/Left/Right Composite")]
public class EnhancedVector2Composite : InputBindingComposite<Vector2>
{
    /// <summary>
    /// Binding for the button that represents the up (that is, <c>(0,1)</c>) direction of the vector.
    /// </summary>
    /// <remarks>
    /// This property is automatically assigned by the input system.
    /// </remarks>
    // ReSharper disable once MemberCanBePrivate.Global
    // ReSharper disable once FieldCanBeMadeReadOnly.Global
    [InputControl(layout = "Axis")] public int up = 0;

    /// <summary>
    /// Binding for the button represents the down (that is, <c>(0,-1)</c>) direction of the vector.
    /// </summary>
    /// <remarks>
    /// This property is automatically assigned by the input system.
    /// </remarks>
    // ReSharper disable once MemberCanBePrivate.Global
    // ReSharper disable once FieldCanBeMadeReadOnly.Global
    [InputControl(layout = "Axis")] public int down = 0;

    /// <summary>
    /// Binding for the button represents the left (that is, <c>(-1,0)</c>) direction of the vector.
    /// </summary>
    /// <remarks>
    /// This property is automatically assigned by the input system.
    /// </remarks>
    // ReSharper disable once MemberCanBePrivate.Global
    // ReSharper disable once FieldCanBeMadeReadOnly.Global
    [InputControl(layout = "Axis")] public int left = 0;

    /// <summary>
    /// Binding for the button that represents the right (that is, <c>(1,0)</c>) direction of the vector.
    /// </summary>
    /// <remarks>
    /// This property is automatically assigned by the input system.
    /// </remarks>
    [InputControl(layout = "Axis")] public int right = 0;

    /// <summary>
    /// How to synthesize a <c>Vector2</c> from the values read from <see cref="up"/>, <see cref="down"/>,
    /// <see cref="left"/>, and <see cref="right"/>.
    /// </summary>
    /// <value>Determines how X and Y of the resulting <c>Vector2</c> are formed from input values.</value>
    /// <remarks>
    /// <example>
    /// <code>
    /// var action = new InputAction();
    ///
    /// // DigitalNormalized composite (the default). Turns gamepad left stick into
    /// // control equivalent to the D-Pad.
    /// action.AddCompositeBinding("2DVector(mode=0)")
    ///     .With("up", "<Gamepad>/leftStick/up")
    ///     .With("down", "<Gamepad>/leftStick/down")
    ///     .With("left", "<Gamepad>/leftStick/left")
    ///     .With("right", "<Gamepad>/leftStick/right");
    ///
    /// // Digital composite. Turns gamepad left stick into control equivalent
    /// // to the D-Pad except that diagonals will not be normalized.
    /// action.AddCompositeBinding("2DVector(mode=1)")
    ///     .With("up", "<Gamepad>/leftStick/up")
    ///     .With("down", "<Gamepad>/leftStick/down")
    ///     .With("left", "<Gamepad>/leftStick/left")
    ///     .With("right", "<Gamepad>/leftStick/right");
    ///
    /// // Analog composite. In this case results in setup that behaves exactly
    /// // the same as leftStick already does. But you could use it, for example,
    /// // to swap directions by binding "up" to leftStick/down and "down" to
    /// // leftStick/up.
    /// action.AddCompositeBinding("2DVector(mode=2)")
    ///     .With("up", "<Gamepad>/leftStick/up")
    ///     .With("down", "<Gamepad>/leftStick/down")
    ///     .With("left", "<Gamepad>/leftStick/left")
    ///     .With("right", "<Gamepad>/leftStick/right");
    /// </code>
    /// </example>
    /// </remarks>
    public Mode mode;

    [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 " +
    "the resulting value is 0. 'Positive' means that 1 will be returned. 'Negative' means that " +
    "-1 will be returned. 'LastPressed' means that 1 or -1 will be returned based on which button was pressed last")]
    public WhichSideWins xAxisWhichSideWins;
    [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 " +
"the resulting value is 0. 'Positive' means that 1 will be returned. 'Negative' means that " +
"-1 will be returned. 'LastPressed' means that 1 or -1 will be returned based on which button was pressed last")]
    public WhichSideWins yAxisWhichSideWins;

    private bool upPressedLastFrame;
    private bool downPressedLastFrame;
    private bool leftPressedLastFrame;
    private bool rightPressedLastFrame;
    private float upPressTimestamp;
    private float downPressTimestamp;
    private float leftPressTimestamp;
    private float rightPressTimestamp;

    /// <inheritdoc />
    public override Vector2 ReadValue(ref InputBindingCompositeContext context)
    {
        Mode mode = this.mode;

        if (mode == Mode.Analog)
        {
            float upValue = context.ReadValue<float>(up);
            float downValue = context.ReadValue<float>(down);
            float leftValue = context.ReadValue<float>(left);
            float rightValue = context.ReadValue<float>(right);

            return DpadControl.MakeDpadVector(upValue, downValue, leftValue, rightValue);
        }

        bool upIsPressed = context.ReadValueAsButton(up);
        bool downIsPressed = context.ReadValueAsButton(down);
        bool leftIsPressed = context.ReadValueAsButton(left);
        bool rightIsPressed = context.ReadValueAsButton(right);

        if (upIsPressed && !upPressedLastFrame) upPressTimestamp = Time.time;
        if (downIsPressed && !downPressedLastFrame) downPressTimestamp = Time.time;
        if (leftIsPressed && !leftPressedLastFrame) leftPressTimestamp = Time.time;
        if (rightIsPressed && !rightPressedLastFrame) rightPressTimestamp = Time.time;

        upPressedLastFrame = upIsPressed;
        downPressedLastFrame = downIsPressed;
        leftPressedLastFrame = leftIsPressed;
        rightPressedLastFrame = rightIsPressed;

        if (upIsPressed && downIsPressed)
            switch (yAxisWhichSideWins)
            {
                case WhichSideWins.LeftOrUp:
                    downIsPressed = false;
                    break;
                case WhichSideWins.RightOrDown:
                    upIsPressed = false;
                    break;
                case WhichSideWins.Neither:
                    downIsPressed = false;
                    upIsPressed = false;
                    break;
                case WhichSideWins.LastPressed:
                    if (upPressTimestamp > downPressTimestamp)
                        downIsPressed = false;
                    else
                        upIsPressed = false;
                    break;
            }
        if (leftIsPressed && rightIsPressed)
            switch (xAxisWhichSideWins)
            {
                case WhichSideWins.LeftOrUp:
                    rightIsPressed = false;
                    break;
                case WhichSideWins.RightOrDown:
                    leftIsPressed = false;
                    break;
                case WhichSideWins.Neither:
                    rightIsPressed = false;
                    leftIsPressed = false;
                    break;
                case WhichSideWins.LastPressed:
                    if (leftPressTimestamp > rightPressTimestamp)
                        rightIsPressed = false;
                    else
                        leftIsPressed = false;
                    break;
            }

        return DpadControl.MakeDpadVector(upIsPressed, downIsPressed, leftIsPressed, rightIsPressed, mode == Mode.DigitalNormalized);
    }

    /// <inheritdoc />
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        Vector2 value = ReadValue(ref context);
        return value.magnitude;
    }

#if UNITY_EDITOR
    static EnhancedVector2Composite() => Initialize();
#endif

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Initialize()
    {
        InputSystem.RegisterBindingComposite<EnhancedVector2Composite>();
    }

    /// <summary>
    /// Determines how a <c>Vector2</c> is synthesized from part controls.
    /// </summary>
    public enum Mode
    {
        /// <summary>
        /// Part controls are treated as analog meaning that the floating-point values read from controls
        /// will come through as is (minus the fact that the down and left direction values are negated).
        /// </summary>
        Analog = 2,

        /// <summary>
        /// Part controls are treated as buttons (on/off) and the resulting vector is normalized. This means
        /// that if, for example, both left and up are pressed, instead of returning a vector (-1,1), a vector
        /// of roughly (-0.7,0.7) (that is, corresponding to <c>new Vector2(-1,1).normalized</c>) is returned instead.
        /// The resulting 2D area is diamond-shaped.
        /// </summary>
        DigitalNormalized = 0,

        /// <summary>
        /// Part controls are treated as buttons (on/off) and the resulting vector is not normalized. This means
        /// that if, for example, both left and up are pressed, the resulting vector is (-1,1) and has a length
        /// greater than 1. The resulting 2D area is box-shaped.
        /// </summary>
        Digital = 1
    }

    public enum WhichSideWins
    {
        LeftOrUp,
        RightOrDown,
        Neither,
        LastPressed
    }
}

9284584–1301086–EnhancedVector2Composite.cs (9.93 KB)

1 Like