Spling v1.2.0 | Fast, Constant Velocity Splines
![]()
- Links / Files:
Download class file:
SplineClass.rbxm (9.9 KB)
Download class test place:
SplineClassTestplace.rbxl (67.8 KB)
![]()
- Introduction:
Hello everyone! ![]()
→ I’ve been wanting to share a spline library I’ve been working on that is designed for smooth, controllable movement along a spline while keeping performance stable even when the spline changes at runtime.
The main goal of this library is to solve a long-time running problem with most spline libraries: controlled movement across a spline without relying on heavy sampling or large cached lookup tables. With this library, you can now move objects along curved paths mantaining their distance accurately, while still being able to edit the spline points at any moment without causing FPS drops or lag!
But there are already hundreds of spline libraries out there, what’s different about this one?
→ I noticed that most spline systems that support constant-speed movement need to sample the curve many times and cache those samples (usually around 1000 samples!). That works, but it can become expensive when the spline changes often, since the cached data has to be rebuilt each time.
This module avoids that problem by calculating spline length accurately with lightweight numerical methods instead of depending on dense sampling. When a point is changed, inserted, or removed, only the nearby affected curve arc lengths are recalculated, instead of recalculating the entire spline from scratch!
That makes it useful for dynamic splines where points may move or change during gameplay.
![]()
- Documentation:
To make a spline you’ll need to provide the class an array of points that it will use to build the spline. You can also provide a table with a set of settings if you want to change how the spline will form.
local SplineClass = require(path.to.SplineClass)
local Points = {
Vector3.new(0, 10, 0),
Vector3.new(25, 5, -5),
Vector3.new(50, 15, 5),
Vector3.new(75, 5, 0),
}
--This table is optional, if you want to toy around with the values feel free to do so
local SplineSettings = {
Tension = 1, --How smooth/snappy the spline will look, default is 1
Looped = false --Whenever the spline loops around itself or not, default is false
}
local Spline = SplineClass.new(Points, SplineSettings)
Spline:DrawDebug()
--This part of the code will create a new part and position it right at the middle of the spline
local MyPoint = Instance.new("Part")
MyPoint.Anchored = true
MyPoint.Size = Vector3.one * 3
MyPoint.Name = `SplinePoint`
MyPoint.Parent = workspace
MyPoint.CFrame = Spline:GetCFrame(0.5) --You can also archieve the same thing by setting it to Spline:GetCFrame(Spline.Length / 2, true)
GetPosition(alpha, isLength)
- Description:
This function will return a position (Vector3) given the alpha value (value between 0-1, 0-Length if isLength is true). - Typechecking:
GetPosition: (
self: Spline,
alpha: number,
sLength: boolean?
) -> Vector3
- Example:
local SplineClass = require(path.to.SplineClass)
local Points = {
Vector3.new(0, 10, 0),
Vector3.new(25, 5, -5),
Vector3.new(50, 15, 5),
Vector3.new(75, 5, 0),
}
local Spline = SplineClass.new(Points)
local MyPoint = workspace.SplinePoint :: Part --example part here
MyPoint.Position = Spline:GetPosition(Spline.Length * 0.5, true)
GetCurve(alpha, isLength)
-
Description:
Returns the internal Bézier curve at the given position along the spline, together with the local alpha of that position inside the returned curve. -
Typechecking:
GetCurve: (
self: Spline,
alpha: number,
isLength: boolean?
) -> (BezierCurve, number)
- Example:
local SplineClass = require(path.to.SplineClass)
local Points = {
Vector3.new(0, 10, 0),
Vector3.new(25, 5, -5),
Vector3.new(50, 15, 5),
Vector3.new(75, 5, 0),
}
local Spline = SplineClass.new(Points)
local Curve, LocalAlpha = Spline:GetCurve(0.5)
print("Curve:", Curve)
print("Local alpha:", LocalAlpha)
print("Start:", Curve.P0)
print("End:", Curve.P3)
print("Curve length:", Curve.Length)
GetClosestWorldPoint(worldPosition, samples, iterations)
- Description:
Returns the closest position on the spline to a given world position. It returns both the closestVector3position and its normalized spline alpha.
samples and iterations are optional and default to 50 and 12 respectively. Increasing them can improve precision at the cost of additional computation.
- Typechecking:
GetClosestWorldPoint: (
self: Spline,
worldPosition: Point,
samples: number?,
iterations: number?
) -> (Vector3, number)
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 10, 10),
Vector3.new(50, 0, 0),
})
local Target = workspace.Target
local ClosestPosition, Alpha = Spline:GetClosestWorldPoint(Target.Position)
print("Closest position:", ClosestPosition)
print("Spline alpha:", Alpha)
Target.Position = ClosestPosition
You can also change the amount of samples and refinement iterations:
local Position, Alpha = Spline:GetClosestWorldPoint(
workspace.Target.Position,
100,
16
)
GetClosestLocalPoint(alpha, isLength)
- Description:
Returns the closest spline control point surrounding a given position along the spline.
Unlike GetClosestWorldPoint(), this does not search for a position along the spline. Instead, it gets the Bézier curve at the provided alpha and returns whichever endpoint of that curve is closest according to its local alpha.
It returns the local curve alpha and the Bézier curve containing the requested position.
- Typechecking:
GetClosestLocalPoint: (
self: Spline,
alpha: number,
isLength: boolean?
) -> (Vector3, number, BezierCurve)
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 10, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 10, 0),
})
local Point, LocalAlpha, Curve = Spline:GetClosestLocalPoint(0.6)
print("Closest control point:", Point)
print("Local curve alpha:", LocalAlpha)
print("Curve:", Curve)
GetCurvePointAlpha(position, isLength)
- Description:
Returns the spline alpha corresponding to a spline curve point along with its Bézier curve.
The supplied position must correspond exactly to the starting point (P0) of one of the spline’s Bézier curves.
If the provided position does not correspond to a curve point, the function returns nil.
- Typechecking:
GetCurvePointAlpha: (
self: Spline,
position: Point,
isLength: boolean?
) -> (number?, BezierCurve?)
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 10, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 10, 0),
})
local Alpha, Curve = Spline:GetCurvePointAlpha(Spline.Points[2])
if Alpha then
print("Point alpha:", Alpha)
print("Curve:", Curve)
end
You can retrieve its distance along the spline instead:
local Distance = Spline:GetCurvePointAlpha(Spline.Points[2], true)
print("Distance from start:", Distance)
GetCFrame(alpha, isLength)
- Description:
Returns aCFrameat the specified position along the spline.
The position is calculated using the same constant-distance system as GetPosition(), while the orientation follows the tangent/direction of the spline at that point. This makes this function specifically useful for objects such as trains, projectiles, cameras, vehicles, or anything else that should automatically face along the spline.
- Typechecking:
GetCFrame: (self: Spline, alpha: number, isLength: boolean?) -> CFrame
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 10, 0),
Vector3.new(25, 5, -5),
Vector3.new(50, 15, 5),
Vector3.new(75, 5, 0),
})
local MyPart = workspace.MyPart
MyPart.CFrame = Spline:GetCFrame(0.5)
It can also be used with distance:
MyPart.CFrame = Spline:GetCFrame(25, true)
SetPoint(point, index)
- Description:
Changes an existing spline point at the provided index.
Only the nearby Bézier curves affected by the changed point are recalculated, instead of rebuilding the entire spline. This is particularly useful for dynamic splines where control points move during runtime.
- Typechecking:
SetPoint: (
self: Spline,
point: Vector3 | Instance | CFrame,
index: number
) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 0, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 0, 0),
})
Spline:SetPoint(
Vector3.new(25, 25, 0),
2
)
Spline:DrawDebug()
You can also use the position of a part:
Spline:SetPoint(workspace.ControlPoint, 2)
InsertPoint(point, index)
- Description:
Inserts a new control point into the spline.
If an index is provided, the point will be inserted at that position in the point array. After insertion, the affected Bézier curves and spline length are recalculated.
- Typechecking:
InsertPoint: (
self: Spline,
point: Point,
index: number?
) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 0, 0),
})
Spline:InsertPoint(
Vector3.new(25, 15, 0),
2
)
Spline:DrawDebug()
This changes the point array from:
{
Vector3.new(0, 0, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 0, 0),
}
Into:
{
Vector3.new(0, 0, 0),
Vector3.new(25, 15, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 0, 0),
}
RemovePoint(index)
- Description:
Removes a control point from the spline at the provided index.
After removing the point, the affected nearby curves are recalculated and Spline.Length is updated automatically.
- Typechecking:
RemovePoint: (self: Spline, index: number) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 15, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 0, 0),
})
Spline:RemovePoint(2)
Spline:DrawDebug()
DrawDebug(samples)
- Description:
Draws a visual representation of the spline insideWorkspace.
Red points are created along the spline while green segments connect the samples together, making it easier to visualize the shape of the spline while developing.
samples controls how many subdivisions are used to display the spline and defaults to 100.
This function is intended for debugging and visualization.
- Typechecking:
DrawDebug: (self: Spline, samples: number?) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 15, 0),
Vector3.new(50, -10, 0),
Vector3.new(75, 0, 0),
})
Spline:DrawDebug()
You can provide your own sample amount:
Spline:DrawDebug(200)
Calling DrawDebug() again will update the existing debug visualization rather than requiring you to manually remove it first.
Spline:SetPoint(Vector3.new(25, 30, 0), 2)
Spline:DrawDebug()
CreateSplineLUT(Samples)
- Description:
This function will sample the spline and create a LUT that will be used by every function to save on performance in exchange of dynamicness, accuracy and memory. This function should be used once you are sure that the spline won’t be updated in the future or often. The performance gain usually sits at around 20-30% so don’t expect much
.
Note: If any points are updated/inserted/removed, the spline will automatically disable the LUT. You will have to call this function again to recalculate the LUT.
- Typechecking:
CreateSplineLUT: (
self: Spline,
Samples: number?
) -> (),
- Example:
local SplineClass = require(workspace.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 10, 0),
Vector3.new(50, 0, 0),
Vector3.new(75, 10, 0),
})
local Start = os.clock()
Spline:GetPosition(0.5)
print(`Without LUT: {os.clock() - Start}s`)
Spline:CreateSplineLUT() --If no sample number is given, default one will be used
local Start = os.clock()
Spline:GetPosition(0.5)
print(`With LUT: {os.clock() - Start}s`)
RemoveDebug()
- Description:
Removes the debug visualization created byDrawDebug()for this spline.
This does not modify or destroy the spline itself.
- Typechecking:
RemoveDebug: (self: Spline) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 15, 0),
Vector3.new(50, 0, 0),
})
Spline:DrawDebug()
task.wait(5)
Spline:RemoveDebug()
Destroy()
- Description:
Destroys the spline object and cleans up its associated data.
This automatically removes its debug visualization and clears the spline’s points, Bézier curves, settings, and internal values.
The spline should no longer be used after calling this function.
- Typechecking:
Destroy: (self: Spline) -> ()
- Example:
local SplineClass = require(path.to.SplineClass)
local Spline = SplineClass.new({
Vector3.new(0, 0, 0),
Vector3.new(25, 15, 0),
Vector3.new(50, 0, 0),
})
Spline:DrawDebug()
--When you're finished with the spline:
Spline:Destroy()
![]()
- Drawbacks:
→ The only noticeable drawback that you’ll get from this module is that calculating the position along the spline will be minimally slower compared to other spline modules. However, I’ve been trying it out myself and the impact seems negligible, you’re welcome to benchmark it if you’d like and share your results!
![]()
- Final notes / Summary:
This module is mainly focused on giving developers accurate spline movement without the usual performance cost of heavy sampling. You still get length-based control, alpha-based control, runtime editing, and orientation.
I’d love to hear feedback, suggestions, or possible improvements! ![]()


