Performance worries: using .Heartbeat to run a function multiple times; contains a loop (for every individual vehicle)

I am currently using .Heartbeat to detect when the vehicle model containing the script, is pushed and also a while loop within that function, which will be activated during the countdown for the respawning of the vehicle. Basically, I want to know the most effective method of using .Heartbeat to run the same function multiple times, containing a loop inside the function inside the script intended for the car for when the respawning mechanism starts.

I understand that using .Heartbeat will cause the function connected to it to run multiple times and I am concerned that running the same script in multiple vehicles separately using .Heartbeat could possibly cause the overall performance of the game to go downhill and lag.

This is the overall structure of the code, intending to be used in multiple vehicles, but the main issue is running .Heartbeat in multiple vehicles, since .Heartbeat would be running for multiple vehicles using the same script to detect the vehicle model’s change in position:

local Seat = script.Parent
local Jeep = Seat.Parent.Parent

local lastOccupant = Seat.Occupant
local lastUpdateTime = tick()
local jumpCount = 0
local originalCooldownValue = CooldownValue  -- Store the original cooldown value

local cancelCooldown = false  -- Variable to track if the cooldown should be canceled
local respawnInProgress = false

local function vehicleCooldown()
	local currentTime = tick()
	local elapsedTime = currentTime - lastUpdateTime

	-- Throttle updates to once every second
	if elapsedTime >= 1 then
		lastUpdateTime = currentTime

		local primaryPart = Jeep.PrimaryPart

		if primaryPart and primaryPart:IsA("BasePart") then
			local distance = (primaryPart.Position - originalCFrame.Position).Magnitude
			
			-- Check if the player is not seated inside the car and distance exceeds the threshold
			if not Seat.Occupant and distance > PositionThreshold then
				-- Start a new cooldown if there isn't any existing cooldown or if the cooldown was canceled
				if not inCooldown and not cancelCooldown then
					--sets respawning progress to true

					while CooldownValue > 0 and respawnInProgress do
						if CooldownValue <= CooldownThreshold then
							--cooldown logic
						end

						task.wait(1) -- Wait for one second

						-- Check if the player returned to the car and cancel the respawn if needed
						if Seat.Occupant then
							--stops cooldown if user returns to car seat
						end

						CooldownValue = CooldownValue - 1
					end

					-- Set respawnInProgress to false when respawn process is completed
					respawnInProgress = false
				elseif inCooldown and not cancelCooldown then
					if CooldownValue and CooldownValue <= 0 then
						-- Check if the Seat and Jeep still exist before respawning
						jeepRespawn()
						--resets cooldown after respawning
					end
				else
					-- Cooldown gets canceled.
					
				end
			end
		end
	end

	-- Update last occupant
	lastOccupant = Seat.Occupant
end

game:GetService("RunService").Heartbeat:Connect(vehicleCooldown)

The questions that I would like to be answered:

  1. Would using .Heartbeat for every individual vehicle model cause performance issues? If so what would be the ways around it and the best solution forward.

  2. Is there a better method, compared to using .Heartbeat which constantly runs the connected function, or could I somehow only run the function when the vehicle actually moves instead of constantly checking in the function which detects if something has moved, which therefore causes lag for multiple vehicles using the same duplicate script?

I just want some guidance of how I could achieve this logic of using .Heartbeat connected to RunService being used in multiple vehicles, without degrading the overall performance of the game or causing lagging issues. I’ve used multiple resources for this issue to help as a guide for the best solution forward, although this has only caused more complexity with multiple possible choices forward rather than simplicity and helping me decide the best solution forward. If anybody needs clarification on this issue, I will be happy to clarify it. If anybody can help me fix this issue, I would be really grateful.

3 Likes
  1. RunService.Heartbeat itself won’t lead to major performance issues, but as mentioned in Scripts - Computation about high frequency events, you should limit the frequency of expensive operations, which you achieved through elapsedTime and the cooldown system.
    It seems that elapsedTime and the cooldown system could be combined into a single Debounce Pattern if applicable.
  2. As suggested in the subsection How to Mitigate of Computation, instead of RunService.Heartbeat, you could consider executing vehicleCooldown() in other events (e.g. change in Seat.Occupant) or in a loop (e.g. an infinite loop with task.wait()).

Also, as a coder myself, vehicleCooldown() looks quite messy with the nested Control Structures.
From my experience, I would suggest the semantic of returning for validation, which is seen in examples like the createPart() function in Remote Runtime Type Validation.
This type of validation can be done on various kinds of conditions by taking:

if conditions then
	-- Do something
end

and changing it into:

if not (conditions) then
	return
end
-- Do something
1 Like

I already have a detection of Seat.Occupant in code below that calls the vehicleCooldown() function when the player leaves the seat (which was not mentioned within this topic). However, with the code provided above I also wanted to somehow constantly monitor if the car has been pushed without anybody ever entering the seat, so it can also respawn in the same place that way also. I assume that there is currently no way to only have to fire the function when the car actually physically moves rather than doing checks constantly for multiple vehicles, to detect if one of them moves?

Moreover, If I am going to be using an infinite loop using task.wait() instead of .Heartbeat (bearing in mind that each vehicle would contain the same script), there would still be multiple loops running constantly in the game. Couldn’t that also cause a performance issue, similar to the use of .Heartbeat?

Even if I did try to control all the cooldowns for all the vehicles in one place which was just an idea, it would most likely create its own set of challenges and make things more difficult.

Well, the documentation for GetPropertyChangedSignal suggested the use of physics-based event to detect changes in position. In that case, you could put a region block and connect the event Touched or TouchEnded to detect when the car moves to or away from a position.

The documentation page Computation mentions that breaking up expensive tasks using task.wait() can spread work across multiple frames to reduce the impact on frame rates.
Even if you have many loops running, the use of task.wait() ensures that they are running at a limited frequency.

I believe :GetPropertyChangedSignal doesn’t work in my use case since:

The event returned by this method does not fire for physics-related changes, such as when the CFrame, AssemblyLinearVelocity, AssemblyAngularVelocity, Position, or Orientation properties of a BasePart change due to gravity.

I tried it without using code to move the model and instead moved it physically when I was testing it but that didn’t work, which brings me back to my previous point:

Correct me if I am wrong though. If only I could simply do it this way instead of constantly having to check the model’s position, that would have pretty much solved my topic here.

I know that GetPropertyChangedSignal doesn’t fire for physics-related changes as shown in the Limitations section:

Thus, I suggested using Touched or TouchEnded to detect when the car model leaves a region.

That could be a possibility. However, would a better way of using .Heartbeat or any other loop on multiple vehicle models, would be to fire a BindableEvent and have that listened to in every vehicle model, so your constantly checking when each car has moved without using .Heartbeat for every vehicle separately.

Also, is .Stepped a better choice than using .Heartbeat in my use case, or is there not much of a difference? In addition, I’ve heard something about using Parallel Luau but I am not sure if this is particularly related or would be particularly useful in my use case, or whether this would just over-complicate things further. Otherwise, by what you suggested regarding using a .Touch event for region blocks where the car would respawn if it touches these blocks, would probably be the best option here.

I believe that there’s little difference in performance in connecting functions to .Heartbeat or to a BindableEvent connected to .Heartbeat.
Also, you could try out CollectionService (or the forum about it) to handle vehicle respawns inside a single script.

Since the vehicle respawn function has a loop check with task.wait(), using .Stepped or .Heartbeat shouldn’t matter since the task.wait() yields then resumes the thread on the next Heartbeat step.

According to Computation again, Parallel Luau (multithreading) can be used if you for expensive tasks that doesn’t access the data model. You could use it by parenting the script to an Actor and calling task.desynchronize() for calculations like Position / Vector distance or cooldown, then task.synchronize() for respawning the car.
However, using multithreading for just a small number of computations could be over-complicating things. You also don’t want to use multithreading for long computations, as mentioned in Best Practices.

1 Like

What would that look like if I tried to implement it into the script instead of using .Heartbeat, or how would I fire the vehicleCooldown() function using CollectionService?

This is the jeepRespawn() function by the way:

local newJeep = originalJeep:Clone()
newJeep:SetPrimaryPartCFrame(originalCFrame)

for _, passenger in ipairs(Seat:GetChildren()) do
	if passenger:IsA("Model") then
		passenger:Destroy()
	end
end

Jeep:Destroy()
newJeep.Parent = workspace
print("Jeep regenerated in the same place!")

If you use CollectionService, you would probably add a tag to each car model, then use something like this:

-- Collection Service functions
local function onInstanceAdded(car : Model)
	-- Validate car
	-- Set up function
end

local function onInstanceRemoved(car : Model)
	-- Validate car
	-- Clean up stored connections
end

local function setUpCars()
	task.defer(function()
		for _, object in CollectionService:GetTagged(tag) do
			onInstanceAdded(object)
		end
	end)
	CollectionService:GetInstanceAddedSignal(tag):Connect(onInstanceAdded)
	CollectionService:GetInstanceRemovedSignal(tag):Connect(onInstanceRemoved)
end

-- Calling functions
setUpCars()

You would probably have to use local scopes, tables, or modules to store the cooldown information for each car.
The .Heartbeat connection can be stored for each car & cleaned up in the instance removed function, or it can be a single function that calls vehicleRespawn() for every car.
The implementation will be slightly complicated, but it is better than creating a script for each individual car.

So I would put all the cars together inside a Folder or something, place the script you provided inside ServerScriptService or the Folder and do it like that?

Also, where you are setting up a function that makes sense but removing a function; how does that work?:

local function onInstanceRemoved(car : Model)
	-- Validate car
	-- Clean up stored connections
end

Finally, you have shown me what the separate script would look like but how would I connect that to each script inside each vehicle model? Would I still keep .Heartbeat or how would that work?

You can put the script inside ServerScriptService, but the car models can be in different folders since they are obtained through tags and CollectionService.

onInstanceRemoved() fires when a tag is removed from an instance. This is often used for cleaning up resources to prevent memory leaks.
The code sample for GetInstanceRemovedSignal shows an example of cleaning up connections when a tag is removed.

I’ll provide an example for what you can do:

  • First, define vehicleCooldown(car) and respawnCar(car) such that they respawn the car specified in the parameter.
  • Next, in onInstanceAdded(car), create and store the connections to vehicleCooldown(car), like how the the code sample in GetInstanceAddedSignal did with .Touched.
    The connections can be:
    • .Heartbeat (in general),
    • .Touched (for region blocks),
    • :GetPropertyChangedSignal("Occupant") (for seats).

This way, you don’t need a separate script inside each vehicle model (except for Data Sharing module scripts).

Therefore:

Do you think using a ModuleScript, where you’d require the ModuleScript from each script inside each vehicle model, would be a more effective approach than compared with using CollectionService, since this sounds more complicated, even though it seems like it might achieve pretty much the same result, which is to reduce the duplication of code in each vehicle model separately?

I think it’s more effective to use a single script to set up the vehicles. CollectionService is a way of retrieving all vehicle in the DataModel in a single script.
The functionality of the ModuleScript in my post was for extra configuration of the vehicle (ex: different cooldown seconds), not for storing general functions like respawnCar().

I meant just using a ModuleScript in general and having it being required in each vehicle model, rather than using CollectionService, entirely. Would there be any advantage of doing this or is there a more rewarding advantage of going down the CollectionService route, even though it could potentially become more complicated whilst achieving similar results, or not?:

I would avoid placing scripts containing functions in each models because it would be harder to manage than if we used CollectionService.
(see the post on CollectionService, section 2. Using CollectionService)

I’ve developed with JToH Tower Creation Kit, which currently uses a ModuleScript for each type of scripted obby mechanism.
When updating the kit, it is quite difficult to convert all of these mechanisms into the newest version. Through CollectionService, you will only need to update a single script to convert the mechanisms.

Yeah I meant the entire script placed in each vehicle model; sorry, I did not make it clear enough. The code I provided in this topic only includes the function connected to the .Heartbeat, but I was thinking of placing the entire script inside a ModuleScript, (including the .Heartbeat connected to the vehicleCooldown() function), and just requiring it in every vehicle model that needs it, instead of doing them separately which could cause greater performance issues, because I would be independently using .Heartbeat in each vehicle script, or is that not how this would work?:

The performance of using .Heartbeat for separate vehicle scripts won’t differ much from using .Heartbeat for each vehicle in a single script.
I only suggested CollectionService because it avoids the use of identical scripts that could be hard to manage in the future.
You could read more about the advantages of using CollectionService in the links attached to the previous posts.

That is what would happen also, if you required the ModuleScript containing the same code, and required it in every script that’s inside every vehicle model and not using CollectionService entirely or; since this seems to be more complex when you could just do it that way, unless there is an advantage of using CollectionService instead?:

1 Like

I can see your point.

Overall, it could just be a preference, but there’s a slight advantage with using CollectionService.

Here are the two methods that we mentioned:

  1. With Scripts in each vehicle model requiring a single ModuleScript, the DataModel will contain multiple Scripts inside Models and a single ModuleScript inside ServerStorage or ReplicatedStorage.

  2. With CollectionService, you would put a single Script in ServerScriptService or StartPlayerScripts, and perhaps an additional ModuleScript to organize the functions.

Comparing the two methods, you’ll see that:

  • The first method requires more Scripts than the second method.
    This creates more Instances, which could be difficult and messy to work with.
  • Since the Scripts in the first method are placed in Models, they will only run on the server side of the client-server model once the model is parented to Workspace. You can’t run client-sided scripts since they only run in certain containers.
    The Script used in the second method can be ran from both the server and client side of the client-server model, allowing for client-sided scripting.
    In the case of respawning vehicles, you won’t need to do client-sided scripting.

Overall, using CollectionService reduces the number of Script instances (which improves manageability and possibly performance), and it allows similar objects to function both on the server and the client side of the client-server model.

Also, using CollectionService wouldn’t make things more complex. You could let the examples in the documentation for CollectionService guide you through ways to use the service.

1 Like