Pular para o conteúdo
PTEntrar

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

RemoteEventRemoteFunction
O cliente descobre por que foi recusadosó com um segundo eventosim, no retorno
Trava o cliente enquanto esperanãosim, até o servidor responder
Servidor lentonada parao botão espera; desative-o nesse meio
Use paradisparar e esquecer: um resgate, um girouma 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

Ultimate Script Pack

Ultimate Script Pack

US$ 40Ver o pack

Quero ser avisado

Em breve

Deixe seu e-mail e a gente te avisa na hora em que este pack sair.