How to make a Top Donators leaderboard

Greetings! I’m delighted to present my first tutorial on this platform. In this session, we’ll be creating a top donators leaderboard that is not only visually appealing but also fully functional. By the end of this tutorial, you’ll be able to confidently design a leaderboard that showcases the most generous contributors to your cause.

What we’re going to learn:

  • Datastore
  • Players Service
  • Particles
  • BadgeService

Step 1: Making the leaderboard

Insert a part into your workspace, and insert a SurfaceGui into the part. Insert a frame as the MainFrame/root. Set the size to {1, 0}, {1, 0}, transparency to 1. Now we’re gonna make the donator list as a ScrollingFrame and set the size to {1, 0}, {1, 0}, and maybe make it a bit transparent. Lastly we’re gonna make the leaderboard title so players know what they’re looking at
(not necessarily needed script-wise)
Leaderboard.rbxm (11.8 KB)

image

Step 2: Where the magic begins

Before we go into scripting we first need to make the donator frame as a way to visualize the donators
image
image
Donator.rbxm (8.1 KB)

Alright, now for the scripting part. Insert a script into the MainFrame and parent the donator frame you just created in the script.

-- services
local DatastoreService = game:GetService('DataStoreService')
local Players = game:GetService('Players')

-- the datastore
local DonatorsDatastore = DatastoreService:GetOrderedDataStore('Donators')

-- the MainFrame
local MainFrame = script.Parent

-- the Donators list
local Donators = MainFrame.Donators

-- how often the leaderboard refreshes (in seconds)
local refreshInterval = 30

-- will only show # donators in the leaderboard
local donatorsShown = 100

local function refresh()
	-- get the donators
	local pages: DataStorePages = DonatorsDatastore:GetSortedAsync(false, donatorsShown, 1, 2^63)
	
	-- clear the previous frames
	for _, v in Donators:GetChildren() do
		if v:IsA('Frame') then
			v:Destroy()
		end
	end
	
	-- loop over the donators
	-- rank is a range from 1 to #donatorsShown
	-- data is a dictionary/table containing the userId and the amount donated
	for rank, data in pages:GetCurrentPage() do	
		local userId = data.key
		local amount = data.value
		
		-- get the name based on the userId
		local name = Players:GetNameFromUserIdAsync(userId)
		
		-- get the headshot profile image of the donator
		local profile = Players:GetUserThumbnailAsync(userId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
		
		-- clone the donator frame
		local clone = script.Donator:Clone()
		
		clone.Username.Text = name
		clone.Profile.Image = profile
		
		clone.Robux.Text = amount
		clone.LayoutOrder = rank
		
		-- set the donator frame parent into the donators list
		clone.Parent = Donators
	end
end

-- main loop
while true do
	refresh()
	task.wait(refreshInterval)
end

:GetSortedAsync will return a DataStorePages which contains sorted individual Page, which we can use it to get the key (userId) and the value (amount). Here is a visual example of how the datastore works

{
  page1 = {
    [1] = {[68174910418] = 100};
    [2] = {[18146901461] = 76};
    [3] = {[5151819109] = 75};
  }
}

Step 3: Donating logic

I’m not gonna explain how to create the gui needed to donate, but if you already know how to do it, you can skip this part. Download and insert the file into StarterGui
DonationGui.rbxm (11.3 KB)

After that you can make the donation amounts as Developer Products in the Monetization tab in the Game Settings

Set the DonationFrame and the TextButtons visibility to true and set the amount & name of each button to match your products
image
image

Insert a RemoteEvent in ReplicatedStorage called “PromptDonation”. Create a badge to show donators apart. Insert a script in ServerScriptService and insert a part with ParticleEmitters for a little show
Emitter.rbxm (4.3 KB)

with badge:
-- services
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local MarketplaceService = game:GetService('MarketplaceService')
local BadgeService = game:GetService('BadgeService')

local DatastoreService = game:GetService('DataStoreService')

local Players = game:GetService('Players')
local Debris = game:GetService('Debris')

-- the datastore
local DonatorsDatastore = DatastoreService:GetOrderedDataStore('Donators')

-- the badgeId to show donators apart from regular players
local badgeId = 2129510374

--[[
	format:
	```
	[productId] = {
		Name = buttonName;
		Price = donationAmount;
	};
	```
]]
local Products = {
	[1338470979] = {
		Name = 'Small';
		Price = 50;
	};
	[1338471308] = {
		Name = 'Big';
		Price = 100;
	};
	[1338471406] = {
		Name = 'Plenty';
		Price = 300;
	};
	[1338471485] = {
		Name = 'Huge';
		Price = 1000;
	};
}

ReplicatedStorage.PromptDonation.OnServerEvent:Connect(function(player: Player, donation: string)
	local product
	
	for id, v in Products do
		if v.Name == donation then
			product = id
			break
		end
	end
	
	if not product then return end
	
	MarketplaceService:PromptProductPurchase(player, product, true, Enum.CurrencyType.Robux)
end)

MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId: number, productId: number, purchased: boolean)
	-- get the player by userId
	local player = Players:GetPlayerByUserId(userId)
	
	-- check if the player purchased it or not
	if purchased then
		-- get the price/amount
		local price = Products[productId].Price
		
		-- award the badge to show gratitude towards them
		pcall(BadgeService.AwardBadge, BadgeService, userId, badgeId)
		
		-- increment the donator's amount
		pcall(DonatorsDatastore.IncrementAsync, DonatorsDatastore, userId, price)
		
		-- get the cframe to position the emitter
		local position = player.Character.HumanoidRootPart.Position - Vector3.new(0, player.Character.Humanoid.HipHeight, 0)
		local cf = CFrame.new(position)
		
		-- clone and show the emitter
		local clone = script.Emitter:Clone()
		clone:PivotTo(cf)
		clone.Parent = workspace
		
		-- destroy the emitter after 10 seconds
		Debris:AddItem(clone, 10)
	end
end)
without badge:
-- services
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local MarketplaceService = game:GetService('MarketplaceService')

local DatastoreService = game:GetService('DataStoreService')

local Players = game:GetService('Players')
local Debris = game:GetService('Debris')

-- the datastore
local DonatorsDatastore = DatastoreService:GetOrderedDataStore('Donators')

--[[
	format:
	```
	[productId] = {
		Name = buttonName;
		Price = donationAmount;
	};
	```
]]
local Products = {
	[1338470979] = {
		Name = 'Small';
		Price = 50;
	};
	[1338471308] = {
		Name = 'Big';
		Price = 100;
	};
	[1338471406] = {
		Name = 'Plenty';
		Price = 300;
	};
	[1338471485] = {
		Name = 'Huge';
		Price = 1000;
	};
}


ReplicatedStorage.PromptDonation.OnServerEvent:Connect(function(player: Player, donation: string)
	local product
	
	for id, v in Products do
		if v.Name == donation then
			product = id
			break
		end
	end
	
	if not product then return end
	
	MarketplaceService:PromptProductPurchase(player, product, true, Enum.CurrencyType.Robux)
end)

MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId: number, productId: number, purchased: boolean)
	-- get the player by userId
	local player = Players:GetPlayerByUserId(userId)
	
	-- check if the player purchased it or not
	if purchased then
		-- get the price/amount
		local price = Products[productId].Price
		
		-- increment the donator's amount
		pcall(DonatorsDatastore.IncrementAsync, DonatorsDatastore, userId, price)
		
		-- get the cframe to position the emitter
		local position = player.Character.HumanoidRootPart.Position - Vector3.new(0, player.Character.Humanoid.HipHeight, 0)
		local cf = CFrame.new(position)
		
		-- clone and show the emitter
		local clone = script.Emitter:Clone()
		clone:PivotTo(cf)
		clone.Parent = workspace
		
		-- destroy the emitter after 10 seconds
		Debris:AddItem(clone, 10)
	end
end)

image

Final Step: Testing

Congratulations, you’ve completed all the necessary steps to set up your donation system and leaderboard! Before you launch your system to the public, it’s crucial to ensure that everything is functioning correctly.

To do so, we recommend testing the donating system and leaderboard thoroughly. You can start by running several test transactions to ensure that donations are being processed correctly and that they are reflected on the leaderboard.

Once you’re confident that everything is working smoothly, you can finally launch your donation system and start accepting contributions from your community. Thank you for following this tutorial, and we wish you the best of luck in your fundraising efforts!

Remember, the key to success is to stay committed to your cause and continue to engage with your donors regularly. With a functional donation system and a visually appealing leaderboard, you’ll be well on your way to achieving your fundraising goals. Goodbye, and best of luck!

If your leaderboard doesn’t work, don’t hesitate to reach out to me; I will try to debug the problem as quickly as possible


some code snippets

remove test donations from the board:

game:GetService('DataStoreService'):GetOrderedDataStore('Donators'):RemoveAsync(userId)

set donations manually to the board:

game:GetService('DataStoreService'):GetOrderedDataStore('Donators'):SetAsync(userId, value)

(paste one of the snippet into the console in game or studio and replace the values)

e.g:

game:GetService('DataStoreService'):GetOrderedDataStore('Donators'):SetAsync(1, 50)

Support me

28 Likes

Included some relevant files into the post

1 Like

perfectly documented & simplified for beginners

great guide! thanks for sharing

2 Likes

thank you! im glad to help! :+1:

2 Likes

Why none of those are working? the one inside for loop or yellow cube

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local TransferLeaderboardData = ReplicatedStorage.TransferLeaderboardData
local Datastore = game:GetService("DataStoreService")
local DonatersDatastore = Datastore:GetOrderedDataStore("Top20Donaters")
-------------------------
local Cooldown = true
local RefreshInterval = 5
local donatorsShown = 20
-------------------------


TransferLeaderboardData.OnServerInvoke = function(player, ScrollingFrame)	
	if Cooldown then
		Cooldown = false
		local pages: DataStorePages = DonatersDatastore:GetSortedAsync(false, donatorsShown,1,2^63)
		------------------------- Refreshing
		for i,v in pairs(ScrollingFrame:GetChildren()) do
			if v:IsA("Frame") then
				v:Destroy()
			end
		end
		print("Refreshing ended")
		-------------------------
		print("Starting for loop")
		for rank, data in pages:GetCurrentPage() do
			print("getting rank and data")
			local userId = data.key
			local DonatedAmount = data.value
			print("Mid for loop")
			local slot = ServerStorage.Rank:Clone()
			slot.Name = player.Name
			slot.Robux.Text = DonatedAmount
			slot.LayoutOrder = rank
			slot.Parent = ScrollingFrame
			print("Cloned and changed")
		end
		print("will wait "..RefreshInterval)
		task.wait(RefreshInterval)
		Cooldown = true
	end
end
1 Like

try printing pages, a for loop will not run if the iterator contents is nil (it might be that the datastore isn’t setting the data properly)

2 Likes

Sorry for the Bump, but do u have this as a model. It does not seem to work for me

1 Like

I can’t get on my pc right now, could you elaborate on what did the output say/did it error, and send the server script and the local script

2 Likes

Never mind, i got it to work, thank you so much. but may you please answer the following questions, if possible:

1-How to make a different color for each podium, example: 1st place = gold, 2nd = silver, 3rd = bronze, others = the normal color

2-how can i make the size of the names get smaller if they are long in order to avoid overlaying?
Because as you can see, my name is pretty long :rofl: , so my name has overlayed the robux icon.

3-How to get the Avatar (rig) of the 1st player, and how to make an animation (dancing for example)?

1 Like

In the refresh function, there’s the rank variable, which you can use to determine the color

local Color = NormalColor

if rank == 1 then Color = Color3.new(252, 255, 51) end
if rank == 2 then Color = ... end
if rank == 3 then Color = ... end

clone.BackgroundColor3 = Color

You can use TextScaled for that, although you can also make the frame longer.

You can use Players:CreateHumanoidModelFromUserId(UserId) for that

2 Likes

May you please tell me where i should put that in the script?

1 Like

you can put it inside the if statement, the top donator is the only one shown

1 Like

i do not quite understand what to do, may you please provide the full version of the code?

1 Like

Heres more information about the method

It returns the character from a given user id, then you can just do whatever you want with it

local character, lastTopUserId = nil -- keep track of the previous character to prevent memory leaks
local function refresh()
    -- ...

    for rank, data in pages:GetCurrentPage() do	
        local userId = data.key
	local amount = data.value

        if rank == 1 then
            if lastTopUserId ~= userId then
                if character then character:Destroy() end -- remove the previous donator character
                character = Players:GetHumanoidModelFromUserId(userId) -- replace with the new donator character
                character.HumanoidRootPart.Anchored = true -- prevent it from being moved
                character:PivotTo(where_you_want_it_to_be)
                character.Parent = workspace -- send it to workspace
                lastTopUserId = userId
            end
        end
        -- ... the rest
    end
end

error: GetHumanoidModelFromUserId is not a valid member of Players “Players”

in:

my mistake, it should be CreateHumanoidModelFromUserId instead of GetHumanoidModelFromUserId

1 Like

thank you so much, it worked.
i tried to make the rig dance and i got this error: LoadAnimation is not a valid member of Model “Workspace.Player”
from the last 3rd line

		if lastTopUserId ~= userId then
			if character then character:Destroy() end -- remove the previous donator character
			character = Players:CreateHumanoidModelFromUserId(userId) -- replace with the new donator character
			character.HumanoidRootPart.Anchored = true -- prevent it from being moved
			local Cframe = CFrame.new(184, 10, 23)
			character:PivotTo(Cframe)
			character.Parent = workspace -- send it to workspace
			lastTopUserId = userId
			--Animation
			local animationId = "rbxassetid://10478368365"
			local animation = Instance.new("Animation")
			animation.AnimationId = "rbxassetid://" .. animationId

			character:LoadAnimation(animation):Play()
		end
	end

Just a neat little thing I thought was important:

You don’t need a decal for the robux icon. The icons for premium and robux can be put down as text using unicode symbols.

image

Credit to this post which is where I got this from.

2 Likes

Hi, Why did u add “UiListLayout” and “UiPadding” for the donation Button
image

Sorry for asking a lot of questions

The UIListLayout serves to position the donation button and the donation frame in a “ordered list” like-layout. While the UIPadding is just there to make sure it doesn’t touch the edge of the screen