I have been trying to do this for a very long time, and I have decided to ask for help since I can’t find any other options.
I am trying to make a smooth number ticker (odometer) using a GUI.
This is the effect I am trying to achieve:
I have been experimenting with Tweening and ClipsDescendants but I haven’t made much progress.
If you can give me tips on creating this it would be greatly appreciated.
It looks like you would need some kind of stacking mechanic for every less significant digit.
Taking the difference between the start number and end number would be useful. for knowing how many transitions to take.
You would need the current digit to know where to start:
local function GetDigit(number, place)
return (math.floor(number/place)*place)%10
end
Using an example to clear ideas out:
local START = 10,000
local END = 20,000
local function GetLength(number)
return math.floor(math.log10(number))
end
You would need a way a way to instantly move backwards/forwards between digits, thank god TweenService:GetValue just came out.
Intended behavior: every digit on the odometer moves by all numbers left of the digit. e.g. a number in the 10s moving to 20,000 would only move 1000 revolutions.
Implementation: For every digit on the odometer, there is a Frame with 11 TextLabels, each for numbers 0-9, and an extra 0 at the end so the loop looks natural. We can change the position’s y scale between 0 and 0.9 to get the right number.
local function ShowNumber(frame,num,xPos)
frame.Position = UDim2.new(xPos,0,(num%10),0)
end
We need a way to store each frame:
local Frames = { -- the 10^index's place is the frame there
[4] = Frame4,
[3] = Frame3,
[2] = Frame2,
[1] = Frame1,
[0] = Frame0
}
Finally, the part that executes the code:
local currentNumber = -- the default value
local function TweenOdometer(newNumber, time)
local t = 0
while t < 1 do
t = math.min(t + RunService.Heartbeat:Wait()/time,1)
for i, frame in pairs(Frames) do
local newNum = math.floor(newNumber/10^i)
local currNum = math.floor(currentNumber/10^i)
local tween = TweenService:GetValue(t,Enum.EasingStyle.Quad,Enum.EasingDirection.InOut)
ShowNumber(frame,currNum*(1 - tween) + newNum*tween,(#Frames - 1)/#Frames - i/(#Frames - 1))
end
end
currentNumber = newNumber
end
Note: This was all just a brainstorm, and I haven’t tested out this code at all, so expect errors. I will be testing my theory in about 30 minutes, so watch out for an update/edit.
I made an example model of the odometer, very simple and disgusting to look at: Odometer.rbxm (7.9 KB)
Place it in StarterGui and watch it do magic I guess.
Edit: Full script used in the model:
--Services
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
--References
local parFrame = script.Parent.Parent
local odometer = script.Parent
local changeTo = parFrame.ChangeTo
local countFrom = parFrame.CountFrom
--Logic
local Interrupt = Instance.new("BindableEvent")
--Finds the number of (relevant) digits in the number
local function GetLength(num)
local counter = 0
local fnNum = num
while fnNum >= 10 do
counter += 1
fnNum /= 10
end
return counter
end
--Updates `frame` so that it is showing `num`
local function ShowNumber(frame,num,xPos)
--[
if num < 1 then
frame["0"].TextTransparency = 1
else
frame["0"].TextTransparency = 0
end
--]]
frame.Position = UDim2.new(xPos,0,-(num%10),0)
end
--Collection of frames
local Frames = {
[4] = odometer.Frame4,
[3] = odometer.Frame3,
[2] = odometer.Frame2,
[1] = odometer.Frame1,
[0] = odometer.Frame0
}
--Stores the current number
local currentNumber = 0
--Main function, tweens the entire odometer over `time` seconds to `newNumber`
local function TweenOdometer(newNumber, time)
coroutine.wrap(function()
local enabled = true
Interrupt:Fire()
Interrupt.Event:Connect(function()
enabled = false
end)
local currLen = GetLength(currentNumber)
local newLen = GetLength(newNumber)
local t = 0
while t < 1 and enabled do
t = math.min(t + RunService.Heartbeat:Wait()/time, 1)
for i, frame in pairs(Frames) do
local newNum = math.floor(newNumber/10^i)
local currNum = math.floor(currentNumber/10^i)
local tween = TweenService:GetValue(t,Enum.EasingStyle.Quint,Enum.EasingDirection.InOut)
ShowNumber(frame, currNum*(1 - tween) + newNum*tween,
(0.1*currLen - 0.2*i + 0.4)*(1 - tween) + (0.1*newLen - 0.2*i + 0.4)*tween
)
end
end
if not enabled then
for i, frame in pairs(Frames) do
local newNum = math.floor(newNumber/10^i)
ShowNumber(frame,newNum,(#Frames - 1)/#Frames - i/(#Frames - 1))
end
end
currentNumber = newNumber
end)()
end
--Button binds
changeTo.FocusLost:Connect(function(enter)
if enter then
TweenOdometer(tonumber(changeTo.Text),3)
end
end)
countFrom.FocusLost:Connect(function(enter)
if enter then
for i = tonumber(countFrom.Text), 0, -1 do
TweenOdometer(i,1)
wait(1)
end
end
end)
One thing, I was messing around with the labels and I was trying to make it so that the zeros at the start of the number don’t appear, but what I had trouble with was making the numbers re-center after the zeros are removed, I’m not sure how I’ll do this, any suggestions?
I was thinking of doing something about that too, you could probably make it so the label with the 0 turns white when the number passed in is below 1.
So you could rewrite the ShowNumber function into something like this:
local function ShowNumber(frame,num,xPos)
if num < 1 then
frame["0"].TextColor3 = Color3.new(1,1,1)
else
frame["0"].TextColor3 = Color3.new(0,0,0)
end
frame.Position = UDim2.new(xPos,0,-(num%10),0)
end
Yeah that’s a good idea. Ive done it but with text transparency instead since I’m using it on a surfacegui. The only problem I have is centering it. You can kind of see it’s slightly offsetted to the right.
Also I was working on some code which inserts commas every 3 characters and a small space infront of the comma, I’m not entirely sure how I’ll do those things, though
I got the offset now, I abused the xPos argument from ShowNumber and a new GetLength function to do it:
--near the beginning of the script,
local function GetLength(num)
local counter = 0
local fnNum = num
while fnNum >= 10 do
counter += 1
fnNum /= 10
end
return counter
end
--...
--in function TweenOdometer,
local currLen = GetLength(currentNumber)
local newLen = GetLength(newNumber)
local t = 0
while t < 1 and enabled do
t = math.min(t + RunService.Heartbeat:Wait()/time, 1)
for i, frame in pairs(Frames) do
local newNum = math.floor(newNumber/10^i)
local currNum = math.floor(currentNumber/10^i)
local tween = TweenService:GetValue(t,Enum.EasingStyle.Quint,Enum.EasingDirection.InOut)
ShowNumber(frame,currNum*(1 - tween) + newNum*tween,
(0.1*currLen - 0.2*i + 0.4)*(1 - tween) + (0.1*newLen - 0.2*i + 0.4)*tween
)
end
end
Edit: I will try to annotate my code so that it’s easier to understand
“Thats really cool, sorry for the late reply. I kind-of made it work so that I can just insert new frames and it automatically detects the correct Odometer frame size and calculates the correct positioning for the numbers, but since you created the code which centers it I’m a little confused and don’t how how I’ll do that. Because I do plan on doing numbers up to 999,999,999. do you have any suggestions on how I could create this?”
Edit: After 4 hours of trying I finally solved it! Whew! (I’m not used to working with code that I didn’t write, so I’m quite proud of myself for this lol)
I’ve been trying to make it so every 3 chars has a comma after it, and have a small gap after the comma so it doesnt look strange, but I am not entirely sure how I’ll do it, how do you think I should go about doing it?
A quick fix could be placing an extra frame inside every digit for the 1,000s place, 1,000,000s place, etc, that contains a comma, anchor pt (0.5, 0), position (1, 0, 0, 0), size (1, 0, 1, 0).
Beware that you will also have to account for making the comma invisible as well.
To be honest, I would try to find a way to make the comma fill as much space as a number would through the xPos argument again, probably through a counter in the for loop that adds an extra 0.2 to the xPos argument.