Is RunService.Heartbeat more efficient than while wait() do?

I’ve been editing an open-sourced boat code to my liking for some time. The script runs a continuous loop, and changes the speed/direction of the boat via the inputs of the driver via a vehicle seat.

It would be obvious to just use :changed but that won’t account for smooth acceleration/deceleration.

Originally it used a while wait() do loop, but it caused significant lag, so I switched to a RunService.Heartbeat one. Will this end all the lag in-game, or is it gonna yield the same issue, and there’s a better solution?

Here’s the code

script.Parent.MaxSpeed = script.Parent.Settings.MaxSpeed.Value
local Vehicle = script.Parent.Parent
local VehicleSeat = script.Parent
maxspeed = script.Parent.MaxSpeed
script.Parent.BodyPosition.position = script.Parent.Position
script.Parent.BodyGyro.cframe = script.Parent.CFrame
value1 = 0 --- real time speed
local bv = VehicleSeat:FindFirstChild("BodyVelocity")
local clone = bv:Clone()
local driver = false

local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function()
		if script.Parent.Throttle == 1 then
			if value1 < maxspeed then value1 = value1 + script.Parent.Settings.Acceleration.Value end
			script.Parent.Driving.Value = true
			script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
			script.Parent.Left.Value = false
			script.Parent.Right.Value = false
		end
		
		if script.Parent.Throttle == 0 then 
			if value1 > 0 then
				value1 = value1 -1.5
			elseif value1 < 0 then
				value1 = value1 +1.5	
			end
			script.Parent.Driving.Value = false
			script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
			script.Parent.Left.Value = false
			script.Parent.Right.Value = false
		end
		
		if script.Parent.Throttle == -1 then
			if value1 > - maxspeed then value1 = value1 -script.Parent.Settings.Acceleration.Value end
				script.Parent.Driving.Value = true
				script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
				script.Parent.Left.Value = false
				script.Parent.Right.Value = false
				
		end
		
		if script.Parent.Steer == 1 then
			script.Parent.BodyGyro.cframe = script.Parent.BodyGyro.cframe * CFrame.fromEulerAnglesXYZ(0,- script.Parent.Settings.TurnSpeed.Value,0)
			script.Parent.Driving.Value = true
			script.Parent.Right.Value = true
			script.Parent.Left.Value = false
		end
		
		if script.Parent.Steer == -1 then
			script.Parent.BodyGyro.cframe = script.Parent.BodyGyro.cframe * CFrame.fromEulerAnglesXYZ(0,script.Parent.Settings.TurnSpeed.Value,0)
			script.Parent.Driving.Value = true
			script.Parent.Left.Value = true
			script.Parent.Right.Value = false
			
		end
		if script.Parent.Position.Y > script.Parent.BodyPosition.Position.Y then
			script.Parent.Driving.Value = false
			value1 = 0
			script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
			script.Parent.Left.Value = false
			script.Parent.Right.Value = false
		end
end)
1 Like

You’re doing a lot of wasted computation here. you want neither, as both are wasteful. Looping infinitely is almost never the answer. What you should do is use the Changed thing you considered, or however you get player input.

If you pair that up with a tween or some body movers it’ll manage to look smooth without all the extra computation.

7 Likes

How would I do that? Again, how would I pair those changes with a :Changed when it wouldn’t be running constantly?

Using a Tween will replace the effect of having a continuous loop. You can get a TweenCompleted event from that as well to let you know when it’s done, which you can attach to another Tween if you need. You can also cancel Tweens. There is enough functionality there that you don’t need a loop.

The only thing you really need a loop for is checking for the states of your throttle, but even then you mention that you can use the :Changed event so if you know how to handle that, you are pretty much set.

3 Likes

Would I use it for changing the boat’s movement instead of via BodyVelocity and the like? I honesty have never used tweens before.

Take a look at the API, it goes in depth pretty well. I’ll explain a couple of things too just to sum up how you might find it useful.

Tween can interpolate for these variable types - this includes CFrames and Vector3, so you can apply that to your velocity. You choose the start velocity and end velocity (current velocity -> max velocity). If you get off the throttle, you stop the Tween and start a deceleration (current velocity -> 0).

If you have any detailed questions let me know, but hopefully those links should be enough :wink:

1 Like

Would this (shortened piece of code) work then?

local TweenService = game:GetService("TweenService")
local speed_Tween
VehicleSeat.Changed:Connect(function()
	if speed_Tween ~= nil then
		speed_Tween:Stop()
		speed_Tween = nil
	end	
	if VehicleSeat.Throttle == 1 then
		speed_Tween = TweenService:Create(value1, TweenInfo.new(0.05), maxspeed.Value)
		speed_Tween:Play()
		script.Parent.Driving.Value = true
		script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	elseif VehicleSeat.Throttle == 0 then
		speed_Tween = TweenService:Create(value1, TweenInfo.new(0.05), 0)
		speed_Tween:Play()
		script.Parent.Driving.Value = false
		script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	elseif VehicleSeat.Throttle == -1 then
		speed_Tween = TweenService:Create(value1, TweenInfo.new(0.05), -maxspeed.Value)
		speed_Tween:Play()
		script.Parent.Driving.Value = false
		script.Parent.BodyVelocity.velocity = script.Parent.CFrame.lookVector*value1
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	end
end)

No that wouldn’t work, since your event will fire once per change. The point isn’t to use a new variable for the speed tween - you would do something like this:

local BodyVelocity = script.Parent.BodyVelocity
local speed_Tween
VehicleSeat.Changed:Connect(function()
    if VehicleSeat.Throttle == 1 then
        speed_Tween = TweeService:Create(BodyVelocity.velocity, TweenInfo.new(0.05), maxspeed.Value)
        speed_Tween:Play()
        script.Parent.Left.Value = false
		script.Parent.Right.Value = false
        script.Parent.Driving.Value = true
    end
end)

I’ve just included a short snippet, but the idea is you are Tweening the bodyvelocity itself, not a variable placeholder and applying it as you would in a loop. It’s a different way to do it and probably more efficient.

Edit: Forgot to add the play() thing.

2 Likes

Wouldn’t I need to stop/play the tween?

1 Like

Yes your logic at the beginning is fine, I’m just showing you how the tween part would work for one of your states. I didn’t want to rewrite the work you already did that was correct.

1 Like

got this error message

DriveSeat.Drive:47: attempt to index global 'maxspeed' (a number value)

i believe that its because the velocity is a vector3 and the maxspeed value is not?

Yes that’s what it is. Do you have a Vector3 for it that you would use otherwise?

1 Like

idk what the max vector3 value is, since steer also affects the speed of it via speed on another axis.

even just doing value1 still yields the issue that i can’t index maxspeed.Value

This is what I think would be the setup. Try it out and see if it works. I’ve taken your maxspeed and applied it to the CFrame that you are using for the direction.

-- Edit 3: Ignore the other edits, final version (I hope)
local TweenService = game:GetService("TweenService")
local speed_Tween
local BodyVelocity = script.Parent.BodyVelocity
VehicleSeat.Changed:Connect(function()
	if speed_Tween ~= nil then
		speed_Tween:Stop()
		speed_Tween = nil
	end	
	if VehicleSeat.Throttle == 1 then
		speed_Tween = TweenService:Create(BodyVelocity, TweenInfo.new(0.05), {Velocity = maxspeed.Value * script.Parent.CFrame.lookVector})
		speed_Tween:Play()
		script.Parent.Driving.Value = true
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	elseif VehicleSeat.Throttle == 0 then
        speed_Tween = TweenService:Create(BodyVelocity, TweenInfo.new(0.05), {Velocity = Vector3.new(0,0,0)})		
        speed_Tween:Play()
		script.Parent.Driving.Value = false
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	elseif VehicleSeat.Throttle == -1 then
        speed_Tween = TweenService:Create(BodyVelocity, TweenInfo.new(0.05), {Velocity = -maxspeed.Value * script.Parent.CFrame.lookVector})		
        speed_Tween:Play()
		script.Parent.Driving.Value = false
		script.Parent.Left.Value = false
		script.Parent.Right.Value = false
	end
end)
1 Like

now it just says Unable to cast value to Object

Edit:

I think I realized why it isn’t working. Sorry for messing this up. It should look like this when you apply a velocity Tween:

local Part = script.Parent
.
.
.
        speed_Tween = TweenService:Create(Part, TweenInfo.new(0.05), {Velocity = Vector3.new(0,0,0)})
.
.
.

Apparently you reference the object, and in the last argument you would reference the property you are tweening. This would also apply for BodyVelocity. Example:

local BodyVel = script.Parent.BodyVelocity
.
.
.
        speed_Tween = TweenService:Create(BodyVel, TweenInfo.new(0.05), {Velocity = Vector3.new(0,0,0)})
.
.
.

Again, so sorry for mixing this up. Now I feel like an idiot for running around in circles and taking you with me D:

1 Like

If you think about this based on player reaction time, you want the boat to be responsive. I somewhat disagree that looping like this is wasteful, simply because it’s important that you do not add too much delay. Anything above 0.1 seconds for controls is cutting it close because a lot of people have reaction times less than that, and generally I’ve found that looping about 50%-75% of a player’s reaction time yields better feeling controls, so I’d disagree that wait() is wasteful in this situation.

wait() is not necessarily more or less expensive than heartbeat, after all both are simply activating the execution of code, and wait does so through yielding, while Heartbeat, being an event, creates a new thread (this could be seen as more expensive, however you can connect thousands of events and never run into issues since this isn’t particularly very expensive).

I would actually very much so suggest using Heartbeat none the less. Heartbeat runs immediately before every physics step, while waiting will mean you are completely out of sync with heartbeat. An example of how sync issues can cause problems comes up with monitors. If your computer is sending visual data to the monitor when the monitor is not ready to display, you can introduce slight tearing or the appearance of low FPS which may not even be noticeable, and similarly you can introduce unexpected results in controls, which while subtle, I’ve found to effect my experience somewhat.

Here are some important things about Heartbeat and Stepped: Heartbeat runs before physics, so making position or physics changes on Heartbeat will yield expected results. Stepped runs after physics, but before anything is replicated to clients. This has very strange implications on what clients will see when changing physics on Stepped, for example, the position of a part can appear completely different than where it is when physics are taking place causing the appearance of extreme lag when none is present, but Stepped is particularly useful if you’re looking to do that in the first place, as well as being a useful marker to know when physics calculations end.

Finally, if you do in fact want to slow down input, you can actually combine both a wait call and sync with physics:

-- Controller loop
wait(0.1) -- Slow down!
RunService.Heartbeat:Wait() -- This will wait at most one frame longer, however, now you're fully synced with physics so when things get replicated after Stepped fires shortly after things can appear much more accurate to players
-- Some physics stuff

Also side note: TweenService now uses Heartbeat anyway, so using Tweens and Changed events with a tweened object is effectively the same as using Heartbeat with the extra overhead of replication and property changes in general. Rather than Tweening an object, you can use TweenService:GetValue() instead on a Heartbeat loop or connection as a nice optimization that additionally yields less overall clutter.

3 Likes

How would a sample code similar to the original look with this then?

No worries, you have been extremely helpful, and taught me something I had no idea about until now!

1 Like

Well, you can do it either two ways, through an event connection or just a while loop with Heartbeat:Wait(). GetValue accepts all of the info that a normal Tween makes, other than the actual value types to tween. CFrames and vectors both offer Lerp functions you can use for this.

local elapsedTime = 0
local tweenTime = 1 -- Example time
-- On Heartbeat:
elapsedTime = elapsedTime + heartbeatStep
local newAlpha = TweenService:GetValue(elapsedTime/tweenTime, Enum.EasingStyle.Circular, Enum.EasingDirection.Out) -- Example values, all must be specified
local newCFrame = cframe1:Lerp(cframe2, newAlpha) -- Essentially what is done internally by TweenService

if elapsedTime >= tweenTime then
    break -- Or disconnect
end
2 Likes