Roblox · Pet Simulator · урок 5

Сохранение данных

Монеты, кристаллы и питомцы сохраняются: вышел и зашёл — всё на месте.

Структура

Синим — что добавляем на этом уроке.

ServerScriptService
└─ MainScript (Script) — вызывает Load и Save
   └─ SaveData (ModuleScript)

Шаги

  1. Опубликуй игру (File → Publish to Roblox).
  2. Game Settings → Security → Enable Studio Access to API Services — без этого DataStore в Studio не работает.
  3. В MainScript — ModuleScript «SaveData». Модуль, а не отдельный скрипт: MainScript сначала создаёт папки игрока, потом загружает в них данные — порядок всегда правильный.
  4. Сохраняются только leaderstats, Pets и Areas. Папку Values (там ObjectValue с объектом) сохранять нельзя — DataStore хранит только числа, текст и таблицы.
  5. Если загрузка не удалась (например, нет доступа к API), сохранение для игрока выключается: иначе пустые данные затёрли бы настоящие.
  6. BindToClose: при выключении сервера скрипт сохраняет всех и ждёт, пока сохранения закончатся.

Скрипты

SaveData

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Загрузка и сохранение данных игрока.

-- хранилище данных Roblox
local DSS = game:GetService("DataStoreService")
-- все игроки
local Players = game:GetService("Players")
-- наше хранилище
local dataStore = DSS:GetDataStore("PetSimulatorData")
-- какие папки игрока сохраняем (Values — нет)
local SAVE = {"leaderstats", "Pets", "Areas"}
-- таблица модуля
local saveData = {}
-- у кого данные загрузились
local loaded = {}

-- загрузить данные игрока
function saveData.Load(player)
    -- запрос может не пройти
    local ok, data = pcall(function()
        -- данные по номеру игрока
        return dataStore:GetAsync(player.UserId .. "Key")
    -- конец попытки
    end)
    -- не загрузилось
    if not ok then
        -- без загрузки не сохраняем — иначе затрём
        warn("Данные не загрузились (" .. tostring(data) .. ") — сохранение для " .. player.Name .. " выключено")
        -- играем без сохранения
        return
    -- конец проверки
    end
    -- можно сохранять
    loaded[player] = true
    -- новый игрок — данных ещё нет
    if not data then return end
    -- каждая сохраняемая папка
    for _, folderName in ipairs(SAVE) do
        -- папка в игроке
        local folder = player:FindFirstChild(folderName)
        -- что сохранено
        local saved = data[folderName]
        -- есть и то и другое
        if folder and saved then
            -- каждое значение
            for _, item in ipairs(saved) do
                -- такое значение уже есть (Coins, Gems)
                local existing = folder:FindFirstChild(item[1])
                -- есть
                if existing then
                    -- просто меняем число
                    existing.Value = item[2]
                -- нет (питомцы, области)
                else
                    -- создаём того же класса
                    local value = Instance.new(item[3])
                    -- имя
                    value.Name = item[1]
                    -- значение
                    value.Value = item[2]
                    -- в папку
                    value.Parent = folder
                -- конец проверки
                end
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- сохранить данные игрока
function saveData.Save(player)
    -- не загружали — не трогаем
    if not loaded[player] then return end
    -- второй раз не сохраняем
    loaded[player] = nil
    -- что запишем
    local data = {}
    -- каждая папка
    for _, folderName in ipairs(SAVE) do
        -- в игроке
        local folder = player:FindFirstChild(folderName)
        -- есть
        if folder then
            -- список значений
            data[folderName] = {}
            -- каждое значение
            for _, value in ipairs(folder:GetChildren()) do
                -- имя, значение, класс
                table.insert(data[folderName], {value.Name, value.Value, value.ClassName})
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- запись может не пройти
    local ok, err = pcall(function()
        -- записываем
        dataStore:SetAsync(player.UserId .. "Key", data)
    -- конец попытки
    end)
    -- результат в Output
    if ok then print("Сохранено: " .. player.Name) else warn(err) end
-- конец функции
end

-- сохранить всех и дождаться (выключение сервера)
function saveData.SaveAll()
    -- сколько сохранений ещё идёт
    local left = 0
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- ещё одно
        left += 1
        -- параллельно
        task.spawn(function()
            -- сохраняем
            saveData.Save(player)
            -- готово
            left -= 1
        -- конец потока
        end)
    -- конец перебора
    end
    -- ждём все сохранения
    while left > 0 do task.wait() end
-- конец функции
end

-- отдаём таблицу модуля
return saveData

MainScript

Где: ServerScriptService · Script
Зачем: + загрузка при входе, сохранение при выходе и выключении.

-- все игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- модуль питомцев
local pet = require(script.Pet)
-- модуль целей
local drop = require(script.Pet.Drop)
-- сохранение
local saveData = require(script.SaveData)
-- события
local events = RS:WaitForChild("Events")
-- надеть / снять питомца
local equipFunction = events:WaitForChild("EquipPetFunction")
-- образцы питомцев
local pets = RS:WaitForChild("Pets")
-- питомцы игроков на карте
local playerPets = workspace:WaitForChild("PlayerPets")
-- цели
local drops = workspace:WaitForChild("Drops")
-- сколько питомцев можно взять с собой
local MAX_EQUIPPED = 3

-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
    -- папка для его питомцев
    local folder = Instance.new("Folder")
    -- имя = имя игрока
    folder.Name = player.Name
    -- в PlayerPets
    folder.Parent = playerPets
    -- папки из PlayerInfo
    for _, info in ipairs(script.PlayerInfo:GetChildren()) do
        -- копия — в игрока
        info:Clone().Parent = player
    -- конец перебора
    end
    -- загружаем монеты, питомцев, области
    saveData.Load(player)
-- конец обработчика
end)

-- игрок вышел
Players.PlayerRemoving:Connect(function(player)
    -- сохраняем
    saveData.Save(player)
    -- его папка
    local folder = playerPets:FindFirstChild(player.Name)
    -- убираем питомцев
    if folder then folder:Destroy() end
-- конец обработчика
end)

-- сервер выключается — сохраняем всех и ждём
game:BindToClose(saveData.SaveAll)

-- подготовить цель
local function SetupDrop(dropObject)
    -- полоска здоровья
    drop.UpdateGui(dropObject)
    -- клик по цели — подключаем ОДИН раз
    dropObject.ClickDetector.MouseClick:Connect(function(player)
        -- питомцы атакуют
        pet.Attack(player, dropObject)
    -- конец обработчика
    end)
-- конец функции
end
-- цели, что уже стоят
for _, obj in ipairs(drops:GetDescendants()) do
    -- это цель
    if obj:IsA("Model") and obj:FindFirstChild("ClickDetector") then SetupDrop(obj) end
-- конец перебора
end
-- цель появилась снова
drops.DescendantAdded:Connect(function(obj)
    -- подключаем и её
    if obj:IsA("Model") and obj:FindFirstChild("ClickDetector") then SetupDrop(obj) end
-- конец обработчика
end)
-- отдельный поток
task.spawn(function()
    -- каждую секунду
    while task.wait(1) do
        -- питомцы наносят урон
        pet.Tick()
    -- конец цикла
    end
-- конец потока
end)

-- игрок нажал на иконку питомца
equipFunction.OnServerInvoke = function(player, petName, equip)
    -- прислали не текст
    if typeof(petName) ~= "string" then return false end
    -- его питомцы на карте
    local folder = playerPets:FindFirstChild(player.Name)
    -- образец
    local template = pets:FindFirstChild(petName)
    -- нет папки или такого питомца
    if not folder or not template then return false end
    -- снять
    if not equip then
        -- этот питомец на карте
        local active = folder:FindFirstChild(petName)
        -- убираем
        if active then active:Destroy() end
        -- готово
        return true
    -- конец проверки
    end
    -- сколько таких есть и сколько уже надето
    local owned, active = 0, 0
    -- купленные питомцы
    for _, value in ipairs(player.Pets:GetChildren()) do
        -- считаем таких
        if value.Name == petName then owned += 1 end
    -- конец перебора
    end
    -- надетые
    for _, petModel in ipairs(folder:GetChildren()) do
        -- считаем таких
        if petModel.Name == petName then active += 1 end
    -- конец перебора
    end
    -- не свой или уже максимум
    if active >= owned or #folder:GetChildren() >= MAX_EQUIPPED then return false end
    -- питомец
    local clone = template:Clone()
    -- появляется у хозяина
    if player.Character then clone:PivotTo(player.Character:GetPivot()) end
    -- сразу к текущей цели
    clone.Attack.Value = player.Values.Target.Value
    -- на карту
    clone.Parent = folder
    -- надет
    return true
-- конец функции
end

Проверка

Запусти Play (или Run) и убедись: