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: se é isso que você quer montar, o guia de sistema de inventário para jogos de RPG cobre a estrutura inteira. 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. Esses números de preço e desconto são decisões de economia de jogo, e vale pensar nelas com o mesmo cuidado que você pensa na mecânica.
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. E repare no tooltip_text que o catálogo já preenche: um tooltip bem feito com descrição e efeito do item economiza uma tela inteira de detalhes.
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, tudo reusa essas mesmas peças: o sistema de craft com receitas consome os mesmos itens do inventário, e a economia inteira do jogo cresce em cima da carteira que você acabou de escrever.
Perguntas frequentes
Como fazer uma loja no Godot 4?
Separe o sistema em quatro peças: itens como Resource customizado (.tres), uma carteira em autoload que valida saldo, um inventário que guarda id e quantidade, e a UI que só mostra estado e repassa cliques. Com essa separação, a loja inteira fica em cerca de 80 linhas de GDScript e cada parte pode ser testada sozinha.
Por que a UI da loja não deve conter a lógica de compra?
Porque a regra de compra espalhada em botões vira fonte de bug: moedas negativas, item duplicado e telas dessincronizadas. A UI repassa a intenção do clique e quem decide é a camada de lógica, consultando a carteira. Assim você troca o visual sem tocar na regra e testa a compra pelo console antes de existir botão.
Como evitar moedas negativas ou item duplicado na loja?
Faça cada operação ser atômica e validar antes de executar. Na compra, o dinheiro sai primeiro: se gastar() falhar, nada acontece. Na venda, o item sai primeiro: sem item, nenhuma moeda é creditada. Com a validação morando junto do dado (carteira e inventário), não importa quantos lugares chamem a função.
Qual preço de venda usar em relação ao de compra?
A convenção de RPG clássico é vender pela metade do preço de compra, e o motivo é design: se a venda devolve 100%, a loja vira armazém grátis e comprar deixa de ser escolha. Jogos mais punitivos usam 25% e mais generosos 75%. Garanta também um mínimo de 1 moeda para item barato não vender por zero.
Como criar itens da loja sem hardcode no Godot?
Use um Resource customizado (class_name ItemData) com id, nome, descrição, ícone e preço exportados. Cada item vira um arquivo .tres editável no Inspector. Adicionar item novo ao jogo passa a ser criar um arquivo, sem escrever código, e o balanceamento de preço fica fora do script.


