Tweening Bezier Curve

So I have somewhat of a understanding of Bezier curves. I created the path of the Bezier but now I’m trying to tween a parts position to follow the path but I have no idea on how to do this.


The neon parts is the path that was created using the Bezier function. The monkey is basically the part that I am trying to tween.

Here is my code:

function lerp(a, b, t)
	return a + (b - a) * t
end

function cubicBezier(t, p0, p1, p2, p3)
	local l1 = lerp(p0, p1, t)
	local l2 = lerp(p1, p2, t)
	local l3 = lerp(p2, p3, t)
	local a = lerp(l1, l2, t)
	local b = lerp(l2, l3, t)
	local cubic = lerp(a, b, t)
	return cubic
end

function cubicBezierTangent(t, p0, p1, p2, p3)
	return
		3 * (1 - t)^2 * (p1 - p0)
		+ 6 * (1 - t) * t * (p2 - p1)
		+ 3 * t^2 * (p3 - p2)
end

local p0, p1, p2, p3 = workspace.Eren.HumanoidRootPart.Position,Vector3.new(0, 20, 0),Vector3.new(20, 10, 0),workspace.Mo.HumanoidRootPart.Position
	

local TweenService = game:GetService("TweenService")


for t = 0, 1, 1/50 do
	local p = Instance.new("Part")
	p.Anchored = true
	p.Size = Vector3.new(3, 3, 3) * 0.1
	p.CFrame = CFrame.new( cubicBezier(t, p0, p1, p2, p3) )
	p.Color = Color3.fromRGB(255, 0, 0)
	p.Parent = game.Workspace
	p.Material = Enum.Material.Neon
	
	local NextPos = cubicBezier(t, p0, p1, p2, p3)
	TweenService:Create(workspace.Monkey, TweenInfo.new(0.1, Enum.EasingStyle.Linear), {Position = NextPos}):Play()
	
	

	local tangent = p:Clone()
	tangent.Size = Vector3.new(0, 0, .5)
	tangent.CFrame = CFrame.new( p.Position, p.Position + cubicBezierTangent(t, p0, p1, p2, p3) )	
	tangent.CFrame += tangent.CFrame.LookVector * tangent.Size.Z * 0.5
	tangent.Color = Color3.fromRGB(0, 55, 255)
	p.Material = Enum.Material.Neon
	tangent.Parent = game.Workspace
end
4 Likes

I’m sure tweening will only produce a linear path which a bezier curve is not. You’d have to use runservice or a loop to iterate the bezier curve’s alpha.

2 Likes

Yes, or if you still want to stick to tweening, you could just tween to each of the neon part positions simultaneously.

1 Like

What I did was create 10-20 dots along the bezier curve using the bezier curve function, then tweening it between them. Worked perfectly

Yeah but that comes with accuracy cost at larger distance.

To some level! But you can always increase the amount of points depending on the distance!

1 Like

It’s literally easier to make a runservice renderstepped on all clients because it then will smoothen and sync with their fps without the cost of accuracy because it’s literally using the bezier curve.

Hmm, you have to sync it each frame though with all other clients, since if someone has a lower fps, theirs would go slower. Besides, I’ve tried it before doing a tween, is around a 10 line script? Sending a message to all clients on the Beziers information seems a bit harder.

To avoid that, you would increment based on the time since the last RenderStepped event was fired (like Heartbeat’s deltaTime parameter), just subtract last tick() from current tick() to get that delta.

EDIT: RenderStepped actually does offer deltaTime now, it didn’t historically :+1:

2 Likes

Just seems a lot more complicated

RenderStepped actually gives you a delta time parameter.

Both ways work, renderstepped is more accurate to the curve and seems usually the best approach, but if you want to accurately maintain a constant speed the tween strategy may become easier :slight_smile:

I would avoid tweens, you would be creating new tweens at each point even though you could feed elapsed time since the start of the throw, using delta time to increment this, and that’s all you need to animate the throw through the curve.

And not to mention if you use tweens you have to get into a bunch of checking distances between points to ensure speed is maintained correctly as points might get closer to each other for shorter throws.

curveTime = curveTime + (1 - curveTime) * math.min(dt, 1)

Just this equation alone you now can position the thrown object anywhere in the curve without needing to reference a group of points, it will be incredibly accurate and follow as close to a curve as possible.

I don’t think so, a lot of local simulation is done like this. Especially for things like projectiles.

I agree with this. The client only needs to know the origin, destination, start time, and bezier curve data. Then you can animate all projectiles visible to the client in a single RenderStepped listener.

Tweening could certainly work for this but it wouldn’t be simpler than running your own calculations on RenderStepped. Plus you have more control over each frame with RenderStepped whereas you’d be starting and stopping potentially hundreds of Tweens. I don’t know the cost of creating and running that many Tweens, but intuitively, I wouldn’t expect that to scale as well in a large game server. Maybe you could prove me wrong with performance benchmarks, I’m really not sure :slight_smile:

Sending the information about the bezier curve should be relatively lightweight. But, I should mention that parabolic arcs can be generated in much simpler ways than with bezier curves, if the goal is to create a physically simulated lobbed projectile.

I’m not opposing you. However, since the projectile is on the client how would you detect the impact while syncing it with the client?

You could Raycast a short distance between the projectile and the next point on the curve each frame.

There are a few ways of syncing the projectile visually between server and client. One way would be to send the time the projectile was fired to the client, and then the client would know how far along the projectile should already be traveling. This has the downside of causing the projectile to look like it has started ahead of the origin. You can measure that by comparing the time when the Remote was fired to the time when the projectile was created (tick()) passed as an argument by the server.

Example:
A projectile needs to travel 100 studs in 5 seconds at a constant speed on the server. A client’s latency is 500 milliseconds, so locally the projectile needs to start 10 studs ahead of the origin.

A second method would be to accelerate the local simulation so that the projectile travels faster than it did on the server, to account for that latency. The biggest downside to that method is sometimes projectiles start and end in a window of time that is shorter than a client’s latency is long. In those cases, it is up to you whether or not a projectile is visualized at all.

Example:
If a projectile takes 5 seconds to travel from its origin to its destination, and a client has a latency of 500 milliseconds, then the client will need to speed up its visualization of the projectile by 10% in order to have it reach the destination at the same time as the server.

1 Like

Hey!, i am late but, you can use this modules, you can fit them to your own likings.

Module 1 :

Example :

local GlowPartTweenInfo = TweenInfo.new(15, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, 0, false, 0)
local Tween = NewBezier:CreateVector3Tween(workspace.GlowPart, {"Position"}, GlowPartTweenInfo)
local Tween2 = NewBezier:CreateCFrameTween(workspace.GlowPart, {"CFrame"}, GlowPartTweenInfo)
Tween2:Play()

Module 2:

Example :

local BezierTween = require(ServerScriptService.BezierTween)
local Waypoints = BezierTween.Waypoints
local P0, P1, P2, P3 = workspace.P0, workspace.P1, workspace.P2, workspace.P3

waypoints = Waypoints.new(P0, P1, P2, P3)
local Part = game.Workspace.Part

local Tween = BezierTween.Create(Part, {
    Waypoints = waypoints,
    EasingStyle = Enum.EasingStyle.Sine,
    EasingDirection = Enum.EasingDirection.In,
    Time = 5,
})
Tween:Play()