Остров звёзд: монеты, магазин и сохранение
Единый серверный скрипт обрабатывает звёзды, покупки, лаву, чекпоинт, победу и DataStore.
Монеты и победы сохраняются, когда доступен DataStore; игроки покупают ускорение, проходят препятствия и финишируют.
1. Подготовьте объект
Где разместить: ServerScriptService → IslandGameServer (Script), рядом с IslandWorldBuilder
ServerScriptService IslandWorldBuilder (Script из урока 96) IslandGameServer (Script) Workspace StarIsland (создаётся автоматически)
- Выполните урок 96. Оставьте IslandWorldBuilder в ServerScriptService.
- Добавьте там же Script IslandGameServer и вставьте код ниже.
- Запустите Play и подождите, пока статус загрузки не сменится на «Готово» или «Только эта сессия».
- Собирайте звёзды (+5 монет), покупайте ускорение за 20 монет, избегайте лавы.
- После чекпоинта проверьте гибель и появление на контрольной точке. Финиш даёт +30 монет и одну победу один раз за игровую сессию.
- Для сохранения опубликуйте ОТДЕЛЬНУЮ тестовую игру и разрешите Studio Access to API Services в настройках безопасности игры. Без доступа к DataStore работает сессионный режим.
- Не добавляйте другие скрипты leaderstats, DataStore, WalkSpeed и магазин из прежних уроков: это самостоятельный проект.
- Проверьте двух игроков и отсутствие ошибок в Output. Не публикуйте проект массово без испытаний.
2. Вставьте код
-- 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, прыжок или освещение, могут влиять друг на друга.
Поменяйте цену ускорения с 20 на 15 монет.
Измените время повторного появления звёзд.
Добавьте вторую контрольную точку и продумайте, какая должна быть активной.
4. Проверьте себя
Почему сервер, а не LocalScript, списывает монеты за ускорение?
Ошибка GetAsync или UpdateAsync означает, что сохранение не гарантируется: проверьте публикацию тестового места и Studio API Services. Не запускайте второй скрипт Coins. В учебном примере нет защиты от одновременного входа на разные серверы. При неработающем магазине проверьте ProximityPrompt и дистанцию.