← Все практические скрипты
Большие проекты · Уверенный · 55 мин

Остров звёзд: монеты, магазин и сохранение

Единый серверный скрипт обрабатывает звёзды, покупки, лаву, чекпоинт, победу и DataStore.

Иллюстрация: Остров звёзд: монеты, магазин и сохранение
🎯 Что получится

Монеты и победы сохраняются, когда доступен DataStore; игроки покупают ускорение, проходят препятствия и финишируют.

1. Подготовьте объект

Где разместить: ServerScriptService → IslandGameServer (Script), рядом с IslandWorldBuilder

Структура в Explorer
ServerScriptService
  IslandWorldBuilder (Script из урока 96)
  IslandGameServer (Script)
Workspace
  StarIsland (создаётся автоматически)
  1. Выполните урок 96. Оставьте IslandWorldBuilder в ServerScriptService.
  2. Добавьте там же Script IslandGameServer и вставьте код ниже.
  3. Запустите Play и подождите, пока статус загрузки не сменится на «Готово» или «Только эта сессия».
  4. Собирайте звёзды (+5 монет), покупайте ускорение за 20 монет, избегайте лавы.
  5. После чекпоинта проверьте гибель и появление на контрольной точке. Финиш даёт +30 монет и одну победу один раз за игровую сессию.
  6. Для сохранения опубликуйте ОТДЕЛЬНУЮ тестовую игру и разрешите Studio Access to API Services в настройках безопасности игры. Без доступа к DataStore работает сессионный режим.
  7. Не добавляйте другие скрипты leaderstats, DataStore, WalkSpeed и магазин из прежних уроков: это самостоятельный проект.
  8. Проверьте двух игроков и отсутствие ошибок в Output. Не публикуйте проект массово без испытаний.

2. Вставьте код

Script
-- Script: ServerScriptService/IslandGameServer
-- Единственный серверный скрипт монет, покупок и сохранения для этого проекта.
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local island = workspace:WaitForChild("StarIsland", 30)
assert(island, "Не найден StarIsland: добавьте IslandWorldBuilder")
local store = DataStoreService:GetDataStore("IgroStarIsland_v1")
local stars = island:WaitForChild("Stars")
local shop = island:WaitForChild("SpeedShop")
local prompt = shop:WaitForChild("ProximityPrompt")
local checkpointPart = island:WaitForChild("Checkpoint")
local finishPart = island:WaitForChild("Finish")

local PRICE = 20
local BOOST_SECONDS = 20
local BASE_SPEED = 16
local BOOST_SPEED = 25
local STAR_VALUE = 5
local RESPAWN_SECONDS = 12
local ready = {}
local canSave = {}
local saving = {}
local checkpoint = {}
local boostUntil = {}
local finished = {}
local shopping = {}

local function announce(player, message)
    player:SetAttribute("IslandNotice", message)
    player:SetAttribute("IslandNoticeNumber", (player:GetAttribute("IslandNoticeNumber") or 0) + 1)
end

local function playerFromHit(hit)
    local character = hit:FindFirstAncestorOfClass("Model")
    local player = character and Players:GetPlayerFromCharacter(character)
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    if player and humanoid and humanoid.Health > 0 then return player, humanoid end
    return nil, nil
end

local function savePlayer(player)
    if not canSave[player] or saving[player] then return end
    local stats = player:FindFirstChild("leaderstats")
    local coins = stats and stats:FindFirstChild("Coins")
    local wins = stats and stats:FindFirstChild("Wins")
    if not coins or not wins then return end
    saving[player] = true
    local snapshot = {Coins = coins.Value, Wins = wins.Value}
    local ok, err = pcall(function()
        store:UpdateAsync("player_" .. player.UserId, function(_old)
            -- Учебная версия: последняя запись побеждает. Не используйте
            -- параллельные игровые серверы без защиты сессии.
            return snapshot
        end)
    end)
    saving[player] = nil
    if ok then
        if player.Parent then player:SetAttribute("IslandSaveStatus", "Сохранено") end
    else
        warn("Остров: ошибка сохранения " .. player.UserId .. ": " .. tostring(err))
        if player.Parent then player:SetAttribute("IslandSaveStatus", "Ошибка сохранения") end
    end
end

local function setupPlayer(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    stats.Parent = player
    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Parent = stats
    local wins = Instance.new("IntValue")
    wins.Name = "Wins"
    wins.Parent = stats
    player:SetAttribute("IslandSaveStatus", "Загрузка")
    player:SetAttribute("IslandNotice", "")

    player.CharacterAdded:Connect(function(character)
        boostUntil[player] = nil
        local humanoid = character:WaitForChild("Humanoid", 10)
        if humanoid then humanoid.WalkSpeed = BASE_SPEED end
        local root = character:WaitForChild("HumanoidRootPart", 10)
        if root and checkpoint[player] then
            task.wait(0.1)
            if character.Parent then character:PivotTo(checkpoint[player]) end
        end
    end)

    local ok, data = pcall(function()
        return store:GetAsync("player_" .. player.UserId)
    end)
    if not player.Parent then return end
    if ok then
        if type(data) == "table" then
            coins.Value = math.clamp(math.floor(tonumber(data.Coins) or 0), 0, 1000000)
            wins.Value = math.clamp(math.floor(tonumber(data.Wins) or 0), 0, 1000000)
        end
        canSave[player] = true
        player:SetAttribute("IslandSaveStatus", "Готово")
    else
        canSave[player] = false
        player:SetAttribute("IslandSaveStatus", "Только эта сессия")
        warn("Остров: загрузка не удалась, сохранение выключено для " .. player.UserId)
    end
    ready[player] = true
end

Players.PlayerAdded:Connect(function(player) task.spawn(setupPlayer, player) end)
for _, player in ipairs(Players:GetPlayers()) do task.spawn(setupPlayer, player) end
Players.PlayerRemoving:Connect(function(player)
    savePlayer(player)
    ready[player], canSave[player], saving[player] = nil, nil, nil
    checkpoint[player], boostUntil[player], finished[player], shopping[player] = nil, nil, nil, nil
end)

-- Одна звезда доступна одному игроку за один цикл появления.
for _, star in ipairs(stars:GetChildren()) do
    if star:IsA("BasePart") then
        local available = true
        star.Touched:Connect(function(hit)
            if not available then return end
            local player = playerFromHit(hit)
            if not player or not ready[player] then return end
            local coins = player:FindFirstChild("leaderstats") and player.leaderstats:FindFirstChild("Coins")
            if not coins then return end
            available = false
            star.Transparency = 1
            star.CanTouch = false
            coins.Value += STAR_VALUE
            announce(player, "+" .. STAR_VALUE .. " монет за звезду")
            task.delay(RESPAWN_SECONDS, function()
                if star.Parent then
                    available = true
                    star.Transparency = 0
                    star.CanTouch = true
                end
            end)
        end)
    end
end

prompt.Triggered:Connect(function(player)
    if shopping[player] or not ready[player] then return end
    shopping[player] = true
    local character = player.Character
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    local root = character and character:FindFirstChild("HumanoidRootPart")
    local coins = player:FindFirstChild("leaderstats") and player.leaderstats:FindFirstChild("Coins")
    if not humanoid or humanoid.Health <= 0 or not root or (root.Position - shop.Position).Magnitude > 15 or not coins then
        shopping[player] = nil
        return
    end
    if os.clock() < (boostUntil[player] or 0) then
        announce(player, "Ускорение ещё действует")
    elseif coins.Value < PRICE then
        announce(player, "Нужно 20 монет")
    else
        coins.Value -= PRICE
        local untilTime = os.clock() + BOOST_SECONDS
        boostUntil[player] = untilTime
        humanoid.WalkSpeed = BOOST_SPEED
        announce(player, "Ускорение на 20 секунд!")
        task.delay(BOOST_SECONDS, function()
            if boostUntil[player] == untilTime then
                boostUntil[player] = nil
                if player.Character == character and humanoid.Parent and humanoid.Health > 0 then
                    humanoid.WalkSpeed = BASE_SPEED
                end
            end
        end)
    end
    shopping[player] = nil
end)

checkpointPart.Touched:Connect(function(hit)
    local player = playerFromHit(hit)
    if not player or not ready[player] then return end
    if checkpoint[player] then return end
    checkpoint[player] = checkpointPart.CFrame + Vector3.new(0, 5, 0)
    announce(player, "Контрольная точка активирована")
end)

for _, hazard in ipairs(island:GetChildren()) do
    if hazard:IsA("BasePart") and hazard.Name:match("^Lava%d+$") then
        hazard.Touched:Connect(function(hit)
            local _, humanoid = playerFromHit(hit)
            if humanoid then humanoid.Health = 0 end
        end)
    end
end

finishPart.Touched:Connect(function(hit)
    local player = playerFromHit(hit)
    if not player or not ready[player] or finished[player] then return end
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end
    finished[player] = true
    stats.Coins.Value += 30
    stats.Wins.Value += 1
    announce(player, "Победа! +30 монет. Вы прошли остров!")
end)

task.spawn(function()
    while true do
        task.wait(60)
        for _, player in ipairs(Players:GetPlayers()) do
            task.spawn(savePlayer, player)
        end
    end
end)

game:BindToClose(function()
    local remaining = 0
    for _, player in ipairs(Players:GetPlayers()) do
        remaining += 1
        task.spawn(function()
            savePlayer(player)
            remaining -= 1
        end)
    end
    local started = os.clock()
    while remaining > 0 and os.clock() - started < 25 do task.wait(0.1) end
end)

3. Проверьте в игре

Нажмите Play в Roblox Studio и проверьте действие механики. Если механика не работает, откройте окно Output и проверьте тип скрипта, его расположение и названия объектов.

Ваш прогресс

Когда проверите механику в Roblox Studio, отметьте практику как выполненную. Отметка хранится только в этом браузере, не в личном кабинете.

Если объединяете несколько скриптов

Добавляйте механики по одной и проверяйте окно Output. Не создавайте несколько одинаковых систем монет или интерфейсов; скрипты, меняющие WalkSpeed, прыжок или освещение, могут влиять друг на друга.

⭐ Три шага к собственной игре
1. Повторите и измените

Поменяйте цену ускорения с 20 на 15 монет.

2. Улучшите

Измените время повторного появления звёзд.

3. Придумайте своё

Добавьте вторую контрольную точку и продумайте, какая должна быть активной.

4. Проверьте себя

Почему сервер, а не LocalScript, списывает монеты за ускорение?

🛠 Если не работает

Ошибка GetAsync или UpdateAsync означает, что сохранение не гарантируется: проверьте публикацию тестового места и Studio API Services. Не запускайте второй скрипт Coins. В учебном примере нет защиты от одновременного входа на разные серверы. При неработающем магазине проверьте ProximityPrompt и дистанцию.