How to make a Global Leaderboard

Hey guys! Today I will be teaching you how to make a Global Leaderboard like you see in all those simulator games.


(Thats what it’ll look like!)

Making the currency

The first thing we need is a currency (or stat) to display on the leaderboard. For this tutorial I will be making a coins leaderstats and a system that adds 50 coins every 5 seconds to the player.
To make this we need a script in ServerScriptService for orginization, we will call it leaderstats.

Screen Shot 2022-07-06 at 5.57.29 PM

Firstly we will make a function that fires when the player joins the game and adds a Folder inside the player called leaderstats. Important Notice: The Folder NEEDS to be names exactly leaderstats or else the player will not be able to see their currency in the player list.

local function OnPlayerAdded(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    stats.Parent = player
end

Next we want to add a stat to this folder, as I mentioned I will be doing coins and I will use an IntValue for this:

local function OnPlayerAdded(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    stats.Parent = player

    local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Value = 0
	coins.Parent = stats
end

Now you should have something that looks like this:
Screen Shot 2022-07-06 at 6.00.05 PM

The last thing we want to in this function is make the player get 50 coins every 5 seconds. For this we will spawn a new thread (so that any thing else can still run in this script) using task.spawn.

local function OnPlayerAdded(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    stats.Parent = player

    local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Value = 0
	coins.Parent = stats

    task.spawn(function()
		while task.wait(5) do
			coins.Value += 50
		end
	end)
end

Finally, we want to make a Players Service variable and connect this function to a PlayerAdded event:

--// Services
local Players = game:GetService("Players")

--// Functions
local function OnPlayerAdded(player)
	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	stats.Parent = player

	local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Value = 0
	coins.Parent = stats
	
	task.spawn(function()
		while task.wait(5) do
			coins.Value += 50
		end
	end)
end

--// Connections
game.Players.PlayerAdded:Connect(OnPlayerAdded)
Saving the data

In this tutorial we will be using OrderedDataStore.
First, open up your leaderstats script and at the top make two new variables: A DataStoreService variable and a variable for your DataStore (I’m calling my data store CoinsBoard):

local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetOrderedDataStore("CoinsBoard1")

After you have that we want to go in our player added function (that we made before), add a plr_key variable (this will be the key we use to access the player’s data) and at the very top try and recieve the data, using a pcall (protected call) function.

local plr_key = "id_"..player.UserId.."_Coins"
local success, data = pcall(function()
    return DataStore:GetAsync(plr_key)
end)

This function will give us our data/coins back in a variable called data, aswell as telling us if the call was successful or not. We will now use this to load or coins value:

if success then
    coins.Value = data or 0 -- 0 is the default value e.g. if this is the first time a player joined.
end

Now that we have successfully loaded our data we must now save it, here is the full PlayerAdded function:

--// Services
local Players = game:GetService("Players")

--// Functions
local function OnPlayerAdded(player)
    local plr_key = "id_"..player.UserId.."_Coins"
    local success, data = pcall(function()
        return DataStore:GetAsync(plr_key)
    end)

	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	stats.Parent = player

	local coins = Instance.new("IntValue")
	coins.Name = "Coins"
    coins.Parent = stats
    if success then 
        coins.Value = data or 0 
    end
	
	task.spawn(function()
		while task.wait(5) do
			coins.Value += 50
		end
	end)
end

--// Connections
game.Players.PlayerAdded:Connect(OnPlayerAdded)

Alright so for the saving of data we will need to connections, PlayerRemoving and game:BindToClose one event runs when the Player leaves the server while the other runs when the server shuts down.

Saving is a pretty simple task. Player leaves (or server shuts down) call a DataStore method (SetAsync) and set the value to the player’s coins value. Just like this:

local function OnPlayerRemoving(player)

end

local function OnServerShutdown()

end

Okay. We will start with the OnPlayerRemoving function. Firstly we need the SAME EXACT player key as before:
local plr_key = "id_"..player.UserId.."_Coins"
after that we will get the coins from the player:
local coins = player.leaderstats.Coins
finally we will make another pcall and save the data:

local success, result = pcall(function()
    return DataStore:SetAsync(plr_key, coins.Value)
end)

After we save the data we want to check if it was successful or not. If not, then we want to warn the error which will be stored in result.

if not success then 
    warn(result)
end

So our PlayerRemoving should look like this:

local function OnPlayerRemoving(player)
    local plr_key = "id_"..player.UserId.."_Coins"

	local coins = player.leaderstats.Coins
	local success, result = pcall(function()
		DataStore:SetAsync(plr_key, coins.Value)
	end)

	if not success then 
		warn(result)
	end
end

Now we copy everything in our PlayerRemoving function into our OnServerShutdown function and just wrap it in a for loop that gets every player. So our OnServerShutdown should look like this:

local function OnServerShutdown()
    for _, player in pairs(game:GetService("Players"):GetPlayers()) do
		local plr_key = "id_"..player.UserId.."_Coins"

		local coins = player.leaderstats.Coins

		local success, result = pcall(function()
		    DataStore:SetAsync(plr_key, coins.Value)
	    end)

	    if not success then 
		    warn(result)
	    end
	end
end

Now last thing for this section is to connect the functions to their events:

Players.PlayerRemoving:Connect(OnPlayerRemoving)
game:BindToClose(OnServerShutdown)

Our final leaderstats code should look like this:

--// Services
local Players = game:GetService("Players")
local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetOrderedDataStore("CoinsBoard1")

--// Functions
local function OnPlayerAdded(player)
	local plr_key = "id_"..player.UserId.."_Coins"
	local success, data = pcall(function()
		return DataStore:GetAsync(plr_key)
	end)

	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	stats.Parent = player

	local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Parent = stats
	print(success, data)
	if success then 
		coins.Value = data or 0 
	end

	task.spawn(function()
		while task.wait(5) do
			coins.Value += 50
		end
	end)
end

local function OnServerShutdown()
	for _, player in pairs(game:GetService("Players"):GetPlayers()) do
		local plr_key = "id_"..player.UserId.."_Coins"

		local coins = player.leaderstats.Coins

		local success, result = pcall(function()
			DataStore:SetAsync(plr_key, coins.Value)
		end)

		if not success then 
			warn(result)
		end
	end
end

local function OnPlayerRemoving(player)
	local plr_key = "id_"..player.UserId.."_Coins"

	local coins = player.leaderstats.Coins
	local success, result = pcall(function()
		DataStore:SetAsync(plr_key, coins.Value)
	end)

	if not success then 
		warn(result)
	end
end

--// Connections
game.Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoving)
game:BindToClose(OnServerShutdown)
The actual leaderboard

Now the final section. The one you’ve all been waiting for. The actual leaderboard. Now, the setup itself is pretty easy but theirs a few things you need. To make things easier, here is a model of the leaderboard I made (it doesn’t include the script).

Okay, lets get started on the scripting. Make a script (normal script) inside of the leaderboard model and open it up.
Screen Shot 2022-07-06 at 5.59.38 PM

We will start off by assigning our variables, both general and UI:

--// GENERAL VARIABLES
local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetOrderedDataStore("CoinsBoard1")

--// UI VARIABLES

local UI = script.Parent.SurfaceGui
local basePlayerFrame = UI.BasePlayerFrame
local boardFrame = UI.ScrollingFrame

After our variables, we will get started with our functions. The first one is just a copy of the BindToClose function from before (we called it OnServerShutdown) and rename it to SaveData:

--// Functions
local function SaveData()
    for _, player in pairs(game:GetService("Players"):GetPlayers()) do
		local plr_key = "id_"..player.UserId.."_Coins"

		local coins = player.leaderstats.Coins

		local success, result = pcall(function()
			DataStore:SetAsync(plr_key, coins.Value)
		end)

		if not success then 
			warn(result)
		end
	end
end

So the next function we will make is one that will get us our Top50Players as we will be doing a top 50 coins leaderboard.

local function getTop50Players()
	local isAscending = false
	local pageSize = 50
	local pages = DataStore:GetSortedAsync(isAscending, pageSize)
	local top50 = pages:GetCurrentPage()

	local top = {}

	for rank, data in ipairs(top50) do
		local dataName = data.key
		local name = game:GetService("Players"):GetNameFromUserIdAsync(dataName:split('_')[2])
		local coins = data.value

		local currentPlayer =  { Player = name, Coins = coins, Rank = rank, }
		table.insert(top, rank, currentPlayer)
	end

	return top
end

So let’s break down that function. We first make some variables. The pageSize variable tells us how many players (max) we want to get back. isAscending represents the order, if it’s false then the player with the highest amount of coins will be on top (and if its true then the lowest amount will be on top). pages gives us our results and top50 gives us our top50 players. Finally we have an empty top table that we will fill up with all the top players.

To do that, we loop through our top50 table and insert a dictionary (storing the Player’s Name, Coins, and Rank) into the top table. We also return the top table at the end so we can loop through it later and get all the players aswell as their Name, Coins and Rank.

The next function we will make is one we will use. to clear the list.

local function ClearList()
	task.spawn(function()	
		for _, plrFrame in pairs(boardFrame:GetChildren()) do
			if not plrFrame:IsA("Frame") then continue end
			plrFrame:Destroy()
			task.wait(0.25)
		end
	end)
end

This function will loop through everything in the board if it is not a frame (e.g it is a UIListLayout) then it will skip that iteration using the guard clause (if). Then it will destroy the frame. Note the task.wait(0.25) is optional, I put it there to give it the deleting 1-by-1 effect.

This next (and final) function will be the one that updates the list. This one is the biggest, so we’ll go step-by-step.
First we start off by declaring the function and calling our SaveData() and ClearList() functions.

local function UpdateList()
	SaveData()
	ClearList()
end

after we call those two methods we want to get our Top 50 players (using the function from before)

local top50 = getTop50Players()

after that we want to spawn a new thread, and loop through all of our Top 50 players using an ipairs loop so it goes in order.

task.spawn(function()	
    for _, plr in ipairs(top50) do

    end
end)

Inside of that loop, we want to do a few things. Clone the base frame and set the parent to the board.

Then we want to set the Plr (player name text label), Amount (The TextLabel for the amount of coins the player has), and the Rank (the text label that displays the players rank on the leaderboard).

All that information comes from the Plr dictionary that we get by looping through our Top 50.

Finally we want to make the frame visible and task.wait(0.25) so that we get the effect of the players being added 1-by-1.

local frame = basePlayerFrame:Clone()
frame.Parent = boardFrame
			
frame.Plr.Text = plr.Player
frame.Amount.Text = plr.Coins
frame.Rank.Text = plr.Rank
			
frame.Visible = true
task.wait(0.25)

Alright, thats it for that function:

local function UpdateList()
	SaveData()
	clearList()
	local top50 = getTop50Players()
	
	task.spawn(function()	
		for _, plr in ipairs(top50) do
			local frame = basePlayerFrame:Clone()
			frame.Parent = boardFrame
			
			frame.Plr.Text = plr.Player
			frame.Amount.Text = plr.Coins
			frame.Rank.Text = plr.Rank
			
			frame.Visible = true
			task.wait(0.25)
		end
	end)
end

Okay, so now the last thing we want to do is update the leaderboard every minute (60 seconds and also have a countdown). Don’t do anything less or else Roblox will start yelling at you about making to many requests (lol).

So to do this we want to spawn a new thread, have a while loop, and then a for loop that will update the text evey second. Very very simple. Oh ya and also yk call the UpdateList() function.

task.spawn(function()
	while true do
		UpdateList()
		for count=60,0,-1 do
			script.Parent.Part.SurfaceGui.Count.Text = "Leaderboards Updating In: "..count
			task.wait(1)
		end
	end
end)

So yeah. Thats it. That was the last thing. We are done:

How Did I Do On This Tutorial, 1-10.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

EDIT: BRAND NEW VIDEO TUTORIAL IS HERE

44 Likes

you forgor images and videos :skull: other than that good job. I didnt red it cuz I know how to do it but yea still.

2 Likes

It is a well organised tutorial, but you really have to add images or any form or media to let the developer, know whether they are doing it correctly.

But asides, as mentioned well organised and formatted separating the command and the script. Keep it up - this should help the new developers getting started! Gets a solid 8.

Keep helping the community :smile:

3 Likes

Thank! I’m going to try to get screenshots up right now.

Update: I followed @Qinrir and @Rinzerinho suggestions and added 4 images.

2 Likes

Top man, this will bump up the ratings for sure.

1 Like

Thanks! I appreciate it. This was basically my first tutorial as somehow a admin ban command tutorial (that I made, it got taken down) is less substantial then a leaderstats tutorials.

1 Like

Alright, fixed one little issue that was there, forgot one word and didn’t realize. All fixed now though!

It is printing “Argument 1 missing or nil” in this line.

		local name = game:GetService("Players"):GetNameFromUserIdAsync(dataName:split('_')[2])

whole script

--// GENERAL VARIABLES
local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetOrderedDataStore("test")

--// UI VARIABLES

local UI = script.Parent.SurfaceGui
local basePlayerFrame = game.ReplicatedStorage.objects.template
local boardFrame = UI.Main.ScrollingFrame

--// Functions
local function SaveData()
	for _, player in pairs(game:GetService("Players"):GetPlayers()) do
		local plr_key = "id_"..player.UserId.."_Wins"

		local wins = player.leaderstats.Wins

		local success, result = pcall(function()
			DataStore:SetAsync(plr_key, wins.Value)
		end)

		if not success then 
			warn(result)
		end
	end
end

local function getTop10Players()
	local isAscending = false
	local pageSize = 10
	local pages = DataStore:GetSortedAsync(isAscending, pageSize)
	local top10 = pages:GetCurrentPage()

	local top = {}

	for rank, data in ipairs(top10) do
		local dataName = data.key
		local name = game:GetService("Players"):GetNameFromUserIdAsync(dataName:split('_')[2])
		local wins = data.value

		local currentPlayer =  { Player = name, Wins = wins, Rank = rank, }
		table.insert(top, rank, currentPlayer)
	end

	return top
end

local function ClearList()
	task.spawn(function()	
		for _, plrFrame in pairs(boardFrame:GetChildren()) do
			if not plrFrame:IsA("Frame") then continue end
			plrFrame:Destroy()
			task.wait(0.25)
		end
	end)
end

local function UpdateList()
	SaveData()
	ClearList()
	local top50 = getTop10Players()

	task.spawn(function()	
		for _, plr in ipairs(top50) do
			local frame = basePlayerFrame:Clone()
			frame.Parent = boardFrame

			frame.name.Text = plr.Player
			frame.wins.Text = plr.Wins
			frame.rank.Text = plr.Rank

			frame.Visible = true
			task.wait(0.25)
		end
	end)
end

task.spawn(function()
	while true do
		UpdateList()
		for count=60,0,-1 do
		--	script.Parent.Part.SurfaceGui.Count.Text = "Leaderboards Updating In: "..count
			task.wait(1)
		end
	end
end)
2 Likes

Your script works for me, the issue might be in you leaderstats/main saving script!

1 Like

Guys, video tutorial is here!!!

1 Like

I just want to know what does :split(’_’)[2] does here because im not sure what does the function :split do

here

local s = "hello:world"
local t = s:split(":") -- or you can do string.split(s)

-- split the string by ':'. the table is now {"hello, "world"}

print("--for loop--")
for index, value in pairs(t) do
     print("[" .. index .. "] " .. value)
end

print("--print--")
print(t[1])

Output:
     --for loop--
     -- [1] hello
     -- [2] world
     --print--
     -- [1] hello

hope this helps you understand

1 Like

I’ll explain the best I can.

So first off what the split function does is it Splits the string into an array by a separator that you pass in, example:

local myStr = "Split At Space"
print(myStr:split(" "))

In this example the separator is the character (space) so what this string:split will do is separate the string into an array and that would look like: ["Split", "At", "Space"] see? It uses the separator we passed to split the string into an array so in this case we got every word (everything in between 2 spaces) and put them in a array.

Now, what this is doing in our code:

You see here in our plr_key it would look like 'id_playersUserId_Coins`
(Keep that in mind for a second)

Here:

dataName is the same thing as that plr_key (And it has the actual player’s UserId)

So if we do:

That would return ["id", 122322, "Coins"],
As you see, the 2nd index is the players UserId, that’s why we do:

And that whole line is used to get the player’s username from their UserId:

2 Likes

It worked for me, but after a bit i get this error

"Players:GetUserIdFromNameAsync() failed: Unknown user"

im not sure what’s wrong…

Is ProfileService applicable to that?

ProfileService unfortunately doesn’t support OrderedDataStores but you can use profile service to save your data and then use the OrderedDataStore in this tutorial just for saving leaderboard stats.

1 Like

This tutorial leaderboard probably has some issues. Like where’s the request system? What if it exceeds into a limit tho

Thanks, I tried a normal datastore (not global leaderboard) before, it reaches its max limit of stuff idk and i forgot the warning, how can I fix this?