Rail Grinding System?

So, If you don’t get what I’m saying, basically : x.com

As you can see, the character steps onto the rail and begins “grinding” on the rail and goes quickly down while being aligned with the rail itself.

How would I accomplish this? I’ve heard that someone used dot product and constraints? Any other ways to do it or on how I would do this? I just thought of maybe getting the lookvector of the part then setting the HumanoidRootPart onto of the rail and moving down the rail as an easier solution though I don’t know if it would be more efficent :P.

9 Likes

cc @Suggyiem (if you’re willing to share your method)

I suggest looking into it in other engines to get an idea of how other people do it and then trying to implement similar methods or use the same concept toy make your own solution

The rail is just a set of connected points. Touching the line places the character on the closest position on top of it and activates the animation. As the time passes, the character gets moved downwards with an acceleration proportionate to the line’s inclination.

4 Likes

I remember making a thing similar to this. You had a bunch of points between which the rails run, and at each point, you placed down an attachment facing towards the next point with it’s x-Axis (you can do this with the constructor that uses two points and then rotating the resulting CFrame). You also order the Points from start to finish. You then have a prismatic constraint in the player, that has Attachment1 being an attachment being at the players feet (from the humanoidRootPart). You start by putting the prismatic’s Attachemnt0 to the first attachment of the rail. Then the player needs to start moving towards the next point (automatic if the rail slopes downhill, otherwise give him a push). If the CurrentPosition value of the PrismaticConstraint exceeds the distances between one attachment to the next, then you have basically overstepped the bounds of one rail and so you set the Attachment0 of the prismatic to be the one of the next point.

1 Like

Oh so I would have to use attachments as the guide and the prismatic constraint in the HumanoidRootPart at feet level to move the character? Question is though, would I need dot product and if so, how would I use it to find the Inclination of the rail.

1 Like

You could weld the player to a part, then move it along the track / parts by numbering the parts in order.

2 Likes

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.

24 Likes

I guess it depends on how you want to go about it! I, for one, tackled this issue a bit. x.com
Would you like to go the route of modularity? In the way that rail parts work independently of each other? Or would you go for the more robust approach that would give more consistent results (like the ones above!)
I went for the first option for ease of content creation, but it is far from perfect! For my case, I had to make a rail that’d function with any layout and retain the same momentum going in and full control for the player.
This requires some dot-products, ray casting, pseudo-position prediction, and I used cylindrical restraints! But I think you have the general idea of what to do (plus @goldenstein64 here has a rather beautiful answer for ya) So I won’t put anything like a big answer unless its needed!

3 Likes

You don’t need to find the inclination, since gravity should move the character.
If you need the inclination for some reason though, it would be math.acos(Attachment.WorldAxis1:Dot(Vector3.new(0,1,0))-math.pi/2

2 Likes

Thanks @goldenstein64, @asmanwoks, and @sonihi. I’ll try a combination of these methods. I’ll try using constraints to move the character while also maybe using dot product and branching the paths together.

1 Like

Surprised to see you using an old tweet of mine. Anyways theres many ways one could accomplish a simple railgrinding script.

Though im not going to share my methods because im extremely personal with my scripts. Dont ask why I just am, however I feel as though @goldenstein64 had a pretty good way to do rail grinding, so I would suggest you experiment on the code hes offered you.

See what can be changed around without breaking it.
Try fixing up the orientation of it so that it matches the part properly.

Make sure tweenings a thing for the orientation so he feels smoothed out on the sharp curves etc.

3 Likes

I guess I’ll orient around more on @goldenstein64’s solution, thanks for the tips though!

1 Like