While true vs runservice

A long time ago now I was told to avoid while true loops, and instead use hearbeat, because an error can break the whole loop, however, using

while true do
	task.wait(t)
	task.spawn(function()
		
	end)
end

fixes this (with t being the time between loops you want). Is this now equivalent to using Heartbeat?:

RunService.HeartBeat:Connect(function()

end)

To me the former is more useful because you can customise how often the function runs (since it is usually unnecessary to update things every frame), is there any significant difference between the two? Am I fine using the former over Heartbeart?

(you can of course wrap the first option with a task.spawn() so that it does not interfere with the rest of the script).

Idk if theres a performance difference, but what I do know is that a while loop and a heartbeat act in very different ways.

While loops just kinda run too my knowledge, whereas a heartbeat runs after every physics simulation, and it gives you the time since it last ran, which can be very useful.

Theres also RunService.PreRender, which happens before the game is rendered, or RunService.PreSimulation, which as you can probably tell, happens before physics are simulated. (PreRender only works on the client btw.)

Basically it really depends on what you’re going for, I personally always use a heartbeat over a while loop, as having the deltatime can be quite useful, and that it runs every frame, so you dont have to worry about certain things going wrong with math and what not.

But yeah, thats just my opinion

2 Likes

I mean the maths going wrong concern isn’t an issue in the while true code sample (any more than it is for Heartbeat), since the task.spawn() makes the loop continue after the function starts, meaning if anything goes wrong in the function it still keeps firing. In effect, if I changed t to 1/60, the top code will run the function 60 times a second, whereas heartbeat will run it every frame (around 60 times a second).

Similar thing applies with the time provided being useful. I’m aware of the use of DeltaTime, but in the first sample I can just specify the time between event calls using t.

Anyway, I suppose it is just preference from what you’ve said, but I’ll wait for a few more responses to see if anyone has some more substantial distinctions I should be aware of.

by math going wrong I meant that said math might be required to run during a specific point of time, such as before or after physics simulations.

I probably should’ve specified that :sob:

But yeah after thinking about it more, the real reason you’d use the RunService options is when you need something specific to run at a certain point, such visual effects taking place before rendering, displaying custom animations before animtions are rendered, etc.

Otherwise I dont think theres much of a difference, but dont quote me on that

Event connections will reuse the same thread if the last invocation didn’t yield/error. The while loop, on the other hand, will spawn a fresh thread every time.

Now if you’re doing something time-based (task.wait every 30 seconds), a while loop is probably the better option. You should only use RunService events for frame-dependent logic.

1 Like

PLEASE FOR THE LOVE OF GOD DO LOOPS LIKE THE FIRST EXMAPLE

Yes, RunService has it’s upsides and is useful when used CORRECTLY, however doing something EVERY GAME FRAME is (generally) NOT how you use it. You’re literally ASKING to lag your game.

Exmaple .25 Loop compared to Heartbeat:

Code

Heartbeat:

local StartPos = Vector3.new(42, 19, -470)

game:GetService("RunService").Heartbeat:Connect(function(delTime)
	workspace:Raycast(StartPos,Vector3.new(0,-100,0))
end)

.25 Loop

local StartPos = Vector3.new(42, 19, -470)

while true do
	workspace:Raycast(StartPos,Vector3.new(0,-100,0))
	task.wait(.25)
end

As you can see Heartbeat uses A LOT of performance compared to the .25 second loop since it’s running every game frame. Now imagine scaling the amount of Raycasts you do, it’s gonna EASILY increase performance by A LOT, causing your game to slow down and overall just be a bad experience.

I see a lot of people nowadays using RunService for literally no reason and it’s getting quite irritating. You DO NOT need to use RunService for Raycasting, Pathfinding, checking a value, or literally half the things you see other people are using it for. Just use a regular while loop at .25 seconds, you’ll barely notice the difference and if you want it to be more exact, you can just decrease it to like .1. Running something every game frame however is just plain bad coding.

1 Like

I’m sure you dont want to create a new virtual thread every second, at least for optimization lol. And i don’t think you should have any errors in your code at first place.

Anyway, you should use both at different situations really. If you’re firing constant raycasts, they might overload with heartbeat so it’s probably better to minimize them along with using RenderStepped or just use any while or repeat loop with waits inside.
Stuff that happens every frame is usually for small visual stuff or non complex projectiles and etc., so ye just keep that in mind

1 Like

RBXScriptConnections by nature offer more customisation than what you describe for a while true do end loop, unless you wrap that in a coroutine that you yourself get to resume or yield, in which case how much boilerplate do you want to write?

1 Like

its 4am so i could be not thinking straight but
its mostly how u use it, task.wait() (no arg) will yield until a new frame, and will also return dt
heartbeat is most precise on frame syncing

LOL? Comparing a whole quarter of a second with a DT is insane, get real

You can achieve a “run code every x seconds” with RunService.Heartbeat:

local RunService = game:GetService("RunService")

local RunCodeEveryXSeconds = 1.5 --do code every 1.5 seconds
local Time = 0

local function OnHeartbeat(Delta)
	Time += Delta
	if Time > RunCodeEveryXSeconds then
		Time -= RunCodeEveryXSeconds
		print("Hello world!")
		--do code
	end
end

RunService.Heartbeat:Connect(OnHeartbeat)

I know you can, but what’s the point of getting the RunService when you can just use a while loop? And the code technically still runs every heartbeat to check if the time is greater than 1.5, though this is me just nitpicking at this point

1 Like

Because this:

local RunService = game:GetService("RunService")

local RunCodeEveryXSeconds = 1.5 --do code every 1.5 seconds
local Time = 0

local function OnHeartbeat(Delta)
	Time += Delta
	if Time > RunCodeEveryXSeconds then
		Time -= RunCodeEveryXSeconds
		print("Hello world!")
		--do code
	end
end

local Connection
Connection = RunService.Heartbeat:Connect(OnHeartbeat)
task.wait(4)
Connection:Disconnect()

Is better than this:

local RunCodeEveryXSeconds = 1.5 --do code every 1.5 seconds

local function WhileTrueDoWrapper()
	local Time = 0
	
	while true do
		Time += task.wait()
		if Time > RunCodeEveryXSeconds then
			Time -= RunCodeEveryXSeconds
			print("Hello world!")
			--do code
		end
	end
end

local Task = task.spawn(WhileTrueDoWrapper)
task.wait(4)
task.cancel(Task)

Equivalence is a bit of a loaded word. It’s functionally equivalent for the end-user, but the means are vastly different.

Connections are built for running their listeners every time the event is triggered. We can assume that connections are basically a big for loop iterating over every connected function with some error-handling in between.

In comparison, using task.wait will throw your thread to the engine scheduler to handle. That means your thread has to be classified, sorted, and lumped in with everything else in the scheduler that wants to be executed. The scheduler needs to perform more work to make sure that your thread (and every other thread in between) is being executed at the right time.

With these assumptions, it’s reasonable to believe (and a simple test seems to agree) that having, for example, 1000 connections will be more performant than 1000 while loops, if your goal is to run code every heartbeat.

Now, if your goal isn’t to run everything every heartbeat, then it’s a completely different game. You could modify the heartbeat callback to track delta time, but it becomes a lot simpler and potentially more performant to have the task scheduler handle it, especially if the delta time starts reaching seconds.

2 Likes

Just wondering: How is it better?

Multiple functions can be connected to RunService.Heartbeat, whereas each “while true do” loop needs its own function wrapper to be compatible with the task library.

1 Like

These are a bit different. In the top example, you can only have one connection using OnHeartbeat because the Time upvalue will be shared between all functions. Both need a wrapper if you want multiple instances of the function.

You don’t choose to use one. They both have their benefits. I’ll list some examples using each option. The heartbeat vs while true do both perform the same code in the examples.

Example 1: Leaderboard updating

RunService Connection

local UPDATE_TIME = 150

local RunService = game:GetService("RunService")

local timePassed = 0

RunService.Heartbeat:Connect(function(deltaTime: number)
	timePassed += deltaTime	

	if timePassed >= UPDATE_TIME then
		timePassed = 0
		UpdateLeaderboard()
	end
end)

while true do

local UPDATE_TIME = 150

while true do
	UpdateLeaderboard()
	
	task.wait(UPDATE_TIME)
end

Hopefully, you can see the difference between these 2 options. Heartbeat is significantly worse for performance and is WAY more code for no reason.

Example 2: Making a part spin

RunService Connection

local ROTATE_AMOUNT = 1 -- Degrees per second

local RunService = game:GetService("RunService")

local part = Instance.new("Part")

RunService.PreRender:Connect(function(deltaTime: number)
	part.CFrame *= CFrame.Angles(0, deltaTime * ROTATE_AMOUNT, 0)
end)

while true do

local ROTATE_AMOUNT = 1 -- Degrees per second

local RunService = game:GetService("RunService")

local part = Instance.new("Part")

local previousUpdateTime = os.clock()

while true do
	local deltaTime = os.clock() - previousUpdateTime
	
	part.CFrame *= CFrame.Angles(0, deltaTime * ROTATE_AMOUNT, 0)
	
	previousUpdateTime = os.clock()
	
	task.wait() -- Wait until next frame
end

This time the RunService connection is less code. It will also perform better since it requires less calculations.

1 Like

I was only intending to convey how any “while true do” loop, providing its body differs from any existing “while true do” loop in the code-base, will need its own function wrapper to be compatible with the task library.

local RunService = game:GetService("RunService")

local function OnHeartbeat1(Delta)
	--First heartbeat function.
end

local function OnHeartbeat2(Delta)
	--Second heartbeat function.
end

local Connection1, Connection2
Connection1 = RunService.Heartbeat:Connect(OnHeartbeat1)
Connection2 = RunService.Heartbeat:Connect(OnHeartbeat2)
task.wait(1)
Connection1:Disconnect()
Connection2:Disconnect()
local function WhileTrueDo1()
	while true do
		task.wait()
		--First while true do loop.
	end
end

local function WhileTrueDo2()
	while true do
		task.wait()
		--Second while true do loop.
	end
end

local Task1 = task.spawn(WhileTrueDo1)
local Task2 = task.spawn(WhileTrueDo2)
task.wait(1)
task.cancel(Task1)
task.cancel(Task2)

Here’s what I was intending to convey abstracted from the “run code every x seconds” implementation.

1 Like

while loops runs every t seconds

heartbeat runs every frame

heartbeat would be better, not only because it runs every frame instead of every few seconds

but also it allows you to access delta time, so you can keep your code consistent at different framerates

though if your going to use task.spawn() put the while loop inside the task.spawn function