So, I created two solutions here, a simple solution for rails that don’t branch, and a more complex one that allows for branching paths.
To be honest, this was more a fun thing for me, it took me maybe 2 hours or so to get it all cleaned up.
Simple Solution
Reproduction: RailPlace.rbxl (145.3 KB)
For the implementation I had, I placed the humanoid in PlatformStand and moved them 3 studs above and along the rail.
RailPart anatomy:
- BasePart - "RailPart"
- Attachment - "Node0"
- Attachment - "Node1"
RailParts are connected node-to-node to achieve a seamless effect.
Moving parts are Lerped from one end of the node to the next at a speed specified by the equation. The speed can be changed to achieve gravitational influence if one so chooses.
AlignPosition argument anatomy:
AlignPosition.ClassName = "AlignPosition"
AlignPosition.Attachment0 = player.Character.HumanoidRootPart
AlignPosition.Attachment1 = workspace.DummyPart -- .Position = (0,0,0)
AlignPosition.Enabled = false
AlignPosition.RigidityEnabled = true
RailParts argument anatomy:
local RailParts = { -- Stored in consecutive order
-- RailPart1,
-- RailPart2,
-- ...
}
The CurrentPart
is either found through a .Touched
signal or magnitude checks, but .Touched
is easier to use, and CurrentPart
has to be a member of RailParts.
local function GrindRail(AlignPosition, RailParts, CurrentPart)
AlignPosition.Attachment1.Position = AlignPosition.Attachment0.WorldPosition
local i = table.find(RailParts, CurrentPart)
local CurrentPosition = AlignPosition.Attachment0.WorldPosition
local dist1 = CurrentPosition - CurrentPart.Node0.WorldPosition
local dist2 = CurrentPart.Node1.WorldPosition - CurrentPart.Node0.WorldPosition
-- alpha between Node0 and Node1
local a = dist2.Unit:Dot(dist1)/dist2.Magnitude
local speed = dist2.Unit:Dot(AlignPosition.Attachment0.Parent.Velocity)
-- All variables should be found by now!
local function GetDist(n0, n1)
return (n1.WorldPosition - n0.WorldPosition).Magnitude
end
local function GetNewPosition(part, a)
return part.Node0.WorldPosition:Lerp(part.Node1.WorldPosition, a)
end
AlignPosition.Enabled = true
local connection
local CurrentRatio = GetDist(CurrentPart.Node1,CurrentPart.Node0)
connection = RunService.RenderStepped:Connect(function(dt)
a += dt*speed/CurrentRatio
if a > 1 then
repeat
CurrentPart = RailParts[i+1]
if CurrentPart then
local NewRatio = GetDist(CurrentPart.Node0, CurrentPart.Node1)
i += 1
a -= 1
a *= NewRatio/CurrentRatio
CurrentRatio = NewRatio
else
connection:Disconnect()
break
end
until a <= 1
elseif a < 0 then
repeat
CurrentPart = RailParts[i-1]
if CurrentPart then
local NewRatio = GetDist(CurrentPart.Node0, CurrentPart.Node1)
i -= 1
a += 1
a *= NewRatio/CurrentRatio
CurrentRatio = NewRatio
else
connection:Disconnect()
break
end
until a >= 0
end
if CurrentPart and Grinding then
AlignPosition.Attachment1.Position = GetNewPosition(CurrentPart, a) + Vector3.new(0, 3, 0)
else
AlignPosition.Enabled = false
wait(2)
Grinding = false
end
end)
end
It doesn’t account for the orientation of the character, although that’s actually pretty simple to figure out with CFrame.lookAt(position, at)
.
RailParts
could be generated by using a parent collection, like a model/folder, calling :GetChildren
on it, and sorting the resulting array by .Name
:
local RailModel = -- rail model here
local RailParts = RailModel:GetChildren()
table.sort(RailParts, function(a, b)
return a.Name < b.Name
end)
You could build a rail in Studio with an algorithm like this:
local lastPart
for _, part in RailParts do
if lastPart then
part.Position = lastPart.Node1.WorldPosition - part.CFrame.Rotation * part.Node0.Position
end
lastPart = part
end
Branch Solution
RailParts
argument anatomy:
local RailParts = {
[RailPart1] = {Part = RailPart1, NextNode = {RailPart2, RailPart3}},
[RailPart2] = {Part = RailPart2, LastNode = {RailPart1}, NextNode = {RailPart4}},
--...
}
RailParts
generation for this system would require a new RailPart anatomy:
- Part - "RailPart"
- Attachment - "Node0"
- Attachment - "Node1"
- Folder - "NextNode"
- ObjectValue - "Value"
- ...
- Folder - "LastNode"
- ObjectValue - "Value"
- ...
Finally made a function for GrindRail here, albeit some logic is missing, which you will have to fill in here, specifically the function GetNewNode
:
local function GrindRail(AlignPosition, RailParts, CurrentPart)
AlignPosition.Attachment1.Position = AlignPosition.Attachment0.WorldPosition
local CurrentNode = RailParts[CurrentPart]
local CurrentPosition = AlignPosition.Attachment0.WorldPosition
local dist1 = CurrentPosition - CurrentPart.Node0.WorldPosition
local dist2 = CurrentPart.Node1.WorldPosition - CurrentPart.Node0.WorldPosition
--alpha between Node0 and Node1
local a = dist2.Unit:Dot(dist1)/dist2.Magnitude
local speed = dist2.Unit:Dot(AlignPosition.Attachment0.Parent.Velocity)
--All variables should be found by now!
local function GetDist(part)
return (part.Node1.WorldPosition - part.Node0.WorldPosition).Magnitude
end
local function GetNewPosition(part, a)
return part.Node0.WorldPosition:Lerp(part.Node1.WorldPosition, a)
end
local function GetNewNode(nodes)
--define custom node-getting logic here
end
AlignPosition.Enabled = true
local connection
local CurrentRatio = GetDist(CurrentNode.Part)
connection = RunService.RenderStepped:Connect(function(dt)
a += dt*speed/CurrentRatio
if a > 1 then
repeat
local nodes = CurrentNode.NextNode
local newPart = GetNewNode(nodes)
if newPart then
CurrentNode = RailParts[newPart]
local NewRatio = GetDist(newPart)
a -= 1
a *= NewRatio/CurrentRatio
CurrentRatio = NewRatio
else
connection:Disconnect()
break
end
until a <= 1
elseif a < 0 then
repeat
local nodes = CurrentNode.LastNode
local newPart = GetNewNode(nodes)
if newPart then
CurrentNode = RailParts[newPart]
local NewRatio = GetDist(newPart)
a += 1
a *= NewRatio/CurrentRatio
CurrentRatio = NewRatio
else
connection:Disconnect()
break
end
until a >= 0
end
if CurrentNode and Grinding then
AlignPosition.Attachment1.Position = GetNewPosition(CurrentNode.Part, a) + Vector3.new(0, 3, 0)
else
AlignPosition.Enabled = false
wait(2)
Grinding = false
end
end)
end
Using this, RailParts
table generation would look like this:
local RailParts = {}
local RailModel = -- rail model here
for _,part in RailModel:GetChildren() do
RailParts[part] = {
Part = part,
NextNode = {},
LastNode = {}
}
for _, object in ipairs(part.NextNode:GetChildren()) do
table.insert(RailParts[part].NextNode, object.Value)
end
for _, object in ipairs(part.LastNode:GetChildren()) do
table.insert(RailParts[part].LastNode, object.Value)
end
end
The new rail generation system would probably be more complicated:
local Closed = {}
local function ConnectNodes(part, parent)
local node = RailParts[part]
part.Position = parent.Node1.WorldPosition + (part.CFrame - part.Position) * part.Node0.Position
Closed[part] = true
if node["NextNode"] then
for _, nextPart in ipairs(node.NextNode) do
if not Closed[nextPart] then
ConnectNodes(nextPart, part)
end
end
end
end
local StartingPart = --part to start with
Closed[StartingPart] = true
ConnectNodes(StartingPart)
Since actually grinding the rail would require much more logic in terms of input, I probably won’t be posting it here unless I have tons of free time, Which honestly, I kind of do…
Note: I have tested the script for simple rail generation, but I have not tested any portion of the branch solution script. This doesn’t have to use AlignPositions
, the HumanoidRootPart
is more than good enough for CFraming.