Como fazer uma loja (GUI) no Roblox?
Escreva o catálogo uma vez, num módulo que os dois lados leem. O cliente monta um card por item a partir de um template; um RemoteFunction pede para comprar e recebe de volta o novo saldo ou o motivo da recusa. O preço nunca é lido da interface, porque a interface está na máquina do jogador.
Em resumo
- Um módulo de catálogo, lido pelo cliente e pelo servidor.
- RemoteFunction, para a recusa carregar um motivo que a interface mostra.
- Cards de um template com UIGridLayout; nunca vinte frames na mão.
A loja é onde a economia de um jogo fica blindada ou escancarada, e a pergunta que decide é pequena: quem sabe o preço? Se o servidor lê o preço do catálogo que é dele, a interface pode dizer o que quiser e nada acontece.
Passo a passo
1. Um catálogo, no ReplicatedStorage
Um ModuleScript que os dois lados dão require. Duas listas de preço é como um item acaba custando 100 na tela e 10 no caixa.
-- ReplicatedStorage/ShopCatalogue (ModuleScript)
return {
{ id = "sword_wood", name = "Espada de madeira", price = 100, icon = "rbxassetid://0" },
{ id = "sword_iron", name = "Espada de ferro", price = 450, icon = "rbxassetid://0" },
{ id = "boots_speed", name = "Botas de velocidade", price = 1200, icon = "rbxassetid://0" },
{ id = "pickaxe_gold", name = "Picareta de ouro", price = 3000, icon = "rbxassetid://0" },
}
2. Monte a grade a partir da tabela
Um frame de template, clonado por item. Acrescentar um item é uma linha no módulo, não um frame arrastado na tela.
-- StarterPlayerScripts (LocalScript)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local catalogo = require(ReplicatedStorage:WaitForChild("ShopCatalogue"))
local comprar = ReplicatedStorage:WaitForChild("BuyItem") -- um RemoteFunction
local gui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("Shop")
local grade = gui.Window:WaitForChild("Items") -- Frame com UIGridLayout
local modelo = gui.Window:WaitForChild("CardTemplate")
local aviso = gui.Window:WaitForChild("Status") -- TextLabel para as recusas
for _, item in catalogo do
local card = modelo:Clone()
card.Name = item.id
card.ItemName.Text = item.name
card.Price.Text = string.format("%d", item.price)
card.Icon.Image = item.icon
card.Visible = true
card.Parent = grade
card.Buy.Activated:Connect(function()
card.Buy.Active = false
-- O id é tudo que viaja. Nunca o preço.
local r = comprar:InvokeServer(item.id)
card.Buy.Active = true
aviso.Text = r.ok and "" or r.reason
end)
end
3. Recuse no servidor, com motivo
Um RemoteFunction porque a interface tem algo a dizer nos dois casos. Valide o id contra o catálogo que o próprio servidor dá require.
-- ServerScriptService (Script)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local catalogo = require(ReplicatedStorage:WaitForChild("ShopCatalogue"))
local porId = {}
for _, item in catalogo do
porId[item.id] = item
end
local comprar = Instance.new("RemoteFunction")
comprar.Name = "BuyItem"
comprar.Parent = ReplicatedStorage
local ultima: { [number]: number } = {}
comprar.OnServerInvoke = function(player: Player, itemId: unknown)
-- Tudo que chega aqui veio de um cliente: confira o tipo primeiro.
local item = type(itemId) == "string" and porId[itemId] or nil
if not item then return { ok = false, reason = "Esse item não existe." } end
local agora = os.clock()
if ultima[player.UserId] and agora - ultima[player.UserId] < 0.5 then
return { ok = false, reason = "Devagar." }
end
ultima[player.UserId] = agora
local stats = player:FindFirstChild("leaderstats")
local moedas = stats and stats:FindFirstChild("Coins")
if not moedas then return { ok = false, reason = "Os seus dados ainda estão carregando." } end
if moedas.Value < item.price then
return { ok = false, reason = `Faltam {item.price - moedas.Value} moedas.` }
end
moedas.Value = moedas.Value - item.price
-- entregarItem é seu: dá a ferramenta, ajusta a estatística, grava o save.
entregarItem(player, item.id)
return { ok = true, balance = moedas.Value }
end
4. Abra e feche a janela
Um botão, um Visible. Uma loja que entra com tween é mais bonita, mas uma loja que fecha é obrigatória.
local gui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("Shop")
gui.Window.Visible = false
gui.OpenButton.Activated:Connect(function()
gui.Window.Visible = not gui.Window.Visible
end)
gui.Window.CloseButton.Activated:Connect(function()
gui.Window.Visible = false
end)
RemoteEvent ou RemoteFunction
| RemoteEvent | RemoteFunction | |
|---|---|---|
| O cliente descobre por que foi recusado | só com um segundo evento | sim, no retorno |
| Trava o cliente enquanto espera | não | sim, até o servidor responder |
| Servidor lento | nada para | o botão espera; desative-o nesse meio |
| Use para | disparar e esquecer: um resgate, um giro | uma compra, onde a resposta importa |
Perguntas
O cliente pode mandar o preço?
Nunca. Tudo que um cliente manda é um valor que o jogador escolhe, então o preço tem de ser o do servidor — lido do módulo que o servidor dá require, por um id que ele validou. É o único erro que esvazia a economia de um jogo.
Por que a minha grade fica numa coluna só?
O UIGridLayout dimensiona as células por CellSize e CellPadding, e um pai sem largura não dá nada para ele dividir. Dê um tamanho real ao frame dos itens (ou um UIAspectRatioConstraint) e confira se o template não está Visible = false no clone — o clone fica com o que o template tinha.
Como mostrar "você já tem" em vez do preço?
Mande os ids que o jogador tem quando a loja abre (um evento) e troque o texto do botão quando o id estiver nesse conjunto. Decidir isso por uma lista no cliente aqui é aceitável: errar só mostra o rótulo errado, e o servidor continua recusando a segunda compra.
A loja fica no StarterGui ou é montada em código?
Monte a janela no StarterGui para ver enquanto desenha, e monte os cards em código a partir do catálogo. Um frame por item colocado na mão é a versão que fica velha na primeira vez que um preço muda.
Atualizado: Testado no Studio: Roblox Studio, outubro de 2026Autor: GustavoLicença: O código deste guia pode ser usado em qualquer experiência do Roblox, inclusive paga
Pack relacionado
