Como Fazer um Sistema de Loja (Shop) no Seu Jogo

Aprenda a criar um sistema de loja jogo completo no Godot 4: moeda, comprar e vender, UI de shop e integração com inventário, com código GDScript real.
Como Fazer um Sistema de Loja (Shop) no Seu Jogo
Quase todo jogo com progressão acaba precisando de uma loja: o mercador do RPG, o shop de upgrades do roguelike, a lojinha entre fases do jogo de nave. E quase todo dev iniciante implementa isso do jeito errado na primeira tentativa, colando a lógica de compra dentro do botão da UI. Funciona na demo, vira pesadelo no jogo de verdade.
Um sistema de loja jogo bem feito é, na real, quatro peças separadas conversando: os dados dos itens, a carteira do jogador, a lógica de compra e venda e a UI. Quando cada peça cuida só do seu pedaço, você troca o visual da loja sem tocar na regra de preço, adiciona item novo sem escrever uma linha de código, e testa a compra sem nem abrir a tela.
Esse tutorial monta as quatro peças do zero em GDScript, Godot 4.x. Todo código roda como está, e a arquitetura serve igual pra Unity ou qualquer engine: muda a sintaxe, não a ideia.
A arquitetura de um sistema de loja
Antes do código, o desenho. A regra de ouro: a UI nunca decide nada. Ela só mostra estado e repassa intenção ("o jogador clicou em comprar poção"). Quem decide se a compra acontece é a camada de lógica, consultando a carteira.
As quatro peças:
- ItemData (Resource): o que é cada item. Nome, ícone, preço. Dados puros, sem lógica.
- Carteira (autoload): quanto dinheiro o jogador tem. Sabe adicionar e gastar, e avisa quando muda.
- Inventário (autoload): o que o jogador carrega. A loja deposita e retira daqui.
- Loja + UI: o catálogo de itens à venda, as regras de compra e venda, e a tela que mostra tudo.
Separar assim parece burocracia pra um jogo pequeno, mas é o contrário: são uns 80 linhas de código no total, e cada pedaço cabe na cabeça.
Itens como Resource: a loja sem hardcode
No Godot, a ferramenta certa pra dados de item é o Resource customizado. Cada item vira um arquivo .tres que você cria e edita no Inspector, sem código:
class_name ItemData
extends Resource
@export var id: String
@export var nome: String
@export var descricao: String
@export var icone: Texture2D
@export var preco_compra: int = 10
Salve como item_data.gd. Agora, no FileSystem, clique com o botão direito, Create New > Resource, escolha ItemData e preencha os campos. Crie pocao_vida.tres, espada_ferro.tres, quantos quiser. Adicionar um item novo ao jogo virou criar um arquivo, e isso é exatamente o que você quer: designer (ou você mesmo no futuro) balanceia preço sem caçar número no meio do script.
O campo id merece atenção. É a chave única do item ("pocao_vida", "espada_ferro"), e é o que o inventário vai usar pra contar quantos o jogador tem. Mantenha em snake_case e nunca mude depois que tiver save game em produção, senão os saves antigos quebram.
A carteira: moeda com sinal
A carteira é um autoload (Project Settings > Globals > Autoload), porque o dinheiro do jogador é estado global de verdade: HUD, loja e save precisam ler o mesmo valor. Registre o script abaixo como Carteira:
extends Node
signal moedas_alteradas(total: int)
var moedas: int = 0
func adicionar(quantia: int) -> void:
moedas += quantia
moedas_alteradas.emit(moedas)
func gastar(quantia: int) -> bool:
if quantia > moedas:
return false
moedas -= quantia
moedas_alteradas.emit(moedas)
return true
Dois detalhes que importam aqui:
gastar() retorna bool e valida antes. O único jeito de tirar dinheiro da carteira já checa se tem saldo. É impossível ficar com moedas negativas, não importa quantos lugares do código chamem isso. Validação mora junto do dado, não espalhada pela UI.
O sinal moedas_alteradas é o que mantém a UI honesta. O HUD se conecta nele e se atualiza sozinho toda vez que o valor muda, seja por compra, venda, drop de inimigo ou recompensa de quest. Você nunca mais escreve "atualizar label de moedas" em dez lugares.
O inventário mínimo pra loja funcionar
Inventário completo (slots, equipamento, peso) é assunto pra outro artigo. Pra loja, basta um dicionário de id pra quantidade, também como autoload (Inventario):
extends Node
signal inventario_alterado
var itens: Dictionary = {}
func adicionar_item(item: ItemData, quantidade: int = 1) -> void:
itens[item.id] = itens.get(item.id, 0) + quantidade
inventario_alterado.emit()
func remover_item(item: ItemData, quantidade: int = 1) -> bool:
if itens.get(item.id, 0) < quantidade:
return false
itens[item.id] -= quantidade
if itens[item.id] == 0:
itens.erase(item.id)
inventario_alterado.emit()
return true
func quantidade_de(item: ItemData) -> int:
return itens.get(item.id, 0)
Mesmo padrão da carteira: remover_item() valida e retorna bool, e o sinal avisa quem quiser saber. Se o seu jogo já tem um inventário próprio, a loja só precisa que ele exponha esses três métodos (ou equivalentes). O resto deste tutorial não muda.
Comprar e vender: a lógica da loja
Agora a peça central. A Loja é um node com um catálogo (array de ItemData exportado, que você preenche arrastando os .tres no Inspector) e duas operações:
class_name Loja
extends Node
const FATOR_VENDA := 0.5
@export var catalogo: Array[ItemData] = []
func comprar(item: ItemData) -> bool:
if not Carteira.gastar(item.preco_compra):
return false
Inventario.adicionar_item(item)
return true
func vender(item: ItemData) -> bool:
if not Inventario.remover_item(item):
return false
Carteira.adicionar(preco_de_venda(item))
return true
func preco_de_venda(item: ItemData) -> int:
return maxi(1, int(item.preco_compra * FATOR_VENDA))
Repare na ordem das operações, porque ela é a proteção contra bug de duplicação:
- Na compra, o dinheiro sai primeiro. Se
gastar()falhar, nada aconteceu. O item só entra no inventário depois que o pagamento foi confirmado. - Na venda, o item sai primeiro. Se o jogador não tem o item,
remover_item()falha e nenhuma moeda é creditada.
Cada operação é atômica: ou completa inteira, ou não faz nada. Em single player isso já elimina a clássica exploit de "vender o mesmo item duas vezes clicando rápido".
Sobre o FATOR_VENDA: vender pela metade do preço de compra é a convenção de RPG clássico, e existe por motivo de design, não de matemática. Se vender devolve 100%, a loja vira um banco de armazenamento grátis e toda decisão de compra perde peso. O desconto é o que torna comprar uma escolha com consequência. Ajuste o fator pro seu jogo (jogos mais punitivos usam 25%, mais generosos usam 75%), mas raramente use 100%. O maxi(1, ...) garante que item barato nunca venda por zero.
Montando a UI do seu sistema de loja
Com a lógica pronta, a UI fica simples de propósito: ela lê o catálogo, gera os botões e repassa cliques. Estrutura de cena:
LojaUI (PanelContainer)
└── Margem (MarginContainer)
└── Coluna (VBoxContainer)
├── LabelMoedas (Label)
├── Lista (VBoxContainer)
└── BotaoFechar (Button)
E o script, gerando uma linha por item do catálogo:
extends PanelContainer
@export var loja: Loja
@onready var label_moedas: Label = $Margem/Coluna/LabelMoedas
@onready var lista: VBoxContainer = $Margem/Coluna/Lista
func _ready() -> void:
Carteira.moedas_alteradas.connect(_atualizar_moedas)
_atualizar_moedas(Carteira.moedas)
_montar_catalogo()
func _montar_catalogo() -> void:
for item in loja.catalogo:
var botao := Button.new()
botao.text = "%s (%d moedas)" % [item.nome, item.preco_compra]
botao.icon = item.icone
botao.tooltip_text = item.descricao
# bind() anexa o item ao callback: cada botão sabe o que vende.
botao.pressed.connect(_on_comprar_pressionado.bind(item))
lista.add_child(botao)
func _on_comprar_pressionado(item: ItemData) -> void:
if not loja.comprar(item):
label_moedas.modulate = Color.RED
await get_tree().create_timer(0.3).timeout
label_moedas.modulate = Color.WHITE
func _atualizar_moedas(total: int) -> void:
label_moedas.text = "Moedas: %d" % total
O bind(item) é o truque que evita um script por botão: o mesmo callback serve pra todos, e cada botão carrega junto o ItemData que representa. Quando a compra falha, o feedback aqui é piscar o contador de moedas em vermelho. É o mínimo aceitável: falha silenciosa em loja é um dos erros de UX mais irritantes que existem. O jogador clica, nada acontece, e ele não sabe se é bug ou falta de dinheiro. Sempre responda algo: cor, som, balanço do mercador reclamando.
Pra aba de venda, a lógica espelha: em vez de iterar o catálogo da loja, itere Inventario.itens, mostre loja.preco_de_venda(item) no botão e chame loja.vender(item) no clique. Conecte Inventario.inventario_alterado pra reconstruir a lista quando um item for vendido. Como a UI não tem regra nenhuma, a aba de venda é só mais um loop de botões.
Detalhes que separam loja de protótipo e loja de jogo lançado
Com o esqueleto funcionando, três refinamentos valem o esforço antes de seguir em frente:
Confirmação em compra cara. Clique errado em item de 5 moedas, paciência. Em item de 5.000, é rage quit. Um ConfirmationDialog nativo do Godot resolve: abra ele no clique e só chame loja.comprar() no sinal confirmed.
Estoque limitado, se fizer sentido. Pra mercador que tem só 3 poções, adicione um dicionário estoque na Loja espelhando o padrão do inventário: decrementa na compra, falha quando zera, e o botão fica desabilitado (botao.disabled = true) quando acaba. São as mesmas dez linhas que você já escreveu duas vezes neste artigo.
Persistência. Carteira.moedas e Inventario.itens precisam entrar no seu save. É por isso que os dois são autoloads com estado simples (um int e um dicionário de strings pra ints): serializar isso pra JSON ou ConfigFile é trivial. O catálogo da loja não entra no save, ele mora nos .tres e é conteúdo do jogo, não progresso do jogador.
Conclusão
Loja é um daqueles sistemas que parecem grandes e na prática são quatro arquivos curtos, desde que cada um faça só o seu trabalho: ItemData guarda dados, Carteira e Inventario validam e avisam por sinal, Loja aplica a regra de compra e venda, e a UI só desenha e repassa cliques.
Monte nessa ordem, testando cada peça antes da próxima (dá pra testar loja.comprar() pelo console antes de existir qualquer botão). Quando a tela ficar pronta, ela vai funcionar de primeira, porque toda a lógica embaixo já estava provada. E daqui pra frente, economia de jogo, crafting, baú de loot: tudo reusa essas mesmas peças.


