Sistema de Craft: Como Fazer Crafting com Receitas no Seu Jogo

Aprenda a criar um sistema de craft pro seu jogo no Godot 4: receitas data-driven com Resources, consumo de itens do inventário e UI de craft funcional.
Sistema de Craft: Como Fazer Crafting com Receitas no Seu Jogo
Crafting é um dos sistemas mais comuns em jogos: Minecraft, Terraria, qualquer survival, metade dos RPGs. E é também um dos sistemas que mais vejo iniciante implementar errado. O erro clássico é chumbar cada receita no código com uma cadeia de if: se tem 3 madeiras e 2 pedras, cria machado. Funciona com 5 receitas. Com 50, vira um monstro que ninguém quer tocar.
Neste tutorial você vai montar um sistema de craft pro seu jogo do jeito que escala: receitas como dados (data-driven), uma lógica central que checa e consome itens do inventário, e uma UI que se monta sozinha a partir das receitas. Todo código é GDScript do Godot 4.x e roda como está, mas a arquitetura serve pra qualquer engine.
Por que um sistema de craft data-driven
Data-driven significa separar a regra dos dados. A regra é uma só: "se o inventário tem todos os ingredientes, remove eles e adiciona o resultado". Os dados são as receitas: machado pede 3 madeiras e 2 pedras, poção pede 1 frasco e 2 ervas.
Quando você separa as duas coisas, ganha três vantagens concretas:
- Adicionar receita não exige código. Você cria um arquivo de receita no editor, preenche os campos e pronto. Dá até pra delegar isso pra alguém do time que não programa.
- Balanceamento vira ajuste de número. O machado está barato demais? Abre a receita, muda 3 pra 5. Sem recompilar lógica, sem caçar um
ifno meio de 400 linhas. - A UI se constrói sozinha. Se a tela de craft lê a lista de receitas, toda receita nova aparece automaticamente, com nome, ícone e ingredientes.
No Godot, o jeito natural de fazer isso é com Resources customizados. Resource é a classe de dados da engine: ela serializa pra arquivo .tres, edita no Inspector e carrega com load(). É exatamente o que uma receita precisa ser.
Modelando os dados: item, ingrediente e receita
Tudo começa com o item. Se o seu jogo ainda identifica itens por string solta espalhada pelo código, esse é o momento de centralizar:
# item_data.gd
class_name ItemData
extends Resource
@export var id: StringName
@export var nome: String
@export var icone: Texture2D
@export var empilha_ate: int = 99
Com a classe criada, cada item do jogo vira um arquivo: clique direito no FileSystem, New Resource, escolha ItemData, preencha e salve como madeira.tres, pedra.tres, machado.tres. O id é o identificador único que o inventário usa; o resto é apresentação.
Um ingrediente é só um par item + quantidade:
# ingrediente_craft.gd
class_name IngredienteCraft
extends Resource
@export var item: ItemData
@export var quantidade: int = 1
E a receita junta tudo:
# craft_recipe.gd
class_name CraftRecipe
extends Resource
@export var ingredientes: Array[IngredienteCraft]
@export var resultado: ItemData
@export var quantidade_resultado: int = 1
No Inspector, o array tipado de IngredienteCraft permite adicionar ingredientes direto na receita: clica em Add Element, cria um IngredienteCraft novo, arrasta o madeira.tres pro campo item, define a quantidade. A receita do machado fica num arquivo machado_receita.tres que qualquer pessoa abre e entende.
Repare no que a gente acabou de ganhar: o "banco de dados" de craft do jogo inteiro é uma pasta de arquivos .tres. Versiona no Git, compara diff de balanceamento, duplica receita pra criar variação. Zero código novo por receita.
O inventário que o craft consome
O craft não vive sozinho: ele lê e escreve no inventário. Se você já tem um, só precisa garantir três operações: contar, adicionar e remover. Se não tem, esse aqui é o mínimo funcional, registrado como autoload (Project Settings > Globals) com o nome Inventario:
# inventario.gd (autoload "Inventario")
extends Node
signal mudou
var itens: Dictionary = {} # id do item -> quantidade
func quantidade_de(id: StringName) -> int:
return itens.get(id, 0)
func adicionar(item: ItemData, quantidade: int = 1) -> void:
itens[item.id] = quantidade_de(item.id) + quantidade
mudou.emit()
func remover(id: StringName, quantidade: int = 1) -> bool:
if quantidade_de(id) < quantidade:
return false
itens[id] -= quantidade
if itens[id] == 0:
itens.erase(id)
mudou.emit()
return true
O detalhe que importa aqui é o sinal mudou. Toda mudança no inventário emite o sinal, e quem precisa reagir (a UI de craft, a HUD, o que for) se conecta nele. Isso desacopla os sistemas: o inventário não sabe que a tela de craft existe, e mesmo assim ela atualiza na hora certa.
Esse inventário é um dicionário simples de id pra quantidade. Pra um jogo com slots, posições e itens com durabilidade você vai precisar de mais estrutura, mas a interface que o craft consome continua sendo essas três operações. É por isso que vale defini-las primeiro.
A lógica de craft: checar e consumir
Com dados e inventário prontos, a lógica central é curta. Dois métodos: um que responde "dá pra craftar?" e um que executa. Também como autoload, com o nome Craft:
# craft.gd (autoload "Craft")
extends Node
signal item_craftado(receita: CraftRecipe)
func pode_craftar(receita: CraftRecipe) -> bool:
for ingrediente in receita.ingredientes:
if Inventario.quantidade_de(ingrediente.item.id) < ingrediente.quantidade:
return false
return true
func craftar(receita: CraftRecipe) -> bool:
if not pode_craftar(receita):
return false
for ingrediente in receita.ingredientes:
Inventario.remover(ingrediente.item.id, ingrediente.quantidade)
Inventario.adicionar(receita.resultado, receita.quantidade_resultado)
item_craftado.emit(receita)
return true
Duas decisões de design escondidas nessas 20 linhas merecem atenção.
Checar tudo antes de remover qualquer coisa. O craftar() valida a receita inteira antes de tocar no inventário. Se você remover ingrediente por ingrediente e descobrir no meio que falta um, o jogador perde itens sem receber nada. Em jogo single-player isso é um bug chato; em jogo online é um exploit. Valida primeiro, executa depois, sempre.
Emitir sinal em vez de chamar a UI. O item_craftado avisa quem quiser ouvir: a UI toca uma animação, o áudio toca um som, o sistema de conquistas conta mais um craft. A lógica não conhece nenhum deles. Quando você quiser adicionar partícula de sucesso daqui a três meses, conecta no sinal e não mexe numa linha do craft.
UI de craft que se monta sozinha
A tela de craft lê a lista de receitas e gera um botão pra cada uma. A estrutura de cena é um PanelContainer com um VBoxContainer dentro (aqui chamado de ListaReceitas):
# craft_ui.gd
extends PanelContainer
@export var receitas: Array[CraftRecipe]
@onready var lista := $MarginContainer/VBoxContainer/ListaReceitas
func _ready() -> void:
Inventario.mudou.connect(_atualizar)
_montar_lista()
func _montar_lista() -> void:
for receita in receitas:
var botao := Button.new()
botao.text = "%s x%d" % [receita.resultado.nome, receita.quantidade_resultado]
botao.icon = receita.resultado.icone
# tooltip lista os ingredientes, montada a partir da própria receita
var linhas: PackedStringArray = []
for ingrediente in receita.ingredientes:
linhas.append("%dx %s" % [ingrediente.quantidade, ingrediente.item.nome])
botao.tooltip_text = "\n".join(linhas)
botao.pressed.connect(_ao_clicar.bind(receita))
lista.add_child(botao)
_atualizar()
func _atualizar() -> void:
for i in lista.get_child_count():
var botao: Button = lista.get_child(i)
botao.disabled = not Craft.pode_craftar(receitas[i])
func _ao_clicar(receita: CraftRecipe) -> void:
Craft.craftar(receita)
O fluxo completo: a tela monta os botões a partir das receitas, desabilita os que não dão pra craftar, e se conecta no sinal mudou do inventário. Quando o jogador crafta (ou coleta, ou gasta qualquer item), o inventário emite o sinal e os botões se atualizam sozinhos. Nada de chamar refresh na mão em dez lugares diferentes.
No Inspector da cena de UI, você arrasta as receitas pro array receitas. Se preferir não manter essa lista na mão, dá pra carregar tudo de uma pasta:
func carregar_receitas(caminho: String) -> Array[CraftRecipe]:
var receitas: Array[CraftRecipe] = []
for arquivo in DirAccess.get_files_at(caminho):
# no build exportado, .tres pode aparecer como .tres.remap
arquivo = arquivo.trim_suffix(".remap")
if arquivo.get_extension() == "tres":
receitas.append(load(caminho.path_join(arquivo)))
return receitas
O detalhe do .remap é uma pegadinha real do Godot: no editor a pasta tem arquivos .tres, mas no jogo exportado eles podem virar .tres.remap. Quem lista a pasta sem tratar isso vê o craft funcionar perfeitamente no editor e quebrar no build. Já perdi uma tarde com isso, fica o aviso.
Pra onde crescer a partir daqui
Esse núcleo aguenta a maioria dos jogos, e as extensões comuns encaixam nele sem refatorar:
Estações de craft. Adicione um campo @export var estacao: StringName na receita (fornalha, bancada, caldeirão) e filtre a lista da UI pela estação que o jogador está usando. A lógica de craftar não muda nada.
Receitas descobertas. Mantenha um Dictionary de receitas conhecidas no save do jogador e mostre na UI só as desbloqueadas. A receita em si continua sendo o mesmo Resource; o que muda é o filtro.
Tempo de craft. Em vez de entregar o resultado na hora, o craftar() consome os ingredientes e agenda a entrega com um timer. O importante se mantém: consumir tudo de uma vez, no início, pra não abrir brecha de duplicação.
Quantidade em lote. Um botão "craftar 10" é só um loop chamando craftar() até retornar false. Como a validação está centralizada, o lote para sozinho quando os ingredientes acabam.
Repare no padrão: cada extensão mexe nos dados ou na UI, quase nunca na lógica central. É o sinal de que a separação está certa.
Fechando
Um sistema de craft bem feito se resume a três peças com fronteiras claras: receitas como dados (Resources no Godot), um inventário que sabe contar, adicionar e remover, e uma lógica central que valida tudo antes de consumir qualquer coisa. A UI é consequência: ela lê os dados e reage aos sinais.
Se você for implementar hoje, sugiro a ordem deste artigo: primeiro os Resources de item e receita, depois o inventário com sinal, depois a lógica, e a UI por último. Em cada etapa dá pra testar com print() antes de ter tela. Quando a UI entrar, o sistema embaixo já está sólido, e adicionar a receita número 50 vai dar exatamente o mesmo trabalho da número 5: nenhum.


