anthonysaade.dev

Bezier Projectiles With De Casteljau’s Algorithm

A generic Luau BezierCurve function that works on any type you can lerp, and how Bard’s Fighting Arena uses it for projectile arcs.

By · · 1 min read · Intermediate

Grenades, leaps, and other arcs in Bard’s Fighting Arena follow Bezier curves. The function behind them does not care whether it is moving a CFrame, a Vector3, or a number. You pass in how to lerp, and it handles the curve.

Wrathful Leap
Radial Slam
Deep Knight
Mana Grenade

De Casteljau in one sentence

To find a point on a Bezier curve at t, lerp between each pair of neighboring control points by t, then repeat on the results until one point remains.

The code

function BezierCurve<T>(Lerp : (T, T, number) -> T, TInfo : TweenInfo, Callback : (T, number) -> any, ... : T)
	local Points = {...}
	local Count = #Points

	local Buffer = table.create(Count)

	local NumberValue = Instance.new("NumberValue") do
		local Tween = game.TweenService:Create(NumberValue, TInfo, Globals.Value1)
		local Thread = task.spawn(function()
			local Changed = NumberValue.Changed
			while true do
				local Alpha = Changed:Wait() do
					for Index = 1, Count do
						Buffer[Index] = Points[Index]
					end

					local WorkingCount = Count
					while WorkingCount > 1 do
						for Index = 1, WorkingCount-1 do
							Buffer[Index] = Lerp(Buffer[Index], Buffer[Index+1], Alpha)
						end
						WorkingCount -= 1
					end
				end

				Callback(Buffer[1], Alpha)
			end
		end)
		Tween:Play()
		Tween.Completed:Once(function()
			pcall(task.cancel, Thread)
			NumberValue:Destroy()
		end)

		return Tween, NumberValue
	end
end

--// Example code (Shown on the DEMO)
local Angle = CFrame.Angles(math.rad(math.random(-360, 360)), math.rad(math.random(-360, 360)), math.rad(math.random(-360, 360)))
BezierCurve(CFrameLerp, TweenInfo.new(Time, Enum.EasingStyle.Linear, Enum.EasingDirection.Out), function(CF, Alpha)
	Grenade.CFrame = CF * CFrame.identity:Lerp(Angle, Alpha)
end, ...)

Why pass the lerp in?

Taking Lerp : (T, T, number) -> T as a parameter makes the function generic. The same curve code moves a CFrame (with a CFrame lerp function) or a position (Vector3.zero.Lerp) without separate versions for each type.

Driving it with TweenService

The curve is sampled from a NumberValue that a tween animates from 0 to 1. That means every easing style TweenService supports works on the curve for free.

How the grenade uses it

The grenade splits the work between the server and the client. The server initializes the projectile: it creates it and sets the curve’s control points toward the direction it was thrown. Each client then animates the projectile along that curve with BezierCurve, which simulates the physics of the throw without sending the projectile’s position over the network every frame.