Безопасный магазин в Roblox делится на две части: LocalScript сообщает серверу ID товара, а сервер сам проверяет цену, баланс и выдаёт предмет. Соберём покупку за Coins, после которой со счёта спишется 50 монет, а Tool появится в Backpack.
Кнопка магазина находится у игрока на клиенте. Монеты и награда должны контролироваться сервером. Если LocalScript сам уменьшает баланс или присылает цену, изменённый клиент сможет купить предмет бесплатно. Поэтому интерфейс просит о покупке, но не решает её исход.
LocalScript отправляет строку speed-coil через RemoteFunction.
Script находит товар в своём каталоге и сверяет Coins игрока.
Только после списания монет Tool копируется в Backpack и StarterGear.
Из каких частей состоит магазин в Roblox
В примере будет один тестовый товар SpeedCoil за 50 Coins. RemoteFunction подходит лучше RemoteEvent, потому что кнопке нужен ответ: покупка прошла или монет не хватает. Позже в серверный каталог можно добавить сколько угодно товаров.
| Часть | Где находится | Что хранит | Чему доверяем |
|---|---|---|---|
| Кнопка BuyButton | StarterGui | ID выбранного товара | Только факту нажатия |
| RemoteFunction BuyItem | ReplicatedStorage | Запрос и ответ | Аргументы всё равно проверяем |
| ShopServer | ServerScriptService | Цены, правила покупки | Серверу |
| SpeedCoil | ServerStorage | Шаблон награды | Серверу |
| Coins | leaderstats игрока | Текущий баланс | Значению на сервере |
Клиент передаёт только ID товара. Цена 50 и имя шаблона SpeedCoil никогда не приходят от кнопки.

Интерфейс показывает результат, но баланс и предмет меняет только ShopServer.
Подготовьте GUI, товар и серверные объекты
Сначала соберите минимальную структуру в Explorer. Дизайн окна можно менять как угодно, имена объектов из инструкции пока оставьте точными. Так WaitForChild() найдёт кнопку и подпись без бесконечного ожидания.
- В
ServerStorageсоздайтеToolс именемSpeedCoil. Для теста поставьтеRequiresHandle = false. После покупки пустой Tool появится в инвентаре, позже его можно заменить готовым оружием или инструментом. - В
StarterGuiдобавьтеScreenGuiс именемShopGui, внутри негоFrameс именемShopFrame. - Внутри
ShopFrameсоздайтеTextButtonBuyButtonиTextLabelStatusLabel. На кнопке напишитеBuy for 50 Coins. - В
ServerScriptServiceдобавьте обычныйScriptShopServer. ВнутрьBuyButtonпозже добавимLocalScript.
Если окно должно одинаково выглядеть на телефоне и компьютере, используйте Scale вместо фиксированных пикселей. Пошаговая настройка размеров и якорей есть в статье про адаптивный ScreenGui в Roblox. Здесь сосредоточимся на покупке.
Серверный скрипт: каталог, Coins и выдача Tool
ShopServer сам создаёт RemoteFunction и тестовый баланс 100 Coins. Каталог ITEMS остаётся на сервере. В нём лежат цена и имя Tool. Перед списанием код проверяет тип ID, наличие товара, частоту запросов, баланс и повторную покупку.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local buyItem = ReplicatedStorage:FindFirstChild("BuyItem")
if not buyItem then
buyItem = Instance.new("RemoteFunction")
buyItem.Name = "BuyItem"
buyItem.Parent = ReplicatedStorage
end
local ITEMS = {
["speed-coil"] = {
price = 50,
toolName = "SpeedCoil",
},
}
local lastRequest = {}
local function reply(ok, message)
return { ok = ok, message = message }
end
local function setupPlayer(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 100
coins.Parent = leaderstats
end
local function ownsTool(player, toolName)
local backpack = player:FindFirstChildOfClass("Backpack")
local starterGear = player:FindFirstChild("StarterGear")
return (backpack and backpack:FindFirstChild(toolName))
or (starterGear and starterGear:FindFirstChild(toolName))
end
local function purchase(player, itemId)
if typeof(itemId) ~= "string" then
return reply(false, "Invalid item")
end
local now = os.clock()
if now - (lastRequest[player] or 0) < 0.5 then
return reply(false, "Please wait")
end
lastRequest[player] = now
local item = ITEMS[itemId]
if not item then
return reply(false, "Item not found")
end
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
local template = ServerStorage:FindFirstChild(item.toolName)
local backpack = player:FindFirstChildOfClass("Backpack")
local starterGear = player:FindFirstChild("StarterGear")
if not coins or not template or not backpack or not starterGear then
return reply(false, "Shop is not ready")
end
if ownsTool(player, item.toolName) then
return reply(false, "Already purchased")
end
if coins.Value < item.price then
return reply(false, "Not enough coins")
end
coins.Value -= item.price
template:Clone().Parent = backpack
template:Clone().Parent = starterGear
return reply(true, "Purchased")
end
buyItem.OnServerInvoke = purchase
Players.PlayerAdded:Connect(setupPlayer)
Players.PlayerRemoving:Connect(function(player)
lastRequest[player] = nil
end)- Строка
speed-coilдолжна существовать в серверномITEMS. - Цена берётся из каталога, а баланс из серверного IntValue Coins.
- Повторный запрос раньше чем через 0,5 секунды отклоняется.
- Сначала проверяются все объекты, затем списываются монеты и выдаётся Tool.

Изменённая кнопка может соврать про цену, но не сможет изменить серверный каталог ITEMS.
Для настоящей игры начальные Coins нельзя каждый раз ставить в 100. Сохраняйте баланс и купленные предметы на сервере. Готовая схема загрузки и сохранения разобрана в статье про DataStore в Roblox.
LocalScript кнопки: отправляем ID и показываем ответ
Добавьте LocalScript внутрь BuyButton. Он временно отключает кнопку, вызывает сервер и выводит сообщение в StatusLabel. pcall() нужен на случай сетевой ошибки, чтобы кнопка не зависла навсегда.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buyItem = ReplicatedStorage:WaitForChild("BuyItem")
local button = script.Parent
local statusLabel = button.Parent:WaitForChild("StatusLabel")
local function onActivated()
if not button.Active then
return
end
button.Active = false
statusLabel.Text = "Checking..."
local success, result = pcall(function()
return buyItem:InvokeServer("speed-coil")
end)
if success and typeof(result) == "table" then
statusLabel.Text = result.message
else
statusLabel.Text = "Connection error"
end
button.Active = true
end
button.Activated:Connect(onActivated)После первого нажатия Coins меняется со 100 на 50, StatusLabel показывает Purchased, а SpeedCoil появляется в Backpack. Второе нажатие возвращает Already purchased и больше не списывает монеты.

Успех возвращается клиенту только после проверки каталога, баланса и выдачи предмета.
Почему магазин не работает или списывает монеты дважды
Откройте View, Output и запустите тест через Server and Clients хотя бы с двумя игроками. Проверяйте первую красную строку. Магазин связан сразу с GUI, сетью и серверным инвентарём, поэтому одиночный Play скрывает часть ошибок.
BuyButton не находит StatusLabel или BuyItem. Сверьте имена и убедитесь, что LocalScript лежит внутри кнопки.
В ServerStorage нет Tool SpeedCoil либо у игрока не появились Backpack, StarterGear или Coins.
Баланс правит LocalScript. Уберите списание с клиента и оставьте изменение coins.Value только в ShopServer.
Как сделать магазин в Roblox Studio за монеты?
Магазин за монеты состоит из ScreenGui, RemoteFunction и серверного Script. Кнопка отправляет ID товара, сервер проверяет Coins, списывает цену и клонирует Tool в Backpack.
Почему цену нельзя хранить только в LocalScript?
Цену нельзя доверять LocalScript, потому что изменённый клиент может отправить другое значение. Безопасная цена хранится в серверной таблице товаров.
Что выбрать для магазина: RemoteEvent или RemoteFunction?
RemoteFunction удобен, когда кнопке сразу нужен ответ Purchased или Not enough coins. RemoteEvent подходит для одностороннего запроса, но ответ придётся отправлять отдельным событием.
Как сохранить Coins после выхода из Roblox?
Coins сохраняют через DataStoreService на сервере. Тестовое значение 100 из статьи создаётся заново при каждом входе и для постоянной игры не подходит.
Почему купленный Tool пропадает после смерти?
Tool пропадает после смерти, если его положили только в Backpack. Добавьте вторую копию в StarterGear, а постоянные покупки сохраняйте в DataStore.
Добавьте второй товар в ITEMS, сделайте ещё одну кнопку и отправьте новый ID. Перед этим полезно разобрать границу клиента и сервера в Roblox и держать под рукой разбор ошибок из Output.
Условия, таблицы и функции из ShopServer можно закрепить в бесплатном курсе Lua. Общая последовательность от первого скрипта до своей игровой механики собрана в дорожной карте Lua и Roblox.
