Loot Drop: Soltar Itens ao Matar Inimigo no Godot

Como montar loot drop godot com tabela de chances, sorteio ponderado, instanciar o item na morte, espalhar com impulso e coletar via Area2D no Godot 4.
Quando um inimigo morre, a parte divertida quase sempre vem depois: o que ele larga no chao. Montar um sistema de loot drop godot que sorteia recompensas com chances diferentes, instancia o item certo na posicao da morte e ainda espalha tudo com um impulsinho deixa o combate bem mais satisfatorio. Neste tutorial a gente constroi isso passo a passo, com codigo real de Godot 4, sem pseudo, comecando pela tabela de loot e terminando na coleta via Area2D.
Loot Drop: Soltar Itens ao Matar Inimigo no Godot
A ideia central e separar tres responsabilidades. Primeiro, uma tabela de loot que descreve o que pode cair e com qual chance. Segundo, um sorteio que decide quais itens saem de fato. Terceiro, a parte visual e fisica: instanciar a cena de cada item, posicionar na morte e jogar com um impulso para os lados. Cada parte funciona sozinha, entao da para testar isoladamente.
Modelando a tabela de loot drop godot com Resource
A forma mais limpa de descrever loot no Godot 4 e usar um Resource customizado. Assim voce edita a tabela direto no Inspector, salva como arquivo .tres e reaproveita entre varios inimigos. Cada entrada precisa de tres coisas: a cena do item, a chance de cair e a quantidade.
Primeiro a entrada individual:
class_name LootEntry
extends Resource
## Cena do item que sera instanciada (ex.: moeda, pocao).
@export var item_scene: PackedScene
## Chance de 0.0 a 1.0 deste item cair de forma independente.
@export_range(0.0, 1.0, 0.01) var drop_chance: float = 0.25
## Quantidade minima e maxima que cai quando o item e sorteado.
@export var min_amount: int = 1
@export var max_amount: int = 1
Agora a tabela, que e so uma lista de entradas mais uma rolagem opcional de "garantido". O metodo roll() decide o que sai e devolve uma lista de pares cena/quantidade prontos para instanciar.
class_name LootTable
extends Resource
## Lista de itens que podem cair, cada um com sua chance.
@export var entries: Array[LootEntry] = []
## Resultado: array de dicionarios { scene, amount }.
func roll() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for entry in entries:
if entry.item_scene == null:
continue
if randf() <= entry.drop_chance:
var amount := randi_range(entry.min_amount, entry.max_amount)
result.append({ "scene": entry.item_scene, "amount": amount })
return result
Cada item rola de forma independente: um inimigo pode soltar pocao e moeda no mesmo abate, ou nada. Esse modelo e o mais previsivel para balancear. Se voce ja trabalhou com Resource customizado no Godot, vai notar que a logica e a mesma: dados editaveis no Inspector, sem precisar mexer no codigo para criar uma tabela nova.
Sorteio ponderado quando so um item pode cair
O modelo acima e "cada item por si". Mas as vezes voce quer exatamente um drop por inimigo, escolhido por peso (60% moeda comum, 30% pocao, 10% item raro). Para isso o sorteio muda: somamos os pesos e jogamos um valor aleatorio na faixa total.
## Sorteia UMA entrada por peso. drop_chance e usado como peso relativo.
func roll_weighted() -> Dictionary:
var total := 0.0
for entry in entries:
total += entry.drop_chance
if total <= 0.0:
return {}
var pick := randf() * total
var acc := 0.0
for entry in entries:
acc += entry.drop_chance
if pick <= acc:
var amount := randi_range(entry.min_amount, entry.max_amount)
return { "scene": entry.item_scene, "amount": amount }
return {}
Os dois metodos convivem na mesma tabela. Use roll() para loot "aditivo" (vida, munição, moedas) e roll_weighted() quando quer um unico premio por baú ou por chefe. Lembre de chamar randomize() uma vez no inicio do jogo (no autoload, por exemplo) para a semente nao ser sempre igual.
Instanciar o item na posicao da morte e espalhar com impulso
Com a tabela resolvida, falta a parte visual. No script do inimigo, exponha a LootTable e, na hora da morte, percorra o resultado do roll() instanciando cada item. O ponto chave: adicione os itens a arvore, nao ao inimigo, porque o inimigo vai sair de cena com queue_free(). Usar get_tree().current_scene resolve isso.
class_name Enemy
extends CharacterBody2D
@export var loot_table: LootTable
@export var drop_impulse: float = 140.0
func die() -> void:
drop_loot()
queue_free()
func drop_loot() -> void:
if loot_table == null:
return
var drops := loot_table.roll()
for drop in drops:
var scene: PackedScene = drop["scene"]
var amount: int = drop["amount"]
for i in amount:
spawn_item(scene)
func spawn_item(scene: PackedScene) -> void:
var item := scene.instantiate()
get_tree().current_scene.add_child(item)
item.global_position = global_position
# Espalha em direcao aleatoria com forca variavel.
var dir := Vector2.RIGHT.rotated(randf() * TAU)
var force := dir * drop_impulse * randf_range(0.6, 1.0)
if item.has_method("launch"):
item.launch(force)
O detalhe de testar has_method("launch") evita acoplar o inimigo a um tipo especifico de item. Qualquer cena que tenha um metodo launch(Vector2) recebe o impulso. Se o seu item nao tiver esse metodo, ele so cai parado na posicao da morte, sem quebrar nada. Para uma visao mais geral sobre carregar e colocar cenas em tempo de execucao, vale revisar como instanciar cenas no Godot.
A cena do item: voar, frear e ser coletado via Area2D
O item em si pode ser um CharacterBody2D simples. Ele recebe o impulso, desacelera com atrito e usa um Area2D filho para detectar o jogador. Estrutura sugerida da cena: no raiz Pickup (CharacterBody2D), com filhos Sprite2D, CollisionShape2D e um Area2D chamado PickupArea (tambem com seu proprio CollisionShape2D).
class_name Pickup
extends CharacterBody2D
signal collected(item_name: String, amount: int)
@export var item_name: String = "coin"
@export var amount: int = 1
@export var friction: float = 600.0
@onready var pickup_area: Area2D = $PickupArea
func _ready() -> void:
pickup_area.body_entered.connect(_on_body_entered)
func launch(force: Vector2) -> void:
velocity = force
func _physics_process(delta: float) -> void:
velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
move_and_slide()
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
collected.emit(item_name, amount)
queue_free()
Repare que o velocity.move_toward(Vector2.ZERO, ...) faz o atrito de forma estavel: o item voa quando recebe launch(), vai freando e para. O move_and_slide() cuida da colisao com paredes, entao moedas nao atravessam o cenario. A coleta acontece no body_entered do Area2D: quando o corpo do jogador entra na area, o item emite collected e se remove.
Para o sinal funcionar, marque o jogador no grupo player (no Inspector, aba Node, secao Groups, ou via codigo com add_to_group("player") no _ready dele). Sem isso, o is_in_group("player") sempre retorna falso e nada e coletado.
Conectando a coleta ao inventario
O Pickup emite collected(item_name, amount), mas nao sabe nada sobre inventario. Quem decide o que fazer e o jogador ou um sistema central. Uma abordagem direta e o jogador escutar o sinal no momento do contato. Como o item se conecta a si mesmo no _ready, o caminho mais limpo e deixar o proprio jogador reagir dentro do body_entered, ou expor um autoload de inventario.
Exemplo de autoload simples que recebe os itens coletados:
extends Node
## Dicionario item_name -> quantidade total.
var items: Dictionary = {}
func add_item(item_name: String, amount: int) -> void:
items[item_name] = items.get(item_name, 0) + amount
print("Coletado: ", item_name, " x", amount, " | total: ", items[item_name])
E no Pickup, em vez de so emitir, voce pode chamar o autoload direto antes de sumir:
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
Inventory.add_item(item_name, amount)
collected.emit(item_name, amount)
queue_free()
A partir daqui da para plugar a UI, sons e efeitos. Se voce quer ir alem da contagem simples e montar slots, stacks e equipamento, o tutorial de sistema de inventario para RPG cobre essa camada inteira.
Ajustes de balanceamento e cuidados comuns
Alguns pontos que costumam dar dor de cabeca depois que o sistema esta de pe.
O primeiro e a coleta instantanea. Se o item nasce em cima do jogador, ele e coletado no mesmo frame e voce nem ve o drop. Uma saida e dar um pequeno atraso antes de ativar o PickupArea, usando um Timer ou desabilitando a colisao da area por meia segundo logo apos o launch(). Assim o item voa, pousa e so depois pode ser pego.
O segundo e a quantidade de nos na cena. Inimigos que soltam dezenas de moedas individuais enchem a arvore rapido. Se isso pesar, agrupe valores: em vez de vinte moedas de 1, solte duas de 10, ou faca as moedas com vida util e queue_free() automatico depois de alguns segundos.
O terceiro e a semente aleatoria. Chame randomize() uma unica vez, no inicio do jogo. Se voce esquecer, os drops ficam identicos a cada execucao, o que e otimo para depurar e ruim para o jogador.
Por fim, balanceie pelo numero esperado de drops, nao pela sensacao. Com roll() independente, a quantidade media que cai e a soma das chances de todas as entradas. Se a soma der mais de 1, em media cai mais de um item por inimigo. Anote esses numeros numa planilha simples antes de sair ajustando no escuro, porque mexer numa chance afeta a economia inteira do jogo.
Com a tabela em Resource, o sorteio (independente ou ponderado), o drop na posicao da morte com impulso e a coleta por Area2D, voce tem um loop de recompensa completo e facil de estender. Troque as cenas dos itens, ajuste as chances no Inspector e cada inimigo passa a ter sua propria lista de premios sem nenhuma linha de codigo extra.


