Search code examples
c#unity-game-enginedouble

Unity 2019 C# Double Tap Script for Input.GetAxisRaw("Horizontal") - Monstrous Code - How to compact/cleanup my mess?


I couldn't find a clear double tap script which uses the Unity function Input.GetAxisRaw("Horizontal"). Below is my monstrous solution for the left and right horizontal axis, which took me too many hours... It serves my purpose although it still allows the player to dash in the direction opposite to the movement direction if he has quick fingers.

If anyone can show me a cleaner solution or has some advice it would be greatly appreciated? Preferably a solution which can easily be implemented for other axis as well. If anyone know of a standard unity function which could provide the same double tap behavior, it would be even better.

If you attach the script to a cube you can see the double tap behavior i am looking for. The attached image shows the logic signal of the double tap behavior.

Logic signal of Double Tap

using System.Collections;
using UnityEngine;

public class DoubleTap : MonoBehaviour
{

float translationSpeedX = 20;
float translationSpeedY = 20;
Vector3 playerPosition;

float maxKeyDownTime = 0.2f;
float maxKeyUpTime = 0.2f;
float delayBetweenDoubleTaps = 0.4f;
bool doubleTapped = false;

bool firstLeftDown = false;
bool firstLeftUp = false;
bool prevLeftDown = false;

bool firstRightDown = false;
bool firstRightUp = false;
bool prevRightDown = false;


// Start is called before the first frame update
void Start()
{
    playerPosition = transform.position;
    StartCoroutine(KeyInputBuffer());
}

// Update is called once per frame
void Update()
{

    //Take keyboard input and translate the player in X and Y directions
    playerPosition.x += Input.GetAxis("Horizontal") * translationSpeedX * Time.deltaTime;
    playerPosition.x = Mathf.Clamp(playerPosition.x, -100, 100);
    playerPosition.y += Input.GetAxis("Vertical") * translationSpeedY * Time.deltaTime;
    playerPosition.y = Mathf.Clamp(playerPosition.y, -100, 100);
    transform.position = playerPosition;


    //Double tap left
    if (firstLeftUp && Input.GetAxisRaw("Horizontal") < 0) 
    {
        Debug.Log("Double Tap Left!");
        playerPosition.x -= 10f;
        firstLeftDown = false;
        firstLeftUp = false;
        StartCoroutine(DoubleTapDelayTimer());
    }
    else if (!firstLeftUp && firstLeftDown && Input.GetAxisRaw("Horizontal") == 0)
    {
        Debug.Log("firstLeftUp Left!");
        firstLeftUp = true;
        StartCoroutine(KeyUpTimer());
    }
    else if (!doubleTapped && !firstLeftDown && !prevLeftDown && Input.GetAxisRaw("Horizontal") < 0) 
    {
        Debug.Log("firstLeftDown Left!");
        firstLeftDown = true;
        StartCoroutine(KeyDownTimer());
    }


    //Double tap right
    if (firstRightUp  && Input.GetAxisRaw("Horizontal") > 0) 
    {
        Debug.Log("Double Tap Right!");
        playerPosition.x += 10f;
        firstRightDown = false;
        firstRightUp = false;
        StartCoroutine(DoubleTapDelayTimer());
    }
    else if (!firstRightUp && firstRightDown && Input.GetAxisRaw("Horizontal") == 0)
    {
        Debug.Log("firstRightUp Right!");
        firstRightUp = true;
        StartCoroutine(KeyRightUpTimer());
    }
    else if (!doubleTapped && !firstRightDown && !prevRightDown && Input.GetAxisRaw("Horizontal") > 0)
    {
        Debug.Log("firstRightDown Right!");
        firstRightDown = true;
        StartCoroutine(KeyRightDownTimer());
    }
}

//Timer which controlls the minimun time between double taps
IEnumerator DoubleTapDelayTimer()
{
    doubleTapped = true;
    yield return new WaitForSeconds(delayBetweenDoubleTaps);
    doubleTapped = false;
}

//Timers for the left horizontal axis
IEnumerator KeyDownTimer()
{
    yield return new WaitForSeconds(maxKeyDownTime);
    firstLeftDown = false;
}

IEnumerator KeyUpTimer()
{
    yield return new WaitForSeconds(maxKeyUpTime);
    firstLeftUp = false;
}


//Timers for the right horizontal axis
IEnumerator KeyRightDownTimer()
{
    yield return new WaitForSeconds(maxKeyDownTime);
    firstRightDown = false;
}

IEnumerator KeyRightUpTimer()
{
    yield return new WaitForSeconds(maxKeyUpTime);
    firstRightUp = false;
}

//Store the raw input conditions for the horizontal axis as reference in next frame
IEnumerator KeyInputBuffer()
{
    while(true)
    {
       yield return new WaitForEndOfFrame();

       if (Input.GetAxisRaw("Horizontal") > 0)
       {
           prevLeftDown = false;
           prevRightDown = true;
       }
       else if (Input.GetAxisRaw("Horizontal") < 0)
       {
           prevLeftDown = true;
           prevRightDown = false;
       }
       else
       {
           prevLeftDown = false;
           prevRightDown = false;
       }
    }
  }}

Below is the improved (and debugged) code as proposed by derHUGO. I am really amazed how much code is required for so little functionality.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum Axis
{
    X,
    Y
}

public enum AxisDirection
{
    Right,
    Left,
    Up,
    Down
}

public class DoubleTap : MonoBehaviour  //StackOverFlow
{
    // With [SerializeField] these are now configured directly via the Inspector 
    // in unity without having to recompile
    // This way you can play with them in Playmode until you have the values the fit your needs
    // For restring to the defaults simply hit Reset in the Context menu of the component
    [SerializeField] private float translationSpeedX = 20;
    [SerializeField] private float translationSpeedY = 20;
    [SerializeField] private float maxKeyDownTime = 0.2f;
    [SerializeField] private float maxKeyUpTime = 0.2f;
    [SerializeField] private float delayBetweenDoubleTaps = 0.4f;
private Dictionary<Axis, string> axisName = new Dictionary<Axis, string>
{
    {Axis.X, "Horizontal"},
    {Axis.Y, "Vertical"}
};

private Dictionary<Axis, AxisDirection> axisPositive = new Dictionary<Axis, AxisDirection>
{
    {Axis.X, AxisDirection.Right},
    {Axis.Y, AxisDirection.Up}
};

private Dictionary<Axis, AxisDirection> axisNegative = new Dictionary<Axis, AxisDirection>
{
    {Axis.X, AxisDirection.Left},
    {Axis.Y, AxisDirection.Down}
};

private Vector3 playerPosition;
private bool doubleTapped = false;

private Dictionary<AxisDirection, bool> axisFirstDown = new Dictionary<AxisDirection, bool>();
private Dictionary<AxisDirection, bool> axisFirstUp = new Dictionary<AxisDirection, bool>();
private Dictionary<AxisDirection, bool> axisPrevDown = new Dictionary<AxisDirection, bool>();

private Dictionary<AxisDirection, Action> doubleTapActions = new Dictionary<AxisDirection, Action>();   //MOD

// Start is called before the first frame update
private void Start()
{
    doubleTapActions.Add(AxisDirection.Left, LeftDoubleTapAction);  //MOD
    doubleTapActions.Add(AxisDirection.Right, RightDoubleTapAction);//MOD
    doubleTapActions.Add(AxisDirection.Up, UpDoubleTapAction);      //MOD
    doubleTapActions.Add(AxisDirection.Down, DownDoubleTapAction);  //MOD

    foreach (var axis in (AxisDirection[])Enum.GetValues(typeof(AxisDirection)))
    {
        axisFirstDown.Add(axis, false);
        axisFirstUp.Add(axis, false);
        axisPrevDown.Add(axis, false);
    }

    playerPosition = transform.position;
    StartCoroutine(KeyInputBuffer());
}

// Update is called once per frame
private void Update()
{
    foreach (var axis in (Axis[])Enum.GetValues(typeof(Axis)))
    {
        // positive
        if (axisFirstUp[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) > 0) //MOD  axisFirstUp || axisFirstDown
        {
            Debug.Log($"Double Tap {axisPositive[axis]}!");
            doubleTapActions[axisPositive[axis]].Invoke(); //MOD
            axisFirstDown[axisPositive[axis]] = false;
            axisFirstUp[axisPositive[axis]] = false;
            StartCoroutine(DoubleTapDelayTimer());
            return;
        }
        else if (!axisFirstUp[axisPositive[axis]] && axisFirstDown[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) == 0)
        {
            Debug.Log($"firstUp {axisPositive[axis]}!");
            axisFirstUp[axisPositive[axis]] = true;
            StartCoroutine(KeyUpTimer(axisPositive[axis])); //MOD axisPositive[axis]) || axis
        }
        else if (!doubleTapped && !axisFirstDown[axisPositive[axis]] && !axisPrevDown[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) > 0)
        {
            Debug.Log($"firstDown {axisPositive[axis]}!");
            axisFirstDown[axisPositive[axis]] = true;
            StartCoroutine(KeyDownTimer(axisPositive[axis])); //MOD axisPositive[axis]) || axis
        }

        // negative
        if (axisFirstUp[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) < 0) //MOD  axisFirstUp || axisFirstDown //MOD2 if || else if
        {
            Debug.Log($"Double Tap {axisNegative[axis]}!");
            doubleTapActions[axisNegative[axis]].Invoke(); //MOD
            axisFirstDown[axisNegative[axis]] = false;
            axisFirstUp[axisNegative[axis]] = false;
            StartCoroutine(DoubleTapDelayTimer());
            return;
        }
        else if (!axisFirstUp[axisNegative[axis]] && axisFirstDown[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) == 0)
        {
            Debug.Log($"firstUp {axisNegative[axis]}!");
            axisFirstUp[axisNegative[axis]] = true;
            StartCoroutine(KeyUpTimer(axisNegative[axis])); //MOD axisPositive[axis]) || axis
        }
        else if (!doubleTapped && !axisFirstDown[axisNegative[axis]] && !axisPrevDown[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) < 0)
        {
            Debug.Log($"firstDown {axisNegative[axis]}!");
            axisFirstDown[axisNegative[axis]] = true;
            StartCoroutine(KeyDownTimer(axisNegative[axis])); //MOD axisPositive[axis]) || axis
        }
    }

    // Do only process the input as movement if there was no double tap this frame

    //Take keyboard input and translate the player in X and Y directions
    playerPosition.x += Input.GetAxis("Horizontal") * translationSpeedX * Time.deltaTime; //MOD || UnityEgine.
    playerPosition.x = Mathf.Clamp(playerPosition.x, -100, 100);
    playerPosition.y += Input.GetAxis("Vertical") * translationSpeedY * Time.deltaTime; //MOD || UnityEgine.
    playerPosition.y = Mathf.Clamp(playerPosition.y, -100, 100);
    transform.position = playerPosition;

    Debug.Log("update");
}


//Timer which controlls the minimun time between double taps
IEnumerator DoubleTapDelayTimer()
{
    doubleTapped = true;
    yield return new WaitForSeconds(delayBetweenDoubleTaps);
    doubleTapped = false;

}


//Timers for the axis
IEnumerator KeyDownTimer(AxisDirection axis)
{
    yield return new WaitForSeconds(maxKeyDownTime);
    axisFirstDown[axis] = false;
}

IEnumerator KeyUpTimer(AxisDirection axis)
{
    yield return new WaitForSeconds(maxKeyUpTime);
    axisFirstUp[axis] = false;
}


//Store the raw input conditions for the horizontal axis as reference in next frame
IEnumerator KeyInputBuffer()
{
    while (true)
    {
        Debug.Log("coroutine");
        yield return new WaitForEndOfFrame();

        foreach (var axis in (Axis[])Enum.GetValues(typeof(Axis)))
        {
            axisPrevDown[axisPositive[axis]] = false;
            axisPrevDown[axisNegative[axis]] = false;

            if (Input.GetAxisRaw(axisName[axis]) > 0) //MOD || UnityEgine.
            {
                axisPrevDown[axisPositive[axis]] = true;
            }
            else if (Input.GetAxisRaw(axisName[axis]) < 0) //MOD || UnityEgine.
            {
                axisPrevDown[axisNegative[axis]] = true;
            }
        }
    }
}


//Double tap actions
private void LeftDoubleTapAction()
{
    playerPosition.x -= 10f;
}
private void RightDoubleTapAction()
{
    playerPosition.x += 10f;
}
private void UpDoubleTapAction()
{
    playerPosition.y += 10f;
}
private void DownDoubleTapAction()
{
    playerPosition.y -= 10f;
}

}


Solution

  • One thing might be to use proper Dictionaries for your bools etc and an enum for the axes so you don't have to implement the same thing multiple times and can easily add a new axis like e.g.

    public enum Axis
    {
        X,
        Y
    }
    
    public enum AxisDirection
    {
        Right,
        Left,
        Up,
        Down
    }
    
    public class DoubleTap : MonoBehaviour
    {
        // With [SerializeField] these are now configured directly via the Inspector 
        // in unity without having to recompile
        // This way you can play with them in Playmode until you have the values the fit your needs
        // For restring to the defaults simply hit Reset in the Context menu of the component
        [SerializeField] private float translationSpeedX = 20;
        [SerializeField] private float translationSpeedY = 20;
        [SerializeField] private float maxKeyDownTime = 0.2f;
        [SerializeField] private float maxKeyUpTime = 0.2f;
        [SerializeField] private float delayBetweenDoubleTaps = 0.4f;
    
    
        private Dictionary<Axis, string> axisName = new Dictionary<Axis, string>
        {
            {Axis.X, "Horizontal"},
            {Axis.Y, "Vertical"}
        };
    
        private Dictionary<Axis, AxisDirection> axisPositive = new Dictionary<Axis, AxisDirection>
        {
            {Axis.X, AxisDirection.Right},
            {Axis.Y, AxisDirection.Up}
        };
    
        private Dictionary<Axis, AxisDirection> axisNegative = new Dictionary<Axis, AxisDirection>
        {
            {Axis.X, AxisDirection.Left},
            {Axis.Y, AxisDirection.Down}
        };
    
        private Vector3 playerPosition;
        private bool doubleTapped = false;
    
        private Dictionary<AxisDirection, bool> axisFirstDown = new Dictionary<AxisDirection, bool>();
        private Dictionary<AxisDirection, bool> axisFirstUp = new Dictionary<AxisDirection, bool>();
        private Dictionary<AxisDirection, bool> axisPrevDown = new Dictionary<AxisDirection, bool>();
    
        // Start is called before the first frame update
        private void Start()
        {
            // setup dictionaries
            foreach (var axis in (AxisDirection[])Enum.GetValues(typeof(AxisDirection)))
            {
                axisFirstDown.Add(axis, false);
                axisFirstUp.Add(axis, false);
                axisPrevDown.Add(axis, false);
            }
    
            playerPosition = transform.position;
            StartCoroutine(KeyInputBuffer());
        }
    
        // Update is called once per frame
        private void Update()
        {
            foreach (var axis in (Axis[])Enum.GetValues(typeof(Axis)))
            {
                // positive
                if (axisFirstDown[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) > 0)
                {
                    Debug.Log($"Double Tap {axisPositive[axis]}");
                    playerPosition.x -= 10f;
                    axisFirstDown[axisPositive[axis]] = false;
                    axisFirstUp[axisPositive[axis]] = false;
                    StartCoroutine(DoubleTapDelayTimer());
                    return;
                }
                else if (!axisFirstUp[axisPositive[axis]] && axisFirstDown[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) == 0)
                {
                    Debug.Log($"firstUp {axisPositive[axis]}!");
                    axisFirstUp[axisPositive[axis]] = true;
                    StartCoroutine(KeyUpTimer(axis));
                }
                else if (!doubleTapped && !axisFirstDown[axisPositive[axis]] && !axisPrevDown[axisPositive[axis]] && Input.GetAxisRaw(axisName[axis]) > 0)
                {
                    Debug.Log($"firstDown {axisPositive[axis]}!");
                    axisFirstDown[axisPositive[axis]] = true;
                    StartCoroutine(KeyDownTimer(axis));
                }
    
                // negative
                else if (axisFirstDown[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) < 0)
                {
                    Debug.Log($"Double Tap {axisNegative[axis]}");
                    playerPosition.x -= 10f;
                    axisFirstDown[axisNegative[axis]] = false;
                    axisFirstUp[axisNegative[axis]] = false;
                    StartCoroutine(DoubleTapDelayTimer());
                    return;
                }
                else if (!axisFirstUp[axisNegative[axis]] && axisFirstDown[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) == 0)
                {
                    Debug.Log($"firstUp {axisNegative[axis]}!");
                    axisFirstUp[axisNegative[axis]] = true;
                    StartCoroutine(KeyUpTimer(axis));
                }
                else if (!doubleTapped && !axisFirstDown[axisNegative[axis]] && !axisPrevDown[axisNegative[axis]] && Input.GetAxisRaw(axisName[axis]) < 0)
                {
                    Debug.Log($"firstDown {axisNegative[axis]}!");
                    axisFirstDown[axisNegative[axis]] = true;
                    StartCoroutine(KeyDownTimer(axis));
                }
            }
    
            // Do only process the input as movement if there was no double tap this frame
    
            //Take keyboard input and translate the player in X and Y directions
            playerPosition.x += UnityEngine.Input.GetAxis("Horizontal") * translationSpeedX * Time.deltaTime;
            playerPosition.x = Mathf.Clamp(playerPosition.x, -100, 100);
            playerPosition.y += UnityEngine.Input.GetAxis("Vertical") * translationSpeedY * Time.deltaTime;
            playerPosition.y = Mathf.Clamp(playerPosition.y, -100, 100);
            transform.position = playerPosition;
    
        }
    
        //Timer which controlls the minimun time between double taps
        private IEnumerator DoubleTapDelayTimer()
        {
            doubleTapped = true;
            yield return new WaitForSeconds(delayBetweenDoubleTaps);
            doubleTapped = false;
        }
    
        //Timers for the axis
        private IEnumerator KeyDownTimer(AxisDirection axis)
        {
            yield return new WaitForSeconds(maxKeyDownTime);
            axisFirstDown[axis] = false;
        }
    
        private IEnumerator KeyUpTimer(AxisDirection axis)
        {
            yield return new WaitForSeconds(maxKeyUpTime);
            axisFirstUp[axis] = false;
        }
    
        //Store the raw input conditions for the horizontal axis as reference in next frame
        private IEnumerator KeyInputBuffer()
        {
            yield return new WaitForEndOfFrame();
    
            foreach (var axis in (Axis[])Enum.GetValues(typeof(Axis)))
            {
                axisPrevDown[axisPositive[axis]] = false;
                axisPrevDown[axisNegative[axis]] = false;
    
                if (UnityEngine.Input.GetAxisRaw(axisName[axis]) > 0)
                {
                    axisPrevDown[axisPositive[axis]] = true;
                }
                else if (UnityEngine.Input.GetAxisRaw(axisName[axis]) < 0)
                {
                    axisPrevDown[axisNegative[axis]] = true;
                }
            }
        }
    }