Should I use tick() to fix a wait(0.05) loop? (Avoiding wait() and why)

@Kampfkarren
I’m posting this based on the thread you talked about “Avoiding wait() and why”, but I was also hoping if someone more experienced than I am could criticize this code.

The code below is supposed to generate the player cash/second but I didn’t just want to do wait(1) because it’ll look sloppy/slow. So I divided the cash and time by 16 and did it that way. So every 1/16 of a second they get cash.

plrData.data[player].clock = function(table)
		while wait(0.05) do
			table.cash = table.cash + (table.cps/16)
			local formatted
			if formatSwitch then
				formatted = formatting.shorten(table.cash)
			else
				formatted = formatting.commaValue(table.cash)
			end
			CashFlow:FireClient(player,formatted)
		end
	end

The thread talks about using alternatives like WaitForChild or RunService, but my function doesn’t rely on those things.

Edit: I have a video demonstrating this that I’m about to post here.

If you want to avoid using default wait(), then you should use custom wait function instead.

Here’s my custom wait function I’ve created months ago:

function BulbWait(time)
    time = time or 0.03
    local et = 0
    local tt
    while et < time do
        _, tt = game:GetService("RunService").Stepped:Wait()
        et += tt
    end
end

And it will look like this if it’s implemented in your code:

function BulbWait(time)
    time = time or 0.03
    local et = 0
    local tt
    while et < time do
        _, tt = game:GetService("RunService").Stepped:Wait()
        et += tt
    end
end

plrData.data[player].clock = function(table)
	while true do
        BulbWait(0.5)
		table.cash = table.cash + (table.cps/16)
		local formatted
		if formatSwitch then
			formatted = formatting.shorten(table.cash)
		else
			formatted = formatting.commaValue(table.cash)
		end
		CashFlow:FireClient(player,formatted)
	end
end
2 Likes

I posted a video in the desc, you can check it out if you want.

Edit: what does += do?

They added += to Luau because it is convenient and used in a lot of languages.
Simply, x = x + 1 can now be written as x += 1

To add to what @WilliamAlezandro suggests, I question whether spamming the network is
justified for such a UI feature–You could still have less-often cash-update events, and handle
any visual smooth upticks on the client side.

2 Likes
local Number = 0
Number += 1 --Basically it is "Number = Number + 1"
Number += 5 -- "Number = Number + 5"
-- You can use "-=" , "*=" , "/=" and few others ig.
2 Likes

Yea, the cpu usage I usually get with 0.1 cash per second is around 20% but it doubled when I set it to 100 cash per second.

I didn’t want the player to touch the cash so I set it instead server side. I might have to rewire it so that the the loop is happening inside the client, not the server.

1 Like

Yes, the authoritative cash amount will always be on the server.
So any monkeying with the client’s copy of the cash amount is irrelevant–This also means
that it’s fine for you to do any cash-related UI niceties on the client side as well.

1 Like

You could also wrap the whole thing in a RunService event, and use the delta time parameter to determine how much to increment.

plrData.data[player].clock = function(table)
	game:GetService("RunService").Stepped:Connect(function(_, dt)
		table.cash = table.cash + (table.cps * dt)
		local formatted
		if formatSwitch then
			formatted = formatting.shorten(table.cash)
		else
			formatted = formatting.commaValue(table.cash)
		end
		CashFlow:FireClient(player,formatted)
	end)
end
4 Likes

Well the problem with using Stepped or Heartbeat is that they use 30.0/s rate on the script performance while some other of my codes doesn’t even go that high. I’m not sure if that’s too taxing or not for the client.

1 Like

I could maybe revert back to cash/second instead of 1/16th of a second, then fire that amount to the client every second so that it does the animation thing instead of firing the event every 1/16 of a second.

1 Like

Yes, and as the others mentioned, if you keep track of the deltas, you don’t need to have the events
extremely regular either. What you can also do is always fire both the before and after values.

1 Like

So if you take a look at @WilliamAlezandro’s BulbWait(), you’ll see that for each Stepped, it does
just a simple elapsed time check, and if it hasn’t elapsed yet, it immediately is waiting again–Not something
to worry about.

Definitely avoid wait() when you want consistency and accuracy like in your case. Definitely use @blokav’s way of running a loop it is the best method out of all listed.

Kampfkarren was definitely right when he says wait() could deviate by seconds but it might be a little exageratted. I tested wait()'s accuracy by running this code: And the results are that out of 1000 runs, wait deviated by at most 2.98 seconds and on average only deviated by around 0.035.

local AverageDeviation = 0
local MaxDeviation = 0
local Deviation = 0

for i = 1,1000 do
	local currentDeviation = math.abs((wait(0.01)-0.01))
	Deviation += currentDeviation
	if currentDeviation > MaxDeviation then
		MaxDeviation = currentDeviation
	end
	print(currentDeviation)
	print(i)
end
AverageDeviation = Deviation/1000

warn("Average: "..tostring(AverageDeviation))
warn("Max: "..tostring(MaxDeviation))
1 Like

Just to clarify for the OP–As you say, “@blokav 's way of running a loop it is the best…”

Yes, very generally, as it does not assume it is being called by a [dedicated] thread that is okay to block.

However in specific, @blokav 's code has flaws:

  1. Fires to client on every Stepped
  2. Has no way to turn it off (like the OP’s code.)
1 Like
local interval = 1/16
local total = 0

local original = interval
game['Run Service'].Heartbeat:Connect(function(dt)
  total += dt
  if total > interval then
    cash += cps * total
    total = (total - interval)
    interval = total + original
    fireclient(...)
  end
end)

Just saying, but you can instead make the cash never increase on the server until a request is sent and have the cash increase on the client

Kinda like this

server:

local lastrequest -- last time they requested this value/ do stuff like buying or whatever, os.clock(), separate for every player

buystuff.OnServerEvent:Connect(function(...)
   player.cash += (os.clock()-lastrequest) * cps
   fireclient(...) -- update their cash here to fix any deviation
   ...

client:

game['Run Service'].Heartbeat:Connect(function(dt) 
 cash.Value += dt * cps
end)
2 Likes

You can just make it look natural with

getfenv(1).wait = BulbWait(time) ... end

I read a little bit about the arguments of Stepped and Heartbeat but I still don’t get it. What does the deltatime parameter do? Does it like get the current time in UTC like os.time()?

Edit: nvm I think I get it.

The Heartbeat event gives you a ‘delta-time’ argument, which is the approximate time that has passed since the last Heartbeat event. Stepped also has delta-time, but also includes an argument for the total time that the system has been running.

Example of Heartbeat with delta-time in an FPS counter:

local updateTime = 1
local t = 0
local frames = 0
game:GetService("RunService").Heartbeat:Connect(function(dt)
	if (t >= updateTime) then
		print("Estimated FPS is "..math.round(frames / updateTime))
		t = 0
		frames = 0
	else
		t += dt
		frames += 1
	end
end)

Same example now also using the time parameter in Stepped:

local updateTime = 1
local t = 0
local frames = 0
game:GetService("RunService").Stepped:Connect(function(t0, dt)
	if (t >= updateTime) then
		print("Estimated FPS is "..math.round(frames / updateTime))
		-- RunService also has functions to tell if code is running on the server/client, or if it is running in studio
		if (game:GetService("RunService"):IsClient()) then
			print("Player has been in game for "..math.round(t0).." seconds")
		else
			print("Server has been running for "..math.round(t0).." seconds")
		end
		t = 0
		frames = 0
	else
		t += dt
		frames += 1
	end
end)

Edit:
It looks like on the wiki page that these events are now recently deprecated. The new events they provide should do the same things and allow you more freedom to specify when the code should be run. (pre/post physics step, pre-render, etc.)

They don’t seem to have the ‘time’ parameter anymore but you can still measure that manually by adding delta-time to a global variable:

local gameTime = 0
local t = 0
-- PreSimulation isn't documented yet but it might be the replacer for Stepped
game:GetService("RunService").PreSimulation:Connect(function(dt)
	gameTime += dt
	if (t >= 1) then
		if (game:GetService("RunService"):IsClient()) then
			print("Player has been in game for "..math.round(gameTime).." seconds")
		else
			print("Server has been running for "..math.round(gameTime).." seconds")
		end
		t = 0
	else
		t += dt
	end
end)