2D topdown dash

Hi all,

I’m working on a topdown rpg and now I’m fighting to implement a dash hability

I like a lot the dash on hyper light drifter

But I can’t get any clearly solution to implement it. Can you give me an advice to implement it?

And other thing, when I increment the velocity a lot I can ignore the collisions with my player, how can I avoid that? (using raycast?)

this is now my code to control the player.

using UnityEngine;
using System.Collections;

public class PlayerControl : MonoBehaviour
{
    public float maxSpeed = 6f;
    public float dashForce = 5f;
    public float dashDuration = 1;
    public float dashCooldown = 2f;
    private Animator anim;
    private RaycastHit2D hit;
    private bool dashing = false;

    void Start()
    {
        anim = GetComponent<Animator>();
    }

    void FixedUpdate()
    {
        float moveH = Input.GetAxis("Horizontal");
        float moveV = Input.GetAxis("Vertical");
        Vector2 movement_vector = new Vector2(moveH * maxSpeed, moveV * maxSpeed);
        if(movement_vector != Vector2.zero)
        {
            anim.SetBool("is_walking", true);
            anim.SetFloat("hSpeed", moveH);
            anim.SetFloat("vSpeed", moveV);
        }else
        {
            anim.SetBool("is_walking", false);
        }
        if (Input.GetButtonDown("Dash") && !dashing)
        {
            dashing = true;
            //dash
            dashing = false;
        }
    }

    void Update()
    {
    }
}

Hello, I believe I can help. I implemented a dashing system into my project that works more or less how you want. Essentially there are about 3 design points that my code follows (which are specific to my project only).

Point 1 : When there is a clear path between Point A and B, the Dash should always get you from Point A to Point B at the same speed every time. You need to be able to set a “distance to travel” and a “travel speed” into the dash function. This allows players to build up muscle memory with the Dashing mechanics and enables them to know exactly when and where they will end up at all times after a dash.

Point 2 : There needs to be a way to dynamically limit the dash distance to avoid situations where the ending position of the dash lands inside of another non-enemy collider.

Point 3 : When Dashing the player needs to be able to collide with the levels terrain and “obstacles” while ignoring collisions with enemies. In order to facilitate this there needs to be a way to push back enemies that happen to be within a certain distance of the Dash’s end position to avoid overlapping colliders.

I’m going to post all the dash related scripts from my project. You are not going to be able to just copy + paste it into your project though since the code structure has been heavily designed around my specific game. I will make notes to explain certain things when there is editor/gameobject related setup involved. The code is pretty messy and all over the place so If you are having some trouble following let me know and I will try to help.

The first component is the base class for all movement components in my project. EnemyMovement and PlayerMovement all derive from this. The Dash and FixedUpdate method in this class handles Point 1 and 2 of my design.

using UnityEngine;
using System.Collections;

public class EntityMovement : EntityComponent
{
    public LayerMask ObstacleLayer;

    public Vector2 Velocity
    {
        get;
        protected set;
    }
    public Vector2 MovementDirection
    {
        get;
        protected set;
    }
    public Rigidbody2D RBody2D
    {
        get;
        protected set;
    }
    public bool IsMoving
    {
        get
        {
            return Velocity.sqrMagnitude > 0.01f;
        }
    }

    public bool IsDashing
    {
        get;
        protected set;
    }
    protected Vector3 dashTarget;
    protected float dashSpeed;

    public override void Initialize()
    {
        base.Initialize();

        RBody2D = GetComponent<Rigidbody2D>();
        ObstacleLayer = LayerMask.GetMask("Obstacles");
    }

    protected virtual void Update()
    {
        Velocity = MovementDirection * owner.Statistics.GetStatValue(StatType.MoveSpeed);
    }
    protected virtual void FixedUpdate()
    {
        if (IsDashing)
        {
            Velocity = Vector2.zero;
            RBody2D.velocity = Vector2.zero;

            float distSqr = (dashTarget - transform.position).sqrMagnitude;
            if (distSqr < 0.1f)
            {
                OnDashFinished();
                IsDashing = false;
                dashTarget = Vector2.zero;
                owner.Vitals.IsInvincible = false;
            }
            else
            {
                owner.Movement.RBody2D.MovePosition(Vector3.Lerp(transform.position,
                    dashTarget, dashSpeed * Time.deltaTime));
            }
        }
        else
        {
            RBody2D.AddForce(Velocity);
        }
    }

    public virtual void ClearVelocity()
    {
        Velocity = Vector2.zero;
        RBody2D.velocity = Vector2.zero;
    }

    public void Dash(float dashDistance, float speed, Vector2 direction)
    {
        IsDashing = true;

        RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, dashDistance, ObstacleLayer);

        if (hit)
        {
            dashTarget = transform.position + (Vector3)((direction * dashDistance)
                * hit.fraction);
        }
        else
        {
            dashTarget = transform.position + (Vector3)(direction * dashDistance);
        }

        dashSpeed = speed;
        owner.Movement.ClearVelocity();
    }

    protected virtual void OnDashFinished()
    {

    }
}

The next component, PlayerDash, controls the players dash input and the states necessary for a dash to be executed. This also has the player specific dash collision setup that I mentioned in the first half of Point 3.

using UnityEngine;
using System.Collections;

public class PlayerDash : EntityComponent
{
    public Collider2D TerrainOnly;
    public Collider2D AllCollisions;

    public StatisticValue Speed;
    public StatisticValue Distance;
    public StatisticValue TempoCost;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (owner.Movement.IsMoving)
            {
                if (owner.Vitals.TrySpendTempo(TempoCost.Total))
                {
                    owner.Vitals.IsInvincible = true;
                    CollideWithTerrainOnly();
                    owner.Movement.Dash(Distance.Total, Speed.Total,
                        owner.Movement.Velocity.normalized);
                }
            }
        }
    }

    public void CollideWithTerrainOnly()
    {
        if (AllCollisions.enabled)
            AllCollisions.enabled = false;
        if (!TerrainOnly.enabled)
            TerrainOnly.enabled = true;
    }
    public void CollideWithAll()
    {
        if (!AllCollisions.enabled)
            AllCollisions.enabled = true;
        if (TerrainOnly.enabled)
            TerrainOnly.enabled = false;
    }
}

The important thing to take from the PlayerDash component is the 2 Collider2D fields. Essentially you need to add 2 non-trigger colliders to your player, one of them should be on your games version of the “collide with everything” layer. The second collider needs to be set up on a Layer that interacts with everything solid Except for Enemies (and should not be enabled outside of the PlayerDash methods). So in the Edit/ProjectSettings/Physics2D menu you need to alter the layer collision matrix for that type of setup. In my case I have an “Enemies” layer and a “TerrainOnly” layer. On the “TerrainOnly” layer row on the matrix, I have the box ticked off under the “Enemies” layer column. This allows my player to collider with the world and any non-enemy obstacles, while completely ignoring collisions with GameObjects that are on the “Enemies” layer. Here are a few pics to show what I mean :
2662434--187743--col_matrix.png 2662434--187744--inspector_all.png
2662434--187742--hierarchy.png2662434--187750--inspector_terrain.png

The last class is the PlayerMovement component, which derives from EntityMovement. This is where I read input to move the player object, and also where the Player specific code for the virtual method OnDashFinished is implemented. This handles the last half of Point 3.

using UnityEngine;
using System.Collections;

public class PlayerMovement : EntityMovement
{
    protected override void Update()
    {
        if (!IsDashing)
        {
            MovementDirection = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
            MovementDirection = MovementDirection.normalized;
        }

        base.Update();
    }
    protected override void OnDashFinished()
    {
        Collider2D[] eColliders = Physics2D.OverlapCircleAll(dashTarget, 0.5f, LayerMask.GetMask("Enemies"));
        if (eColliders != null)
        {
            for (int i = 0; i < eColliders.Length; i++)
            {
                WorldEntity e = eColliders[i].GetComponent<WorldEntity>();
                Vector2 dir = e.transform.position - transform.position;
                e.Movement.RBody2D.velocity = dir.normalized * 15;
            }
        }

        Player p = (Player)owner;
        p.Dash.CollideWithAll();

        base.OnDashFinished();
    }
}

Hopefully this helps you out a little :slight_smile:

2 Likes

Thanks a lot for the answer, It helps me a lot, I didn’t know about Vector2.normalized. And with Raycast we avoid the problem with collide with obstacle/terrain