How to move a train without LinearVelocity but nodes?

Hello. I asked AI what methods of train movement in Roblox are, and I decided to use node-based system (good choice for big games), I mean bogie moves from one CFrame to another one. How to implement this? I’m just a beginner, as I can only use pure physics or LinearVelocity + Gliders.

1 Like

If by “node based” you mean basically tweening between nodes of a spline path, this could work for simple track geometry, like a subway with mostly straight path or simple curves, but A) You’d still want to use physics constraints like AlignPosition and AlignOrientation to apply the CFrames to the models to get client-side interpolation, and B) Most Roblox games with trains don’t do this, because they want to make more use of the physics engine to get realistic behavior. CFraming, even if it uses a physics constraint, is basically a kinematic system, so if you want realistic acceleration, traction, momentum, etc.. you have to code it all yourself.

Most Roblox train games model the train physically, like with actual driven wheels putting the power down through friction with the rails. You can make a “glider” where the wheels are essentially cosmetic effects, and your train is pushed along by a velocity or force constraint, but then you’re still back to having to implement more physics on your own for all the acceleration, braking, etc. It is an option though.

Most of the time that Roblox trains have a part that’s gliding, it’s basically just being used as an extra constraint mechanism to keep the train from derailing. If you model train wheels the way it’s done in real life, with a flange keeping the wheel on the rail, it can still be pretty easy for trains to derail because collisions in Roblox are not continuous and a thin flange can easily glitch through a thin rail (especially on curves). So instead, you have a much larger invisible “pin” part that runs in a low-friction channel, basically like a slot car (and some monorails).

Okay, I asked AI to write a code of this and I get this (it works):

-- CODE BY GOOGLE GEMINI. REMEMBER THAT IT CAN MAKE MISTAKES.

local RunService = game:GetService("RunService")

-- ====================================================================
-- NODES
-- ====================================================================
local nodes: {Vector3} = {
	Vector3.new(0, 5, 0),
	Vector3.new(0, 5, 150),
	Vector3.new(50, 5, 250),
	Vector3.new(150, 5, 300),
	Vector3.new(300, 5, 300)
}

-- Precomputation of segment lengths for perfomance
local segmentLengths: {number} = {}
local totalTrackLength = 0

for i = 1, #nodes - 1 do
	local len = (nodes[i+1] - nodes[i]).Magnitude
	segmentLengths[i] = len
	totalTrackLength = totalTrackLength + len
end

-- ====================================================================
-- Get Position At Distance
-- ====================================================================
local function getPositionAtDistance(targetDist: number): Vector3
	targetDist = math.clamp(targetDist, 0, totalTrackLength)
	
	local accumulated = 0
	for i = 1, #segmentLengths do
		local segLen = segmentLengths[i]
		if accumulated + segLen >= targetDist then
			local remainder = targetDist - accumulated
			local alpha = remainder / segLen
			return nodes[i]:Lerp(nodes[i+1], alpha)
		end
		accumulated = accumulated + segLen
	end
	
	return nodes[#nodes] --AI told me this line is for safety reasons if the table returns nil
end

-- ====================================================================
-- TRAIN CONFIG AND HEARTBEAT
-- ====================================================================
local trainModel = workspace:WaitForChild("TrainCarriage") :: Model

local wheelbase = 16          -- Distance between the centers of the bogies (in studs)
local currentDistance = 20    -- Start position in studa
local speed = 40              -- Speed in studs per second

RunService.Heartbeat:Connect(function(dt: number)
	if totalTrackLength == 0 then return end

	-- Distance update based on speed and time since last frame
	currentDistance = math.clamp(currentDistance + speed * dt, wheelbase / 2, totalTrackLength - wheelbase / 2)

	-- 1. Calculation of the front and rear carriage positions
	local frontBogiePos = getPositionAtDistance(currentDistance + (wheelbase / 2))
	local rearBogiePos  = getPositionAtDistance(currentDistance - (wheelbase / 2))

	-- 2. The center of the wagon and its orientation
	local carriageCenter = (frontBogiePos + rearBogiePos) / 2
	local carriageCFrame = CFrame.lookAt(carriageCenter, frontBogiePos)

	-- 3. Updating the model's position relative to the world
	trainModel:PivotTo(carriageCFrame)
end)

And did you mean CFrame interpolation?

The interpolation I was referring to is the interpolation of replicated CFrames from the server (at roughly 20 Hz) to full render framerate on your client. Roblox automatically handles smoothing out the updates of parts that are physically simulated. If you just set CFrames of parts directly from server code, the movement looks choppy and irregular on clients, because it’s just doing 20 fps movement, and not even respecting real time, it just updates the parts when the packets arrive at your client, so you can visually see the irregularity in your ping show up in the movement of the parts.

If you use the CFrame as target values for constraints, like AlignPosition, AlignOrientation, or the older BodyPosition and BodyGyro, it’s all nicely smoothed out for you to full framerate.

As for the generated code: The node-based system will work. Because you’re modelling the train cars as having separate front and rear wheel bogies, you can actually get away with pretty sparse node spacing on curves. Even though the whole path is just straight line segments with only C0 continuity, the fact that the bogies can be on adjacent segments gives the overall center of mass of the train car a curved path, a quadratic Bezier curve. The segment that connects the bogies is basically doing one step of De Casteljau’s algorithm (not exactly, if the track segments vary in length, but the overall effect is the same, a curve that hides the discontinuous change of track direction).
An animation of a quadratic Bézier curve shows two layers of pairwise linear interpolation between the curve's three points and then the interpolated points, with the second interpolation finding the point on the curve.