Voltar para o Blog
Quest Log

Timer no Godot: Cooldown de Tiro, Spawn Ritmado e Eventos por Tempo

Personagem de jogo atirando com um anel de cooldown e relógios flutuando ao redor da arma

Aprenda a usar Timer no Godot 4: cooldown de tiro, spawn de inimigos, one shot vs loop, SceneTreeTimer e quando contar tempo na mão com delta.

Timer no Godot: Cooldown de Tiro, Spawn Ritmado e Eventos por Tempo

Quase tudo num jogo acontece "daqui a X segundos": o tiro que só pode sair de novo depois de meio segundo, o inimigo que nasce a cada dois, o power-up que acaba em dez. E o jeito mais comum de errar isso é espalhar contador solto pelo código até ninguém mais saber quem conta o quê. O Timer no Godot existe exatamente pra isso, e ele resolve 90% dos casos com zero matemática.

Só que existem três jeitos de contar tempo no Godot 4: o node Timer, o SceneTreeTimer (aquele do await) e a contagem manual com delta. Cada um tem um lugar certo, e usar o errado é o que gera bug de cooldown que "às vezes falha". Esse tutorial mostra os três, com os casos clássicos: cooldown de tiro, spawn ritmado e a diferença entre one shot e loop. Todo código é GDScript do Godot 4.x.

O node Timer: como funciona

O Timer é um node como qualquer outro. Você adiciona na cena, configura o tempo de espera e conecta o sinal timeout, que dispara quando a contagem chega a zero. As três propriedades que importam:

  • wait_time: quanto tempo ele conta, em segundos.
  • one_shot: se ligado, conta uma vez e para. Se desligado, reinicia sozinho e dispara timeout em loop.
  • autostart: se ligado, começa a contar assim que entra na cena. Útil pra spawner; inútil pra cooldown.

O uso mínimo:

extends Node2D

@onready var timer = $Timer

func _ready():
    timer.wait_time = 2.0
    timer.one_shot = true
    timer.timeout.connect(_on_timer_timeout)
    timer.start()

func _on_timer_timeout():
    print("2 segundos se passaram")

Você também pode configurar tudo pelo Inspector e conectar o sinal pela aba Node do editor, que é o caminho que eu recomendo quando o timer faz parte da estrutura da cena. Código pra timer dinâmico, editor pra timer fixo.

Dois métodos e uma propriedade completam o kit: stop() cancela a contagem, start() aceita um tempo opcional que sobrescreve o wait_time daquela rodada (timer.start(0.5)), e time_left diz quanto falta. Esse último é ouro pra UI: é dele que sai aquele aro de recarga em volta do ícone da habilidade.

One shot vs loop: a decisão que define o uso

Essa única checkbox muda completamente o papel do Timer, e é o coração do ângulo desse artigo.

One shot ligado transforma o Timer num "lembrete único": conta, dispara timeout uma vez, fica parado até alguém chamar start() de novo. É o modo de cooldown, de duração de buff, de atraso antes de uma animação. O estado do jogo é quem decide quando rearmar.

One shot desligado transforma o Timer num metrônomo: dispara, reinicia sozinho, dispara de novo, pra sempre, até você chamar stop(). É o modo de spawn ritmado, de dano por segundo de veneno, de tick de regeneração.

Na prática a pergunta é só uma: o próximo disparo depende de uma ação do jogo (one shot) ou do relógio (loop)? Cooldown de tiro depende do jogador atirar, então é one shot. Onda de inimigos depende só do tempo passar, então é loop.

Cooldown de tiro com Timer

O padrão clássico, e o motivo de metade das buscas por timer godot: limitar a cadência de uma arma. A lógica é simples, o tiro só sai se o timer de cooldown estiver parado.

Estrutura da cena:

Player (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D
└── ShootCooldown (Timer)

No Inspector do ShootCooldown: wait_time = 0.4, one_shot ligado, autostart desligado. O script:

extends CharacterBody2D

@onready var shoot_cooldown = $ShootCooldown

const BULLET_SCENE = preload("res://bullet.tscn")

func _process(delta):
    if Input.is_action_pressed("shoot") and shoot_cooldown.is_stopped():
        shoot()
        shoot_cooldown.start()

func shoot():
    var bullet = BULLET_SCENE.instantiate()
    bullet.global_position = global_position
    get_tree().current_scene.add_child(bullet)

O detalhe que carrega tudo é o is_stopped(). Timer one shot parado significa "cooldown vencido, pode atirar". Timer rodando significa "ainda em recarga". Não tem variável booleana, não tem contador, não tem como dessincronizar: o próprio Timer é a fonte da verdade.

Repare que usei is_action_pressed (segurar o botão atira em cadência) em vez de is_action_just_pressed (um tiro por clique). Pra metralhadora, o primeiro; pra pistola, o segundo. E se a arma tem upgrade de cadência, é uma linha: shoot_cooldown.wait_time = 0.2.

Pra mostrar a recarga na tela, uma TextureProgressBar resolve:

func _process(delta):
    if shoot_cooldown.is_stopped():
        cooldown_bar.value = 1.0
    else:
        # time_left vai de wait_time até 0; invertemos pra barra encher.
        cooldown_bar.value = 1.0 - (shoot_cooldown.time_left / shoot_cooldown.wait_time)
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

Spawn ritmado de inimigos

Agora o modo loop. Um spawner que cria um inimigo a cada intervalo é um Timer com one_shot desligado e autostart ligado:

extends Node2D

const ENEMY_SCENE = preload("res://enemy.tscn")

@onready var spawn_timer = $SpawnTimer

func _ready():
    spawn_timer.wait_time = 2.0
    spawn_timer.timeout.connect(_on_spawn_timer_timeout)
    spawn_timer.start()

func _on_spawn_timer_timeout():
    var enemy = ENEMY_SCENE.instantiate()
    enemy.global_position = global_position
    get_tree().current_scene.add_child(enemy)

A cada 2 segundos nasce um inimigo, sem nenhum código de contagem seu. E como o intervalo é uma propriedade viva, escalar a dificuldade é só apertar o relógio:

func _on_spawn_timer_timeout():
    spawn_enemy()
    # Cada spawn encurta o intervalo em 5%, com piso de 0.5s.
    spawn_timer.wait_time = max(spawn_timer.wait_time * 0.95, 0.5)

Um detalhe de comportamento: mudar wait_time não afeta a contagem que já está rodando, vale a partir do próximo ciclo. Se quiser aplicar na hora, chame spawn_timer.start() de novo, que reinicia com o valor novo.

Pra variar o ritmo e não parecer máquina, sorteie o intervalo a cada ciclo:

func _on_spawn_timer_timeout():
    spawn_enemy()
    spawn_timer.wait_time = randf_range(1.5, 3.0)

SceneTreeTimer: o timer descartável do await

Nem todo atraso merece um node na cena. Pra "espera 0.5 segundo e segue", o Godot tem o create_timer, que cria um timer descartável direto na SceneTree e combina com await:

func morrer():
    sprite.play("death")
    await get_tree().create_timer(0.8).timeout
    queue_free()

A função pausa ali, espera 0.8 segundo e continua. O timer se destrói sozinho depois do disparo. É perfeito pra sequência: piscar invencibilidade, atraso entre falas, knockback que trava o controle por um instante.

As regras de bolso pra escolher entre os dois:

  • Timer node quando você precisa consultar (time_left, is_stopped()), cancelar (stop()) ou repetir (loop). Cooldown e spawner são Timer node.
  • SceneTreeTimer quando é um atraso de uma vez, disparar e esquecer. Você não consegue dar stop() nele de jeito limpo.

E o cuidado clássico do await: se o node for destruído enquanto a função espera, o código depois do await ainda tenta rodar sobre um objeto morto. Em código que pode sobreviver à morte do dono, proteja com is_instance_valid() ou cheque is_inside_tree() antes de mexer na cena.

Outro comportamento que pega gente de surpresa: por padrão o SceneTreeTimer continua contando com o jogo pausado, porque o segundo parâmetro (process_always) nasce como true. Pra ele respeitar a pausa: get_tree().create_timer(0.8, false). O Timer node, por outro lado, respeita a pausa conforme o process_mode do node, como tudo na cena.

Quando contar na mão com delta

Existe um terceiro jeito, sem node nenhum: acumular delta numa variável. Parece gambiarra, mas é a ferramenta certa quando a contagem está amarrada na lógica de movimento, frame a frame:

var dash_cooldown = 0.0

func _physics_process(delta):
    dash_cooldown = max(dash_cooldown - delta, 0.0)

    if Input.is_action_just_pressed("dash") and dash_cooldown == 0.0:
        executar_dash()
        dash_cooldown = 1.5

Eu uso contagem manual quando o tempo interage com a física no mesmo passo (coyote time e jump buffer são exatamente isso), quando são muitos contadores pequenos num node só, ou dentro de uma state machine onde o tempo é parte do estado. Pra todo o resto, Timer node, que é mais legível e dá sinal de graça.

Uma armadilha relacionada: o Timer node conta no processamento idle por padrão. Se um cooldown precisa estar em sincronia exata com o passo de física, mude o process_callback dele pra TIMER_PROCESS_PHYSICS no Inspector. Pra cooldown de tiro comum a diferença é imperceptível, mas em lógica competitiva de frame exato isso importa.

Erros que eu vejo toda semana

Booleana de cooldown duplicando o Timer. Manter var pode_atirar = true ao lado de um Timer é pedir dessincronização. O is_stopped() já é o estado; use ele.

Timer one shot que "não funciona de novo". Funciona, mas one shot não rearma sozinho. Faltou chamar start() depois do uso. Se a intenção era repetir, a checkbox certa era loop.

await dentro de _process. O _process roda todo frame; cada frame inicia uma espera nova e você acaba com dezenas de continuações pendentes. await de tempo vive em funções de evento (tiro, morte, diálogo), não no loop.

Spawner que acelera até quebrar. Se o intervalo encurta a cada ciclo, ponha um piso com max(). Sem ele, o wait_time despenca e a tela vira uma parede de inimigos.

Esquecer que start() reinicia. Chamar start() num timer rodando zera a contagem. Às vezes é o que você quer (renovar duração de buff ao pegar outro power-up), às vezes é o bug (cooldown que nunca vence porque algo chama start() todo frame).

Fechando

Timer no Godot se resume a três ferramentas e um critério. Timer node one shot pra cooldown e duração, Timer node em loop pra ritmo constante, SceneTreeTimer com await pra atraso descartável, e delta na mão quando o tempo é parte da lógica de física. O critério: quem decide o próximo disparo, o relógio ou o estado do jogo?

Pra fixar, monta uma cena pequena com os três casos juntos: um player com cooldown de tiro, um spawner que acelera com piso, e uma morte com atraso via await. São uns vinte minutos de trabalho e cobrem praticamente todo uso de tempo que um jogo 2D pede. Depois disso, ler time_left pra desenhar uma barra de recarga é detalhe.