Incorrect Curve Generation when working with float curves

I’ve been experimenting with float curves in order to generate paths from 1 node to another such that I can tween a part between two nodes in a smoothly curving way. Kind of like a bezier curve with control points.

Example gif of what I’m trying to achieve:

The problem with this is that when I try to tween a part to show the path it takes, it is nothing at all what I’d like from it.

The blue line is the desired path that I want it to take,
The red line is the actual path that the part takes.


This is the most accurate visualization I could make.

I did follow a tutorial video on how to make float curves and tweening with them and expanded on what I learned to incorporate using angles so that it looks like the part is following a path and not just remaining static in one orientation.

I’ve tried changing the left and right tangents of both nodes to see if anything changed but visually they were almost the same. I’m not even sure if I’m using float curves the way they were intended and if I should move to a different curve generation like bezier curves.

Module script that generates the path:

local module = {}
module.__index = module

local TweenService = game:GetService("TweenService")
local part = game.ReplicatedStorage:WaitForChild("Part")

module.new = function(positions, rotations, speed)
	local self = setmetatable({}, module)

	self.Part = part:Clone()
	self.Part.Position = positions[1]
	self.Part.Parent = workspace

	self.Curve = Instance.new("Vector3Curve")
	self.CurveRot = Instance.new("EulerRotationCurve")
	local curveX, curveY, curveZ = self.Curve:X(), self.Curve:Y(), self.Curve:Z()
	local curveRotX, curveRotY, curveRotZ = self.CurveRot:X(), self.CurveRot:Y(), self.CurveRot:Z()

	self.Time = 0

	for index, position in positions do
		if index > 1 then self.Time += (position - positions[index - 1]).Magnitude / speed end
		
		-- positions
		local curveKeyX = FloatCurveKey.new(self.Time, position.X, Enum.KeyInterpolationMode.Cubic)
		curveKeyX.LeftTangent = math.rad(rotations[index].Y)
		curveKeyX.RightTangent = math.rad(rotations[index].Y)
		curveX:InsertKey(curveKeyX)
		
		local curveKeyY = FloatCurveKey.new(self.Time, position.Y, Enum.KeyInterpolationMode.Cubic)
		curveY:InsertKey(curveKeyY)
		
		local curveKeyZ = FloatCurveKey.new(self.Time, position.Z, Enum.KeyInterpolationMode.Cubic)
		curveZ:InsertKey(curveKeyZ)
		curveKeyX.LeftTangent = math.rad(rotations[index].Y)
		curveKeyX.RightTangent = math.rad(rotations[index].Y)
		
		-- rotations
		local curveRotKeyX = FloatCurveKey.new(self.Time, rotations[index].X, Enum.KeyInterpolationMode.Cubic)
		curveRotX:InsertKey(curveRotKeyX)

		local curveRotKeyY = FloatCurveKey.new(self.Time, rotations[index].Y, Enum.KeyInterpolationMode.Cubic)
		curveRotY:InsertKey(curveRotKeyY)

		local curveRotKeyZ = FloatCurveKey.new(self.Time, rotations[index].Z, Enum.KeyInterpolationMode.Cubic)
		curveRotZ:InsertKey(curveRotKeyZ)
	end

	return self
end

module.Set = function(self, curveTime)
	self.Part.Position = Vector3.new(table.unpack(self.Curve:GetValueAtTime(curveTime)))
	self.Part.Orientation = Vector3.new(table.unpack(self.CurveRot:GetAnglesAtTime(curveTime)))
end

module.Tween = function(self, curveTime, easingStyle, easingDirection)
	local alpha = curveTime / self.Time
	local tween = TweenService:GetValue(alpha, easingStyle, easingDirection)
	self.Part.Position = Vector3.new(table.unpack(self.Curve:GetValueAtTime(tween * self.Time)))
	self.Part.Orientation = Vector3.new(table.unpack(self.CurveRot:GetAnglesAtTime(tween * self.Time)))
end

module.Destroy = function(self)
	self.Part:Destroy()
	self.Curve:Destroy()
end

return module

local script that does all the initializations.

local RunService = game:GetService("RunService")
local PathModule = require(game.ReplicatedStorage:WaitForChild("Path"))
local pathsFolder = workspace:WaitForChild("Paths")
local paths = {}

local function ChildAdded(model)
	local positions = {}
	local rotations = {}
	for index,child in model:GetChildren() do
		positions[tonumber(child.Name)] = child.Position
		rotations[tonumber(child.Name)] = child.Orientation
	end
	
	paths[model] = PathModule.new(positions, rotations, 10)
end

local function ChildRemoved(model)
	paths[model]:Destroy()
	paths[model] = nil
end

local function Loop(deltaTime)
	local serverTime = workspace:GetServerTimeNow()
	
	for model, path in paths do
		local loopTime = serverTime % path.Time
		path:Tween(loopTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
	end
end

for index, child in pathsFolder:GetChildren() do ChildAdded(child) end
pathsFolder.ChildAdded:Connect(ChildAdded)
pathsFolder.ChildRemoved:Connect(ChildRemoved)
RunService.Heartbeat:Connect(Loop)

I’m sorry for the lack of comments, I’m still getting into the habit of leaving comments on my scripts.