Circular Vertical Loop Generation Issue

I’m in the midst of coding something that’s meant to use parts to generate a loop. It works for the most part but requires some adjustments that I’m unsure how to fix. These are the results of both attempts:

Image Results (1st attempt)

I’d like for those parts to be inline just like the rest of them. I did manage to make one that did keep them inline… but the whole loop itself was slanted.

Image Result (2nd attempt)

As you could imagine, this is not the result that I’m looking for, and I myself am not particularly math savvy. What would it take to fix this?

Here’s the generation code for reference:

Generation Code (1st attempt)
--Po1 and Po2 are the start and end positions (chosen outside of the function).
--Code is just a custom module script for making some functions more convenient.
--There are other variables outside of this function; feel free to ignore those.

function DisplayLoop() if not Po1 or not Po2 then return end
local Radius,LoopWidth,Segments = (Po1-Po2).Magnitude,5,24
local CenterCF = (CFrame.new(Po1,Po2):ToWorldSpace(CFrame.new(-LoopWidth,0,-Radius))).Position
	CenterCF = CFrame.new(Po1,CenterCF):ToWorldSpace(CFrame.new(0,Radius,0))
--Set up loop points
local GetLoopPoints = {}
	for N = 1,Segments,1 do
	local Rad = math.rad((360*N)/Segments)
	local NewPoint = (CenterCF*CFrame.fromEulerAnglesXYZ(Rad,0,0)):ToWorldSpace(CFrame.new((N/Segments)*5,-Radius,0)).Position
	table.insert(GetLoopPoints,NewPoint)
	end
local ParentTo = Code.new"Model"{Name="Loop",Parent=workspace}
--Make the rail connecting the initial and starting point of the loop
local NewMag = (Po1-GetLoopPoints[1]).Magnitude
local NewC = CFrame.new(Po1,GetLoopPoints[1]):ToWorldSpace(CFrame.new(0,0,NewMag/-2))
local StartRail = Code.new"Part"{Name=NewName,Parent=ParentTo,Material=Material,Color=PartColor,Anchored=true,
	CanCollide=CanCollide,CFrame = NewC,Size = Vector3.new(Width,Height,NewMag)}
Code.ChangeSame(Enum.SurfaceType.SmoothNoOutlines,StartRail){
		"BackSurface","BottomSurface","FrontSurface","LeftSurface","RightSurface","TopSurface"}
--Start making the actual loop
local Len = (GetLoopPoints[1]-GetLoopPoints[2]).Magnitude	
	for N,P in pairs(GetLoopPoints) do
	local IsFinal = not (N<#GetLoopPoints)
	local NewLen = (GetLoopPoints[N]-Po2).Magnitude
	local NewCF = not IsFinal and CFrame.new(GetLoopPoints[N],GetLoopPoints[N+1]):ToWorldSpace(CFrame.new(0,0,Len/-2))
		or CFrame.new(GetLoopPoints[N],Po2):ToWorldSpace(CFrame.new(0,0,NewLen/-2))
	local NewSize = not IsFinal and Vector3.new(Width,Height,Len) or Vector3.new(Width,Height,NewLen)
	local Rail = Code.new"Part"{Name=(NewName .. tostring(N)),Parent=ParentTo,Material=Material,Color=PartColor,
		Anchored=true,CanCollide=CanCollide,CFrame = NewCF,Size = NewSize}
	Code.ChangeSame(Enum.SurfaceType.SmoothNoOutlines,Rail){
		"BackSurface","BottomSurface","FrontSurface","LeftSurface","RightSurface","TopSurface"}
	end
--End the process
SwitchMode(M1)
end
Generation Code (2nd attempt)
--Po1 and Po2 in this case is simply trying to make a loop from the
--facing-front position of the selected part instead of trying to link
--1 end to another perfectly. The connection will occur after the loop is made
--(not shown in this example).

--Variables
local Po1,Po2 = workspace.PointA.Position,workspace.PointA.CFrame:ToWorldSpace(CFrame.new(0,0,-1)).Position
local Code = require(script.Code)
local NewName,CanCollide,Material,PartColor = "Rail",true,Enum.Material.SmoothPlastic,BrickColor.palette(123).Color
local Width,Height = 0.5,2
local Segments,Radius,LoopWidth = 20,10,8
--Make the initial rail:
local ParentTo = Code.new"Model"{Name="Loop",Parent=workspace}
local NewC = CFrame.new(Po1,Po2):ToWorldSpace(CFrame.new(0,0,Radius/-2))
local StartRail = Code.new"Part"{Name=NewName,Parent=ParentTo,Material=Material,Color=PartColor,Anchored=true,
	CanCollide=CanCollide,CFrame = NewC,Size = Vector3.new(Width,Height,Radius)}
--Do stuff:
local Rails,Angle,Prev = {},360/Segments,StartRail
	for N = 1,Segments-2,1 do
	local NewCF = Prev.CFrame:ToWorldSpace(CFrame.new(0,0,Radius/-2))
		NewCF = NewCF*CFrame.fromEulerAnglesXYZ(math.rad(Angle),math.rad(LoopWidth),0)
		NewCF = NewCF:ToWorldSpace(CFrame.new(0,0,Radius/-2))
	local NewSize = Vector3.new(Width,Height,Radius)
	Prev = Code.new"Part"{Name=(NewName .. tostring(N)),Parent=ParentTo,Material=Material,Color=PartColor,
		Anchored=true,CanCollide=CanCollide,CFrame = NewCF,Size = NewSize}
	table.insert(Rails,Prev)
	end
--Make the final rail:
NewC = Prev.CFrame:ToWorldSpace(CFrame.new(0,0,Radius/-2))
local NewDist = (NewC.Position-Po2).Magnitude
NewC = CFrame.new(NewC.Position,Po2):ToWorldSpace(CFrame.new(0,0,NewDist/-2))
Code.new"Part"{Name=NewName,Parent=ParentTo,Material=Material,Color=PartColor,Anchored=true,
	CanCollide=CanCollide,CFrame = NewC,Size = Vector3.new(Width,Height,NewDist)}
--Quick final adjustment:
Code.ChangeSame(Enum.SurfaceType.SmoothNoOutlines,unpack(ParentTo:GetChildren())){
	"BackSurface","BottomSurface","FrontSurface","LeftSurface","RightSurface","TopSurface"}

Explaining some variables just in case:
• Radius: How tall/wide it should be.
• LoopWidth: How far it should go from left-to-right or vice-versa.
• Segments: How many individual parts it takes to make it.

I’ve tried looking around the forums already and haven’t found a similar issue. If there is one and this ends up being a repost of some sort, I apologize.

3 Likes

Relative positions of the loop seem to generate finely, but the overall structure drifts off the plane it is supposed to be on.

I am assuming that you want the first and the last parts to line up (correct me if I’m wrong). That means the whole shape must be able to be on one plane (you are expecting the loop to be “flat” like putting a ring on a table). If that plane is the “Y” plane (meaning the plane is a infinitely large rectangle that is turned 90 degrees), then I do not think that you would need to change the part’s Y orientation.

Instead of

Would NewCF = NewCF*CFrame.fromEulerAnglesXYZ(math.rad(Angle),0,0) work … ?

Sorry if I am off, I am getting confused myself. And it is not your fault, it’s just that I’m a bit “overwhelmed” with all this math either.

1 Like

While what you said would make it a ring, my goal isn’t a ring. I wish to make a loop like the kinds you’d see on roller coasters.

2 Likes

Scratch that. I managed to figure out the problem on my own.

Success Screenshots:

See more details here:

1 Like

Care to share the secret so we know how to do it when we need to?

1 Like

Due to a couple of requests about the solution, I may as well provide it. I was only reluctant to post the code itself because I found it to be a janky solution that I do not personally recommend putting into practice. Though the general idea behind the solution was simpler than expected.

Since I already knew where the center of the loop was, I’d rotate the whole thing so that every new segment being made was placed flat at the bottom. So it can take advantage of using CFrame.lookAt to do the rotation without any of the previous jank. Then when they were all made, I’d place the loop model back in the starting position. It’s a cheap, yet effective, workaround.

If you wanna try understanding this, go ahead. It's a bit outdated.

One variable that I neglected to mention before is Code, which just refers to functions this module I had. Though you don’t really need to know anything about it to see what’s going on (I think).

function DisplayLoop()
local Po1 = Point.CFrame:ToWorldSpace(CFrame.new(0,0,Point.Size.Z/-2)).Position
LoopPreview = Code.new("Model",workspace){Name="Loop"}

--Make the initial rail:
local CenterCF = Point.CFrame:ToWorldSpace(CFrame.new(0,Radius,Point.Size.Z/-2))
local PrimaryPart = Code.new("Part"){Parent=LoopPreview,Name="Center",Shape=Enum.PartType.Ball,Size=Vector3.new(1,1,1),
Color = Color3.new(1,1,1),Material = Enum.Material.Neon,CFrame = CenterCF,Transparency = 1}

LoopPreview.PrimaryPart = PrimaryPart

--Set up loop points
local GetLoopPoints,Angle = {},360/Segments
	for N = 1,Segments,1 do
	local Rad = math.rad((360*N)/Segments)
	local NewPoint = (CenterCF*CFrame.fromEulerAnglesXYZ(Rad,0,0)):ToWorldSpace(CFrame.new((N/Segments)*LoopWidth,-Radius,0)).Position
	NewPoint = Code.new"Attachment"{Parent=PrimaryPart,WorldPosition=NewPoint}
	table.insert(GetLoopPoints,NewPoint)
	end

--Make the rail connecting the initial and starting point of the loop
local NewMag = (Po1-GetLoopPoints[1].WorldPosition).Magnitude
local NewC = CFrame.new(Po1,GetLoopPoints[1].WorldPosition):ToWorldSpace(CFrame.new(0,0,NewMag/-2))
local StartRail = Code.new("Part"){Name=NewName,Parent=LoopPreview,Material=Material,Color=PartColor,Anchored=true,
CanCollide=CanCollide,CFrame = NewC,Size = Vector3.new(Width,Height,NewMag)}

--Start making the actual loop
local Len = (GetLoopPoints[1].WorldPosition-GetLoopPoints[2].WorldPosition).Magnitude
	for N,P in pairs(GetLoopPoints) do
	local IsFinal = N>=#GetLoopPoints
		if IsFinal then
		LoopPreview:SetPrimaryPartCFrame(CenterCF)
		break
		else
		LoopPreview:SetPrimaryPartCFrame(PrimaryPart.CFrame*CFrame.fromEulerAnglesXYZ(math.rad(-Angle),0,0))
		local NewCF = CFrame.new(GetLoopPoints[N].WorldPosition,GetLoopPoints[N+1].WorldPosition):ToWorldSpace(CFrame.new(0,0,Len/-2))
		local NewSize = Vector3.new(Width,Height,Len)
		local Rail = Code.new("Part"){Name=NewName,Parent=LoopPreview,Material=Material,Color=PartColor,Anchored=true,CanCollide=CanCollide,CFrame = NewCF,Size = NewSize,Transparency = 0.25}
		end
	end
Code.ChangeSame(Enum.SurfaceType.SmoothNoOutlines,LoopPreview:GetChildren()){"BackSurface","BottomSurface","FrontSurface","LeftSurface","RightSurface","TopSurface"}
PrimaryPart:Destroy()
end
1 Like