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

21 Likes

Included some relevant files into the post

perfectly documented & simplified for beginners

great guide! thanks for sharing

1 Like

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

1 Like

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)

1 Like