Voltar para o Blog
Quest Log

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

Bancada de crafting de um jogo com ingredientes sendo combinados em um novo item

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 if no 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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

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.