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. Esse formato de bancada é padrão em survival, onde o craft é o sistema central; se é esse o seu projeto, o guia de como fazer um jogo de sobrevivência mostra onde ele se encaixa no loop de coleta e construção.
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.
Validar as receitas no editor com um script @tool
Receita data-driven tem um ponto cego: nada impede alguém do time de criar um .tres com o campo resultado vazio ou com um ingrediente sem item. No editor parece tudo certo, e o craft quebra só quando o jogador tenta usar aquela receita no jogo rodando. Dá pra pegar isso cedo com uma validação que você roda antes de exportar o build.
O jeito mais direto para um Resource é um método de checagem na própria receita, que você chama num loop por toda a pasta de .tres:
# craft_recipe.gd (trecho)
func esta_valida() -> bool:
if resultado == null:
push_warning("Receita sem resultado definido")
return false
if ingredientes.is_empty():
push_warning("Receita sem ingredientes")
return false
for ingrediente in ingredientes:
if ingrediente == null or ingrediente.item == null:
push_warning("Ingrediente sem item na receita")
return false
if ingrediente.quantidade <= 0:
push_warning("Ingrediente com quantidade invalida")
return false
return true
Como o CraftRecipe é um Resource, essa validação é barata e pega o erro humano antes do jogador. Se você ainda não domina o modelo de Resource customizado que sustenta esse sistema inteiro, vale ler como criar um Resource customizado no Godot, porque é a base de tudo que a gente montou aqui.
Craft com qualidade e chance de bônus
Muitos jogos não entregam sempre a mesma quantidade: às vezes sai um item a mais. Isso cabe no sistema sem tocar na lógica central, só estendendo os dados e o momento de entrega.
Adicione os campos na receita:
# craft_recipe.gd (campos extras)
@export var chance_bonus: float = 0.0 # 0.0 a 1.0
@export var quantidade_bonus: int = 1
E ajuste só o cálculo da quantidade final na hora de entregar, dentro do craftar():
func _quantidade_final(receita: CraftRecipe) -> int:
var total: int = receita.quantidade_resultado
if randf() < receita.chance_bonus:
total += receita.quantidade_bonus
return total
Repare que o consumo dos ingredientes não muda: você ainda valida tudo antes e remove tudo de uma vez. A aleatoriedade entra só na saída, nunca na checagem, pra não abrir brecha. Esse tipo de variação conversa direto com o inventário que recebe o resultado, e se o seu projeto é um RPG vale ver como montar um sistema de inventário para jogos de RPG, que é o outro lado dessa mesma moeda.
Erros comuns que quebram um sistema de craft
Depois de ver muita gente implementar craft, os mesmos tropeços se repetem:
- Remover antes de validar. Já falei na lógica, mas é o erro número um: se você consome ingrediente enquanto confere, uma receita faltando um item deixa o jogador no prejuízo. Valide tudo, depois consuma.
- Compartilhar o mesmo Resource entre instâncias sem querer. Resource no Godot é referência. Se você guardar estado mutável dentro do
ItemData(tipo durabilidade atual), todos os itens daquele tipo compartilham o valor. Estado que muda por instância fica no inventário, não no Resource de dados. - Esquecer de salvar as receitas descobertas. Se o jogador desbloqueia receitas, essa lista precisa entrar no save, senão ela funciona na sessão e some quando o jogo fecha. O sistema de save e load no Godot resolve isso, e vale integrar desde cedo.
- Craft em lote sem revalidar. Um botão "craftar 10" que não chama
pode_craftar()a cada iteração pode consumir errado quando os ingredientes acabam no meio. Como a validação está centralizada, o loop para sozinho, desde que você respeite o retornofalse.
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.


