Ima de itens: atracao de coletaveis no Godot 4

Aprenda a criar um ima de itens godot que atrai moedas e loot ate o player com aceleracao progressiva. Tutorial pratico de GDScript em Godot 4.
Poucos detalhes deixam a coleta de itens tao gostosa quanto ver as moedas voarem ate o personagem. Aquele efeito de varrer o chao sem precisar passar exatamente por cima de cada peca de loot e o que chamamos de ima de itens, e nesse tutorial voce vai montar um ima de itens godot do zero, com raio ajustavel e aceleracao que cresce conforme o coletavel se aproxima. A ideia e simples de implementar e rende um ganho enorme de game feel.
Ima de itens: atracao de coletaveis no Godot 4
A mecanica tem duas partes que conversam entre si. De um lado, o player carrega uma area de deteccao (o campo do ima). Do outro, cada coletavel sabe entrar em estado "atraido" e se mover em direcao ao alvo. Quando o coletavel encosta de fato no corpo do player, ele e coletado. Vamos construir isso em camadas para ficar facil de ajustar depois.
Montando a area de ima no player
O player precisa de uma Area2D extra, separada da hitbox de dano ou de colisao fisica. Essa area representa o alcance do ima. Use um CollisionShape2D com um CircleShape2D para que o raio seja redondo e previsivel. A grande vantagem de expor o raio como variavel e poder mexer nele em tempo de execucao, por exemplo quando o jogador pega um upgrade que aumenta o alcance da coleta.
extends CharacterBody2D
@export var magnet_radius: float = 120.0
@onready var magnet_area: Area2D = $MagnetArea
@onready var magnet_shape: CollisionShape2D = $MagnetArea/CollisionShape2D
func _ready() -> void:
_apply_magnet_radius()
magnet_area.body_entered.connect(_on_magnet_area_body_entered)
magnet_area.area_entered.connect(_on_magnet_area_area_entered)
func _apply_magnet_radius() -> void:
var shape := magnet_shape.shape as CircleShape2D
if shape:
shape.radius = magnet_radius
func set_magnet_radius(value: float) -> void:
magnet_radius = value
_apply_magnet_radius()
Aqui o magnet_radius controla o tamanho do CircleShape2D diretamente. Quando o player pega um upgrade, basta chamar set_magnet_radius(180.0) e o campo do ima cresce na hora. Note que conectamos tanto area_entered quanto body_entered. Isso porque os coletaveis podem ser Area2D (mais comum para itens que so detectam, sem fisica) ou corpos. Vamos focar no caso dos itens como Area2D, que e o mais leve e o mais usado para moedas e gemas.
O coletavel como Area2D
Cada item e uma Area2D independente. Ela guarda uma referencia ao alvo (o player) e um booleano que diz se ja foi atraida. Enquanto nao estiver atraida, fica parada no chao. Quando o campo do ima a alcanca, ela liga o estado e comeca a perseguir o player todo frame.
extends Area2D
class_name Collectible
@export var base_speed: float = 60.0
@export var max_speed: float = 480.0
@export var acceleration: float = 6.0
var target: Node2D = null
var is_attracted: bool = false
var current_speed: float = 0.0
func _ready() -> void:
current_speed = base_speed
body_entered.connect(_on_body_entered)
func start_attraction(player: Node2D) -> void:
if is_attracted:
return
target = player
is_attracted = true
current_speed = base_speed
O start_attraction e o gatilho. Ele recebe o player como alvo e marca o item como atraido. A guarda if is_attracted evita que o item reinicie a velocidade caso entre e saia do campo varias vezes. Sem ela, um coletavel na borda do raio poderia ficar "engasgando" entre parado e atraido, perdendo a aceleracao acumulada.
Movimento com aceleracao progressiva
O coracao do game feel esta no _physics_process. A cada frame, calculamos a direcao ate o player, aumentamos a velocidade um pouco e movemos o item. O segredo e que a velocidade nao e constante. Ela parte de um valor baixo e cresce ate um teto, dando aquela sensacao de que o item "ganha impulso" conforme vai chegando perto.
func _physics_process(delta: float) -> void:
if not is_attracted or target == null:
return
var to_target: Vector2 = target.global_position - global_position
var distance: float = to_target.length()
# acelera ate o teto, sem nunca passar de max_speed
current_speed = move_toward(current_speed, max_speed, acceleration * max_speed * delta)
var direction: Vector2 = to_target.normalized()
global_position += direction * current_speed * delta
Repare no uso de move_toward para a velocidade. Em vez de somar um valor fixo e checar limites na mao, o move_toward(current_speed, max_speed, passo) cuida disso sozinho, parando exatamente em max_speed. O passo acceleration * max_speed * delta torna a aceleracao proporcional ao teto, entao se voce aumentar max_speed a curva continua coerente. O resultado e um item que sai devagar e chega voando, exatamente o tipo de movimento que o cerebro do jogador acha satisfatorio.
Disparando a atracao a partir do player
Falta ligar os dois lados. Quando a area de ima do player detecta um coletavel, ela chama o start_attraction daquele item passando o proprio player como alvo. Como os coletaveis sao Area2D, a deteccao acontece via sinal area_entered.
func _on_magnet_area_area_entered(area: Area2D) -> void:
if area is Collectible:
area.start_attraction(self)
func _on_magnet_area_body_entered(body: Node2D) -> void:
# fallback caso algum coletavel seja um corpo
if body.has_method("start_attraction"):
body.start_attraction(self)
A checagem area is Collectible deixa o codigo claro e evita atrair qualquer area que entre no campo, como gatilhos de cenario ou zonas de dano. So o que for da classe Collectible reage ao ima. Se voce tiver tipos diferentes de loot, todos podem herdar dessa mesma classe ou implementar o metodo start_attraction, e o player nao precisa saber os detalhes de cada um.
A coleta no contato
Atrair e metade do trabalho. A outra metade e coletar quando o item finalmente encosta no player. Para isso, o coletavel tambem observa o proprio body_entered. Quando o corpo que entra e o player, o item aplica seu efeito (somar moeda, dar vida, o que for) e se remove da cena.
func _on_body_entered(body: Node2D) -> void:
if body == target or body.is_in_group("player"):
_collect(body)
func _collect(player: Node2D) -> void:
if player.has_method("add_coins"):
player.add_coins(1)
# efeito visual ou som entrariam aqui
queue_free()
Usar o grupo player deixa o coletavel funcionar mesmo antes de ser atraido. Se o jogador simplesmente andar por cima de uma moeda parada, ela e coletada do mesmo jeito, sem depender do ima. O ima e um conforto, nao uma obrigacao. Lembre de adicionar o player ao grupo no editor ou via add_to_group("player") no _ready.
Ajustando o game feel
Com a base pronta, o que separa um ima morno de um ima delicioso sao os numeros. Vale brincar com tres variaveis.
A primeira e o base_speed. Se ele for alto demais, o item ja sai rapido e perde aquele momento de "puxao". Um valor baixo, em torno de 60, da o arranque suave que valoriza a aceleracao.
A segunda e o max_speed. Esse e o teto da velocidade. Itens que precisam chegar quase instantaneamente (moedas em cascata, por exemplo) pedem um teto alto. Loot mais raro, que voce quer que o jogador veja viajando pela tela, pode ter um teto menor para durar mais no ar.
A terceira e a acceleration. Valores baixos deixam a curva longa e dramatica. Valores altos fazem o item disparar quase de imediato. Comece em algo como 6 e ajuste no olho, com o jogo rodando.
Um truque extra de game feel e adicionar um leve atraso na coleta usando lerp na escala do sprite, encolhendo o item conforme ele se aproxima. Isso reforca a leitura de que o coletavel esta sendo "sugado". Outra opcao e tocar um som com pitch que sobe a cada moeda coletada em sequencia, criando aquela escala musical viciante quando o ima varre uma pilha de itens de uma vez.
Cuidados de performance
Com dezenas de itens na tela, o _physics_process de cada um roda todo frame. Na pratica isso e barato, porque a conta e so um vetor e um move_toward. Ainda assim, vale garantir que itens nao atraidos retornem cedo (foi o que fizemos com a guarda no topo do _physics_process). Assim, apenas os coletaveis dentro do campo do ima gastam processamento de movimento. Os outros ficam dormindo ate alguem chegar perto.
Se o seu jogo tiver centenas de itens simultaneos, considere desligar o _physics_process por completo enquanto o item esta parado e religar so no start_attraction com set_physics_process(true). E uma micro otimizacao que raramente importa, mas existe se voce precisar.
Onde levar a partir daqui
O ima que voce montou e a fundacao de varios sistemas de coleta. Ele combina muito bem com power-ups coletaveis, onde um item especial pode aumentar o magnet_radius por alguns segundos, transformando o player num aspirador de loot. Tambem encaixa direto com loot drop de itens, ja que cada peca que cai de um inimigo pode usar exatamente essa mesma classe Collectible e ser atraida sem nenhum codigo extra.
A partir daqui, experimente variar o comportamento por tipo de item. Moedas com ima forte e instantaneo, gemas raras com puxao lento e cinematografico, fragmentos que so reagem ao ima depois de pousar no chao. Cada ajuste muda a sensacao da coleta, e e justamente esse controle fino que faz a diferenca entre um jogo que parece bom e um que parece otimo nas maos do jogador.


