Leaderboard with Datastores sometimes works in studio, never works on roblox player

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

I want my times on my leaderboard UI to show when the player joins.

  1. What is the issue? Include screenshots / videos if possible!

It does not show at on the Roblox player, and sometimes shows on Roblox studio. On roblox studio, if I launch 3 players, Maybe 1 or 2 would actually have a working leaderboard. Also sometimes, some players do not have their leaderboards update!

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?

I have mainly messed around with the server and client scripts but to no avail. I’m sure it would helpful if you could look at them.

Client (localscript in startergui)

-- References to GUI elements
local frame = script.Parent.Stopwatch
local timeLabel = frame.TimeLabel
local leaderboardFrame = script.Parent.LeaderboardFrame
local leaderboardLabels = {
	leaderboardFrame["1"],
	leaderboardFrame["2"],
	leaderboardFrame["3"],
	leaderboardFrame["4"],
	leaderboardFrame["5"]
}

-- Variables for time tracking
local isRunning = false
local startTime = 0
local elapsedTime = 0
local timer

-- Get the LocalPlayer
local player = game.Players.LocalPlayer

-- Store original WalkSpeed and JumpPower
local originalWalkSpeed
local originalJumpPower

-- Function to set the character's movement
local function setCharacterMovement(character, walkSpeed, jumpPower)
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid.WalkSpeed = walkSpeed
		humanoid.JumpPower = jumpPower
	end
end

-- Function to freeze the character
local function freezeCharacter(character)
	if character and not originalWalkSpeed then
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if humanoid then
			originalWalkSpeed = humanoid.WalkSpeed
			originalJumpPower = humanoid.JumpPower
			setCharacterMovement(character, 0, 0)
		end
	end
end

-- Function to unfreeze the character
local function unfreezeCharacter(character)
	if character and originalWalkSpeed and originalJumpPower then
		setCharacterMovement(character, originalWalkSpeed, originalJumpPower)
		originalWalkSpeed, originalJumpPower = nil, nil
	end
end

-- Function to update the time display
local function updateTimeDisplay()
	local totalMilliseconds = math.floor(elapsedTime * 1000)
	local hours = math.floor(totalMilliseconds / 3600000)
	local minutes = math.floor((totalMilliseconds % 3600000) / 60000)
	local seconds = math.floor((totalMilliseconds % 60000) / 1000)
	local milliseconds = totalMilliseconds % 1000
	timeLabel.Text = string.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds)
end

-- Function to update the leaderboard display
local function updateLeaderboard(leaderboard)
	for i = 1, 5 do
		if leaderboard[i] then
			local entry = leaderboard[i]
			leaderboardLabels[i].Text = string.format("%s - %02d:%02d:%02d.%03d", entry.username, entry.hours, entry.minutes, entry.seconds, entry.milliseconds)
		else
			leaderboardLabels[i].Text = ""
		end
	end
end

-- RemoteEvent and RemoteFunction for leaderboard updates
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local updateLeaderboardEvent = ReplicatedStorage:WaitForChild("UpdateLeaderboard")
local getLeaderboardFunction = ReplicatedStorage:WaitForChild("GetLeaderboard")

updateLeaderboardEvent.OnClientEvent:Connect(updateLeaderboard)

-- Function to start the timer
local function startTimer(character)
	if not isRunning then
		isRunning = true
		freezeCharacter(character)
		task.wait(2)
		unfreezeCharacter(character)
		startTime = tick() - elapsedTime
		timer = game:GetService("RunService").RenderStepped:Connect(function()
			elapsedTime = tick() - startTime
			updateTimeDisplay()
		end)
	end
end

-- Function to stop the timer
local function stopTimer()
	if isRunning then
		isRunning = false
		timer:Disconnect()
		ReplicatedStorage.UpdateLeaderboard:FireServer(elapsedTime)
	end
end

-- Function to reset the timer
local function resetTimer()
	stopTimer()
	elapsedTime = 0
	updateTimeDisplay()
end

-- Function to handle part touches
local function handlePartTouch(part, action)
	part.Touched:Connect(function(hit)
		local hitPlayer = game.Players:GetPlayerFromCharacter(hit.Parent)
		if hitPlayer == player then
			action(player.Character)
		end
	end)
end

local Required = {"Start", "Stop", "Reset"}
local AllFound = false
while not AllFound do
	AllFound = true
	for _, item in ipairs(Required) do
		if not game.Workspace:FindFirstChild(item) then
			AllFound = false
		end
	end
	task.wait(0.4)
end

-- Connect the start, stop, and reset events to touch events
handlePartTouch(game.Workspace.Start, startTimer)
handlePartTouch(game.Workspace.Stop, stopTimer)
handlePartTouch(game.Workspace.Reset, resetTimer)

-- Initialize display
updateTimeDisplay()

-- Request and update leaderboard on client start
local success, leaderboard = pcall(function()
	return getLeaderboardFunction:InvokeServer()
end)

if success then
	updateLeaderboard(leaderboard)
else
	warn("Failed to load leaderboard on client: " .. tostring(leaderboard))
end

Server (script in ServerScriptService)

local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local leaderboardDataStore = DataStoreService:GetDataStore("StopwatchLeaderboard")

-- Initialize RemoteFunction for synchronous data requests
local getLeaderboardFunction = Instance.new("RemoteFunction")
getLeaderboardFunction.Name = "GetLeaderboard"
getLeaderboardFunction.Parent = ReplicatedStorage

-- Initialize RemoteEvent for asynchronous updates
local updateLeaderboardEvent = Instance.new("RemoteEvent")
updateLeaderboardEvent.Name = "UpdateLeaderboard"
updateLeaderboardEvent.Parent = ReplicatedStorage

-- Initialize leaderboard
local leaderboard = {}

-- Load the leaderboard from DataStore
local function loadLeaderboard()
	local success, data = pcall(function()
		return leaderboardDataStore:GetAsync("Leaderboard")
	end)
	if success and data then
		leaderboard = data
	else
		warn("Failed to load leaderboard: " .. tostring(data))
	end
end

-- Save the leaderboard to DataStore
local function saveLeaderboard()
	local success, err = pcall(function()
		leaderboardDataStore:SetAsync("Leaderboard", leaderboard)
	end)
	if not success then
		warn("Failed to save leaderboard: " .. tostring(err))
	end
end

-- Function to compare times
local function compareTimes(a, b)
	local aTime = a.hours * 3600 + a.minutes * 60 + a.seconds + a.milliseconds / 1000
	local bTime = b.hours * 3600 + b.minutes * 60 + b.seconds + b.milliseconds / 1000
	return aTime < bTime
end

-- Handle leaderboard updates from clients
updateLeaderboardEvent.OnServerEvent:Connect(function(player, elapsedTime)
	local totalMilliseconds = math.floor(elapsedTime * 1000)
	local newEntry = {
		username = player.Name,
		hours = math.floor(totalMilliseconds / 3600000),
		minutes = math.floor((totalMilliseconds % 3600000) / 60000),
		seconds = math.floor((totalMilliseconds % 60000) / 1000),
		milliseconds = totalMilliseconds % 1000
	}

	-- Check if the player already has an entry
	local existingEntryIndex = nil
	for i, entry in ipairs(leaderboard) do
		if entry.username == player.Name then
			existingEntryIndex = i
			break
		end
	end

	-- Update the entry if the new time is better or add a new entry
	if existingEntryIndex then
		local existingEntry = leaderboard[existingEntryIndex]
		if compareTimes(newEntry, existingEntry) then
			leaderboard[existingEntryIndex] = newEntry
		end
	else
		table.insert(leaderboard, newEntry)
	end

	-- Sort and trim the leaderboard to top 5
	table.sort(leaderboard, compareTimes)
	while #leaderboard > 5 do
		table.remove(leaderboard, 6)
	end

	saveLeaderboard()

	-- Send updated leaderboard to all clients
	updateLeaderboardEvent:FireAllClients(leaderboard)
end)

-- Handle synchronous leaderboard data requests
getLeaderboardFunction.OnServerInvoke = function()
	return leaderboard
end

-- Provide the latest leaderboard to players joining after the initial update
game.Players.PlayerAdded:Connect(function(player)
    pcall(function()
        task.delay(1, function()
            -- Slight delay to ensure the player is fully connected
            getLeaderboardFunction:InvokeClient(player, leaderboard)
        end)
    end)
end)



-- Load leaderboard on server start
loadLeaderboard()

I don’t get a single error on output. So what am I doing wrong?

bump, sorry

charcharcharcharchar

I added some debug to my script and I’m getting this ingame.

ignore the sanitized id errors

Bumping again. I’ve added some stuff to my server script to try to combat the issue. I have added task.wait()'s to the script to allow more time for a player to load in, I have added a :BindToClose to prevent any errors when a server shuts down, but I am still getting the same exact issue from before! While studio works a bit more frequently, the player still does not.

-- Server Script
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local leaderboardDataStore = DataStoreService:GetDataStore("StopwatchLeaderboard")

local getLeaderboardFunction = Instance.new("RemoteFunction")
getLeaderboardFunction.Name = "GetLeaderboard"
getLeaderboardFunction.Parent = ReplicatedStorage

local updateLeaderboardEvent = Instance.new("RemoteEvent")
updateLeaderboardEvent.Name = "UpdateLeaderboard"
updateLeaderboardEvent.Parent = ReplicatedStorage

local leaderboard = {}

-- Load the leaderboard from DataStore
local function loadLeaderboard()
	print("Loading leaderboard...")
	local success, data = pcall(function()
		return leaderboardDataStore:GetAsync("Leaderboard")
	end)
	if success and data then
		leaderboard = data
		print("Leaderboard loaded successfully.")
	else
		warn("Failed to load leaderboard: " .. tostring(data))
		leaderboard = {}
	end
	task.wait(1)  -- Allow some time for the data to settle
end

-- Save the leaderboard to DataStore
local function saveLeaderboard()
	print("Saving leaderboard...")
	local success, err = pcall(function()
		leaderboardDataStore:SetAsync("Leaderboard", leaderboard)
	end)
	if not success then
		warn("Failed to save leaderboard: " .. tostring(err))
	end
	task.wait(1)  -- Allow some time for the data to save
end

local function compareTimes(a, b)
	local aTime = a.hours * 3600 + a.minutes * 60 + a.seconds + a.milliseconds / 1000
	local bTime = b.hours * 3600 + b.minutes * 60 + b.seconds + b.milliseconds / 1000
	return aTime < bTime
end

updateLeaderboardEvent.OnServerEvent:Connect(function(player, elapsedTime)
	print("Received leaderboard update from:", player.Name)
	local totalMilliseconds = math.floor(elapsedTime * 1000)
	local newEntry = {
		username = player.Name,
		hours = math.floor(totalMilliseconds / 3600000),
		minutes = math.floor((totalMilliseconds % 3600000) / 60000),
		seconds = math.floor((totalMilliseconds % 60000) / 1000),
		milliseconds = totalMilliseconds % 1000
	}

	local existingEntryIndex = nil
	for i, entry in ipairs(leaderboard) do
		if entry.username == player.Name then
			existingEntryIndex = i
			break
		end
	end

	if existingEntryIndex then
		local existingEntry = leaderboard[existingEntryIndex]
		if compareTimes(newEntry, existingEntry) then
			leaderboard[existingEntryIndex] = newEntry
		end
	else
		table.insert(leaderboard, newEntry)
	end

	table.sort(leaderboard, compareTimes)
	while #leaderboard > 5 do
		table.remove(leaderboard, 6)
	end

	saveLeaderboard()
	task.wait(1)  -- Allow time for saving before firing update
	updateLeaderboardEvent:FireAllClients(leaderboard)
end)

getLeaderboardFunction.OnServerInvoke = function()
	task.wait(1)  -- Allow some delay before returning data
	return leaderboard
end

Players.PlayerAdded:Connect(function(player)
	print("Player added:", player.Name)
	updateLeaderboardEvent:FireClient(player, leaderboard)
end)

-- Wait for all players to join before loading data
task.wait(3) -- Adjust this delay if necessary
loadLeaderboard()

-- BindToClose to save leaderboard on server shutdown
game:BindToClose(function()
	print("Server is shutting down. Saving leaderboard...")
	saveLeaderboard()
end)

Do not use a normal data store for this. It will quickly run out of space.

Instead, use an OrderedDataStore.

--get your store
local store = game:GetService("DataStoreService"):GetOrderedDataStore("SomeAwesomeDataStore")
local players = game:GetService("Players")

local shouldReverse = false --reverse data?
local pageSize = 100 --entries per page?

local function update()
    --get the info
    local success, pages = pcall(store.GetSortedAsync, store, shouldReverse, pageSize)
    if not success then warn(pages) return nil end

    local page = pages:GetCurrentPage() --get top page of entries

    for rank, entry in next, page, nil do --iterate over top entry page
        --key used
        local key = page[rank]
        local success, name = pcall(players.GetNameFromUserIdAsync, players, tonumber(key.key)) --get username
        print(`Player at rank #{rank} (@{name}) has value {entry.value}`)
    end
end

local function save(player:Player, value:number) --save a player's data, the value must be a number
    local success, result = pcall(store.SetAsync, store, player.UserId, value) --leaderboards should only mimic data and it is not a table so we use SetAsync here

    --handle errors and stuff
end

--call stuff here ig u need
1 Like