По большому счёту у нас осталась только одна, не раскрытая, задача — хранение данных игроков между игровыми сессиями. А всё остальное можно назвать причёсыванием, прилизыванием и расширением игрового опыта.
Пойдём по пути наименьшего сопротивления и сдобрим наше блюдо лёгким кодовым извращением. За основу возьмём модуль для работы с БД: ProfileStore (бывший ProfileService) и сделаем его данные доступными из любого места игры. Само собой что на стороне клиента они будут исключительно в режиме только для чтения. А вот на стороне сервера в полноценном варианте — на чтение и запись из любого скрипта.
Для начала, нам нужно добавить сам модуль ProfileStore. По указанной ссылке можно получить инструкции по скачиванию, установке и минимальному использованию. Поэтому на этом моменте я заострять внимание не буду. Скажу лишь, что в начале модуля есть комментарий с краткой шпаргалкой по его использованию.
Но прежде чем начнём что-то делать нужно немного определиться в том, что же можно хранить в БД? На самом деле не так уж и много: числа, слова, логические значения и таблицы из них состоящие. Собственно БД это и есть таблица со значениями.
И ещё одна мелочь. Документации с примерами на этот модуль практически нет. Авторы уверены что его и так легко понять и использовать. А значит.. я и буду его использовать так, как смог понять.
За основу в игре можем взять: серверный модуль и/или переменные в игроке. Значения из серверного модуля клиенту реплицироваться не будут, а вот переменные из игрока — будут. Есть только один момент — нельзя создать переменную с массивом. Но есть банальное решение — превратить таблицу в JSON строку. А для этого нам будет нужен HttpService. Он умеет как кодировать, так и декодировать таблицы и JSON строки. Но как отличать строковые данные от JSON записи? Тут можно использовать одно допущение — JSON будет всегда начинаться с открывающей квадратной скобки.
Что ещё нам может понадобиться? Темплейт — первичное описание структуры БД для каждого игрока со значениями по умолчанию. В DataStore2 имеется возможность доступа к значению в БД в ОЗУ из любого серверного скрипта без его предопределения. (Здесь этого нет, т.к. используется другой подход. Вероятно по этой причине он мне более симпатичен.) Итак, переходим к созданию темплейта. Для этого запустим нашу игру и посмотрим, какие данные у нас уже имеются и что их них мы можем, а тем более должны сохранить в БД. Ну и сразу создадим модуль в который их и запишем в виде таблицы значений. Первое что у нас есть это сытость и здоровье персонажа. А второе — конечно содержимое инвентаря. Собственно пока что у нас больше ничего и нет. Вот и создадим DB_Template:
-- Состав БД по умолчанию для нового игрока return { Satiety = 100, -- сытость по умолчанию Health = 100, -- здоровье Inventory = {}, -- наш инвентарь }
Следующий шаг, это скрипт что будет загружать данные по игроку при его входе, фиксировать их изменение и сохранять периодически в БД и при выходе игрока из игры. Так и назовём наш скрипт — DB_Preloaded. Само собой, код у нас будет на основе имеющегося в образцах:
local Players = game:GetService("Players") local HttpService = game:GetService("HttpService") local ServerStorage = game:GetService("ServerStorage") local ProfileStore = require(ServerStorage.ProfileStore) -- темплейт БД по умолчанию local PROFILE_TEMPLATE = require(ServerStorage:WaitForChild("DB_Template")) type TypedProfile = ProfileStore.Profile<typeof(PROFILE_TEMPLATE)> local PlayerStore = ProfileStore.New("PlayerStore", PROFILE_TEMPLATE) -- БД в ОЗУ сервера для игроков local Profiles: {[Player]: TypedProfile} = require(ServerStorage:WaitForChild("DB_Storage")) -- но лучше менять значение в самом player.DataBase так оно будет всем доступно -- соответственно на сервере для записи, а на клиенте только для чтения local function PlayerAdded(player) -- Начало сессии конкретного игрока local profile = PlayerStore:StartSessionAsync(`{player.UserId}`, { Cancel = function() return player.Parent ~= Players end, }) -- Проверка нового профиля и возможность загрузки if profile ~= nil then profile:AddUserId(player.UserId) -- GDPR compliance profile:Reconcile() -- Заполнение отсутствующих значений из темплейта profile.OnSessionEnd:Connect(function() Profiles[player] = nil player:Kick(`Profile session end - Please rejoin`) end) if player.Parent == Players then Profiles[player] = profile print(`Profile loaded for {player.Name}!`) -- флаг успешной загрузки БД local loadedDB = Instance.new("BoolValue") loadedDB.Name = "LoadedDB" loadedDB.Value = true loadedDB.Parent = player else -- Игрок ушёл до начала сеанса profile:EndSession() return end else -- Это состояние должно возникать только при отключении сервера Roblox player:Kick(`Profile load fail - Please rejoin`) return end ------------------------------------------------------------- --warn(Profiles) -- общая таблица всех игроков -- Работа с БД конкретного игрока local BD = Profiles[player].Data -- -- BD.NewTest = "Test" -- добавить новое -- BD.Test = nil -- удалить существующее -- BD.Cash += 100 -- изменить существующее -- print(BD) -- БД конкретного игрока ------------------------------------------------------------- print("-- create local database:", player) local playerBD = Instance.new("Folder") playerBD.Name = "DataBase" playerBD.Parent = player for a, b in pairs(BD) do -- print(a, b) local p = nil if type(b) == "table" then local c = HttpService:JSONEncode(b) p = Instance.new("StringValue") p.Name = a p.Value = c p.Parent = playerBD elseif type(b) == "string" then p = Instance.new("StringValue") p.Name = a p.Value = b p.Parent = playerBD elseif type(b) == "number" then p = Instance.new("NumberValue") p.Name = a p.Value = b p.Parent = playerBD elseif type(b) == "boolean" then p = Instance.new("BoolValue") p.Name = a p.Value = b p.Parent = playerBD else warn("NO CORRECT BD VALUE TYPE") end if p ~= nil then -- при изменении значения изменится и в БД p.Changed:Connect(function(value) warn(p.Name, value) if type(value) == "string" then if string.sub(value,1,1) == "[" then -- JSON Profiles[player].Data[a] = HttpService:JSONDecode(value) else Profiles[player].Data[a] = value end else Profiles[player].Data[a] = value end end) end end -- флаг загрузки БД игрока local BDloaded = Instance.new("BoolValue") BDloaded.Value = true BDloaded.Name = "BDloaded" BDloaded.Parent = player print("-- end create database:", player) -- любой загруженный персонаж даёт сигнал о загруженности игры workspace.GameLoaded.Value = true end -- От шустрых игроков for _, player in Players:GetPlayers() do task.spawn(PlayerAdded, player) end Players.PlayerAdded:Connect(PlayerAdded) -- Отключение игрока Players.PlayerRemoving:Connect(function(player) local profile = Profiles[player] if profile ~= nil then print(player,"remove from game. End DB session.") profile:EndSession() end end)
DB_Storage — это не что иное как пустая таблица в модуле:
-- это временное СЕРВЕРНОЕ хранилище загруженных данных по игрокам в ОЗУ игры return {}
А GameLoaded это просто булева переменная в workspace.

Увидев такое сообщение (в первой строке скриншота) не стоит пугаться. Это всего лишь говорит нам о том, что у нас игра не опубликована с разрешением с доступом к API Роблокса. Т.е. значения не будут сохраняться в БД.
Собственно, на этом всё — минимум для работы с БД сделан. Теперь достаточно в игре изменить значение переменой в DataBase у игрока и это значение сохранится в БД между сессиями игры.
Теперь изменим скрипт PlayerLoaded, чтобы наши данные из БД интегрировать в загрузку игрока:
-- будет работать при подключении игрока и спавне/респавне персонажей local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local HttpService = game:GetService("HttpService") local config = require( ReplicatedStorage:WaitForChild("GameConfig") ) local function onCharacterAdded(character) print(character.Name .. " has spawned") local player = Players:GetPlayerFromCharacter(character) local statuses = player:FindFirstChild("Statuses") -- привяжем изменение здоровья local humanoid = character:FindFirstChild("Humanoid") humanoid.HealthChanged:Connect(function() local BD = player:FindFirstChild("DataBase") if BD then -- игнорируем, если если БД ещё не загружена local healthBD = BD:FindFirstChild("Health") if healthBD then healthBD.Value = humanoid.Health end end end) end local function onCharacterRemoving(character) print(character.Name .. " is despawning") end local function onPlayerAdded(player) player.CharacterAdded:Connect(onCharacterAdded) player.CharacterRemoving:Connect(onCharacterRemoving) -- добавить папку инвентаря local invent = Instance.new("Folder") invent.Name = "Inventory" invent.Parent = player -- добавить папку статусов local statuses = Instance.new("Folder") statuses.Name = "Statuses" statuses.Parent = player -- добавить значение сытости local satiety = Instance.new("NumberValue") satiety.Name = "Satiety" satiety.Value = config.SatietyMaximum -- первичное значение satiety.Parent = statuses if statuses then -- для установки стартового значения сытости после воскрешения local satiety = statuses:FindFirstChild("Satiety") if satiety then satiety.Value = config.SatietyMaximum end end -- подождём пока БД данного игрока загрузится local flagBD = false repeat task.wait() until player:FindFirstChild("BDloaded") ~= nil -- обновим значение сытости из БД в набор статусов local satietyBD = player.DataBase:FindFirstChild("Satiety") if satietyBD then satiety.Value = satietyBD.Value -- привяжем изменение статуса к БД satiety.Changed:Connect(function() satietyBD.Value = satiety.Value end) end -- восстановим инвентарь local inventBD = player.DataBase:FindFirstChild("Inventory") if inventBD then local inventory = HttpService:JSONDecode(inventBD.Value) -- вернули к таблице for a, b in pairs(inventory) do local p = nil if type(b) == "table" then local c = HttpService:JSONEncode(b) p = Instance.new("StringValue") p.Name = a p.Value = c p.Parent = invent elseif type(b) == "string" then p = Instance.new("StringValue") p.Name = a p.Value = b p.Parent = invent elseif type(b) == "number" then p = Instance.new("NumberValue") p.Name = a p.Value = b p.Parent = invent elseif type(b) == "boolean" then p = Instance.new("BoolValue") p.Name = a p.Value = b p.Parent = invent else warn("NO CORRECT BD VALUE TYPE") end end -- при изменениях в инвентаре, нужно изменить в БД local connection = {} local function InventChange(flag) if flag then -- добавлено или удалено if flag.Parent == nil then -- удалено connection[flag] = nil else -- добавлено if connection[flag] == nil then connection[flag] = flag.Changed:Connect(function() InventChange() -- у такого вызова нет параметра end) end end else -- значение было изменено end -- собираем всех в массив local toBD = {} for a, b in pairs(invent:GetChildren()) do toBD[b.Name] = b.Value end local JSON = HttpService:JSONEncode(toBD) inventBD.Value = JSON end invent.ChildAdded:Connect(InventChange) -- у этих функций есть параметр invent.ChildRemoved:Connect(InventChange) end end Players.PlayerAdded:Connect(onPlayerAdded)
Немного пробежавшись, контролим что все значения дублируются в папке БД в нужном виде.

Нам остаётся опубликовать игру и включить работу с API чтобы проверить между запусками сохранение значений.

Запустим игру, чего нибудь набьём и выйдем. А после запустим ещё раз и посмотрим на наш инвентарь, здоровье и сытость.

Работает всё.. кроме восстановления уровня здоровья у персонажа. Можно, конечно, и так оставить, но можно и просто добавить в конец onPlayerAdded кусок кода:
-- установим значение здоровья персонажа из БД local character = player.Character if character then local humanoid = character:FindFirstChild("Humanoid") if humanoid then humanoid.Health = player.DataBase.Health.Value end end
Вот теперь всё, касающееся БД работает как нужно.
По факту, минимальный скелет игры готов. Остаётся нарастить мышцы (игровые моменты), натянуть кожу (добавить озвучку) и конечно одеть нашу игрушку (сделать приличный визуал). Т.е. прилизать текущий макет игры.
Укажу текущую ссылку на игру: RPG 09
И приложить локальный сейв плейса: