XP Bar Logic / Storing Remainder + Tweening Problems [SOLVED]

Hi everyone. I’m having trouble creating a smooth and accurate XP/Level bar with a GUI element. I have a functional module script that handles adding and subtracting XP and leveling logic. However, when I try to reflect this on my frame bar (top left in the video), the yellow progress bar doesn’t align correctly with actual stored XP numbers (the numbers inside the yellow frame are accurate). At first, tweeting seems fine, but after repeatedly adding XP and leveling up, the frame’s size becomes inaccurate.

I also store the remainder of XP after a level-up, which likely contributes to the inconsistency. I tried rounding the values using various math formulas, but the problem persists. The code might be messy, and I’m not sure how to fix it.

Does anyone have any suggestions for solving this?

– The Tweening Script:

local Players = game:GetService("Players")
local player = Players.LocalPlayer or Players.PlayerAdded
local playerGUI = player.PlayerGui
local levelXPGUI = playerGUI:WaitForChild("LevelXPGUI")
local blankLVLBar = levelXPGUI.BlankLVLBar
local progressBar = blankLVLBar.ProgressLVLBar
local TweenService = game:GetService("TweenService")
local RS = game:GetService("ReplicatedStorage")
local XPMod = require(RS.Modules.XPDistribution)
local XPModRemote = RS.XPModRemote


local defaultColor = progressBar.BackgroundColor3
local defaultColorBlank = blankLVLBar.BackgroundColor3


local xpGainSound = workspace.pick_up2
local LevelUpSound = workspace.LevelUp
local choir = workspace["Swinging Holiday"]
local yes = workspace.yeS


progressBar.Text = "  XP: " .. 0 .."/"..200

local function TweenLVL(amount, MAX_XP_NEXT, currentStats, updatedStats, CURRENT_MAX)
	
	local xpRemainder = 0
	local currentXP = currentStats.CurrentXP
	
	local updatedXP = updatedStats.CurrentXP
	print(updatedXP .. " test2")
	local currentWidthScale = progressBar.Size.X.Scale
	
	print(amount .. " XP added")
	xpGainSound:Play()
	
	if MAX_XP_NEXT >= 617870 or CURRENT_MAX >= 617870 then
		print(MAX_XP_NEXT .. CURRENT_MAX .. " LIMIT")
		return
	end
	
	progressBar.Text = "  XP: " .. updatedXP .."/"..MAX_XP_NEXT
	
	local change = amount/CURRENT_MAX 
	change = math.floor(change * 10^3 + 0.5) / 10^3
	print(change .. "change")
	local newWidthScale = currentWidthScale + change 
	newWidthScale = math.floor(newWidthScale * 10^3 + 0.5) / 10^3
	print(newWidthScale .. "new")
	
	local targetColor = Color3.new(0.0901961, 1, 0.498039)
	local targetColorBlank = Color3.new(0.0901961, 1, 0.498039)
	
	while newWidthScale >= 1 do
		xpRemainder = newWidthScale - 1
		xpRemainder = math.floor(xpRemainder * 10^3 + 0.5) / 10^3
		newWidthScale = 0
		newWidthScale = xpRemainder + newWidthScale
		
		local tweenColorInfoBlank = TweenInfo.new(.15, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
		local tweenColorBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = targetColorBlank})
		tweenColorBlank:Play()
		
		tweenColorBlank.Completed:Connect(function()
			local revertTweenBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = defaultColorBlank})
			revertTweenBlank:Play()  -- Play the revert tween
		end)
		
		print(newWidthScale)
		LevelUpSound:Play()
		choir:Play()
		yes:Play()
		
	end
	

	local targetSize = UDim2.new(newWidthScale, 0, 1, 0)

	local tweenInfo = TweenInfo.new(.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) 
	local tweenColorInfo = TweenInfo.new(.2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

	local tween = TweenService:Create(progressBar, tweenInfo, {Size = targetSize})
	local tweenColor = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = targetColor})

	
	tween:Play()
	tweenColor:Play()
	
	tweenColor.Completed:Connect(function()
		local revertTween = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = defaultColor})
		revertTween:Play()  
	end)
	
	print("XP Remainder:", xpRemainder)  
end

XPModRemote.OnClientEvent:Connect(TweenLVL)

– Table from my module with XP required for each level:

local sculptingMastery = {
	{Level = "Noobie", XP = 0},
	{Level = "Beginner", XP = 200},
	{Level = "Novice", XP = 375},
	{Level = "Rookie", XP = 600},
	{Level = "Apprentice", XP = 960},
	{Level = "Skilled", XP = 1600},
	{Level = "Adept", XP = 2720},
	{Level = "Journeyman", XP = 4760},
	{Level = "Expert", XP = 8600},
	{Level = "Master", XP = 16340},
	{Level = "Grandmaster", XP = 31860},
	{Level = "Legendary", XP = 63790},
	{Level = "Mythical", XP = 130770},
	{Level = "Divine", XP = 274610},
	{Level = "Transcendent", XP = 617870},
}

Server Script where I actually add XP to the player (xpAmount += 100) and pass arguments for the tween, each time after I finish the “sculpting action”:

local Players = game:GetService("Players")
local RS = game:GetService("ReplicatedStorage")
local DSS = game:GetService("DataStoreService")
local XPModRemote = RS.XPModRemote
local XPMod = require(RS.Modules.XPDistribution)


Players.PlayerAdded:Connect(function(player)
	XPMod.newLevel(player)
end)

XPModRemote.OnServerEvent:Connect(function(plr, check)
	
	if not plr then
		warn("Player not found")
		return
	end
	
	if not check then
		warn("Check value not received")
		return
	end
	
	local xpAmount = 0
	
	if plr and check then
		
		print(plr, check)
		xpAmount += 100
		
		local CURRENT_MAX = XPMod:XPTillNextLevel(plr)
		local currentStats = XPMod:GetLevel(plr)
		XPMod:AddXP(plr, xpAmount)
		local updatedStats = XPMod:GetLevel(plr)
		local MAX_XP_NEXT = XPMod:XPTillNextLevel(plr)
		XPModRemote:FireClient(plr, xpAmount, MAX_XP_NEXT, currentStats, updatedStats, CURRENT_MAX)
	
	end
	
	if not plr then
		print("problem transferring xp - Player Not Found")
		return
	end
	
	if not check then
		print("problem transferring xp - Sculpt finish check wasn't received")
		return
	end
	
end)

After spending quite a while looking through the code and running some LocalScript tests of my own with a barebones setup that tried to recreate how the tweening script was structured, I realized that the issue was hiding in plain sight (and yes, it has to do with the xpRemainder).


TL;DR

  • The xpRemainder is returning an accurate percentage for the Scale value based on the CURRENT_MAX (current level’s maximum).

  • As a result, the xpRemainder that was calculated is NOT an accurate percentage for the MAX_XP_NEXT (next level’s maximum).


Whenever xpRemainder is greater than 0 (which happens every time the player levels up while exceeding the required amount of XP), it sets the Scale of the X Axis for the progress bar to a percentage that is accurate given updatedXP / CURRENT_MAX (because the Scale is increased according to amount / CURRENT_MAX).

However, because the player has just leveled up, the newWidthScale will no longer be accurate since it was based on the maximum XP required for the previous level (updatedXP / CURRENT_MAX), whereas it should be divided by the new MAX_XP_NEXT, instead. Since that wasn’t accounted for in the original code block, this results in the issue showcased at 0:20 in the video where the progress bar resets even though the player has not earned enough XP yet to level up.


As an example, here’s a timeline based on the print statements that were shown in the video, highlighting moments when it was accurate and inaccurate:

  • 0:02

    • updatedXP == 100 *
    • CURRENT_MAX == 200 *
    • newWidthScale == 0.5 (Accurate)
    • CURRENT_MAX Calculation: 100 / 200 == 0.5
  • 0:03 (Level Up!)

    • updatedXP == 0 (200) *
    • CURRENT_MAX == 200 *
    • MAX_XP_NEXT == 375 *
    • newWidthScale == 1 and then newWidthScale == 0 (Accurate)
    • xpRemainder == 0
    • CURRENT_MAX Calculation: 0 / 200 == 0
    • MAX_XP_NEXT Revised Calculation: 0 / 375 == 0

(Skipping to the point right before the next level up)

  • 0:06

    • updatedXP == 300 *
    • CURRENT_MAX == 375 *
    • newWidthScale == 0.801 (Accurate)
    • CURRENT_MAX Calculation: 300 / 375 == ~0.801
  • 0:07 (Level Up!)

    • updatedXP == 25 (400) *
    • CURRENT_MAX == 375 *
    • MAX_XP_NEXT == 600 *
    • newWidthScale == 1.068 and then newWidthScale == 0.068 (Inaccurate)
    • xpRemainder == 0.068
    • CURRENT_MAX Calculation: 25 / 375 == ~0.066
    • MAX_XP_NEXT Revised Calculation: 25 / 600 == ~0.041 xpRemainder (Accurate)

(Skipping to the next level up)

  • 0:13 (Level Up!)
    • updatedXP == 25 (625) *
    • CURRENT_MAX == 600 *
    • MAX_XP_NEXT == 960 *
    • newWidthScale == 1.07 and then newWidthScale == 0.07 (Inaccurate)
    • xpRemainder == 0.07
    • CURRENT_MAX Calculation: 25 / 600 == ~0.041
    • MAX_XP_NEXT Revised Calculation: 25 / 960 == ~0.026 xpRemainder (Accurate)

(Skipping to the second to last time the function was called in the video)

  • 0:19

    • updatedXP == 825 *
    • CURRENT_MAX == 960 *
    • newWidthScale == 0.902 (Inaccurate)
    • CURRENT_MAX Calculation: 825 / 960 == ~0.859
  • 0:20 (False ‘Level Up!’)

    • updatedXP == 925 *
    • CURRENT_MAX == 960 *
    • MAX_XP_NEXT == 1600
    • newWidthScale == 1.006 and then newWidthScale == 0.006 (Inaccurate)
    • xpRemainder == 0.006
    • CURRENT_MAX Calculation: 925 / 960 == ~0.963 (this is what the newWidthScale should be by this point, but it has accumulated an extra ~0.043 as a result of the xpRemainder being calculated according to CURRENT_MAX instead of MAX_XP_NEXT upon leveling up).

Possible Solution

With this in mind, the solution would probably involve changing how the newWidthScale is calculated in the section of code that runs while newWidthScale >= 1.

If I understand correctly, replacing the first 4 lines of code within the loop to the following should resolve the problem (and this would also eliminate the need for defining the xpRemainder):

while newWidthScale >= 1 do
    local newPercentage = updatedXP / MAX_XP_NEXT
    newWidthScale = math.floor(newPercentage * 10^3 + 0.5) / 10^3
    -- Continue

While it seems like it’s fixed, sooner or later the tween will become inaccurate again. (at the end of the video)

local function TweenLVL(amount, MAX_XP_NEXT, currentStats, updatedStats, CURRENT_MAX)
	
	local xpRemainder = 0
	local currentXP = currentStats.CurrentXP
	
	local updatedXP = updatedStats.CurrentXP
	print(updatedXP .. " test2")
	local currentWidthScale = progressBar.Size.X.Scale
	
	print(amount .. " XP added")
	xpGainSound:Play()
	
	if MAX_XP_NEXT >= 617870 or CURRENT_MAX >= 617870 then
		print(MAX_XP_NEXT .. CURRENT_MAX .. " LIMIT")
		return
	end
	
	progressBar.Text = "  XP: " .. updatedXP .."/"..MAX_XP_NEXT
	
	local change = amount/CURRENT_MAX 
	change = math.floor(change * 10^3 + 0.5) / 10^3
	print(change .. "change")
	local newWidthScale = currentWidthScale + change 
	newWidthScale = math.floor(newWidthScale * 10^3 + 0.5) / 10^3
	print(newWidthScale .. "new")
	
	local targetColor = Color3.new(0.0901961, 1, 0.498039)
	local targetColorBlank = Color3.new(0.0901961, 1, 0.498039)
	
	while newWidthScale >= 1 do
		local newPercentage = updatedXP / MAX_XP_NEXT
		newWidthScale = math.floor(newPercentage * 10^3 + 0.5) / 10^3
		print(newPercentage .. " new percentage")
		
		
		
		local tweenColorInfoBlank = TweenInfo.new(.15, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
		local tweenColorBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = targetColorBlank})
		tweenColorBlank:Play()
		
		tweenColorBlank.Completed:Connect(function()
			local revertTweenBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = defaultColorBlank})
			revertTweenBlank:Play()  -- Play the revert tween
		end)
		
		print(newWidthScale .. " after percentage")
		LevelUpSound:Play()
		choir:Play()
		yes:Play()
		
	end
	

	local targetSize = UDim2.new(newWidthScale, 0, 1, 0)

	local tweenInfo = TweenInfo.new(.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) 
	local tweenColorInfo = TweenInfo.new(.2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

	local tween = TweenService:Create(progressBar, tweenInfo, {Size = targetSize})
	local tweenColor = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = targetColor})

	
	tween:Play()
	tweenColor:Play()
	
	tweenColor.Completed:Connect(function()
		local revertTween = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = defaultColor})
		revertTween:Play()  
	end)
end

XPModRemote.OnClientEvent:Connect(TweenLVL)
1 Like

Oh yeah, by the time it reaches 4745 / 4760 XP (which would equate to ~0.997 for the X Axis Scale of the progress bar), the scale is only at 0.972.

There’s a chance that this discrepancy could be caused by the combination of how quickly the XP is being increased and the way that the LocalScript refers to the current size of the progress bar and subsequently updates it:

Given that the Tween which updates the size of the progress bar has a duration of 0.5 seconds, there’s a chance that it hadn’t reached the goal value before the function had been called again, meaning that currentWidthScale would be referring to a smaller value than what it was actually supposed to be.


If that’s what the issue in this case happens to be, then the way that newWidthScale is calculated toward the top of the function could probably be adjusted to a similar format as the calculation in the loop.

This revision would simultaneously eliminate the need to add the change / increase in XP manually to the currentWidthScale (since the updatedXP variable already accounts for that) while ensuring that the script will always update it to the intended new value for Scale of the X Axis, even if the Tween had not yet finished playing:

local levelPercentage = updatedXP / CURRENT_MAX
local newWidthScale = math.floor(levelPercentage * 10^3 + 0.5) / 10^3
1 Like

I’ve actually come up with a fix minutes before noticing your reply. My brain is a bit fried but the code seems to work as intended with any xpAmount number. I changed the while loop to the if statement, just because the loop used to freak out when I added huge numbers (and then even with small ones) and I don’t know the exact reason.

Other than that, currently trying to figure out how to stop the XP tween logic after reaching the max level, but that’s probably for a different topic. I’m sure your suggestions about waiting for the tween to end are reasonable and I’ll keep that in mind for the future. Thanks.

When working with big numbers:

Basically, this is what I added:

local expectedScale = updatedXP / MAX_XP_NEXT

And then switched newWidthScale to expectedScale :

local targetSize = UDim2.new(expectedScale, 0, 1, 0)
local function TweenLVL(amount, MAX_XP_NEXT, currentStats, updatedStats, CURRENT_MAX, masteries, nextMastery)
	
	local currentXP = currentStats.CurrentXP
	local updatedXP = updatedStats.CurrentXP
	print(updatedXP .. " test2")
	local currentWidthScale = progressBar.Size.X.Scale
	local expectedScale = updatedXP / MAX_XP_NEXT
	print(amount .. " XP added")
	xpGainSound:Play()
	
	
	
	progressBar.Text = "  XP: " .. updatedXP .."/"..MAX_XP_NEXT

	
	local change = amount/CURRENT_MAX 
	change = math.floor(change * 10^3 + 0.5) / 10^3
	print(change .. "change")
	local newWidthScale = currentWidthScale + change 
	newWidthScale = math.floor(newWidthScale * 10^3 + 0.5) / 10^3
	print(newWidthScale .. "new")
	
	local targetColor = Color3.new(0.0901961, 1, 0.498039)
	local targetColorBlank = Color3.new(0.0901961, 1, 0.498039)
	local targetTextColor = Color3.new(1, 0.298039, 0.309804)
	
	if newWidthScale >= 1 then
		if nextMastery == "GOD" then
			newWidthScale = UDim2.new(1, 0, 1, 0)
			progressBar.Text = "XP: " .. 617870 .. "/" .. 617870
			LvlName.Text = "Mastery: GOD"  -- Set to the final level name
			print("MAX LEVEL")
			return  -- Exit to prevent further processing
		end
		
		local newPercentage = updatedXP / CURRENT_MAX
		newWidthScale = math.floor(newPercentage * 10^3 + 0.5) / 10^3
		print(newPercentage .. " new percentage")
		LvlName.Text = "Mastery: " .. masteries
		
		
		local tweenColorInfoBlank = TweenInfo.new(.15, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
		local tweenColorBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = targetColorBlank})
		tweenColorBlank:Play()
		
		tweenColorBlank.Completed:Connect(function()
			local revertTweenBlank = TweenService:Create(blankLVLBar, tweenColorInfoBlank, {BackgroundColor3 = defaultColorBlank})
			revertTweenBlank:Play()  -- Play the revert tween
		end)
		
		local MasteryTextTween = TweenService:Create(LvlName, tweenColorInfoBlank, {TextColor3 = targetTextColor})
		MasteryTextTween:Play()
		
		MasteryTextTween.Completed:Connect(function()
			local revertTweenMastery = TweenService:Create(LvlName, tweenColorInfoBlank, {TextColor3 = LvlNameDefaultColor})
			revertTweenMastery:Play()  -- Play the revert tween
		end)

		
		
		print(newWidthScale .. " after percentage")
		LevelUpSound:Play()
		choir:Play()
		yes:Play()
		
	end
	

	local targetSize = UDim2.new(expectedScale, 0, 1, 0)

	local tweenInfo = TweenInfo.new(.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out) 
	local tweenColorInfo = TweenInfo.new(.2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

	local tween = TweenService:Create(progressBar, tweenInfo, {Size = targetSize})
	local tweenColor = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = targetColor})

	
	tween:Play()
	tweenColor:Play()
	
	tweenColor.Completed:Connect(function()
		local revertTween = TweenService:Create(progressBar, tweenColorInfo, {BackgroundColor3 = defaultColor})
		revertTween:Play()  
	end)
end

XPModRemote.OnClientEvent:Connect(TweenLVL)

1 Like