Power-ups e Coletaveis Temporarios no Godot 4

Aprenda a montar um power up godot com Area2D, aplicar efeito temporario controlado por Timer, empilhar ou renovar a duracao e avisar a UI sem repetir codigo.
Quase todo jogo tem aquele momento em que o jogador pega um item e fica mais rapido, ganha um escudo ou dispara mais forte por alguns segundos. Montar um power up godot que funcione bem envolve tres partes que costumam ficar baguncadas: detectar a coleta, aplicar um efeito que se reverte sozinho e contar isso para o resto do jogo (player e interface). Neste post vamos construir essas tres partes passo a passo, com codigo real de Godot 4, e deixar a estrutura pronta para criar varios tipos de power-up sem copiar e colar a mesma logica.
Power-ups e Coletaveis Temporarios no Godot 4
A ideia central e separar responsabilidades. O coletavel cuida apenas de detectar o jogador e mandar o efeito. O player cuida de aplicar o efeito no seu proprio estado. E um pequeno gerenciador cuida do tempo de duracao e de avisar a UI. Quando essas camadas ficam separadas, adicionar um power-up novo vira questao de criar uma cena e um recurso, sem mexer no resto.
O coletavel com Area2D
O coletavel e uma cena simples: um Area2D com um Sprite2D e um CollisionShape2D. A Area2D so precisa detectar o corpo do jogador e disparar a coleta. Se voce ainda tem duvida sobre quando usar Area2D em vez de corpos fisicos, vale ler Area2D vs Body no Godot, porque a escolha muda como a deteccao se comporta.
Conecte o sinal body_entered da Area2D ao script. A verificacao por grupo evita que inimigos ou projeteis acionem o item por engano.
extends Area2D
# Tipo do power-up, definido por um recurso (ver mais abaixo).
@export var power_up: PowerUpData
func _on_body_entered(body: Node2D) -> void:
if not body.is_in_group("player"):
return
if body.has_method("apply_power_up"):
body.apply_power_up(power_up)
queue_free()
Repare que o coletavel nao sabe o que o efeito faz. Ele apenas entrega o recurso power_up para o jogador e some. Toda a regra de "o que acontece" mora em outro lugar, e e isso que mantem a cena reutilizavel.
Descrevendo cada power-up com um Resource
Em vez de criar um script diferente para cada item, descrevemos os power-ups com um Resource. Assim, velocidade, escudo e tiro rapido sao todos o mesmo coletavel, mudando so os dados.
class_name PowerUpData
extends Resource
enum Tipo { VELOCIDADE, ESCUDO, TIRO_RAPIDO }
@export var tipo: Tipo = Tipo.VELOCIDADE
@export var duracao: float = 5.0
@export var valor: float = 1.5
@export var nome_exibicao: String = "Power-up"
Com esse recurso, voce cria arquivos .tres no editor: um para "Botas de Velocidade" com tipo = VELOCIDADE e valor = 1.5, outro para "Escudo" com tipo = ESCUDO e duracao = 8.0. Cada coletavel na fase aponta para o recurso que quiser, sem nenhuma linha de codigo nova.
Aplicando o efeito temporario com Timer
Agora a parte do jogador. Ele recebe o recurso e precisa de duas coisas: aplicar o efeito imediatamente e agendar a reversao quando a duracao acabar. Para isso usamos um Timer dedicado por efeito ativo. Criar o Timer em tempo de execucao deixa o sistema flexivel, porque varios efeitos podem rodar ao mesmo tempo, cada um com seu proprio contador.
extends CharacterBody2D
@export var velocidade_base: float = 200.0
var velocidade_atual: float = velocidade_base
var escudo_ativo: bool = false
# Guarda os timers ativos por tipo, para evitar duplicar.
var efeitos_ativos: Dictionary = {}
func apply_power_up(dados: PowerUpData) -> void:
_ativar_efeito(dados)
_agendar_reversao(dados)
A funcao _ativar_efeito decide o que muda no estado do jogador conforme o tipo. Como tudo passa por um match, adicionar um tipo novo e so incluir mais um caso.
func _ativar_efeito(dados: PowerUpData) -> void:
match dados.tipo:
PowerUpData.Tipo.VELOCIDADE:
velocidade_atual = velocidade_base * dados.valor
PowerUpData.Tipo.ESCUDO:
escudo_ativo = true
PowerUpData.Tipo.TIRO_RAPIDO:
# Aqui voce reduziria o cooldown do tiro, por exemplo.
pass
A reversao usa o mesmo match, mas voltando ao estado original. Manter a ativacao e a reversao lado a lado ajuda a nao esquecer de desfazer nada.
func _reverter_efeito(dados: PowerUpData) -> void:
match dados.tipo:
PowerUpData.Tipo.VELOCIDADE:
velocidade_atual = velocidade_base
PowerUpData.Tipo.ESCUDO:
escudo_ativo = false
PowerUpData.Tipo.TIRO_RAPIDO:
pass
Empilhar ou renovar a duracao
Aqui aparece a decisao de design mais importante. Se o jogador pega um power-up de velocidade enquanto ja esta acelerado, o que acontece? Existem duas respostas comuns: renovar (reseta o relogio para a duracao cheia) ou empilhar (soma o tempo restante). A maioria dos jogos usa renovar, porque e mais previsivel para o jogador. Vamos implementar a renovacao guardando um Timer por tipo no dicionario efeitos_ativos.
func _agendar_reversao(dados: PowerUpData) -> void:
var chave: int = dados.tipo
# Se ja existe um timer para esse tipo, apenas renova a duracao.
if efeitos_ativos.has(chave):
var timer_existente: Timer = efeitos_ativos[chave]
timer_existente.start(dados.duracao)
return
var timer := Timer.new()
timer.one_shot = true
add_child(timer)
timer.timeout.connect(_on_efeito_expirou.bind(dados, timer))
timer.start(dados.duracao)
efeitos_ativos[chave] = timer
Quando o Timer dispara o timeout, revertemos o efeito, limpamos o registro e liberamos o Timer para nao acumular nos na arvore.
func _on_efeito_expirou(dados: PowerUpData, timer: Timer) -> void:
_reverter_efeito(dados)
efeitos_ativos.erase(dados.tipo)
timer.queue_free()
Se em vez de renovar voce quisesse empilhar, bastaria trocar o start(dados.duracao) por algo como timer_existente.start(timer_existente.time_left + dados.duracao). A vantagem de centralizar isso em uma unica funcao e que a regra fica em um lugar so, valendo para todos os tipos. Se voce ja trabalhou com Timer e cooldown no Godot, vai notar que o padrao de one_shot e start() com tempo customizado e o mesmo.
Avisar a UI sem acoplar
A interface precisa saber quando um efeito comeca e quando termina, para mostrar um icone com contagem regressiva, por exemplo. Em vez de o jogador acessar a UI diretamente, ele emite sinais. Quem quiser ouvir, se conecta. Isso mantem o player ignorante sobre a existencia da interface, o que deixa o codigo mais facil de testar e de mudar depois.
signal power_up_iniciado(dados: PowerUpData)
signal power_up_terminado(dados: PowerUpData)
Agora basta emitir nos dois momentos certos. No comeco, dentro de apply_power_up, e no fim, quando o efeito expira de fato (e nao quando ele e apenas renovado).
func apply_power_up(dados: PowerUpData) -> void:
var renovacao := efeitos_ativos.has(dados.tipo)
_ativar_efeito(dados)
_agendar_reversao(dados)
if not renovacao:
power_up_iniciado.emit(dados)
func _on_efeito_expirou(dados: PowerUpData, timer: Timer) -> void:
_reverter_efeito(dados)
efeitos_ativos.erase(dados.tipo)
timer.queue_free()
power_up_terminado.emit(dados)
Do lado da UI, um script ouve esses sinais e cria ou remove um icone. Para a contagem regressiva, a UI pode guardar a referencia do Timer ou simplesmente recriar a duracao a partir de dados.duracao. Um caminho enxuto e a UI pedir o tempo restante ao player.
extends Control
func _ready() -> void:
var player := get_tree().get_first_node_in_group("player")
player.power_up_iniciado.connect(_mostrar_icone)
player.power_up_terminado.connect(_remover_icone)
func _mostrar_icone(dados: PowerUpData) -> void:
print("Power-up ativo: ", dados.nome_exibicao)
# Instancie o icone correspondente ao dados.tipo aqui.
func _remover_icone(dados: PowerUpData) -> void:
print("Power-up terminou: ", dados.nome_exibicao)
# Remova o icone correspondente aqui.
Movimento que respeita o power-up
Para o efeito de velocidade ter sentido, o movimento do jogador precisa usar velocidade_atual em vez de um valor fixo. O _physics_process fica direto, lendo a velocidade que o power-up modificou.
func _physics_process(_delta: float) -> void:
var direcao := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = direcao * velocidade_atual
move_and_slide()
Com o escudo, a mesma logica se aplica ao seu sistema de dano: antes de tirar vida, verifique escudo_ativo. Se voce ja tem um controle de vida e dano montado, integrar e simples, e o nosso guia de sistema de vida e dano no Godot mostra um bom ponto de encaixe para essa checagem.
Por que essa estrutura escala
O ganho real aparece quando voce precisa do quinto power-up. Nada de novo script de coletavel, nada de novo Timer escrito a mao, nada de logica de UI duplicada. Voce cria um .tres com os dados, adiciona um caso no _ativar_efeito e no _reverter_efeito, e (se for um efeito visual diferente) um icone na UI. As tres camadas continuam isoladas: o coletavel detecta, o player aplica, os sinais avisam.
Tres pontos merecem atencao no seu projeto. Primeiro, decida cedo entre renovar e empilhar, porque isso afeta o equilibrio do jogo. Segundo, sempre limpe os Timer criados em tempo de execucao com queue_free(), senao eles ficam pendurados na arvore. Terceiro, prefira sinais a chamadas diretas entre player e UI, porque amanha voce pode querer mostrar o mesmo power-up no minimapa, no HUD e em um efeito de tela sem reescrever a logica.
Com essa base, da para evoluir para coisas mais ricas: power-ups que se combinam, itens que so funcionam em certas fases, ou efeitos que pulsam em intervalos. A fundacao continua a mesma, e e por isso que vale gastar um tempo deixando essas tres camadas bem separadas desde o comeco.


