По большому счёту у нас осталась только одна, не раскрытая, задача — хранение данных игроков между игровыми сессиями. А всё остальное можно назвать причёсыванием, прилизыванием и расширением игрового опыта.
Пойдём по пути наименьшего сопротивления и сдобрим наше блюдо лёгким кодовым извращением. За основу возьмём модуль для работы с БД: 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
И приложить локальный сейв плейса: