I’m currently making a player leaderboard where a player can click through pages of names. I already made a scrolling leaderboard before, but I feel like it didn’t fit right with the style of the game.
I’ve looked around the API and found Pages:AdvanceToNextPageAsync(), which I thought would be easy to implement. But, there wasn’t any function to go back to the previous page. Am I missing something, or completely misunderstood what it does? Any help would be apreciated!
Leaderboard script (which currently just displays first ten keys, gui button events aren't present either)
local function updateLeaderboard(datastore,leaderboard,connectEvent)
local surfaceGui = leaderboard.SurfaceGui
local frame = surfaceGui.Leaderboard
local UIGridLayout = frame.UIGridLayout
local leftPageArrow = surfaceGui.LeftPageArrow
local rightPageArrow = surfaceGui.RightPageArrow
local success, pages = pcall(function()
return datastore:GetSortedAsync(false,10)
end)
if success then
if pages then
local function getPage()
local currentPage = pages:GetCurrentPage()
for _,v in ipairs(frame:GetChildren()) do
if v:IsA("Frame") then
v:Destroy()
end
end
for i,data in ipairs(currentPage) do
local username
pcall(function()
username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
end)
if username then
username = string.upper(username)
if data["value"] > 0 then
local leaderboardRankClone = leaderboardRank:Clone()
leaderboardRankClone.Parent = frame
local rank = leaderboardRankClone:WaitForChild("Rank")
local usernameGui = leaderboardRankClone:WaitForChild("Username")
local value = leaderboardRankClone:WaitForChild("Value")
local denominations = {"ST", "ND", "RD", "TH"}
local denomination
if i == 1 then
denomination = denominations[1]
elseif i % 10 == 2 then
denomination = denominations[2]
elseif i % 10 == 3 then
denomination = denominations[3]
else
denomination = denominations[4]
end
local textColor
if i == 1 then
textColor = Color3.fromRGB(255, 255, 0)
elseif i == 2 then
textColor = Color3.fromRGB(192, 192, 192)
elseif i == 3 then
textColor = Color3.fromRGB(205, 127, 50)
else
textColor = Color3.fromRGB(255, 255, 255)
end
rank.TextColor3 = textColor
usernameGui.TextColor3 = textColor
value.TextColor3 = textColor
rank.Text = i .. denomination
usernameGui.Text = username
value.Text = data["value"]
end
end
end
end
end
end
end
Here is what I’m planning to do. There will probably be an issue where players try to change pages at the same time, but there are only 4 players in a server so eh.
Hm, I don’t quite know how to get the key and value of each page since it’s its own object, and then a table. I tried yanking some code in the DevHub, but I don’t really understand what it exactly does.
Code
local function updateLeaderboard(datastore,leaderboard,connectEvent)
local surfaceGui = leaderboard.SurfaceGui
local frame = surfaceGui.Leaderboard
local UIGridLayout = frame.UIGridLayout
local page = surfaceGui.Page
local leftPageArrow = surfaceGui.LeftPageArrow
local rightPageArrow = surfaceGui.RightPageArrow
local pageIndex = leaderboard.PageIndex
local success, pages = pcall(function()
return datastore:GetSortedAsync(false,10)
end)
if success then
if pages then
local function iterPageItems(pages)
return coroutine.wrap(function()
local pagenum = 1
while true do
for _, item in ipairs(pages:GetCurrentPage()) do
coroutine.yield(item, pagenum)
end
if pages.IsFinished then
break
end
pages:AdvanceToNextPageAsync()
pagenum += 1
end
end)
end
for item, pageNo in iterPageItems(pages) do
print(item, " ", pageNo)
pagesCache[pageNo] = item
end
local function getPage(page,pageIndex)
if pageIndex.Value > MAX_PAGES then
pageIndex.Value = 1
elseif pageIndex.Value < 1 then
pageIndex.Value = MAX_PAGES
end
local currentPage = page[pageIndex.Value]
for key,value in pairs(page) do
for k,val in pairs(value) do
print(k, val)
end
end
for _,v in ipairs(frame:GetChildren()) do
if v:IsA("Frame") then
v:Destroy()
end
end
for i,data in ipairs(currentPage) do
local username
pcall(function()
username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
end)
if username then
username = string.upper(username)
if data["value"] > 0 then
local leaderboardRankClone = leaderboardRank:Clone()
leaderboardRankClone.Parent = frame
local rank = leaderboardRankClone:WaitForChild("Rank")
local usernameGui = leaderboardRankClone:WaitForChild("Username")
local value = leaderboardRankClone:WaitForChild("Value")
local denomination = getDenomination(i)
local rankColor = getRankColor(i)
rank.TextColor3 = rankColor
usernameGui.TextColor3 = rankColor
value.TextColor3 = rankColor
rank.Text = i .. denomination
usernameGui.Text = username
value.Text = data["value"]
page.Text = pageIndex .. " / " .. MAX_PAGES
end
end
end
end
getPage(pagesCache,pageIndex)
if connectEvent then
print("connect")
leftPageArrow.MouseButton1Click:Connect(function()
print("left")
pageIndex -= 1
getPage(pagesCache,pageIndex)
end)
rightPageArrow.MouseButton1Click:Connect(function()
print("right")
pageIndex += 1
getPage(pagesCache,pageIndex)
end)
end
end
end
end
It’d probably be best for you to break a Pages object into a dictionary if you want to freely iterate back and forth between pages. Each key of your dictionary would be the page number and the value would be the key-value pairs fetched from the DataStore.
local dataDictionary = {}
local success, pages = pcall(function ()
return datastore:GetSortedAsync(false, 10)
end)
if success and pages then
local pageNumber = 1
while not pages.IsFinished do
local pageCache = {}
dataDictionary[pageNumber] = pageCache
for _, dataPair in ipairs(pages:GetCurrentPage()) do
table.insert(pageCache, dataPair)
end
if not pages.IsFinished then
pages:AdvanceToNextPageAsync()
pageNumber += 1
end
end
end
Something along these lines.
You know, that being said though, with the advent of UIPageLayout I think it would be much more intuitive for you to use this as your way of paginating instead because this actually allows you to flip back and forth between “pages”. This way, your leaderboard and all its pages are immediately written out and changing pages is just a matter of a simple visual update handled by the Gui engine.
Expected flow:
For the first time in the server or every time the leaderboards are to be updated, use GetSortedAsync to query data. Only use GetSortedAsync during an auto update or for the first time since the server started.
Use a UIPageLayout on your leaderboard. Give it whatever properties you want. For SortOrder, make sure the sorting mode is LayoutOrder.
For every page that exists, create a new frame and make its LayoutOrder is the number of the current page you’re going through. Populate the frame as you go with the entries of players. You’re ahead of the game if you’re using UIListLayout with LayoutOrder SortOrder - each player entry’s LayoutOrder should be the key’s value.
You’re done working with DataStores. Now every time a player clicks the forward or back arrow, use the Next or Previous functions accordingly.
Ok, I finally got around to doing this, but I’m having a hard time trying to use UIPageLayout and pages in general.
I used your code for iterating through pages but I realized the while loop never executes anything because my ordered datastore only has 1 page, so pages.IsFinished returns true. I can’t find another way to loop through them.
You’re ahead of the game if you’re using UIListLayout with LayoutOrder SortOrder - each player entry’s LayoutOrder should be the key’s value.
Does UIListLayout replace UIGridLayout? Why would I need a sort order when the datastore is already sorted for us? I think I’m confusing too many things right now hehe
Code
local function updateLeaderboard(datastore,leaderboard)
local surfaceGui = leaderboard.SurfaceGui
local frame = surfaceGui.Leaderboard
local UIPageLayout = frame.UIPageLayout
local pageText = surfaceGui.Page
local leftPageArrow = surfaceGui.LeftPageArrow
local rightPageArrow = surfaceGui.RightPageArrow
local success, pages = pcall(function()
return datastore:GetSortedAsync(false,10)
end)
if success and pages then
local pageNumber = 1
while not pages.IsFinished do
local pageClone = pageFrame:Clone()
pageClone.Parent = frame
pageClone.LayoutOrder = pageNumber
for key,value in pairs(pages:GetCurrentPage()) do
local username
pcall(function()
username = Players:GetNameFromUserIdAsync(string.sub(key,8,string.len(key)))
end)
if username then
username = string.upper(username)
if value > 0 then
local leaderboardRankClone = leaderboardRank:Clone()
leaderboardRankClone.Parent = pageClone
local rank = leaderboardRankClone:WaitForChild("Rank")
local usernameGui = leaderboardRankClone:WaitForChild("Username")
local value = leaderboardRankClone:WaitForChild("Value")
local denomination = getDenomination(1)
local rankColor = getRankColor(1)
rank.TextColor3 = rankColor
usernameGui.TextColor3 = rankColor
value.TextColor3 = rankColor
rank.Text = denomination
usernameGui.Text = username
value.Text = value
--page.Text = .. " / " .. MAX_PAGES
end
end
end
if not pages.IsFinished then
pages:AdvanceToNextPageAsync()
pageNumber += 1
end
end
end
end
UIPageLayout is intended to display elements in a paged form as the name suggests. What you want to look to be doing is setting up page frames where in each of those frames contains all the leaderboard data of the current DataStore page.
If the while loop doesn’t execute anything, my bad, I tried to shorthand and cut some corners without doing some testing. There usually will always be at minimum one page available even if the page is empty, so a normal while true loop would be good enough to pack the DataStore pages into.
New loop:
while true do
local pageCache = {}
dataDictionary[pageNumber] = pageCache
for _, entry in ipairs(pages:GetCurrentPage()) do
table.insert(pageCache, entry)
end
if pages.IsFinished then
break
end
pages:AdvanceToNextPageAsync()
pageNumber += 1
end
UIListLayout and UIGridLayout cannot both be used at the same time, but I’m under the assumption that you will be using UIListLayout since you don’t exactly have a grid view going on and you can pack everything into lines worth of content as well. As for SortOrder, that’s to force the layouts to display in their intended order because the sorted data has no effect on how the Gui elements are sorted.
Yesss I got the leaderboard to work with UIPageLayout and UIListLayout! Thank you so much for helping me!
But, I have this one small problem where the gui buttons won’t fire when I click on them. Should they be located in a different script, because I currently have them connected in the same script where I create the leaderboard.
Code
local function updateLeaderboard(data,leaderboard)
local surfaceGui = leaderboard.SurfaceGui
local frame = surfaceGui.Leaderboard
local UIPageLayout = frame.UIPageLayout
local pageText = surfaceGui.Page
local leftPageArrow = surfaceGui.LeftPageArrow
local rightPageArrow = surfaceGui.RightPageArrow
local pageIndex = surfaceGui.Page
local pageNumber = 1
local numOfPages = #data
for _,page in ipairs(data) do
local pageClone = pageFrame:Clone()
pageClone.Parent = frame
pageClone.LayoutOrder = pageNumber
for i,data in pairs(page) do
local username
pcall(function()
username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
end)
if username then
username = string.upper(username)
if data["value"] > 0 then
local leaderboardRankClone = leaderboardRank:Clone()
leaderboardRankClone.Parent = pageClone
leaderboardRankClone.LayoutOrder = -data["value"]
local rank = leaderboardRankClone:WaitForChild("Rank")
local usernameGui = leaderboardRankClone:WaitForChild("Username")
local value = leaderboardRankClone:WaitForChild("Value")
local denomination = getDenomination(i)
local rankColor = getRankColor(i)
rank.TextColor3 = rankColor
usernameGui.TextColor3 = rankColor
value.TextColor3 = rankColor
rank.Text = i .. denomination
usernameGui.Text = username
value.Text = data["value"]
pageIndex.Text = pageNumber .. "/" .. numOfPages
end
end
end
leftPageArrow.Activated:Connect(function()
print("left")
UIPageLayout:Next()
end)
rightPageArrow.Activated:Connect(function()
print("right")
UIPageLayout:Previous()
end)
end
end
It probably has to do with SurfaceGuis only catching input when the SurfaceGui is in PlayerGui. You can either have the server send leaderboard data to the client so they can organise it themselves or move the page controls to a separate SurfaceGui. The latter option should really only be used in case of trying to make the leaderboard work quickly for release; the former option (RemoteEvents) would be the option for doing it properly.