Creating Global Leaderboards With GUI!

Introduction

Hey! This is my first community tutorial so point out anything that is wrong.

Anyways, today I wanted to explain how one would go about creating a global leaderboard in a GUI. Now, there are tons and tons of tutorials on how to make global leaderboards with SurfaceGuis, but global leaderboards with ScreenGuis are a bit more tricky.

Things i suggest before completing this tutorial

DISCLAIMER: There are most likely other ways to do this, I’m going to be showing the way that worked best for me

Lets Get Started

Let’s first set up our global leaderboard system

Lets create a folder, a empty script, and a ScreenGui for now!
image
Also feel free to name your scripts and gui’s different than mine just know that our code will look a bit different!

In our LeaderboardHandler script in ServerScriptService let’s right a few lines of code.

--// SERVICES //--

local Replicated = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService('DataStoreService')
local Players = game:GetService('Players')

--// DATASTORE //--

local DataStore = DataStoreService:GetOrderedDataStore('Test')

--// FOLDER //--

local Folder = Replicated:WaitForChild('Leaderboard') -- YOUR FOLDER IN REPLICATED STORAGE TO KEEP ENTRIES

--// SETTINGS //--

local EntriesPerPage = 50
local EntriesPerPage = 5
local RefreshTime = 60 -- In Seconds

-- We'll add more here soon!

Lets also create our ScreenGui and a few scripts!

Here is a quick GUI I made, yours can look completely different!

Next, let’s create a template for entries for the Global Leaderboard!
image
image

Here is what the inside of your ScreenGui should look like with the addition of a new Local Script!
image

Make sure that the scrolling frame canvas size is set to {0,0},{0,0} and AutomaticCanvasSize is se to Y looking like this:
image

The Main System

First, let’s figure out what we need to do for this global leaderboard system to actually work!

In the server, we are going to have a function that loops every x seconds or the refresh time. This function will sort through the sorted datastore and grab every entry and either:

  • Create a new Value Instance at the entry rank.
    // or //
  • Update a value instance at the entry rank that already exists

In our LocalScript, we will have an event that detects whenever a new Value Instance is created. Once it is created we will attach a function that detects if the value updates. Once either of these events fire we can update our ScreenGui!

Visual I made (don’t judge the quality)

Let’s Start Coding!

In our LeaderboardHandler script in ServerScriptService let’s start to begin with a function that loops!

--// SERVICES //--

local Replicated = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService('DataStoreService')
local Players = game:GetService('Players')

--// DATASTORE //--

local DataStore = DataStoreService:GetOrderedDataStore('Test')

--// FOLDER //--

local Folder = Replicated:WaitForChild('Leaderboard') -- YOUR FOLDER IN REPLICATED STORAGE TO KEEP ENTRIES

--// SETTINGS //--

local EntriesPerPage = 50
local EntriesPerPage = 5
local RefreshTime = 60 -- In Seconds

-- // FUNCTIONS //--

local function Main()
	-- Going to add stuff here in a second!
end

-- // LOOP // --

while true do
	Main()
	task.wait(RefreshTime)
end

In our Main function let’s start to go through the entries in our datastore!

local function Main()
	local success, pages = pcall(function()
		return DataStore:GetSortedAsync(false, EntriesPerPage)
	end)
	if success then
		for i=0,TotalEntires,EntriesPerPage do
			local entries = pages:GetCurrentPage()
			for rank, entry in pairs(entries) do
				updateValue(rank, entry.key, entry.value)
			end

			if pages.IsFinished then
				break
			else
				pages:AdvanceToNextPageAsync()
			end
		end			
	end
end

Let’s create a new function that creates or updates an Instance, which shouldn’t be hard at all!

local function updateValue(rank, key, value)	
	if Folder:FindFirstChild(rank) then
		local found = Folder:FindFirstChild(rank)
		if found.Key.Value ~= key then
			found.Key.Value = key
			found.Value.Value = value
		end
	else
		local new = Instance.new('StringValue', Folder)
		new.Name = rank
		
		local newKey = Instance.new('StringValue',new)
		local newValue = Instance.new('StringValue',new)
		
		newKey.Name = 'Key'
		newValue.Name = 'Value'
		
		newKey.Value = key
		newValue.Value = value
	end
end

Now, let’s move onto our LocalScript!

Let’s get the basics out of the way.

--// SERVICES //--

local Replicated = game:GetService('ReplicatedStorage')

--// FOLDER //--

local Folder = Replicated:WaitForChild("Leaderboard")

--// TEMPLATE //--

local Template = script:WaitForChild("Template")

--// FUNCTIONS // --

local function UpdateEntry(instance)
	
end

local function Set()
	
end

--// EVENTS // --

Folder.ChildAdded:Connect(UpdateEntry)

Let’s now work on our UpdateEntry function!
This function fires whenever a new instance or child is added to the folder.

local function UpdateEntry(instance : Instance)
	if instance:IsA("StringValue") then
		local Rank = instance.Name
		local Key = instance:FindFirstChild('Key')
		local Value = instance:FindFirstChild('Value')
		
		local Values = {Key; Value;}
		
		if ScrollingFrame:FindFirstChild(Rank) then
			local found = ScrollingFrame:FindFirstChild(Rank)
			found:FindFirstChild('Name').Text = tostring(Key.Value)
			found:FindFirstChild('Rank').Text = tostring(Rank)
			found:FindFirstChild('Value').Text = tostring(Value.Value)
		else
			local new = Template:Clone()
			new.Parent = ScrollingFrame
			new.LayoutOrder = Rank
			new:FindFirstChild('Name').Text = tostring(Key.Value)
			new:FindFirstChild('Rank').Text = tostring(Rank)
			new:FindFirstChild('Value').Text = tostring(Value.Value)
		end
		
		for _, v in pairs(Values) do
			v.Changed:Connect(function()
				UpdateEntry(instance)
			end)			
		end
	end
end

This UpdateEntry function handles all of the scrolling frame sorting and template creating!

Finally, let’s work on the set function.
This set function will fire once a player joins getting all of the entries already in ReplicatedStorage

local function Set()
	for _, item in pairs(Folder:GetChildren()) do
		UpdateEntry(item)
	end
end

Set()

Once all of these elements are put together it should work!

Conclusion

Full Scripts

Server (LeaderboardHandler):

--// SERVICES //--

local Replicated = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService('DataStoreService')
local Players = game:GetService('Players')

--// DATASTORE //--

local DataStore = DataStoreService:GetOrderedDataStore('Test')

--// FOLDER //--

local Folder = Replicated:WaitForChild('Leaderboard') -- YOUR FOLDER IN REPLICATED STORAGE TO KEEP ENTRIES

--// SETTINGS //--

local TotalEntires = 50
local EntriesPerPage = 5
local RefreshTime = 15 -- In Seconds

-- // FUNCTIONS //--

local function updateValue(rank, key, value)	
	if Folder:FindFirstChild(rank) then
		local found = Folder:FindFirstChild(rank)
		if found.Key.Value ~= key then
			found.Key.Value = key
			found.Value.Value = value
		end
	else
		local new = Instance.new('StringValue', Folder)
		new.Name = rank
		
		local newKey = Instance.new('StringValue',new)
		local newValue = Instance.new('StringValue',new)
		
		newKey.Name = 'Key'
		newValue.Name = 'Value'
		
		newKey.Value = key
		newValue.Value = value
	end
end

local function Main()
	local success, pages = pcall(function()
		return DataStore:GetSortedAsync(false, EntriesPerPage)
	end)
	if success then
		for i=0,TotalEntires,EntriesPerPage do
			local entries = pages:GetCurrentPage()
			for rank, entry in pairs(entries) do
				updateValue(rank, entry.key, entry.value)
			end

			if pages.IsFinished then
				break
			else
				pages:AdvanceToNextPageAsync()
			end
		end			
	end
end

-- // LOOP // --

while true do
	print('Refreshing')
	Main()
	task.wait(RefreshTime)
end

Client (ClientHandler):

--// SERVICES //--

local Replicated = game:GetService('ReplicatedStorage')

--// FOLDER //--

local Folder = Replicated:WaitForChild("Leaderboard")

--// TEMPLATE //--

local Template = script:WaitForChild("Template")

--// UI //--

local ui = script.Parent
local Main = ui.Main
local ScrollingFrame = Main.ScrollingFrame

--// FUNCTIONS // --

local function UpdateEntry(instance : Instance)
	if instance:IsA("StringValue") then
		local Rank = instance.Name
		local Key = instance:FindFirstChild('Key')
		local Value = instance:FindFirstChild('Value')
		
		local Values = {Key; Value;}
		
		if ScrollingFrame:FindFirstChild(Rank) then
			local found = ScrollingFrame:FindFirstChild(Rank)
			found:FindFirstChild('Name').Text = tostring(Key.Value)
			found:FindFirstChild('Rank').Text = tostring(Rank)
			found:FindFirstChild('Value').Text = tostring(Value.Value)
		else
			local new = Template:Clone()
			new.Parent = ScrollingFrame
			new.LayoutOrder = Rank
			new:FindFirstChild('Name').Text = tostring(Key.Value)
			new:FindFirstChild('Rank').Text = tostring(Rank)
			new:FindFirstChild('Value').Text = tostring(Value.Value)
		end
		
		for _, v in pairs(Values) do
			v.Changed:Connect(function()
				UpdateEntry(instance)
			end)			
		end
	end
end

local function Set()
	for _, item in pairs(Folder:GetChildren()) do
		UpdateEntry(item)
	end
end

Set()

--// EVENTS // --

Folder.ChildAdded:Connect(UpdateEntry)

Open Source Baseplate:
Tutorial.rbxl (46.2 KB)

If you need to reach out to me here our my socials:
Discord: Towphy#6174
Twitter: https://twitter.com/towphyReal

Please ask any sort of questions or suggestions you have!
This is my first tutorial!

How Well Did I Do On This Tutorial? (1-10)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

36 Likes

It’s alright, could be better. But imo a tutorial means that everything is completely documented, rather than just telling people what something does and where to put it. So the tutorial just needs to be more in-depth. Good job though :slight_smile:

3 Likes

More in-depth on what? OP wrote >1200 words, and while words doesn’t necessarily equal quality, that’s a pretty broad suggestion that OP might have trouble implementing.


Nice tutorial. OP, not sure if this is the same criticism of @2jammers, but I think you could do more to explain how each piece of code does what it does. Right now, you do a good job of explaining the end result, but isn’t always the most clear on how it does that.

For example, I could very easily imagine someone getting confused over what this code specifically does

Example
local function Main()
	local success, pages = pcall(function()
		return DataStore:GetSortedAsync(false, EntriesPerPage)
	end)
	if success then
		for i=0,TotalEntires,EntriesPerPage do
			local entries = pages:GetCurrentPage()
			for rank, entry in pairs(entries) do
				updateValue(rank, entry.key, entry.value)
			end

			if pages.IsFinished then
				break
			else
				pages:AdvanceToNextPageAsync()
			end
		end			
	end
end

Nevertheless, love the flow charts and the overall idea here. I think it’s a good starting point for anyone trying to conceptualize the basics of creating a global leaderboard and I think it does a good job of being pretty to the point with hitting the basic concept on the head.

5 Likes

Thanks for pointing that out! I will for sure edit my post to make it more understandable for less experienced scripters that are new to datastores. Once again thanks for the constructive ciritsm!

2 Likes

Awesome tutorial. However, I was wondering, how could I do this if I didn’t use ordered datastores? I have a singular datastore that saves a table that contains all the values that I want to save.

local tableToSave = {
		player.leaderstats.GoldCoins.Value; -- First value from the table
		player.BoughtFiveHourBook.Value; -- Second value from the table
		player.FiveHourBookBoostLeft.Value;
		player.leaderstats.Highscore.Value,
		player.Experience.Value;
	}

	local success, err = pcall(function()
		dataStore:UpdateAsync(player.UserId, tableToSave) -- Save the data with the player UserId, and the table we wanna save
	end)

I want to make a global leaderboard GUI with the player.leaderstats.Highscore.Value. How could I do this?

1 Like

Is this guide outdated? Because the open source doesn’t work for me

1 Like

Should still work, nothing has changed

2 Likes

Sorry for the late reply thanks for confirming

Why is EntriesPerPage set twice in the script lol

I am still looking at how to implement that; the best I have so far is a replica of the unordered datastore.

Basically, each “key” is indexed. For example, I want to store a speedrun submitted by a player. I first check the datastore containing the index value. For simplicity, let’s say the very first submission will start at 1 and increment by 1. This way, for each new key, we can do an increment async on that key and know what the next index should be, but this has racing issues, so it’s not very practical. The ordered datastore only contains the index of the speedrun as the key and the order factor, let’s say the time of the speedrun. This way, you can easily get the ordered list, but you have no metadata. You don’t know the player who submitted it or anything else. That’s why we have a second replica (or the original) of the data. It is the exact same thing with the exact same indexing for each run, but it contains a table that has all of the data.

What’s the issue, though? Well, it’s racing conditions. If the incrementing of the index happens too often or several servers try to fetch the current index at the same time, then a “racing condition” will happen and some servers might override a run.

For small games, I see no issues with using this, but I don’t see how it will scale well. Another stupid idea was to host a SQL server and use HTTP requests, but who will invest their time and money into such a thing? Just useless. I’m still looking for some guidance or implementation of “metadata” in ordered datastores, so if anyone can share, I will be very grateful. :pray: