Давно хотел написать урок по созданий доски лидеров и.. тут получил тестовое задание по его написанию. Так уж сошлись звёзды.
Когда-то давно, года так два назад, понадобилась мне данная фича и на просторах интернета я нашёл нечто подходящее. На основе этого и делал свою таблицу лидеров.
Итак постановка — хранить значения в базе данных по каждом игроку. Выводить первую сотню в порядке уменьшения позиции, т.е. от лучшего к худшему. Естественно что все разом не уместятся и потому должна быть прокрутка списка лидеров. По возможности, вывести аватарки игроков.
Добавочное условие — сама таблица лидеров должна быть не зависима от всего, что происходит в игре и пользоваться только данными из БД. Таким образом получим, что БД меняется в любом месте игры.
Подготовка стенда
Создаём новый плейс в 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)
- указываем оба значения ключей
- если надо, меняем внешний вид
Всё. С этого момента оно будет работать само по себе.
Протестировать можно здесь.