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.

Ferramenta:Godot

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. Se voce quer que o item voe ate o jogador em vez de exigir o toque exato, da pra somar um ima de coletaveis por cima dessa mesma Area2D.

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

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.

Avisar o jogador que o efeito vai acabar

Um power-up temporario que some de surpresa frustra. Aquele escudo que acaba bem na hora do hit, sem aviso, parece injusto. O padrao que quase todo jogo bom usa e piscar o icone (ou o proprio personagem) nos ultimos segundos, dando ao jogador a chance de agir. Como cada efeito ja tem um Timer dedicado no dicionario efeitos_ativos, a informacao que precisamos (time_left) esta a um passo de distancia.

A UI pode ler o tempo restante todo frame e decidir quando comecar a piscar. Repare que time_left ja e um float em segundos, entao a checagem e direta:

extends Control

@export var tempo_de_aviso: float = 2.0

var player: Node2D

func _ready() -> void:
    player = get_tree().get_first_node_in_group("player")
    player.power_up_iniciado.connect(_mostrar_icone)
    player.power_up_terminado.connect(_remover_icone)

func _process(_delta: float) -> void:
    for chave in player.efeitos_ativos:
        var timer: Timer = player.efeitos_ativos[chave]
        if timer.time_left <= tempo_de_aviso:
            _piscar_icone(chave, timer.time_left)

func _piscar_icone(chave: int, restante: float) -> void:
    # Pisca mais rapido conforme o tempo acaba.
    var visivel: bool = fmod(restante, 0.4) > 0.2
    # Ajuste a visibilidade do icone correspondente a 'chave' aqui.
    print("Piscando icone %d, restante: %.1f, visivel: %s" % [chave, restante, visivel])

O detalhe de design: comece a piscar com folga suficiente pro jogador reagir (dois segundos costumam bastar) e acelere o pisca conforme chega perto de zero, porque a aceleracao comunica urgencia sem precisar de texto. Se o power-up for descrito por um Resource, da pra guardar o tempo_de_aviso dentro do proprio .tres, deixando cada efeito com seu ritmo de alerta. Esse e mais um motivo pra dominar Resources customizados no Godot: eles transformam ajustes como esse em dado editavel, nao em numero perdido no codigo.

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.