How would I script a non-lagging text counter animation

How would I recreate this effect:

My current take is this:

local function animateGemCount(oldamount, newAmount)
	local gemText = script.Parent.Amount
	local duration = 1  -- Total duration for the count-up animation
	local increments = newAmount - oldamount  -- Number of increments (differences between old and new amount)

	-- Each increment will take this amount of time to complete
	local incrementDuration = duration / increments

	local currentValue = oldamount

	-- Loop until we reach the newAmount
	for i = 1, increments do
		currentValue = currentValue + 1  -- Increment by 1
		gemText.Text = tostring(currentValue)  -- Update the text

		-- Wait for the duration of each increment (so the total duration is 1 second)
		task.wait(incrementDuration)
	end
end

but it takes longer than the set duration and is most likely laggy. Any help is appreciated

1 Like
local Module = {}

function Module.UrdateText(Gui: TextLabel, goal: number)
      local actualValue = tonumber(GUI.Text)
      local time = (actualValue / goal)  * 0.5
      coroutine.wrap(function()
            for v = actualValue, goal, 10 do
                  Gui.Text = goal
                  task.wait(time)
            end
      end)()
end

return Module

but I think if you call it twice, it will break

Thanks for the suggestion, sadly this doesn’t really work. The time would never be 1 second combined

The reason this takes longer is because task.wait has a minimum wait time.

Here is how I would code this:

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local displayLabel = script.Parent
local player = Players.LocalPlayer

-- Write a function to get the most up-to-date gem count
function getGemCount()
    -- TODO: Write a function to get your gem counts. This is an example:
    local leaderstats = player:FindFirstChild("leaderstats")
    if not leaderstats then return 0 end

    local gems = leaderstats:FindFirstChild("Gems")
    if not gems then return 0 end

    return gems.Value
end

-- Write a function to convert a gem count to text for the label
local function countToLabelText(gemCount)
    -- TODO: make a function to match how you want the text to look
    return "+"..tostring(gemCount)
end

local animationDuration = 1  -- It takes one second to finish the animation
local lastChangeTime = tick()

local lastGemCount = getGemCount()
local lastChangeGemCount = lastGemCount
local displayedGemCount = lastGemCount

-- Update the text every heartbeat (this is totally fine in terms of performance)
RunService.Heartbeat:Connect(function()
    local gemCount = getGemCount()
    local currentTime = tick()
    if lastGemCount ~= gemCount then
        lastChangeTime = currentTime
        lastChangeGemCount = displayedGemCount
    end
    
    -- Animate between the `lastChangeGemCount` and `gemCount` based on the time since the last change and the `animationDuration`
    local animationPercent = (currentTime - lastChangeTime) / animationDuration
    animationPercent = math.min(animationPercent, 1)  -- Finish animating once the `animationDuration` is reached
    displayedGemCount = lastChangeGemCount + (gemCount - lastChangeGemCount) * animationPercent
    displayedGemCount = math.round(displayedGemCount)  -- Round to a whole number
    displayedGemCount = math.min(displayedGemCount, gemCount)  -- Make sure not to show gems above the actual count

    displayLabel.Text = countToLabelText(displayedGemCount)

    lastGemCount = gemCount
end)
1 Like

I just adjusted the first script:

local function animateGemCount(goal: number)
	local flip = true
	local actualValue = tonumber(script.Parent.Amount.Text)
	local difference = goal - actualValue
	local waitTime = 0.005 -- that means you need this times 200 to reach our goal of 1 sec
	local increment = math.floor(difference / 200)
	
	coroutine.wrap(function()
		for v = actualValue, goal, increment do
			script.Parent.Amount.Text = v
			if flip then
				flip = false
				script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
			else
				flip = true
				script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
			end
			task.wait(waitTime)
		end
	end)()
	script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
	script.Parent.Amount.Text = goal
end

and I will try out yours now

2 Likes

I would recommend thinking about potential overlapping loops. For example, if two calls are made to animateGemCount, there would probably be two loops trying to modify the text at once but to different values.

That is a good fix though other than that! If you know you won’t be calling animate within waitTime * 200 it seems like that should work!

1 Like

this is how it looks right now, pretty much what I wanted to accomplish it just takes too long:

@BendsSpace version will take some time to setup

The local function ONLY gets called when it’s called by a remote event. This only happens when you get a gem reward and you can logically only get one at a time in my game. I could add a debounce tho so it doesn’t happen

1 Like

Here is an updated version based on your code:

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local displayLabel = script.Parent
local player = Players.LocalPlayer

-- Write a function to get the most up-to-date gem count
function getGemCount()
    return tonumber(script.Parent.Amount.Text)
end

-- Write a function to convert a gem count to text for the label
local function countToLabelText(gemCount)
    return tostring(gemCount)
end

local animationDuration = 1  -- It takes one second to finish the animation
local flipsPerSecond = 15  -- How many times to flip the size per second
local lastChangeTime = tick()

local lastGemCount = getGemCount()
local lastChangeGemCount = lastGemCount
local displayedGemCount = lastGemCount

-- Update the text every heartbeat (this is totally fine in terms of performance)
RunService.Heartbeat:Connect(function()
    local gemCount = getGemCount()
    local currentTime = tick()
    if lastGemCount ~= gemCount then
        lastChangeTime = currentTime
        lastChangeGemCount = displayedGemCount
    end
    
    -- Animate between the `lastChangeGemCount` and `gemCount` based on the time since the last change and the `animationDuration`
    local animationPercent = (currentTime - lastChangeTime) / animationDuration
    animationPercent = math.min(animationPercent, 1)  -- Finish animating once the `animationDuration` is reached
    displayedGemCount = lastChangeGemCount + (gemCount - lastChangeGemCount) * animationPercent
    displayedGemCount = math.round(displayedGemCount)  -- Round to a whole number
    displayedGemCount = math.min(displayedGemCount, gemCount)  -- Make sure not to show gems above the actual count

    displayLabel.Text = countToLabelText(displayedGemCount)

    -- Do the flipping:
    if displayedGemCount == gemCount then
        script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
    else
        -- Flip every `1 / flipsPerSecond` seconds
        if currentTime % (1 / flipsPerSecond * 2) > 1 / flipsPerSecond then
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        else
            script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
        end
    end

    lastGemCount = gemCount
end)

What’s up with the run service?

currently in my script im calling a local function "animateGemCount(currentGemValue) " and providing the changed gem value. Your version seems to “always” run because of the run service?
just confused on how your version works

It basically uses one loop to update the value. It’s just like your code basically but it never stops and it doesn’t ever make multiple loops. It’s fine for performance because it’s event based and the amount of processing is absolutely tiny (e.g. each Humanoid has dozens of loops like that internally).

The RunService lets you get an event that runs every update cycle. Basically the code in the function connected to Heartbeat runs like code in a loop.

So I can remove that and only make it run when it is called until it is done?

Ah wait. Sorry, there is a problem. Let me fix it. (The problem is that the loop uses the “real” value as the text label but is also updating the text label.)

Now you can use your animateGemCount function to animate the text.

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local displayLabel = script.Parent
local player = Players.LocalPlayer

local realGemCount = 0
local function animateGemCount(goal: number)
    realGemCount = goal
end

-- Write a function to get the most up-to-date gem count
function getGemCount()
    return realGemCount

-- Write a function to convert a gem count to text for the label
local function countToLabelText(gemCount)
    return tostring(gemCount)
end

local animationDuration = 1  -- It takes one second to finish the animation
local flipsPerSecond = 15  -- How many times to flip the size per second
local lastChangeTime = tick()

local lastGemCount = getGemCount()
local lastChangeGemCount = lastGemCount
local displayedGemCount = lastGemCount

-- Update the text every heartbeat (this is totally fine in terms of performance)
RunService.Heartbeat:Connect(function()
    local gemCount = getGemCount()
    local currentTime = tick()
    if lastGemCount ~= gemCount then
        lastChangeTime = currentTime
        lastChangeGemCount = displayedGemCount
    end
    
    -- Animate between the `lastChangeGemCount` and `gemCount` based on the time since the last change and the `animationDuration`
    local animationPercent = (currentTime - lastChangeTime) / animationDuration
    animationPercent = math.min(animationPercent, 1)  -- Finish animating once the `animationDuration` is reached
    displayedGemCount = lastChangeGemCount + (gemCount - lastChangeGemCount) * animationPercent
    displayedGemCount = math.round(displayedGemCount)  -- Round to a whole number
    displayedGemCount = math.min(displayedGemCount, gemCount)  -- Make sure not to show gems above the actual count

    displayLabel.Text = countToLabelText(displayedGemCount)

    -- Do the flipping:
    if displayedGemCount == gemCount then
        script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
    else
        -- Flip every `1 / flipsPerSecond` seconds
        if currentTime % (1 / flipsPerSecond * 2) > 1 / flipsPerSecond then
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        else
            script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
        end
    end

    lastGemCount = gemCount
end)

TBH I think you might just want to use a tween :sweat_smile:. They’re nice because they cancel themselves.

local tweenValue = -- IntValue

local function animateGemCount(goal)
    tweenValue.Value = tostring(gemCount)
    tween = -- Todo: Make a tween for tweenValue.Value to goal
    local connection
    connection = tweenValue.Changed:Connect(function()
        displayLabel.Text = tostring(tweenValue.Value)
        -- Flipping
        if tweenValue.Value % 2 == 0 then
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        else
            script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
        end

        -- End the connection when the end is reached
        if tween.PlaybackState ~= Enum.PlaybackState.Playing then
            connection:Disconnect()
            tween:Destroy()
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        end
    end
end
1 Like

aaah man there is so much stuff not working there, one sec let me error brute force this

how is getgemcount supposed to go any further if it directly returns something

It sets a variable that the continuous loop then looks at and updates based on.

The tweening code might be easier though:

local tweenValue = Instance.new("IntValue")
local animationDuration = 1

local function animateGemCount(goal)
    -- Tween from the current displayed value to goal
    tweenValue.Value = tostring(script.Parent.Amount.Text)
    tween = TweenService:Create(tweenValue, TweenInfo.new(animationDuration), {Value = goal})
    
    -- Update the label every time the tween updates
    local valueConnection
    valueConnection = tweenValue.Changed:Connect(function()
        -- Update the text
        displayLabel.Text = tostring(tweenValue.Value)

        -- Flip between even and odd numbers
        if tweenValue.Value % 2 == 0 then
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        else
            script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
        end
    end

    -- End the connection when the end is reached
    local endConnection
    endConnection = tween.Completed:Connect(function()
        endConnection:Disconnect()
        valueConnection:Disconnect()
        tween:Destroy()
        script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
    end)

    -- Start the animation
    tween:Play()
end

Sorry, I might have over complicated things. The tween code matches the coding pattern you were using.

You can tween numbers??? let me try that

You can tween IntValues, then use the Value of the IntValue to update your number/text. But yeah basically.

modified it, will test now

the modulo thing is a good way to switch

local tweenValue = Instance.new("IntValue")
local animationDuration = 1

local function animateGemCount(goal)
	tweenValue.Value = tostring(script.Parent.Amount.Text)
	local tween = TweenService:Create(tweenValue, TweenInfo.new(animationDuration), {Value = goal})
	local connection
	connection = tweenValue.Changed:Connect(function()
		script.Parent.Amount.Text = tostring(tweenValue.Value)
		if tweenValue.Value % 2 == 0 then
			script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
		else
			script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
		end

		if tween.PlaybackState ~= Enum.PlaybackState.Playing then
			connection:Disconnect()
			tween:Destroy()
			script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
		end
	end)
	tween:Play()
end
1 Like

I added some fixes to it also:

local tweenValue = Instance.new("IntValue")
local animationDuration = 1

local function animateGemCount(goal)
    -- Tween from the current displayed value to goal
    tweenValue.Value = tostring(script.Parent.Amount.Text)
    tween = TweenService:Create(tweenValue, TweenInfo.new(animationDuration), {Value = goal})
    
    -- Update the label every time the tween updates
    local valueConnection
    valueConnection = tweenValue.Changed:Connect(function()
        -- Update the text
        displayLabel.Text = tostring(tweenValue.Value)

        -- Flip between even and odd numbers
        if tweenValue.Value % 2 == 0 then
            script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
        else
            script.Parent.Amount.Size = UDim2.new(0.5, 0,0.67, 0)
        end
    end

    -- End the connection when the end is reached
    local endConnection
    endConnection = tween.Completed:Connect(function()
        endConnection:Disconnect()
        valueConnection:Disconnect()
        tween:Destroy()
        script.Parent.Amount.Size = UDim2.new(0.472, 0,0.625, 0)
    end)

    -- Start the animation
    tween:Play()
end