ROBLOX — Создание Leaderboard

Автор: | 13 ноября, 2022
Поделиться...

Давно хотел написать урок по созданий доски лидеров и.. тут получил тестовое задание по его написанию. Так уж сошлись звёзды.

Когда-то давно, года так два назад, понадобилась мне данная фича и на просторах интернета я нашёл нечто подходящее. На основе этого и делал свою таблицу лидеров.

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

Добавочное условие — сама таблица лидеров должна быть не зависима от всего, что происходит в игре и пользоваться только данными из БД. Таким образом получим, что БД меняется в любом месте игры.

Подготовка стенда

Создаём новый плейс в Roblox Studio.

Ставим парт и растягиваем его до нужных размеров.

Вешаем на него SurfaceGui. (По факту этот тот-же ScreenGui, но ограниченный конкретной гранью парта на который подвешен.) Указываем ему Face = Front, просто для удобства.

На данный SurfaceGui вешаем ScrollingFrame (нам же нужна прокрутка). И обычный Frame для заголовков будущего списка. И туда же Script (серверный) для работы с БД. Выглядеть это будет примерно так: (названия не критичны)

Пока что ничего сложного. Можно сказать, что это обычная работа с GUI натянутым на part.

База данных — проблемы

Прежде чем продолжим, немного о работе БД в Roblox. Есть два варианта DataStore и OrderedDataStore. С первым мы уже знакомились и его прекрасно заменяет надстройка DataStore2, т.к. это обычное хранение данных в виде таблицы по ключу из ID и значения. А вот второе как раз нам и понадобится, т.к. у него есть два замечательных свойства: сортировка получаемых данных и их постраничное чтение. Чего, к сожалению, нет в обычном DataStore. Но и про первое не забудем, так как актуальные данные у нас будут находится в DataStore2 из-за его кэширования процедур записи значений в БД и они могут быть уже изменены. С этим может быть небольшой «косяк» но о нём и его решении позже.

Отступление. Работа с БД возможна только при включенном API в настройках игры. Но чтобы это сделать, надо, как минимум, сохранить наш плейс в Roblox.

Начинка списка

Для начала создадим то, что хотим в итоге увидеть. Для этого отредактируем наш TopBar. Что у нас должно отображаться?

  • Image — Изображение аватарки игрока
  • Place — Порядковый номер
  • PName — Его имя
  • Value — Рейтинговое число

Image — имеет тип Image, остальное TextLabel.

Можно считать, что это минимальный, комфортный набор данных для списка лидеров.

По итогу нашей работы мы получим нечто похожее:

Теперь, копируем наш TopBar в под Script и даём имя Sample. Это будет заготовка, которую мы будем помещать в ScrollingFrame для каждого игрока, после её заполнения.

В сам ScrollingFrame поместим UIListLayout. Он нужен будет нам для задания порядка отображаемых строк в списке.

Таким образом будем иметь итоговый вид:

Программный код

Настало время программирования заполнения нашего списка лидеров. Определимся с тем что и как у нас должно работать.

  • Мы должны брать данные из OrderedDataStore (ODS) и сравнивать их с текущими данными в DataStore2. Тут есть два варианта — игрок может быть в игре и его данные изменились и игрока может уже не быть в игре. К вопросу о «косяке», он как раз и будет в том, что в ODS данные будут устаревшими и нам нужно решить каким данным мы больше доверяем или обновить данные в ODS. Второй шаг более верный и тут выходит ещё одно «но», в игре может присутствовать игрок, которого ещё нет в ODS. А значит его данные мы должны туда добавить.
  • Мы должны как-то достать «аватар» игрока с сайта Roblox.
  • Сортировать список полученных данных

Приступим. Сперва объявим ссылки на GUI и работу с базами данных.

-- объявляем ссылки на элементы интерфейса
local sg = script.Parent 						-- Surface GUI
local sample = script:WaitForChild("Sample") 	-- Our Sample frame
local sf = sg:WaitForChild("ScrollingFrame") 	-- The scrolling frame
local ui = sf:WaitForChild("UI") 				-- The UI list layout

-- Объявляем работу с БД игры
local DataStore2 = require(1936396537) -- если не скачивать модуль с его установкой

-- объявляем работу с сортированной БД 
-- https://developer.roblox.com/en-us/api-reference/class/DataStoreService
local dataStoreService = game:GetService("DataStoreService")
-- Задаём работу с сортированной БД
local dataStore = dataStoreService:GetOrderedDataStore("Leaderboard1") -- тут указан ключ таблицы списка нашей игры

Это кусок элементарных вещей. Разве что требуется требуется пояснение последней строки. «Leaderboard1» это ключ таблицы с которой мы будем работать. Т.е. для создания новой доски нам понадобится.. только поменять название ключа.

Второй шаг. Обновим данные в нашей ODS для текущих игроков:

	-- Обновление/актуализация данных по текущим игрокам
	for i, plr in pairs(game.Players:GetChildren()) do-- перебор всех активных игроков в игре
		if plr.UserId>0 then -- проверка на то, что игрок активен
			MaxHeightStore = DataStore2("MaxHeight1", plr) -- создаём ссылку на хранилище денег и т.д. и т.п. указанного игрока
			local w=MaxHeightStore:Get(0) -- получаем данные из DataStore2
			if w then	-- если имеется значение, запишем его в OrderedDataStore
				-- оборачиваем в pcall чтобы у нас не случилось беды, если БД будет не доступна
				-- возвращаемое значение нам не интересно, нам просто нужно обновить БД если это возможно
				pcall(function()
					dataStore:UpdateAsync(plr.UserId,function(w)
						w=MaxHeightStore:Get(0) -- для сопрограммы вновь считываем значение
						 return tonumber(w) -- !!! возвращаемое значение исключительно числовое, для записи в UpdateAsync
					end)
				end) 
			end
		end
	end

MaxHeightStore — объявляем вне цикла, для доступа в любом месте.

Теперь можно получить данные из ODS:

	-- Запрос данных из OrderedDataStore
	local smallestFirst = false	-- false = 2 перед 1, true = 1 перед 2
    local numberToShow = 100	-- Любое число в интервале 1-100, Большие значения замедлят работу
    local minValue = 0 			-- Минимальное получаемое значение
	local maxValue = 10e30		-- (10^30), максимальное получаемое значение
	-- запрос страницы данных из OrderedDataStore
	-- объявление параметров страницы
    local pages = dataStore:GetSortedAsync(smallestFirst, numberToShow, minValue, maxValue)
    -- Получение данных
    local top = pages:GetCurrentPage()	-- Получаем первую страницу

Теперь займёмся магией. Нам надо получить имя и картинку игрока из ресурсов сайта Roblox.

	-- получение изображений игроков (где-то стырено)
	local data = {}				-- объявляем новый набор данных
	for a,b in ipairs(top) do	-- перебираем все записи из полученной страницы
		local userid = b.key	-- Получаем User id
		local points = b.value	-- Получаем значение для сортировки
		local username = "[Failed To Load]"	-- Имя по умолчанию (ошибка загрузки)
		local s,e = pcall(function()	-- оборачиваем в pcall т.к. это сторонний запрос
			username = game.Players:GetNameFromUserIdAsync(userid)	-- Получить имя игрока
		end)
		if not s then	-- Если что-то пошло не так
			warn("Error getting name for "..userid..". Error: "..e)
		end
		-- Магия получения картинки аватара игрока
		local image = game.Players:GetUserThumbnailAsync(userid, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150)
		-- создаём образ в нашей таблице
		table.insert(data,{username,points,image})	-- Добавляем в нашу таблицу.. новую таблицу
	end
	

Я честно попытался объяснить всё что происходит в данном фрагменте кода. По крайней мере как я это понял.

В следующем куске выводим полученные данные в нужном виде на наш ScrollingFrame.

	-- отображение данных
	ui.Parent = script		-- перемещаем сортировщик
	sf:ClearAllChildren()	-- Удаляем все оставшиеся элементы в ScrollingFrame 
	ui.Parent = sf			-- возвращаем сортировщик
	for number,d in pairs(data) do	-- перебираем все данные из созданного ранее массива
		local name = d[1]	-- имя
		local val = d[2]	-- значение
		local image = d[3]	-- картинка
		-- немного красивости на уровне кода
		local color = Color3.new(1,1,1)			-- Задаём цвет по умолчанию
		if number == 1 then
			color = Color3.new(1,1,0)			-- Цвет игрока на первом месте
		elseif number == 2 then
			color = Color3.new(0.9,0.9,0.9)		-- Цвет игрока на втором месте
		elseif number == 3 then
			color = Color3.fromRGB(166, 112, 0)	-- Цвет игрока на третьем месте
		end
		-- Здесь формируем новый элемент ScrollingFrame
		local new = sample:Clone()			-- Создаём клон из нашего образца
		new.Name = name						-- Устанавливаем имя
        new.LayoutOrder = number			-- UIListLayout изспользует это число для сортировки элементов
		new.Image.Image = image				-- Устанавливаем изображение
		new.Image.Place.Text = number		-- Добавляем порядковый номер
		new.Image.Place.TextColor3 = color	-- Задаём ранее расчитанный цвет
		new.PName.Text = name				-- Устанавливаем имя
		new.Value.Text = val				-- Устанавливаем значение из БД
		new.Value.TextColor3 = color		-- Задаём ранее расчитанный цвет
		new.PName.TextColor3 = color		-- Задаём ранее расчитанный цвет
		new.Parent = sf						-- Помещаем подготовленный клон в ScrollingFrame
	end
	wait()	-- мгновенная пауза (не помню зачем поставил)
	-- Устанавливаем реальный размер внутренней области ScrollingFrame на основе всех помещённых данных
	sf.CanvasSize = UDim2.new(0,0,0,ui.AbsoluteContentSize.Y)	 

Ну вот собственно и практически весь код. Полный код скрипта будет ниже.

Что можно или нужно ещё сделать?

  • Главное — вынести критичные значения в переменные в шапку скрипта — ключи, чтобы не искать их в коде при размножении лидборда. (Сделаем это в общем коде)
  • Вместо бесконечного цикла, можно повесить обновление на событие. Правда это чревато частыми обновлениями при большом числе игроков.
  • Украсить ещё как-нибудь…

-- объявление переменных для простоты копирования лидборда
-- ключ значений у игрока в DataStore2
local DS2key = "MaxHeight1"
-- ключ значений у игрока в OrderedDataStore
local OrderKey = "Leaderboard1"
-- Могут быть и одинаковыми, но лучше использовать так: какой ключ игрока, в каком лидборде

-- объявляем ссылки на элементы интерфейса
local sg = script.Parent 						-- Surface GUI
local sample = script:WaitForChild("Sample") 	-- Our Sample frame
local sf = sg:WaitForChild("ScrollingFrame") 	-- The scrolling frame
local ui = sf:WaitForChild("UI") 				-- The UI list layout

-- Объявляем работу с БД игры
local DataStore2 = require(1936396537) -- если не скачивать модуль с его установкой

-- объявляем работу с сортированной БД 
-- https://developer.roblox.com/en-us/api-reference/class/DataStoreService
local dataStoreService = game:GetService("DataStoreService")
-- The data store service
local dataStore = dataStoreService:GetOrderedDataStore(OrderKey)

local MaxHeightStore -- наша глобальная переменная

while true do
	-- Обновление/актуализация данных по текущим игрокам
	for i, plr in pairs(game.Players:GetChildren()) do-- перебор всех активных игроков в игре
		if plr.UserId>0 then -- проверка на то, что игрок активен
			MaxHeightStore = DataStore2(DS2key, plr) -- создаём ссылку на хранилище денег и т.д. и т.п. указанного игрока
			local w=MaxHeightStore:Get(0) -- получаем данные из DataStore2
			if w then	-- если имеется значение, запишем его в OrderedDataStore
				-- оборачиваем в pcall чтобы у нас не случилось беды, если БД будет не доступна
				-- возвращаемое значение нам не интересно, нам просто нужно обновить БД если это возможно
				pcall(function()
					dataStore:UpdateAsync(plr.UserId,function(w)
						w=MaxHeightStore:Get(0) -- для сопрограммы вновь считываем значение
						 return tonumber(w) -- !!! возвращаемое значение исключительно числовое, для записи в UpdateAsync
					end)
				end) 
			end
		end
	end
	
	-- Запрос данных из OrderedDataStore
	local smallestFirst = false	-- false = 2 перед 1, true = 1 перед 2
    local numberToShow = 100	-- Любое число в интервале 1-100, Большие значения замедлят работу
    local minValue = 0 			-- Минимальное получаемое значение
	local maxValue = 10e30		-- (10^30), максимальное получаемое значение
	-- запрос страницы данных из OrderedDataStore
	-- объявление параметров страницы
    local pages = dataStore:GetSortedAsync(smallestFirst, numberToShow, minValue, maxValue)
    -- Получение данных
    local top = pages:GetCurrentPage()	-- Получаем первую страницу

	-- получение изображений игроков (где-то стырено)
	local data = {}				-- объявляем новый набор данных
	for a,b in ipairs(top) do	-- перебираем все записи из полученной страницы
		local userid = b.key	-- Получаем User id
		local points = b.value	-- Получаем значение для сортировки
		local username = "[Failed To Load]"	-- Имя по умолчанию (ошибка загрузки)
		local s,e = pcall(function()	-- оборачиваем в pcall т.к. это сторонний запрос
			username = game.Players:GetNameFromUserIdAsync(userid)	-- Получить имя игрока
		end)
		if not s then	-- Если что-то пошло не так
			warn("Error getting name for "..userid..". Error: "..e)
		end
		-- Магия получения картинки аватара игрока
		local image = game.Players:GetUserThumbnailAsync(userid, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150)
		-- создаём образ в нашей таблице
		table.insert(data,{username,points,image})	--Добавляем в нашу таблицу.. новую таблицу
	end
	
	-- отображение данных
	ui.Parent = script		-- перемещаем сортировщик
	sf:ClearAllChildren()	-- Удаляем все оставшиеся элементы в ScrollingFrame 
	ui.Parent = sf			-- возвращаем сортировщик
	for number,d in pairs(data) do	-- перебираем все данные из созданного ранее массива
		local name = d[1]	-- имя
		local val = d[2]	-- значение
		local image = d[3]	-- картинка
		-- немного красивости на уровне кода
		local color = Color3.new(1,1,1)			-- Задаём цвет по умолчанию
		if number == 1 then
			color = Color3.new(1,1,0)			-- Цвет игрока на первом месте
		elseif number == 2 then
			color = Color3.new(0.9,0.9,0.9)		-- Цвет игрока на втором месте
		elseif number == 3 then
			color = Color3.fromRGB(166, 112, 0)	-- Цвет игрока на третьем месте
		end
		-- Здесь формируем новый элемент ScrollingFrame
		local new = sample:Clone()			-- Создаём клон из нашего образца
		new.Name = name						-- Устанавливаем имя
        new.LayoutOrder = number			-- UIListLayout изспользует это число для сортировки элементов
		new.Image.Image = image				-- Устанавливаем изображение
		new.Image.Place.Text = number		-- Добавляем порядковый номер
		new.Image.Place.TextColor3 = color	-- Задаём ранее расчитанный цвет
		new.PName.Text = name				-- Устанавливаем имя
		new.Value.Text = val				-- Устанавливаем значение из БД
		new.Value.TextColor3 = color		-- Задаём ранее расчитанный цвет
		new.PName.TextColor3 = color		-- Задаём ранее расчитанный цвет
		new.Parent = sf						-- Помещаем подготовленный клон в ScrollingFrame
	end
	wait()	-- мгновенная пауза (не помню зачем поставил)
	-- Устанавливаем реальный размер внутренней области ScrollingFrame на основе всех помещённых данных
	sf.CanvasSize = UDim2.new(0,0,0,ui.AbsoluteContentSize.Y)	 
	wait(10)	-- Ожидание до следующего такта обновления
end

Ну вот, собственно у нас готова заготовка лидерборда для чего угодно.

Использование

Подошли к самому главному. Как использовать сиё чудо?

  • копируем в любое место workspace (можем просто повесить на любую существующую стенку копию SurfaceGui)
  • указываем оба значения ключей
  • если надо, меняем внешний вид

Всё. С этого момента оно будет работать само по себе.

Протестировать можно здесь.


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

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

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