Client Timer In Sync

Quenty’s module is most likely a solution, but since the thread doesn’t have a marked solution here’s what I do:

When a timer needs to start, I fire all clients telling them how much time is left. From there, I let the client do the countdown itself. However, to try and get it closer to the server’s timer, every two seconds or so I would send another signal to each client telling them, again, how much time is left. Once the client receives the signal, compare how much time the client thinks is left to how much time the server told it was left. If the client thinks there’s less time left than the server just said, then don’t listen to the server and stick with what the client has. If the server says there’s less time left than the client thought before getting the signal, jump down to how much time the server said.

By doing what I said above, the timer on the client will stick as close to the correct time as possible. Some signals will come faster than others, which is why you always stick with the lower amount of time left.

1 Like

When I call my client with the new stage countdown time I do something like this.

local Diff = SlaveClock:GetTime()-Args.MasterClockTime
CountdownTime = Args.CountdownTime-Diff

I use diff to compensate for the time it took the client to receive the event. However, at times, the client gets negative or very extreme values for “Diff”. Any idea why and how to fix it?

1 Like

The clock system already accounts for ping time. Just use :GetTime() to get the accounted for time. If you want to print out ping for some reason you can write out _pneWayDelay.

2 Likes

The :GetTime() from the one sent from the server to to the client via remote events or the :GetTime() right here on the client?

I’m not quite sure how I can incorporate this for a counting down timer thats in sync between client and server, thats why.

Time is shared between the client and the server. So :GetTime() returns basically the same value between both.

So you just send the client the start time of your clock, and it’s length. Here’s some sample code.

You cannot just copy this code in, but it should give you some indication of how to program it.

--- Shows time countdown using TimeSyncManager
-- @classmod TimerTextLabel
-- @author Quenty

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local BasicPane = require("BasicPane")
local TimeSyncManager = require("TimeSyncManager")

local TimerTextLabel = setmetatable({}, BasicPane)
TimerTextLabel.__index = TimerTextLabel
TimerTextLabel.ClassName = "TimerTextLabel"

function TimerTextLabel.new(textLabel)
	local self = setmetatable(BasicPane.new(textLabel), TimerTextLabel)
	self._length = 0
	self._startOffset = 0

	self.Gui.Visible = false
	self._maid:GiveTask(self.VisibleChanged:Connect(function(isVisible, doNotAnimate, visibleMaid)
		self.Gui.Visible = isVisible

		if isVisible then
			-- Setup animation
			local alive = true
			spawn(function()
				while alive do
					self:_update()
					wait(0.1)
				end
			end)

			visibleMaid:GiveTask(function()
				alive = false
			end)
		end
	end))

	self:_update()

	return self
end

function TimerTextLabel:WithStartOffset(startOffset)
	self._startOffset = startOffset or error("No startOffset")

	return self
end

function TimerTextLabel:WithLength(length)
	self._length = length or error("No length")
	self:_update()

	return self
end

function TimerTextLabel:WithStartTimeValue(startTimeValue)
	self._startTimeValue = startTimeValue or error("No startTimeValue")

	self._maid:GiveTask(self._startTimeValue.Changed:Connect(function()
		self:_update()
	end))
	self:_update()

	return self
end

function TimerTextLabel:_formatTime(time)
	if time <= 0 then
		return "0:00"
	elseif time > 60*60 then
		return string.format("%.2d:%.2d:%.2d", time/(60*60), time/60%60, time%60)
	else
		return string.format("%.1d:%.2d", time/60%60, time%60)
	end
end

function TimerTextLabel:_update()
	if not self._startTimeValue then
		self.Gui.Text = "0:00"
		return
	end

	local timePassed = TimeSyncManager:GetTime() - (self._startTimeValue.Value + self._startOffset)
	local timeLeft = self._length - timePassed
	self.Gui.Text = self:_formatTime(timeLeft)
end

return TimerTextLabel
4 Likes

So how do I fix the case of players first joining and they need the countdown updated instantly for them when the clock is not sync when the player first joins?

I notice that sometimes my friend’s clock is off sync at around 5 seconds when he joins.

The clock automatically synchronizes itself, as stated above. You can just wait. If your clock is structured like mine above, it’ll auto-correct. This is nice, since it accounts for ping, et cetera.

Honestly, it’s a lot easier to just use a NumberValue and update it every 0.1 seconds on the server, if this is too confusing.

:stuck_out_tongue:

2 Likes

There was an issue about how the client does not pick up change on server values if the change happens too fast and the connection is low.

1 Like

So when my stage changed and I had a client Changed event, it didn’t pick up the change, since changing certain changes (loading in heavy maps n stuff) dropped the connection

1 Like

FYI there is no such thing as a perfectly synced time across all devices using a certain service, anywhere in the world. You can fetch time and adjust it based on round-trip time, but that’s about the best you can do.

The only difference between tick() and os.time() is that os.time() is in UTC timezone and tick() is in your local timezone. They can both be completely wrong or inaccurate if your system time isn’t set correctly, and they will definitely not do anything except a timezone correction. Their values don’t attempt to sync with Roblox servers or anything like that – you’ll have to estimate that offset yourself and use it.

Pretty much the only thing you can assume somewhat safely is that os.time() will be similar (within a few seconds threshold) across all Roblox servers (not clients, clients are the wild west because the user owns the time configuration there).

1 Like