Roblox — База данных — DataBase Roblox

Автор: | 22 марта, 2025
Поделиться...

По большому счёту у нас осталась только одна, не раскрытая, задача — хранение данных игроков между игровыми сессиями. А всё остальное можно назвать причёсыванием, прилизыванием и расширением игрового опыта.

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

И приложить локальный сейв плейса:


Поделиться...

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *