Voltar para o Blog
Quest Log

Dash na Unity 2D: Impulso, Cooldown e Invencibilidade

Personagem 2D em pixel art avançando num dash horizontal com um rastro luminoso atrás e um inimigo sendo atravessado sem dano

Unity dash 2D em C#: impulso sem gravidade, duração com coroutine, cooldown, invencibilidade trocando a layer, TrailRenderer e o dash que atravessa parede.

Ferramenta:UnityC#

Dash na Unity 2D: Impulso, Cooldown e Invencibilidade

Um unity dash 2D bem feito é um estado curto e controlado: o personagem ignora a gravidade, segue numa velocidade fixa por uma fração de segundo, fica imune a dano enquanto isso e só pode repetir depois de um cooldown. Este tutorial monta o PlayerDash2D.cs com essas quatro partes, mais rastro visual com TrailRenderer, em C# do Unity 6.3 LTS com Rigidbody2D.linearVelocity e Input System.

No fim você também vai saber resolver os dois bugs que todo dash tem na primeira versão: o dash infinito, que deixa o jogador atravessar a fase voando, e o dash que atravessa parede.

Unity dash 2d: o que precisa

Antes do código, decida três coisas, porque cada uma muda o script:

  1. Direção. Na direção do input (oito direções, estilo Celeste e jogos top-down) ou na direção em que o personagem olha (só horizontal, estilo Hollow Knight). O script abaixo suporta as duas com um checkbox.
  2. Duração e distância. Distância = velocidade vezes duração. Com 22 unidades por segundo e 0,15 segundo, o dash cobre 3,3 unidades. Dash curto e rápido parece explosão; longo e lento parece deslizar.
  3. O que o dash ignora. Gravidade, sim, quase sempre. Input de movimento, sim. Dano de inimigo, depende do jogo: em jogos de ação o dash é a esquiva, então precisa de invencibilidade.

Pré-requisitos: um personagem com Rigidbody2D Dynamic e Collision Detection em Continuous, e um script de movimento horizontal. Se ainda não tem, o script de movimento 2D na Unity é a base que este dash espera.

Criar a ação Dash no Input System

O asset InputSystem_Actions dos projetos novos do Unity 6 não traz uma ação de dash. Crie a sua:

  1. Dê dois cliques em InputSystem_Actions na pasta Assets.
  2. No mapa Player, clique no + da coluna Actions e nomeie Dash. Action Type: Button.
  3. Adicione uma binding de teclado (por exemplo, Left Shift se o seu jogo não tem corrida, ou K) e uma de controle (Right Shoulder ou Button West).
  4. Salve o asset (Save Asset no topo da janela, ou deixe Auto-Save marcado).

No código, InputSystem.actions.FindAction("Dash") encontra a ação nova.

Dash com linearVelocity e gravidade zerada

O núcleo do dash são três linhas:

rb.gravityScale = 0f;
rb.linearVelocity = direction * dashSpeed;
// ... espera a duração ...
rb.gravityScale = originalGravity;

Zerar o gravityScale faz o dash ser uma linha reta. Sem isso, um dash horizontal no ar cai numa curva e perde a sensação de impulso. Definir linearVelocity (e não somar força) garante que todo dash tenha a mesma distância, não importa se o personagem estava parado, correndo ou caindo.

Tem um detalhe que quebra a maioria das primeiras versões: o script de movimento. Ele escreve a velocidade X a cada FixedUpdate, então no passo seguinte ao início do dash ele substitui a velocidade do dash pela de andar, e o dash dura um frame. A solução é desligar os scripts que mexem na velocidade enquanto o dash acontece. O script recebe uma lista deles no Inspector e liga e desliga com enabled.

O script PlayerDash2D completo

Crie PlayerDash2D.cs:

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerDash2D : MonoBehaviour
{
    [Header("Dash")]
    [SerializeField] private float dashSpeed = 22f;
    [SerializeField] private float dashDuration = 0.15f;
    [SerializeField] private float dashCooldown = 0.4f;
    [SerializeField] private bool eightDirections = false;
    [SerializeField] private int maxAirDashes = 1;
    [SerializeField, Range(0f, 1f)] private float endSpeedMultiplier = 0.3f;

    [Header("Invencibilidade")]
    [SerializeField] private string dashLayerName = "PlayerDash";

    [Header("Chão")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private Vector2 groundCheckSize = new Vector2(0.8f, 0.1f);
    [SerializeField] private LayerMask groundLayer;

    [Header("Referências")]
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private Transform visual;
    [SerializeField] private TrailRenderer trail;
    [SerializeField] private MonoBehaviour[] disableWhileDashing;

    private InputAction dashAction;
    private InputAction moveAction;
    private float nextDashTime;
    private int airDashesLeft;
    private int originalLayer;
    private int dashLayer;
    private bool isDashing;

    public bool IsDashing => isDashing;

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

        dashAction = InputSystem.actions.FindAction("Dash");
        moveAction = InputSystem.actions.FindAction("Move");
        originalLayer = gameObject.layer;
        dashLayer = LayerMask.NameToLayer(dashLayerName);

        if (dashAction == null)
        {
            Debug.LogError("Crie a ação 'Dash' no mapa Player do InputSystem_Actions.");
        }

        if (trail != null)
        {
            trail.emitting = false;
        }
    }

    private void Update()
    {
        if (IsGrounded())
        {
            airDashesLeft = maxAirDashes;
        }

        if (dashAction != null && dashAction.WasPressedThisFrame() && CanDash())
        {
            StartCoroutine(DashRoutine(GetDashDirection()));
        }
    }

    private bool CanDash()
    {
        if (isDashing || Time.time < nextDashTime)
        {
            return false;
        }

        return IsGrounded() || airDashesLeft > 0;
    }

    private Vector2 GetDashDirection()
    {
        Vector2 input = moveAction.ReadValue<Vector2>();

        if (eightDirections && input.sqrMagnitude > 0.01f)
        {
            return input.normalized;
        }

        if (Mathf.Abs(input.x) > 0.01f)
        {
            return new Vector2(Mathf.Sign(input.x), 0f);
        }

        Transform facingSource = visual != null ? visual : transform;
        float facing = facingSource.localScale.x >= 0f ? 1f : -1f;
        return new Vector2(facing, 0f);
    }

    private IEnumerator DashRoutine(Vector2 direction)
    {
        isDashing = true;
        nextDashTime = Time.time + dashDuration + dashCooldown;

        if (!IsGrounded())
        {
            airDashesLeft--;
        }

        SetScriptsEnabled(false);
        float originalGravity = rb.gravityScale;
        rb.gravityScale = 0f;

        if (dashLayer >= 0)
        {
            gameObject.layer = dashLayer;
        }

        if (trail != null)
        {
            trail.emitting = true;
        }

        float endTime = Time.time + dashDuration;
        while (Time.time < endTime)
        {
            rb.linearVelocity = direction * dashSpeed;
            yield return new WaitForFixedUpdate();
        }

        rb.linearVelocity = direction * dashSpeed * endSpeedMultiplier;
        rb.gravityScale = originalGravity;
        gameObject.layer = originalLayer;

        if (trail != null)
        {
            trail.emitting = false;
        }

        SetScriptsEnabled(true);
        isDashing = false;
    }

    private void SetScriptsEnabled(bool value)
    {
        foreach (MonoBehaviour script in disableWhileDashing)
        {
            if (script != null)
            {
                script.enabled = value;
            }
        }
    }

    private bool IsGrounded()
    {
        if (groundCheck == null)
        {
            return false;
        }

        return Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer) != null;
    }
}

No Inspector: arraste o Rigidbody2D, o objeto visual (o mesmo que vira de lado no movimento), o TrailRenderer e, em Disable While Dashing, os seus scripts de movimento e pulo.

Duração com coroutine

A DashRoutine é uma coroutine porque o dash é uma sequência no tempo: começa, dura, termina. O laço while reaplica a velocidade a cada WaitForFixedUpdate, ou seja, a cada passo de física. Por que reaplicar em vez de definir uma vez? Se o personagem raspar numa quina, a física reduz a velocidade e o resto do dash sai mais curto. Reaplicando, o dash mantém a velocidade até o fim ou até bater de frente numa parede.

O endSpeedMultiplier decide como o dash termina. Com 0, o personagem para seco, o que é bom para dashes de precisão. Com 0,3, sobra um pouco de impulso, que o script de movimento recém-ligado desacelera, e a saída do dash fica mais natural.

Cooldown

nextDashTime = Time.time + dashDuration + dashCooldown marca o primeiro instante em que outro dash é aceito. Somar a duração garante que o cooldown começa a contar no fim do dash, não no começo.

O cooldown sozinho não impede o dash infinito no ar: com 0,4 segundo de espera, o jogador dá dash, espera, dá outro e atravessa um abismo inteiro. Por isso existe airDashesLeft. Ele recarrega só quando o personagem toca o chão, e cada dash no ar gasta um. Com maxAirDashes = 1, é o comportamento clássico de "um dash por pulo".

Dash, pulo e movimento já são três scripts disputando a mesma velocidade, e ainda faltam ataque, dano, knockback e animação de cada estado. Quando cada habilidade nova quebra uma antiga, o projeto pede uma estrutura (estados, prioridades, quem manda na velocidade) que tutorial isolado não monta. Organizar essas peças num jogo que chega ao fim é o que o curso de Unity faz em projeto, e é a etapa em que mais gente desiste sozinha.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Invencibilidade durante o dash

A invencibilidade mais limpa na física 2D da Unity é trocar a layer do personagem durante o dash:

  1. Edit > Project Settings > Tags and Layers: crie as layers Player, PlayerDash, Enemy e EnemyAttack (se ainda não existem). O personagem fica em Player.
  2. Edit > Project Settings > Physics 2D > Layer Collision Matrix: desmarque o cruzamento de PlayerDash com Enemy e com EnemyAttack. Mantenha PlayerDash colidindo com Ground, senão o dash atravessa o cenário.
  3. O script troca gameObject.layer para PlayerDash no início e volta no fim.

A matriz de colisão vale também para triggers. Se o dano do inimigo vem de um OnTriggerEnter2D, ele simplesmente não dispara enquanto o player está na layer de dash.

A alternativa é Physics2D.IgnoreLayerCollision(playerLayer, enemyLayer, true) no começo e false no fim. Funciona, mas a regra vale para todos os objetos daquelas layers: num jogo multiplayer local, o dash de um jogador deixaria os outros invencíveis. A troca de layer afeta só quem está dando dash.

Atenção a filhos: gameObject.layer muda só o objeto do script. Se a hurtbox do personagem é um filho com collider próprio, troque a layer dele também.

Efeito visual com TrailRenderer

O TrailRenderer desenha um rastro atrás de um objeto em movimento, e a propriedade emitting liga e desliga a geração de novos pontos sem apagar o rastro que já existe. É o que o script usa.

Configuração para um dash de pixel art:

  • Adicione Trail Renderer num filho do personagem, na altura do centro do corpo.
  • Time: 0.15. É quanto tempo cada ponto do rastro dura.
  • Width: uma curva que começa na largura do corpo (por exemplo 0,8) e termina em 0.
  • Color: gradiente da cor principal do personagem com alpha 1 para alpha 0.
  • Material: Sprites-Default serve para começar.
  • No Sorting Layer, coloque o rastro atrás do sprite do personagem.

Um rastro curto e um leve tremor de câmera no início do dash fazem mais pela sensação do que aumentar a velocidade. O post de game feel e juice tem outras técnicas que combinam com o dash: hit stop, partícula de poeira na saída, squash no sprite.

Erros comuns: dash infinito e atravessar parede

Dash infinito. Falta cooldown, ou o cooldown existe mas não há limite de dashes no ar. Use os dois, como no script.

Dash dura um frame. O script de movimento sobrescreve a velocidade. Coloque-o em Disable While Dashing.

Dash atravessa parede. Três causas possíveis:

  • O dash move o transform (transform.position += ...), teleportando o personagem. Use linearVelocity.
  • Collision Detection está em Discrete. Com 22 unidades por segundo e passo de física de 0,02 segundo, o personagem anda 0,44 unidade por passo, mais que a espessura de uma parede fina. Use Continuous.
  • A layer PlayerDash ficou sem colisão com Ground na matriz.

Dash para cima voa alto demais. Num dash de oito direções, a velocidade final para cima vira um super pulo. Reduza o endSpeedMultiplier ou zere a componente Y da velocidade ao terminar.

Personagem continua invencível depois do dash. A coroutine foi interrompida (o objeto foi desativado, ou StopAllCoroutines foi chamado) antes de restaurar a layer. Se o seu jogo desativa o player ao morrer, restaure a layer e a gravidade também no OnDisable.

Dash na direção errada parado. O script lê o lado do personagem pela escala do objeto visual. Se você vira o sprite com flipX, troque essa leitura por spriteRenderer.flipX.

Próximo passo

Com o dash funcionando, ajuste os números em pares: velocidade e duração definem a distância, cooldown e dashes no ar definem quanto o jogador pode abusar. Monte uma sala com um vão que só passa com dash e um inimigo que só se evita com a invencibilidade, e teste se os dois exigem a mecânica sem frustrar.

Para comparar com outra engine, o tutorial de dash no Godot 4 faz o mesmo sistema com Timers e mostra como transformar o dash num estado de uma state machine, o caminho natural quando o personagem acumula habilidades.

Perguntas frequentes

Como fazer um dash simples na Unity 2D?

Zere o gravityScale do Rigidbody2D, defina linearVelocity como direção vezes velocidade do dash, espere a duração numa coroutine e restaure a gravidade. Durante o dash, impeça que o script de movimento sobrescreva a velocidade.

Como deixar o personagem invencível durante o dash?

Troque temporariamente a layer do personagem para uma layer de dash que não colide com inimigos na Layer Collision Matrix, ou use Physics2D.IgnoreLayerCollision entre a layer do player e a dos inimigos. No fim do dash, volte a layer original.

Como colocar cooldown no dash na Unity?

Guarde o momento em que o dash pode ser usado de novo, com Time.time mais o tempo de cooldown, e só aceite o input quando Time.time passar desse valor. Para dash no ar, conte quantos dashes ainda restam e recarregue ao tocar o chão.

Por que meu dash atravessa paredes?

Normalmente porque o dash move o transform direto, teleportando o personagem. Mova pelo Rigidbody2D com linearVelocity e deixe Collision Detection em Continuous. Também confira se a layer de dash não ficou sem colisão com o cenário.