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.

Ferramenta:Godot

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: float = 220.0
const DASH_SPEED: float = 600.0
const GRAVITY: float = 980.0

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

var is_dashing: bool = false
var facing: int = 1  # 1 = direita, -1 = esquerda

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

func _physics_process(delta: float) -> void:
    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() -> void:
    is_dashing = true
    velocity = Vector2(facing * DASH_SPEED, 0.0)
    dash_duration.start()
    dash_cooldown.start()

func _on_dash_duration_timeout() -> void:
    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() -> void:
    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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot 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 Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 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() -> void:
    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() -> void:
    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) -> void:
    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. E se em vez de cópias você quiser uma fita contínua atrás do personagem, o trail com Line2D é o primo direto dessa técnica.

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: float = 0.04

var ghost_timer: float = 0.0

func _physics_process(delta: float) -> void:
    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() -> void:
    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: float = 600.0
const DASH_TIME: float = 0.2

var time_left: float = 0.0

func enter() -> void:
    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: float) -> void:
    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() -> void:
    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. E se você quer somar mais uma habilidade de movimento no mesmo CharacterBody2D, agachar e deslizar (crouch) segue exatamente essa mesma lógica de estado, e o mesmo vale pro gancho grappling hook 2D quando o personagem ganha mobilidade vertical. Já se a ideia é limitar o dash por recurso em vez de cooldown puro, um sistema de stamina no Godot faz corrida, esquiva e ataque gastarem a mesma barra de energia.

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.