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)

Шаги

  1. Монета: Part, Shape — Cylinder, размер 0.4 × 2.4 × 2.4 (стоит ребром), Anchored вкл., CanCollide выкл. Workspace → Folder «Coins», монеты — туда; копий можно сколько угодно.
  2. Ценность монеты — атрибут: выдели монету → Properties → Attributes → «+» → имя Amount, тип number, значение 1. Атрибут Respawn — через сколько секунд монета вернётся (по умолчанию 10).
  3. ServerScriptService → ModuleScript «Coins» и Script «CoinsServer». Модуль — прямо в ServerScriptService: его подключают и другие скрипты — require(game.ServerScriptService.Coins).
  4. StarterGui → ScreenGui «CoinsGui» (ResetOnSpawn выкл.) → Frame с Icon, TextLabel «Amount» и «Multiplier» (Visible выкл.), UIScale. В CoinsGui — Sound «Pickup» и LocalScript «CoinsLocal».
  5. Как работает: деньги меняет только сервер (Coins.Add / Coins.Spend) — подделать их с компьютера игрока нельзя. Счётчик и вращение монет — у игрока: так плавно и без нагрузки на сервер.
  6. Сохранение: опубликуй игру и включи Game Settings → Security → Enable Studio Access to API Services. Если загрузка не удалась, сохранение для игрока выключается — пустой кошелёк не затрёт настоящий.
  7. В своих скриптах: local Coins = require(game.ServerScriptService.Coins); Coins.Add(player, 5) — дать, if Coins.Spend(player, 100) then … end — купить.

Скрипты

Coins

Где: 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

CoinsServer

Где: 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)

CoinsLocal

Где: 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) и убедись:

Собрать систему монет одной командой