Roblox · Системы
Основа экономики игры: у каждого игрока кошелёк (виден по Tab и в углу экрана), монеты на карте вращаются, подбираются касанием и возвращаются, монеты сохраняются между заходами. Модуль Coins — единая точка для всех покупок и наград: магазин, сундук, магнит, двойные монеты работают через него.
Синим — что нужно создать.
Workspace └─ Coins (Folder) — монеты на карте └─ Coin (Part) — Cylinder, Anchored, CanCollide выкл.; атрибуты Amount и Respawn ServerScriptService ├─ Coins (ModuleScript) — Add, Spend, Get, Collect └─ CoinsServer (Script) — кошелёк, сохранение, подбор StarterGui └─ CoinsGui (ScreenGui) — ResetOnSpawn выкл. ├─ Frame (Frame) — плашка справа вверху │ ├─ Icon (Frame) — золотой кружок │ ├─ Amount (TextLabel) — число монет │ ├─ Multiplier (TextLabel) — «×2», Visible выкл. │ └─ UIScale (UIScale) — для «прыжка» плашки ├─ Pickup (Sound) — звук подбора └─ CoinsLocal (LocalScript)
Где: ServerScriptService · ModuleScript
Зачем: Кошелёк: дать, потратить, узнать, подобрать монету.
-- таблица модуля
local Coins = {}
-- через сколько секунд подобранная монета появляется снова
local RESPAWN = 10
-- кошелёк игрока
local function Wallet(player)
-- его таблица
local stats = player:FindFirstChild("leaderstats")
-- IntValue Coins или nil
return stats and stats:FindFirstChild("Coins")
-- конец функции
end
-- сколько монет у игрока
function Coins.Get(player)
-- кошелёк
local wallet = Wallet(player)
-- число
return wallet and wallet.Value or 0
-- конец функции
end
-- дать монеты; возвращает, сколько дали на самом деле
function Coins.Add(player, amount)
-- кошелёк
local wallet = Wallet(player)
-- кошелька ещё нет
if not wallet then return 0 end
-- с учётом «двойных монет»
local total = math.floor(amount * (player:GetAttribute("CoinMultiplier") or 1))
-- кладём
wallet.Value += total
-- отдаём
return total
-- конец функции
end
-- потратить; true — если хватило
function Coins.Spend(player, amount)
-- кошелёк
local wallet = Wallet(player)
-- не хватает
if not wallet or wallet.Value < amount then return false end
-- списываем
wallet.Value -= amount
-- куплено
return true
-- конец функции
end
-- игрок подобрал монету с карты
function Coins.Collect(player, coin)
-- её уже подобрали
if not coin.CanTouch then return end
-- второй раз не подобрать
coin.CanTouch = false
-- исчезает
coin.Transparency = 1
-- в кошелёк; атрибут Amount — ценность монеты
Coins.Add(player, coin:GetAttribute("Amount") or 1)
-- через несколько секунд
task.delay(coin:GetAttribute("Respawn") or RESPAWN, function()
-- снова видна
coin.Transparency = 0
-- и подбирается
coin.CanTouch = true
-- конец отложенной функции
end)
-- конец функции
end
-- отдаём таблицу модуля
return CoinsГде: ServerScriptService · Script
Зачем: Кошелёк каждому игроку, сохранение и подбор монет.
-- все игроки
local Players = game:GetService("Players")
-- сохранения
local DSS = game:GetService("DataStoreService")
-- модуль монет
local Coins = require(script.Parent:WaitForChild("Coins"))
-- хранилище монет
local store = DSS:GetDataStore("CoinsData")
-- монеты на карте
local folder = workspace:WaitForChild("Coins")
-- сколько монет у нового игрока
local START = 0
-- у кого сохранение загрузилось
local loaded = {}
-- загрузить монеты
local function Load(player)
-- запрос может не пройти
local ok, saved = pcall(function()
-- по номеру игрока
return store:GetAsync("coins_" .. player.UserId)
-- конец попытки
end)
-- не загрузилось (например, Studio без API Services)
if not ok then
-- без загрузки не сохраняем — иначе затрём
warn("Монеты не загрузились — сохранение для " .. player.Name .. " выключено")
-- играем без сохранения
return
-- конец проверки
end
-- можно сохранять
loaded[player] = true
-- сохранённые монеты или стартовые
player.leaderstats.Coins.Value = saved or START
-- конец функции
end
-- сохранить монеты
local function Save(player)
-- не загружали — не трогаем
if not loaded[player] then return end
-- второй раз не сохраняем
loaded[player] = nil
-- сколько монет
local value = Coins.Get(player)
-- запись может не пройти
local ok, err = pcall(function()
-- записываем
store:SetAsync("coins_" .. player.UserId, value)
-- конец попытки
end)
-- причина — в Output
if not ok then warn(err) end
-- конец функции
end
-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
-- таблица по Tab (общая с другими системами)
local stats = player:FindFirstChild("leaderstats") or Instance.new("Folder")
-- имя, которое понимает Roblox
stats.Name = "leaderstats"
-- в игрока
stats.Parent = player
-- кошелёк
local wallet = Instance.new("IntValue")
-- имя
wallet.Name = "Coins"
-- пока стартовое
wallet.Value = START
-- в таблицу
wallet.Parent = stats
-- загружаем сохранение
Load(player)
-- конец обработчика
end)
-- вышел — сохраняем
Players.PlayerRemoving:Connect(Save)
-- сервер выключается (и Stop в Studio)
game:BindToClose(function()
-- сколько сохранений идёт
local left = 0
-- каждый игрок
for _, player in ipairs(Players:GetPlayers()) do
-- ещё одно
left += 1
-- параллельно
task.spawn(function()
-- сохраняем
Save(player)
-- готово
left -= 1
-- конец потока
end)
-- конец перебора
end
-- ждём все сохранения
while left > 0 do task.wait() end
-- конец функции
end)
-- подключить монету
local function Setup(coin)
-- монета — одна деталь
if not coin:IsA("BasePart") then return end
-- монеты кто-то коснулся
coin.Touched:Connect(function(otherPart)
-- это персонаж игрока?
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
-- да — подбирает
if player then Coins.Collect(player, coin) end
-- конец обработчика
end)
-- конец функции
end
-- монеты, что уже лежат
for _, coin in ipairs(folder:GetChildren()) do Setup(coin) end
-- и новые
folder.ChildAdded:Connect(Setup)Где: StarterGui → CoinsGui · LocalScript
Зачем: Счётчик на экране, звук и вращение монет.
-- события каждого кадра
local RunService = game:GetService("RunService")
-- анимации
local TS = game:GetService("TweenService")
-- этот игрок
local player = game.Players.LocalPlayer
-- его кошелёк
local wallet = player:WaitForChild("leaderstats"):WaitForChild("Coins")
-- CoinsGui
local gui = script.Parent
-- плашка
local frame = gui:WaitForChild("Frame")
-- число монет
local amount = frame:WaitForChild("Amount")
-- надпись «×2»
local multiplier = frame:WaitForChild("Multiplier")
-- масштаб плашки — для «прыжка»
local scale = frame:WaitForChild("UIScale")
-- звук подбора
local pickup = gui:WaitForChild("Pickup")
-- монеты на карте
local coinsFolder = workspace:WaitForChild("Coins")
-- скорость вращения монет (радиан в секунду)
local SPIN = 2
-- сколько было
local last = wallet.Value
-- обновить счётчик
local function Update()
-- число
amount.Text = tostring(wallet.Value)
-- монет стало больше
if wallet.Value > last then
-- звук
pickup:Play()
-- плашка подпрыгивает
scale.Scale = 1.25
-- и плавно возвращается
TS:Create(scale, TweenInfo.new(0.25, Enum.EasingStyle.Back), {Scale = 1}):Play()
-- конец проверки
end
-- запоминаем
last = wallet.Value
-- конец функции
end
-- показать «×2», пока действуют двойные монеты
local function UpdateMultiplier()
-- множитель
local m = player:GetAttribute("CoinMultiplier") or 1
-- больше 1 — показываем
multiplier.Visible = m > 1
-- текст
multiplier.Text = "×" .. m
-- конец функции
end
-- показываем сразу
Update()
-- и множитель
UpdateMultiplier()
-- монеты изменились
wallet.Changed:Connect(Update)
-- множитель изменился
player:GetAttributeChangedSignal("CoinMultiplier"):Connect(UpdateMultiplier)
-- каждый кадр
RunService.RenderStepped:Connect(function(dt)
-- каждая монета
for _, coin in ipairs(coinsFolder:GetChildren()) do
-- видимая
if coin:IsA("BasePart") and coin.CanTouch then
-- поворачиваем — только у себя, серверу не мешает
coin.CFrame = coin.CFrame * CFrame.Angles(0, SPIN * dt, 0)
-- конец проверки
end
-- конец перебора
end
-- конец обработчика
end)Запусти Play (или Run) и убедись: