Voltar para o Blog
Quest Log

Power-ups e Coletaveis Temporarios no Godot 4

Personagem 2D coletando um item de escudo brilhante em uma cena de plataforma no Godot

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
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

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.