How to make 2D topdown dash ability which happens only once per the button pressed

Hello Unity community, I am very new to unity and attempting to make 2D topdown dash ability for my character. The following function is what I would like to implement.

  • “Space” is pressed

  • the player dash towards the direction of whatever key pressed (WASD)

  • while dashing, the player is ① not able to dash ② will not collide with bullet and enemies

  • after finished dashing, the player is ① able to dash again ② will collide with bullet and enemies

I saw couples of YouTube tutorials and threads to implement this feature, however I was not skillful enough to change and customize those scripts. Therefore, I am here to ask for help.

Btw, this is my current character movement script

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

public class PlayerMovement : MonoBehaviour
{
    public Rigidbody2D rb;
   
    public float moveSpeed; //Player Movement Speed
    Vector2 movement;
  
    bool isRight = false; //Player Sprite Flip
    public Transform weapon;


    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        //Plyaer Movement Input
        movement.x = Input.GetAxisRaw("Horizontal");
        movement.y = Input.GetAxisRaw("Vertical");
    }

    private void FixedUpdate()
    {
        movement.Normalize();
        rb.velocity = new Vector2(movement.x * moveSpeed * Time.fixedDeltaTime, movement.y * moveSpeed * Time.fixedDeltaTime);
    }

    void Flip() //Player Sprite Flip Function
    {
        isRight = !isRight;
        this.transform.Rotate(0f, 180f, 0f);
        weapon.transform.Rotate(0f, 180f, 0);
    }
}

Hey,

So this is going to be a pretty long chunk of code, but hopefully it works as a decent reference moving forward. For things that involve timing, I always find them easier to implement with coroutines. If you don’t already know how those work, they’re a great thing to learn: Unity - Manual: Coroutines
Unity - Scripting API: Coroutine

I won’t write all of your code for you, but here is an example that you may be able to fit into your own code:

using System.Collections;
using UnityEngine;

public class DashControl : MonoBehaviour
{
    [SerializeField] float startDashTime = 1f;
    [SerializeField] float dashSpeed = 1f;

    Rigidbody2D rb;

    float currentDashTime;

    bool canDash = true;
    bool playerCollision = true;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        if (canDash && Input.GetKeyDown(KeyCode.Space))
        {
            if (Input.GetKey(KeyCode.W))
            {
                StartCoroutine(Dash(Vector2.up));
            }

            else if (Input.GetKey(KeyCode.A))
            {
                StartCoroutine(Dash(Vector2.left));
            }

            else if (Input.GetKey(KeyCode.S))
            {
                StartCoroutine(Dash(Vector2.down));
            }

            else if (Input.GetKey(KeyCode.D))
            {
                StartCoroutine(Dash(Vector2.right));
            }

            else
            {
                // Whatever you want.
            }

        }
    }

    IEnumerator Dash(Vector2 direction)
    {
        canDash = false;
        playerCollision = false;
        currentDashTime = startDashTime; // Reset the dash timer.

        while (currentDashTime > 0f)
        {
            currentDashTime -= Time.deltaTime; // Lower the dash timer each frame.

            rb.velocity = direction * dashSpeed; // Dash in the direction that was held down.
            // No need to multiply by Time.DeltaTime here, physics are already consistent across different FPS.

            yield return null; // Returns out of the coroutine this frame so we don't hit an infinite loop.
        }

        rb.velocity = new Vector2(0f, 0f); // Stop dashing.

        canDash = true;
        playerCollision = true;
    }
}

It’s rather long, but it shouldn’t be too difficult to understand if you know coroutines. You can also attach this script exactly as is to a sprite and it will work; that may give you some insight if you’re struggling to figure things out. If you have any questions, please feel free to ask.

Essentially, you use the canDash and playerCollision bools to control if the player can dash or collide with enemies; you turn the bools false at the start of the coroutine and back to true at the end. The coroutine controls the dash itself by using a timer to count down to 0.

If you aren’t familiar with coroutines, they are basically a way of stopping your code partway for a specific amount of time (“yield return null” for a single frame), and then resuming exactly where they left off. If you insert a yield return into a “while” statement, the coroutine will not continue its code unless the “while” is false.

This is a lot of text, so apologies for that, but hopefully it helps.

Once again, feel free to ask questions.

1 Like

Hi, Thank you for the big help! You gave me most of the idea that i needed to implement dash ability.
but, how can i find the direction which was held down?

Hey, glad it helped!

Knowing the direction the player is holding down is already implemented in the code example. On line 53 IEnumerator Dash(Vector2 direction) you can see that there is a Vector2 as a parameter; this is for the direction. We need to pass this value in when we call the coroutine. In the code, I’m passing in the Vector2 value for the coroutine on lines 27, 32, 37, and 42. To get to those lines, we use an long series of “if” and “if else” statements to check which key is being held down.

On line 63 rb.velocity = direction * dashSpeed; we are multiplying the dash speed (dashSpeed) by the Vector2 that was passed in to the coroutine when we called it from lines 27, 32, 37, and 42 (direction).

Please feel free to ask more questions if anything is unclear.

1 Like

Thank you for your thorough explanation. So the rb.velocity = direction * dashSpeed; determines how far the object will dash. Is that correct? also, if I want to make the dash diagonally, how could i do that?

I have been trying to implement simple dash function first, which moves player object when Space is pressed, but i am failing to do so.

Hey

rb.velocity = direction * dashSpeed; sets the velocity to the pressed direction (a Vector2) multiplied by the dash speed (dashSpeed), so it determines which direction the player dashes and how fast the player moves each frame. How far the player dashes is partially determined by the dash speed, but is mainly determined by the dash time (currentDashTime).

The easiest way to implement a diagonal dash is to simply add more key combinations and directions as “if” and “else if” statements to the “if” statement on line 23 in Update(). Just remember to put the diagonal directions first so they have priority.

Here is an example:

            if (Input.GetKey(KeyCode.W) && Input.GetKey(KeyCode.D))
            {
                StartCoroutine(Dash(new Vector2(1f, 1f)));
            }

The parameter of new Vector2(1f, 1f) means right and up, which corresponds to holding the W and D keys at the same time.

I highly recommend attaching the original script I posted to an object and seeing how it functions. Before worrying about adding additional features, it’s important to understand what’s already there.

Good luck!

1 Like

As you suggested, I simply attached your example code (added diagonal movement). However, the object does not dash, although Im pressing Space. What am I doing wrong?

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

public class PlayerMovement : MonoBehaviour
{
    public Rigidbody2D rb;

    public float moveSpeed; //Player Movement Speed
    Vector2 movement;

    bool isRight = false; //Player Sprite Flip
    public Transform weapon;

    [SerializeField] float startDashTime = 1f;
    [SerializeField] float dashSpeed = 1f;

    float currentDashTime;

    bool canDash = true;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        //Plyaer Movement Input
        movement.x = Input.GetAxisRaw("Horizontal");
        movement.y = Input.GetAxisRaw("Vertical");

        if (canDash && Input.GetKeyDown(KeyCode.Space))
        {
            if (Input.GetKeyDown(KeyCode.W) && Input.GetKeyDown(KeyCode.D))
            {
                StartCoroutine(Dash(new Vector2(1f, 1f)));
            }

            else if (Input.GetKeyDown(KeyCode.S) && Input.GetKeyDown(KeyCode.D))
            {
                StartCoroutine(Dash(new Vector2(1f, -1f)));
            }

            else if (Input.GetKeyDown(KeyCode.A) && Input.GetKeyDown(KeyCode.W))
            {
                StartCoroutine(Dash(new Vector2(-1f, 1f)));
            }

            else if (Input.GetKeyDown(KeyCode.A) && Input.GetKeyDown(KeyCode.S))
            {
                StartCoroutine(Dash(new Vector2(-1f, -1f)));
            }

            else if (Input.GetKey(KeyCode.W))
            {
                StartCoroutine(Dash(Vector2.up));
            }

            else if (Input.GetKey(KeyCode.A))
            {
                StartCoroutine(Dash(Vector2.left));
            }

            else if (Input.GetKey(KeyCode.S))
            {
                StartCoroutine(Dash(Vector2.down));
            }

            else if (Input.GetKey(KeyCode.D))
            {
                StartCoroutine(Dash(Vector2.right));
            }
        }
    }

    void FixedUpdate()
    {
        movement.Normalize();
        rb.velocity = new Vector2(movement.x * moveSpeed * Time.fixedDeltaTime, movement.y * moveSpeed * Time.fixedDeltaTime);
    }

    void Flip() //Player Sprite Flip Function
    {
        isRight = !isRight;
        this.transform.Rotate(0f, 180f, 0f);
        weapon.transform.Rotate(0f, 180f, 0);
    }

    IEnumerator Dash(Vector2 direction)
    {
        canDash = false;

        currentDashTime = startDashTime; // Reset the dash timer.

        while (currentDashTime > 0f)
        {
            currentDashTime -= Time.deltaTime; // Lower the dash timer each frame.

            rb.velocity = direction * dashSpeed; // Dash in the direction that was held down.
                                                 // No need to multiply by Time.DeltaTime here, physics are already consistent across different FPS.

            yield return null; // Returns out of the coroutine this frame so we don't hit an infinite loop.
        }

        rb.velocity = new Vector2(0f, 0f); // Stop dashing.

        canDash = true;

    }
}

Haha, that doesn’t exactly look like the code I posted above.

It’s going to be tough to Frankenstein the code I posted into your own if you aren’t sure how it works. The reason you can’t dash is because you’re setting your velocity on line 81 and that is overriding the dash. You needed to disable player movement while dashing.

I made some changes to your script and it works now:

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

public class PlayerMovement : MonoBehaviour
{
    Rigidbody2D rb; // CHANGE --- No need for public, we're already getting the reference on line 24.

    public float moveSpeed; //Player Movement Speed
    Vector2 movement;

    bool isRight = false; //Player Sprite Flip
    public Transform weapon;

    [SerializeField] float startDashTime = 0.3f; // CHANGE --- Better starting number.
    [SerializeField] float dashSpeed = 15f; // CHANGE --- Better starting number.

    float currentDashTime;

    bool canDash = true;
    bool canMove = true; // CHANGE --- Need to disable movement when dashing.

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        //Plyaer Movement Input
        movement.x = Input.GetAxisRaw("Horizontal");
        movement.y = Input.GetAxisRaw("Vertical");

        if (canDash && Input.GetKeyDown(KeyCode.Space))
        {
            if (Input.GetKeyDown(KeyCode.W) && Input.GetKeyDown(KeyCode.D))
            {
                StartCoroutine(Dash(new Vector2(1f, 1f)));
            }

            else if (Input.GetKeyDown(KeyCode.S) && Input.GetKeyDown(KeyCode.D))
            {
                StartCoroutine(Dash(new Vector2(1f, -1f)));
            }

            else if (Input.GetKeyDown(KeyCode.A) && Input.GetKeyDown(KeyCode.W))
            {
                StartCoroutine(Dash(new Vector2(-1f, 1f)));
            }

            else if (Input.GetKeyDown(KeyCode.A) && Input.GetKeyDown(KeyCode.S))
            {
                StartCoroutine(Dash(new Vector2(-1f, -1f)));
            }

            else if (Input.GetKey(KeyCode.W))
            {
                StartCoroutine(Dash(Vector2.up));
            }

            else if (Input.GetKey(KeyCode.A))
            {
                StartCoroutine(Dash(Vector2.left));
            }

            else if (Input.GetKey(KeyCode.S))
            {
                StartCoroutine(Dash(Vector2.down));
            }

            else if (Input.GetKey(KeyCode.D))
            {
                StartCoroutine(Dash(Vector2.right));
            }
        }
    }

    void FixedUpdate()
    {
        if (canMove == true) // CHANGE --- Need to disable movement when dashing.
        {
            movement.Normalize();
            rb.velocity = new Vector2(movement.x * moveSpeed, movement.y * moveSpeed); // CHANGE --- No need to multiply by Time.deltaTime / Physics are already frame rate independent.
        }   
    }

    void Flip() //Player Sprite Flip Function
    {
        isRight = !isRight;
        this.transform.Rotate(0f, 180f, 0f);
        weapon.transform.Rotate(0f, 180f, 0);
    }

    IEnumerator Dash(Vector2 direction)
    {
        canDash = false;
        canMove = false; // CHANGE --- Need to disable movement when dashing.
        currentDashTime = startDashTime; // Reset the dash timer.

        while (currentDashTime > 0f)
        {
            currentDashTime -= Time.deltaTime; // Lower the dash timer each frame.

            rb.velocity = direction * dashSpeed; // Dash in the direction that was held down.
                                                 // No need to multiply by Time.DeltaTime here, physics are already consistent across different FPS.

            yield return null; // Returns out of the coroutine this frame so we don't hit an infinite loop.
        }

        rb.velocity = new Vector2(0f, 0f); // Stop dashing.

        canDash = true;
        canMove = true; // CHANGE --- Need to enable movement after dashing.
    }
}

I added comments with “CHANGE” in every spot that I made a change and explained why I made the change there. There are more changes I would have made, but I didn’t want to completely re-write everything the way I would do it, so I stopped myself.

Please try and understand why things work now when they didn’t before, and really try and learn what each piece of the code is actually doing. If you do that, you’ll be able to solve issues like this yourself eventually.

1 Like

Okay, and one more mistake I just noticed…

When you’re checking for diagonal dashes on lines 37, 42, 47 and 52 (line references are from the code I posted just above), you’re using Input.GetKeyDown() and that won’t work, you need to use Input.GetKey(). The way you have it set up, the player would need to press both keys on the same frame, which is virtually impossible.

1 Like

Okay, so that was the reason why the dash was not working. Thank you for teaching me, I really appreciate you.
I added direction.Normalize(); before the rb.velocity = direction * dashSpeed; so I can maintain same speed for the diagonal dash. Now, I should add the collision system. Would you mind if I ask you more questions if I encounter some problems.

1 Like

Of course, feel free to ask away, but give it your best shot first!

1 Like

Hi. I was able to ignore the collision while dashing by using the “Physics2D.IgnoreLayerCollision”.
The player indeed ignore the collision while dashing, however, when player stop dashing in middle of enemy’s body, player is pushed out of enemy. This is something that i dont want to see in the game, so maybe I will only make it with bullet.

 IEnumerator Dash(Vector2 direction)
    {
        canDash = false; // When Player Dash, Player Cannot Move
        canMove = false; // And Player Cannot Dash
      
        currentDashTime = startDashTime; // Reset the dash timer.

        while (currentDashTime > 0f)
        {
            currentDashTime -= Time.deltaTime; // Lower the dash timer each frame.

            direction.Normalize();
            rb.velocity = direction * dashSpeed; // Dash in the direction that was held down.
                                                 // No need to multiply by Time.DeltaTime here, physics are already consistent across different FPS.
          
            Physics2D.IgnoreLayerCollision(6, 8, true);
            Physics2D.IgnoreLayerCollision(6, 7, true);

            yield return null; // Returns out of the coroutine this frame so we don't hit an infinite loop.
        }

        rb.velocity = new Vector2(0f, 0f); // Stop dashing.

        canDash = true;
        canMove = true; // CHANGE --- Need to enable movement after dashing.
        Physics2D.IgnoreLayerCollision(6, 8, false);
        Physics2D.IgnoreLayerCollision(6, 7, false);

    }