More efficient way to tween / lerp towards constant-moving goal?

Okay so there’s a bit of a small thing I’ve sort of ran into and I’m not entirely sure how to make it optimal.

So let’s say I’ve got some weapon that needs to sway, or a object that constantly needs to tween to a new location, every frame, at the same speed.

I’m very familiar with lerping and tween service, but there’s just a few things I can’t seem to fully achieve with these methods of procedural animation or movement.

TweenService

So, I like TweenService. The only issue with it is that if you constantly have to tween something that has a new value every time then you end up constantly creating/destroying tweens and I feel like this negatively impacts performance if done for a lot of objects.

I also generally like to stick to my own way of designing systems which is trying to create/destroy as least objects as possible.
And tweens are reusable, but not if the goal post is constantly moving around.

Lerp

This might actually be my solution, but I’m not sure what kind of math to use to get the desired result.

I’ve tried this:

RunService.RenderStepped:Connect(function(delta)

 part.CFrame = part.CFrame:Lerp(goal, delta)

end)

Now please don’t roast me for this, this probably is the “bad” way of doing it and I was trying random things at some point.

The problem with this approach is, it kiiinda works?
But caused said object to move slower the closer it is to it’s goal and also, it doesn’t quite reach it’s goal 100% ever, even if the goal is not moving.

This is like letting an ant walk on a rubber band that keeps infinitely stretching so the ant never makes it to the tip but does appear to get closer every step.

Whatever the heck this approach is.

Just imagine that this is in some RenderStepped loop.

local goal = {...} -- Imagine this is a constantly moving/changing vector, decomposed into a table for convenience.

local coords = {startVector.X, startVector.Y, startVector.Z}

for i = 1, 3 do

 if coords[i] > goal[i] then
  coords[i] -= stepSize * frameDelta

 elseif coords[i] < goal[i] then
  coords[i] += stepSize * frameDelta
 end

end

local composedVector = Vector3.new(coords[1], coords[2], coords[3])

Now this would kinda work too?

Only issue is that if we get REALLY close to the goal vector and the distance is between our current position and goal position is smaller than the step size, we’ll over-step.

So I’ll have to write extra code, just to check if the distance between our current position and goal position is small enough to skip the whole interpolation process all together.
Which I was not sure of what the most optimal way to do this would be.

The code shown here above is also roughly how I programmed spring physics before (just add acceleration / deceleration to the code).

But the spring would never stop moving or jittering after it stopped bouncing because it kept over-stepping near it’s goal position.
So I had to write some clunky code to check if the speed / distance was small enough to just make the spring completely stop moving and that ended up looking weird too.

I cannot provide a lot of examples but I do know there’s plenty of things I need this sort of interpolation for.
And I’m looking for a way to make it fast, performant and dead-simple and keep the code very small.

I haven’t been doing a lot on my maths lately so I likely missed a method or simple formula for interpolating at constant-speed towards a constantly moving goal post without over-stepping.

The answer could be right in front of me or something very obvious and maybe I currently just don’t have the brain capacity to realize it or figure it out right away.

2 Likes

About your ant on a rubber band reference, you could always check if the lerps delta is close enough to its goal. If it’s within a small decimal, then just set its property directly to its goal.

1 Like
local distanceThreshold = 1e-2
local speed = -- how many studs per second
RunService.Heartbeat:Connect(function(deltaTime)
	local vectorFromCurrentPositionToGoal = goalPosition - partToMove.Position
	if vectorFromCurrentPositionToGoal.Magnitude < distanceThreshold then
		-- preventing division by a very small number when calculating the unit vector
		return
	end
	partToMove.Position += vectorFromCurrentPositionToGoal.Unit * math.min(speed * deltaTime, vectorFromCurrentPositionToGoal.Magnitude)
end)

Edit: Here’s an alternative. Both should behave in the same way.

local distanceThreshold = 1e-2
local speed = -- how many studs per second
RunService.Heartbeat:Connect(function(deltaTime)
	local vectorFromCurrentPositionToGoal = goalPosition - partToMove.Position
	if vectorFromCurrentPositionToGoal.Magnitude < distanceThreshold then
		-- preventing division by a very small number when calculating lerp alpha.
		return
	end
	partToMove.Position = partToMove.Position:Lerp(goalPosition,  math.min(speed * deltaTime / vectorFromCurrentPositionToGoal.Magnitude, 1)
end)
2 Likes

I’ve considered this actually but likely completely forgot about it.
If it’s a teeny tiny increment then it likely won’t cause much visual weirdness.

It’s a neat interpolation method, just kind of a bummer that it slows down the closer to the goal you are.
It would’ve been almost the perfect method if I could keep the speed consistent somehow.

I’ve tried using distance in the alpha of the lerp() function but this also resulted in odd behavior, especially if the frame rate was inconsistent (frame drops, etc).

1 Like

you could keep the speed constant by saving the original property as a variable

local position = item.Position

for i = 1,100 do
 item.Position:Lerp(position, Vector3.new(), i/100)
 runService.RenderStepped:Wait()
end

i am sure this could be optmized greatly. i wrote this right off the top of my head.

responding to your last comment about the alpha, you could also save tick() and lerp the position along based off of the amount of time it has been since the start of the tick()

1 Like

This works but has the downside that it stops once the loop finishes.
When the target position is moved or changed I actually need the lerp to “reset” every frame.

So our part can lerp towards it’s goal if the goal is stationary, but if the goal is moving, the part has to move as well without randomly going faster or slower.

I much appreciate the help though!
I’m thinking of a way to make the lerp “reset” or basically not have a start/end as long as the goal keeps moving.