Sistema de Crafting na Godot 4 com GDScript do Zero

Como montar um sistema de crafting funcional na Godot 4 com GDScript tipado: item e receita como Resource, inventário, função craft() e UI ligada por sinal.
Fabricar itens a partir de ingredientes é uma daquelas mecânicas que parecem simples de fora e viram bagunça quando você senta pra escrever. Poção de vida = 2 ervas + 1 frasco. Espada = 3 lingotes + 1 cabo. Fácil no papel, mas se você chumbar isso direto no código, cada receita nova vira um if gigante e um mês depois ninguém entende mais nada. Este tutorial mostra como montar um sistema de crafting na Godot 4 com GDScript de um jeito que escala: dados separados da lógica, receitas editáveis fora do código e uma UI que conversa por sinal. Todo o código aqui é tipado e roda no Godot 4 real, sem API inventada.
A ideia central é uma só: item e receita são dados, não comportamento. Quem sabe fabricar é uma classe de lógica. Quem mostra os botões é a UI. Cada peça ignora as outras o máximo possível. Vamos por partes.
Modelando o item como Resource
Resource é a classe base da Godot pra dado reutilizável e salvável em arquivo. Você define os campos, dá um class_name, e cada instância vira um arquivo .tres que dá pra editar no Inspector sem abrir o editor de código. É o oposto de espalhar strings mágicas pelos scripts.
Comece pelo item. Crie um script item.gd:
class_name Item
extends Resource
@export var id: String = ""
@export var nome: String = ""
@export var icone: Texture2D
@export_multiline var descricao: String = ""
Salve, e no editor use New Resource -> Item pra criar erva.tres, frasco.tres, pocao_vida.tres e por aí vai. O id é o que importa pra lógica (uma string curta e estável tipo "erva"); o resto é pra mostrar na tela. Manter um id explícito em vez de depender do nome do arquivo evita dor de cabeça quando você renomeia coisas depois.
Modelando a receita
A receita é onde a mágica acontece. Ela precisa de duas coisas: quais ingredientes consome e o que produz. Ingredientes são um mapa de item pra quantidade, então um Dictionary resolve. O resultado é um item mais uma quantidade (algumas receitas rendem mais de uma unidade).
class_name Receita
extends Resource
@export var resultado: Item
@export var quantidade_resultado: int = 1
@export var ingredientes: Dictionary = {}
O ingredientes guarda pares de Item para int. No Inspector você adiciona as chaves arrastando os .tres dos itens e digita a quantidade de cada um. Uma receita de poção fica com { erva.tres: 2, frasco.tres: 1 } e resultado = pocao_vida.tres, quantidade_resultado = 1.
Um detalhe prático: dá pra usar o Item inteiro como chave do dicionário, e funciona, porque cada .tres carregado é uma referência única. Mas se você for salvar o inventário em disco depois, é mais seguro indexar tudo pelo id (a String). Vou usar id como chave no inventário justamente por isso, e converter na hora de checar a receita. Fica explícito e não quebra na hora de serializar.
O inventário
O inventário mais simples que resolve é um dicionário de id do item para quantidade. Nada de lista de objetos, nada de slots ainda: só "quanto de cada coisa eu tenho". Um script inventario.gd que estende Node (pra virar um nó na cena ou um autoload):
class_name Inventario
extends Node
signal inventario_mudou
var itens: Dictionary = {}
func adicionar(id: String, quantidade: int = 1) -> void:
if quantidade <= 0:
return
itens[id] = itens.get(id, 0) + quantidade
inventario_mudou.emit()
func remover(id: String, quantidade: int = 1) -> bool:
var atual: int = itens.get(id, 0)
if atual < quantidade:
return false
itens[id] = atual - quantidade
if itens[id] <= 0:
itens.erase(id)
inventario_mudou.emit()
return true
func tem(id: String, quantidade: int = 1) -> bool:
return itens.get(id, 0) >= quantidade
Repare no uso de get(id, 0): se o item nunca entrou no dicionário, tratamos como zero em vez de dar erro. O remover retorna bool porque ele pode falhar (não dá pra tirar 3 ervas de quem tem 1), e quem chama precisa saber disso. E o sinal inventario_mudou avisa a UI que a quantidade mudou, sem o inventário saber nada sobre telas. Esse mesmo padrão de dicionário guia jogos com drag and drop de inventário na Godot, onde os slots visuais leem dessa mesma fonte de dados.
A função que checa os ingredientes
Antes de fabricar, a pergunta é: dá pra fabricar? Isso é uma função separada, e vale que seja separada, porque a UI também vai usar ela pra decidir se o botão fica ativo ou cinza. Ela percorre os ingredientes da receita e confere cada um contra o inventário.
func pode_fabricar(receita: Receita) -> bool:
if receita == null or receita.resultado == null:
return false
for ingrediente in receita.ingredientes:
var id: String = ingrediente.id
var necessario: int = receita.ingredientes[ingrediente]
if not inventario.tem(id, necessario):
return false
return true
A validação de null no começo não é frescura: uma receita mal configurada no Inspector (sem resultado, por exemplo) chegaria aqui e explodiria mais na frente, num ponto bem mais difícil de debugar. Barrar cedo é mais barato. O laço sai no primeiro ingrediente que falta com return false; só chega no return true quem passou por todos.
A função craft()
Agora o núcleo. craft() faz três coisas nesta ordem: valida, consome os ingredientes, adiciona o resultado. A ordem importa. Se você adicionar o resultado antes de consumir e algo falhar no meio, o jogador ganha item de graça. Validar tudo primeiro garante que ou a fabricação inteira acontece, ou nada acontece.
signal item_fabricado(resultado: Item, quantidade: int)
func craft(receita: Receita) -> bool:
if not pode_fabricar(receita):
return false
for ingrediente in receita.ingredientes:
var id: String = ingrediente.id
var necessario: int = receita.ingredientes[ingrediente]
inventario.remover(id, necessario)
inventario.adicionar(receita.resultado.id, receita.quantidade_resultado)
item_fabricado.emit(receita.resultado, receita.quantidade_resultado)
return true
Como pode_fabricar já garantiu que tem tudo, o laço de remover nunca vai falhar aqui, e por isso não precisa checar o retorno. Depois de consumir, adiciona o resultado e emite item_fabricado. Esse sinal é o que liga a lógica ao resto do jogo: a UI escuta pra mostrar feedback, um sistema de conquistas pode escutar pra contar "fabricou 10 poções", e nenhum deles precisa conhecer o outro. craft() devolve bool pra quem chamou saber se deu certo.
Um CraftingSystem completo junta tudo:
class_name CraftingSystem
extends Node
signal item_fabricado(resultado: Item, quantidade: int)
@export var inventario: Inventario
@export var receitas: Array[Receita] = []
func pode_fabricar(receita: Receita) -> bool:
if receita == null or receita.resultado == null:
return false
for ingrediente in receita.ingredientes:
if not inventario.tem(ingrediente.id, receita.ingredientes[ingrediente]):
return false
return true
func craft(receita: Receita) -> bool:
if not pode_fabricar(receita):
return false
for ingrediente in receita.ingredientes:
inventario.remover(ingrediente.id, receita.ingredientes[ingrediente])
inventario.adicionar(receita.resultado.id, receita.quantidade_resultado)
item_fabricado.emit(receita.resultado, receita.quantidade_resultado)
return true
O receitas é um Array[Receita] exportado: você arrasta os .tres das receitas pro Inspector e a lista de crafting do jogo está definida sem uma linha de código. Adicionar receita nova é criar arquivo e soltar na lista.
Ligando na UI de crafting
A UI não fabrica nada. Ela pergunta ao CraftingSystem o que dá pra fazer, mostra os botões e, quando o jogador clica, chama craft(). Todo o resultado volta pelo sinal. Monte uma cena com um VBoxContainer chamado ListaReceitas e um script:
extends Control
@export var crafting: CraftingSystem
@onready var lista: VBoxContainer = $ListaReceitas
func _ready() -> void:
crafting.item_fabricado.connect(_ao_fabricar)
crafting.inventario.inventario_mudou.connect(_atualizar_botoes)
_montar_lista()
func _montar_lista() -> void:
for receita in crafting.receitas:
var botao: Button = Button.new()
botao.text = receita.resultado.nome
botao.pressed.connect(_ao_clicar.bind(receita))
lista.add_child(botao)
_atualizar_botoes()
func _ao_clicar(receita: Receita) -> void:
crafting.craft(receita)
func _atualizar_botoes() -> void:
var indice: int = 0
for receita in crafting.receitas:
var botao: Button = lista.get_child(indice) as Button
botao.disabled = not crafting.pode_fabricar(receita)
indice += 1
func _ao_fabricar(resultado: Item, quantidade: int) -> void:
print("Fabricou %d x %s" % [quantidade, resultado.nome])
O bind(receita) amarra a receita específica ao clique daquele botão, então cada botão sabe o que fabrica sem você guardar índices soltos. O _atualizar_botoes roda no _ready e toda vez que o inventário muda: botão de receita sem ingredientes fica desabilitado sozinho. E quando craft dá certo, o sinal item_fabricado cai em _ao_fabricar, que aqui só imprime, mas é onde você toca som, mostra popup ou anima o item novo. Esse mesmo desenho de sinal e botão aparece em qualquer tela transacional, como no sistema de loja e shop de um jogo, que compra e vende com a mesma estrutura de validar, consumir e emitir.
Boas práticas que fazem a diferença
Três coisas separam esse sistema de um que apodrece em três meses.
Dado é dado, lógica é lógica. Item e receita são Resource, sem uma linha de comportamento dentro. Quem sabe fabricar é o CraftingSystem. Esse corte é o que deixa um designer sem programar criar cem receitas em .tres enquanto você mexe no código do jogo. Se você começar a colocar func craft() dentro da classe Item, parabéns, acabou de acoplar dado e lógica e vai se arrepender.
Sinais no lugar de o sistema chamar a UI. O CraftingSystem nunca faz ui.mostrar_mensagem(). Ele emite item_fabricado e segue a vida. Quem quiser reagir se conecta. Isso deixa você trocar a UI inteira, adicionar som, adicionar conquista, tudo sem tocar na lógica de fabricação. É o mesmo princípio de desacoplamento que vale pra efeitos de status como buff e debuff: o sistema aplica o efeito e avisa, quem escuta que reaja.
Escalabilidade vem da arquitetura, não de otimização prematura. Com receitas em Resource, ir de 5 pra 200 receitas não muda o código de craft(), só a quantidade de arquivos. Se um dia forem centenas, você carrega todas de uma pasta com um loop no ResourceLoader e indexa por id num dicionário, mas o núcleo continua igual. Não tente resolver o problema das 200 receitas quando você tem 5; resolva a arquitetura, e as 200 chegam de graça.
Uma escolha que atravessa tudo isso é a linguagem. GDScript, como usei aqui, é direto e integra sem atrito com os Resource e sinais da engine. Pra sistemas muito pesados de CPU tem quem prefira C#, e se essa dúvida te pega, vale ler o comparativo de C# contra GDScript na Godot antes de decidir. Pra 95% dos jogos com crafting, GDScript dá conta sobrando.
Fechando
Você tem um sistema de crafting completo: Item e Receita como Resource editáveis fora do código, um Inventario que é só um dicionário com sinal, pode_fabricar pra validar, craft pra executar de forma atômica e uma UI que se liga por sinal e não conhece as tripas da lógica. Daqui dá pra crescer em qualquer direção: adicionar tempo de fabricação, exigir uma bancada por perto, dar chance de falha, categorizar receitas. Todas essas extensões cabem sem reescrever o núcleo, justamente porque os dados estão separados da lógica desde o começo. Comece pequeno, com duas ou três receitas rodando de ponta a ponta, e só depois adicione tempero. Aqui no CursoGame.Dev a gente insiste nisso: sistema que escala é sistema que nasceu organizado, não o que ficou esperto demais cedo demais.
Perguntas frequentes
Por que modelar item e receita como Resource em vez de dicionário puro?
Resource dá arquivos .tres editáveis no Inspector, reuso entre cenas e tipagem forte com class_name. Você cria uma receita nova sem tocar em código, só duplicando um arquivo e trocando os campos. Dicionário solto no script vira uma pilha de strings mágicas que ninguém consegue manter depois de vinte itens.
Como valido se o jogador tem os ingredientes antes de fabricar?
Percorra o dicionário de ingredientes da receita e compare cada quantidade com o que o inventário tem. Se qualquer item faltar ou tiver quantidade insuficiente, retorne false imediatamente. Só depois de passar por todos os ingredientes é que a fabricação pode consumir e adicionar o resultado.
Preciso de banco de dados para o crafting funcionar?
Não para começar. Um dicionário de itens em memória mais os arquivos .tres das receitas resolvem um jogo pequeno ou médio inteiro. Banco de dados só entra se você tem inventário persistente entre sessões, e aí o que você salva é o dicionário de quantidades, não os Resources.
Como a UI sabe que um item foi fabricado?
A lógica emite um sinal quando o craft dá certo, tipo item_fabricado(resultado, quantidade). A UI conecta nesse sinal e reage: mostra a mensagem, atualiza a lista, toca um som. Assim a lógica não conhece a interface e você troca a UI sem mexer no sistema de crafting.
Esse sistema serve para jogo grande com centenas de receitas?
Serve como base. Com item e receita em Resource, adicionar receita é criar arquivo, não editar código. Para centenas delas vale carregar tudo de uma pasta com um loop no ResourceLoader e indexar por id, mas a arquitetura de dados separados da lógica continua a mesma.


