Voltar para o Blog
Quest Log

Como programar dash no Godot 4 (2D): cooldown, i-frames e ghost trail

Personagem 2D em pixel art executando um dash com rastro fantasma atrás dele

Tutorial completo de dash godot: cooldown com Timer, direção do input ou do facing, i-frames durante o dash, ghost trail e integração com state machine.

Como programar dash no Godot 4 (2D): cooldown, i-frames e ghost trail

Dash é daqueles mecanismos que parecem uma linha de código e viram uma tarde inteira. Aplicar velocidade na direção certa é fácil; o trabalho de verdade está nos detalhes: o cooldown que impede spam, a decisão entre direção do input ou do facing, a invencibilidade que faz o dash servir de esquiva, e o feedback visual que vende o movimento. Esse tutorial de dash godot cobre tudo isso, em GDScript do Godot 4.x, num CharacterBody2D que você consegue colar no seu projeto hoje.

Vou montar a versão direta primeiro, com flags e um Timer, porque ela funciona e é o que a maioria dos jogos pequenos precisa. No final mostro como o mesmo dash vira um estado dentro de uma state machine, que é o caminho quando o personagem começa a acumular habilidades. Assumo que você já tem um movimento básico funcionando; se não tiver, comece pelo guia de movimento de personagem em plataforma 2D e volte aqui.

A estrutura do dash godot: três perguntas antes do código

Todo dash responde três perguntas, e vale decidir antes de digitar qualquer coisa:

  1. Quanto dura? Dash bom é curto. Algo entre 0.15 e 0.25 segundo já dá sensação de explosão sem tirar o controle do jogador por tempo demais.
  2. Pra onde vai? Na direção que o jogador está segurando (estilo twin-stick e jogos top-down) ou na direção que o personagem está olhando (estilo plataforma clássico). São códigos diferentes e a escolha muda o feeling do jogo.
  3. O que ele interrompe? Durante o dash, a gravidade age? O jogador pode virar? Pode atacar? A resposta mais comum é: nada. Dash trava a direção e suspende a gravidade enquanto durar.

Com isso decidido, a cena fica assim:

Player (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D
├── DashDuration (Timer)
└── DashCooldown (Timer)

Os dois timers são one_shot ligado e autostart desligado. Um conta quanto o dash dura, o outro conta quanto tempo até poder dar outro. Se Timer ainda é nebuloso pra você, eu destrinchei o node inteiro no artigo sobre Timer e cooldown no Godot.

O dash básico: velocidade, duração e cooldown

A versão mínima funcional, num plataforma 2D com facing:

extends CharacterBody2D

const SPEED = 220.0
const DASH_SPEED = 600.0
const GRAVITY = 980.0

@onready var dash_duration = $DashDuration
@onready var dash_cooldown = $DashCooldown
@onready var sprite = $Sprite2D

var is_dashing = false
var facing = 1  # 1 = direita, -1 = esquerda

func _ready():
    dash_duration.wait_time = 0.2
    dash_cooldown.wait_time = 0.8
    dash_duration.timeout.connect(_on_dash_duration_timeout)

func _physics_process(delta):
    if is_dashing:
        # Direção travada, gravidade suspensa: só anda e segue.
        move_and_slide()
        return

    var input_dir = Input.get_axis("move_left", "move_right")
    if input_dir != 0:
        facing = sign(input_dir)
        sprite.flip_h = facing < 0

    velocity.x = input_dir * SPEED
    velocity.y += GRAVITY * delta

    if Input.is_action_just_pressed("dash") and dash_cooldown.is_stopped():
        start_dash()

    move_and_slide()

func start_dash():
    is_dashing = true
    velocity = Vector2(facing * DASH_SPEED, 0.0)
    dash_duration.start()
    dash_cooldown.start()

func _on_dash_duration_timeout():
    is_dashing = false
    # Corta a velocidade pra não sair deslizando depois do dash.
    velocity.x = facing * SPEED

Os pontos que importam nesse código:

  • is_dashing curto-circuita o _physics_process. Enquanto dura o dash, nada de gravidade, nada de input de movimento, nada de virar. A velocidade foi definida em start_dash() e fica congelada até o timer disparar.
  • velocity.y = 0 no início do dash. Está embutido no Vector2(facing * DASH_SPEED, 0.0). Dash horizontal que mantém a queda vira uma diagonal estranha; zerar o eixo Y dá aquele dash reto que segura o personagem no ar.
  • O corte de velocidade no timeout. Sem ele, o personagem termina o dash a 600 de velocidade e o motor de física leva alguns frames pra normalizar. Cortar pra velocidade de corrida na mesma direção dá um final limpo.
  • dash_cooldown.is_stopped() é a única checagem de cooldown. Nada de booleana paralela. O Timer parado significa dash disponível, rodando significa em recarga.

Direção do input ou do facing

O código acima usa o facing: o dash vai pra onde o personagem olha, mesmo que o jogador esteja parado. É o comportamento certo pra plataforma, onde o dash quase sempre é horizontal.

Em jogo top-down, o jogador espera dar dash na direção que está segurando, incluindo diagonais. A mudança é trocar a fonte da direção:

func start_dash():
    var input_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    var dash_dir: Vector2
    if input_vector != Vector2.ZERO:
        dash_dir = input_vector.normalized()
    else:
        # Sem input, cai no facing como plano B.
        dash_dir = Vector2(facing, 0.0)
    is_dashing = true
    velocity = dash_dir * DASH_SPEED
    dash_duration.start()
    dash_cooldown.start()

O normalized() ali não é detalhe: sem ele, dash na diagonal sai mais rápido que dash reto, porque o vetor (1, 1) tem comprimento maior que 1. É o mesmo bug clássico do movimento diagonal, só que mais visível porque dash é rápido.

O fallback pro facing também é decisão de design, não só de código. Dash com o analógico no neutro precisa fazer alguma coisa, e "vai pra onde estou olhando" é a resposta que ninguém questiona. A alternativa, ignorar o input e não dar dash, parece bug pra quem joga.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

I-frames: o dash como esquiva

Se o seu jogo tem combate, dash sem invencibilidade é meio dash. Os i-frames (frames de invencibilidade) são o que transforma o movimento em ferramenta defensiva: o jogador atravessa o ataque em vez de só correr dele.

No Godot, o jeito limpo de fazer isso é desligar a camada de colisão de hurtbox durante o dash. Supondo que o dano chega por uma Area2D chamada Hurtbox no player:

@onready var hurtbox = $Hurtbox

func start_dash():
    is_dashing = true
    velocity = Vector2(facing * DASH_SPEED, 0.0)
    hurtbox.set_deferred("monitorable", false)
    dash_duration.start()
    dash_cooldown.start()

func _on_dash_duration_timeout():
    is_dashing = false
    velocity.x = facing * SPEED
    hurtbox.set_deferred("monitorable", true)

O set_deferred é obrigatório aqui: mexer em propriedade de colisão no meio do passo de física pode disparar erro ou comportamento inconsistente, então o Godot pede pra adiar a mudança pro fim do frame.

Uma alternativa, se o seu dano é checado por código (um método take_damage no player), é simplesmente ignorar o dano enquanto is_dashing for verdadeiro:

func take_damage(amount: int):
    if is_dashing:
        return
    health -= amount

Mais simples, e funciona bem enquanto todo dano passa por essa função. A versão de camada de colisão é mais robusta porque vale pra qualquer fonte de dano, inclusive as que você adicionar daqui a três meses e esquecer de checar a flag.

Cuidado com a duração: i-frames que cobrem o dash inteiro mais um respiro no final deixam a esquiva generosa demais. Em geral, invencibilidade durante o dash e vulnerabilidade imediata no fim já dá o equilíbrio certo. Se quiser apertar, ligue os i-frames só nos primeiros dois terços da duração.

Ghost trail: o rastro que vende o dash

O dash funciona sem feedback visual, mas parece teleporte engasgado. O ghost trail, aquelas cópias semitransparentes que ficam pra trás, é barato de fazer e muda completamente a leitura do movimento.

A ideia: durante o dash, a cada intervalo curto, criar um Sprite2D que copia a textura e a posição do personagem e some com um fade. Um Timer em loop dentro do player resolve o intervalo, mas dá pra fazer ainda mais direto com acumulador de delta:

const GHOST_INTERVAL = 0.04

var ghost_timer = 0.0

func _physics_process(delta):
    if is_dashing:
        ghost_timer -= delta
        if ghost_timer <= 0.0:
            spawn_ghost()
            ghost_timer = GHOST_INTERVAL
        move_and_slide()
        return
    # ... resto do movimento

E a função que cria cada fantasma:

func spawn_ghost():
    var ghost = Sprite2D.new()
    ghost.texture = sprite.texture
    ghost.flip_h = sprite.flip_h
    ghost.global_position = sprite.global_position
    ghost.modulate = Color(1.0, 1.0, 1.0, 0.5)
    get_tree().current_scene.add_child(ghost)

    var tween = ghost.create_tween()
    tween.tween_property(ghost, "modulate:a", 0.0, 0.3)
    tween.tween_callback(ghost.queue_free)

O Tween anima a transparência até zero em 0.3 segundo e depois destrói o fantasma. Como cada ghost cuida do próprio ciclo de vida, o player não precisa rastrear nada. Se o seu sprite é animado por AnimatedSprite2D, troque sprite.texture por sprite.sprite_frames.get_frame_texture(sprite.animation, sprite.frame) pra copiar o frame atual.

Dois ajustes finos que valem o teste: tingir o ghost com a cor do jogo (ghost.modulate = Color(0.4, 0.7, 1.0, 0.5) pra um rastro azulado) e espaçar mais os fantasmas em dash mais longo. Com intervalo de 0.04 e duração de 0.2, saem uns cinco fantasmas por dash, que é o suficiente.

Dash dentro da state machine

A flag is_dashing com return no topo do _physics_process aguenta bem até o personagem ter três ou quatro comportamentos. Quando entram wall jump, ataque, knockback e escada, as flags começam a brigar entre si e todo bug vira "por que ele dá dash no meio do ataque?".

A resposta é promover o dash a um estado. Se você já usa o padrão que mostrei no artigo de state machine no Godot, o dash vira uma classe de estado com entrada, atualização física e saída bem definidas:

extends State
class_name DashState

const DASH_SPEED = 600.0
const DASH_TIME = 0.2

var time_left = 0.0

func enter():
    time_left = DASH_TIME
    player.velocity = Vector2(player.facing * DASH_SPEED, 0.0)
    player.hurtbox.set_deferred("monitorable", false)
    player.dash_cooldown.start()

func physics_update(delta):
    time_left -= delta
    player.move_and_slide()
    if time_left <= 0.0:
        if player.is_on_floor():
            transition_to("Run")
        else:
            transition_to("Fall")

func exit():
    player.hurtbox.set_deferred("monitorable", true)
    player.velocity.x = player.facing * player.SPEED

Repare que aqui eu troquei o Timer node pela contagem manual com delta. Dentro de um estado isso faz sentido: a duração é parte da lógica do estado e morre junto com ele, sem sinal pra conectar nem node pra consultar. O cooldown continua sendo Timer node no player, porque ele precisa sobreviver à troca de estados.

O ganho real da versão com estado é que as regras de interrupção ficam explícitas. Quem decide se pode entrar no dash é a transição (só de Run, Fall e Jump, por exemplo, nunca de Hurt), e a saída sempre passa pelo exit(), então os i-frames nunca ficam ligados por acidente. Aquele bug de "ficou invencível pra sempre" simplesmente deixa de existir, porque não há caminho de saída que pule a limpeza.

Se o seu jogo tem dash aéreo, o estado também é o lugar natural pra regra de uma carga por pulo: um contador no player que zera ao tocar o chão, no mesmo espírito do contador de pulos que aparece no artigo de coyote time e sistema de pulo.

Fechando

Dash no Godot 4 é uma receita de camadas: velocidade travada por um tempo curto, dois timers one shot pra duração e cooldown, direção vinda do input ou do facing conforme o gênero, hurtbox desligada com set_deferred pros i-frames, e ghosts com Tween pro rastro. Cada camada é pequena, e juntas elas fazem o movimento que o jogador descreve como "gostoso" sem saber explicar por quê.

Minha sugestão de ordem: faça o dash básico funcionar primeiro, sinta a duração e a velocidade no controle, e só então adicione i-frames e trail. Ajustar DASH_SPEED e wait_time com o jogo rodando vale mais que qualquer número que eu te der aqui. E quando o personagem passar de três habilidades, migre pra state machine antes que as flags cobrem o pedágio.