Creating a Drivable Train System

What do I want to achieve?
I’m trying to create a train driving system for a game based around railroading. The problems I’m having specifically relate to the system for driving the trains, however, there are some important elements of the game that may impact what system will work. Players must be able to drive the trains themselves, and trains must be able to be rerouted at switches to other lines. Trains may be just a locomotive or a series of cars coupled together. Ideally, trains would also be able to enter sidings and couple to different train cars, although this is a separate challenge to solve.

What is the issue, and what solutions have I tried so far??
I’ve tried several approaches to making a train, but each have had their own issues. As far as I can tell, there are three approaches I can take to making a train, and I’ve experimented with each. I’ve put some of the problems I’ve had and where I’ve gotten stuck with each one in the below sections.

1. Fully Physically-Based Train

This was the first approach I tried, many years ago when I started experimenting with making stuff in Roblox Studio. This approach involves using a CylindricalConstraint on a wheel with a small rim on the inner portion. I talked about this approach in some more detail in a previous post.

The Problem
While the most successful version of this approach I made can go decently fast with good stability on straight sections of track, it dramatically slows down on even minor curves. I suspect this is due to the inner rim being too close to the rail, getting stuck on curves, but my current attempt to recreate this system in my newer world with a new train and different wheels has been unsuccessful, as the wheels keep getting stuck inside the train for some reason.
Here’s the wheel design:


I’ve also noticed this system is pretty resilient to derailments, which is nice in normal conditions, although I’d like it if they could derail in some situations (such as too fast on a curve), but it’s not a requirement.

What I’m Asking Help For
I think this system would be more complicated than the next approach to implement, and I’ve played many games which have used a similar system where the trains were super buggy due to physics being weird, so I’ve been avoiding this approach for a while. However, if someone has an idea on how to make this work properly, I might reconsider using this approach.

2. Physics-Enabled Train (Gliders) with AssemblyLinearVelocity

This system is the one I’ve spent the most time working on. It still uses physics to stay on the rails by utilizing “gliders” or parts on one or more sides of the rail to keep the train bogeys aligned. As it approaches a curve, the gliders force the bogeys to rotate, which then rotates the train. By applying AssemblyLinearVelocity in the direction the train is facing, I can directly control the trains speed, while making track construction simple and enabling potential derailments in cases such as taking a curve too fast.

This is the portion of the script that controls the train’s movement. Note that TRAIN refers to the floor of the train and that speed is a value I can control as the player. DOWN_FORCE is an additional force applied directly towards ground in an attempt to reduce the train bouncing.

local function moveForward()
	local a = coroutine.wrap(function()
		while trainMoving do
			local trainCFrame = TRAIN.CFrame
			local speed = trainSpeed.Value
			TRAIN.AssemblyLinearVelocity = Vector3.new(trainCFrame.LookVector.X*speed, TRAIN.AssemblyLinearVelocity.Y - DOWN_FORCE, trainCFrame.LookVector.Z*speed)
			RunService.Heartbeat:Wait()
		end
	end)
	a()
end

The Problem
I made a pretty successful version of this system, one that was good enough that I started trying to make a game with it, however, I messed up the scale, which made everything too big compared to the Roblox player. So, I tried again at a proper scale, however, it seems that by making the train and rails smaller, I significant reduced the stability of the system.
Previously, the train could operate at very high speed on straight sections of track with minimal bumps, and curves could be navigated at reasonable speed before a derailment occurred. Now, even traveling in a straight line, I cannot exceed much over 50-70 (I think it’s measured in studs per second) before the train either pops off the rails or the inner guide parts clip through the rail. Additionally, any movement whatsoever is very bumpy, and it seems more extreme when the train is traveling in reverse.

What I’ve Tried
I tried adding more parts to more completely surround the rail, however, any approach results in these parts clipping through the small rail. I also messed with many of the physics properties, such as friction, density, collision groups, etc. (I don’t fully remember everything I tried, as I’ve been working on this on and off for a long time)

I really prefer this approach, it seems like it has the potential to be the most stable, and it allows me to do things like collisions and derailments, but currently it is simply not stable enough for it to be enjoyable to use.

What I’m Asking Help For
I’ve seen a few other posts here with similar issues, but none of them seem to have solutions that are practical for my situation or the solutions didn’t work when I tried them. I’m pretty sure other train games have used gliders at the scale I’m working at successfully, but I don’t know how they made them work properly. I’d love to hear if anyone else has solved this problem or has advice on making the gliders more reliable.

3. CFrame/Tween Train

This is a new approach I am trying out. I have seen many people recommend using CFrame and Tweens to move the trains, and it has some obvious benefits. Most notably, it is the most stable, as you directly control where the train is going, and although derailments would never naturally occur, it could be programmed into it, giving me more control over the system.

I’ve come up with a system for building track (in this case, just nodes, visual tracks would be a separate problem) that would allow me to still utilize switches for the train, and I could even create more advanced bezier curves for the track, which could speed up how quickly I can build switches and track curves.

The Problem
This approach is far more complicated and quite complex mathematically. That’s not to say I couldn’t figure out the complex math involved, but it does pose some real challenges. One of the biggest problems is how will the player control the train. I can modify the time for a Tween movement to be performed, however, I don’t believe the speed can be modified during the animation. Consequently, it wouldn’t be possible to simply tell the train to move from point A to point B for straight segments. I’d have to make many very short Tweens along the entire route from one point to another, and only after each Tween could the speed be updated.
I am worried this approach would be require making too many calculations, leading to significant performance loss or lag. Additionally, even with many short Tweens, I am worried that the trains wouldn’t feel responsive enough due to the wait between the player input and the current animation finishing so the train can respond to the new input. It could cause the train acceleration and deceleration to be very rough.
Additionally, trains are usually made of several connected carriages, each with bogeys that rotate independently from the car. While I’m waiting to work on carriages until I have a locomotive that works properly, this approach also raises significant concerns, as each car or bogey would need to follow the path independently, but also maintain a fixed distance from each other.

What I’ve Tried
As previously mentioned, I’ve made some code to connect points and create bezier curves, and I’ve made some very basic code for moving a train along the path, which works perfectly fine for demonstration purposes. However, to make it player controllable would require a lot of work, which would be a lot of time wasted if the performance would be terrible, so I haven’t gone any further.

Here’s the current movement code, although it’s a mess, so I apologize in advance:

local function movePath()
	local tweenTime = 1
	local tweenInfo = TweenInfo.new(tweenTime, Enum.EasingStyle.Linear, Enum.EasingDirection.In)
	
	-- For each node in the current test path I made
	for i = 1, 10 + 2, 1 do
		while currentPart.NextPart.Value == nil do
			task.wait()
		end
		
		currentPart = currentPart.NextPart.Value
		
		local tween = TweenService:Create(Model, tweenInfo, {CFrame =  CFrame.lookAt(currentPart.Position, currentPart.NextPart.Value.Position)})
		tween:Play()
		task.wait(tweenTime)
	end
end

What I’m Asking Help For
I’d like to hear if anyone else has tried anything like this so I don’t go wasting my time pursuing an approach that might already be known to be too performance intensive or just not practical for the type of game I’d like to make. I think if it could work, using CFrames and Tweens could work well.

Fully Physical-Based Train Video

Physics-Enabled Train with AssemblyLinearVelocity Video

CFrame/Tween Train Video

I’ve also heard about making trains using prismatic constraints, which I don’t believe would work for this situation, but I’d be open to ideas on how to utilize prismatic constraints for trains if they would be able to do all the things I’m looking for.

What am I looking for?
Simply put, I would like some advice on either how to fix some of the issues with these train systems and/or advice on which one is best suited for the type of game I am trying to make so I don’t waste my time trying to make something work which never will. I appreciate any advice you may have to offer.

5 Likes

About the CFrame approach, it is definitely more complicated mathematically (and I would suggest against using tweens, in favor of manually updating the position of the train along the track every frame, moving it forward by speed * dt). It gets even more complicated when handling carriages, what has worked best in my case is setting the position of the train wheels to be behind the leading wheel by a set amount, and set the position of the carriage to be in between the wheels (This does cause the wheels to move relative to the carriage on sharp turns, but I ignored this issue)

The main downside of the CFrame approach is complexity. Responsiveness will depend on how you implement it, and a good implementation will have very good responsiveness. Performance is actually a benefit. While it does involve a lot of calculations, none of them are particularly expensive (the most expensive will be CFrame manipulations). From experience, performance issues on roblox usually stem from using Roblox objects (for a train, it would be setting the CFrame of the train, see the links below for how to optimize this). I don’t know exactly the cost of the physics engine, but it is likely much more important than the CFrame approach

General information about making a CFramed Train
Roblox place file with a CFramed train system I made
Approach idea for handing switches

I will let others talk about physical approaches as that is not an approach I have even tried

4 Likes

Updating the position of the train every frame for the CFrame approach was something I was thinking about doing, but I was a bit worried about carriages and performance…

I’ve spent the last couple of days working on trying to get CFrame movement working, but I’m still experiencing several issues. Moving straight from point to point was relatively easy, however, the moment I add a curve, chaos ensues.

I’ve been trying to get a train to navigate bezier curves, and it never seems to work properly. Unlike straight lines, where one can calculate the distance needed to travel by speed * deltaTime and moving the train forward that much, curves require knowing which direction to face and the path to travel.

The simplest approach I can see here is precalculating several points along the curve and simply moving the train straight to each one. However, when I attempted this with only a few points, it was quite visually obvious when the train passed a node and suddenly rotated, which looked terrible. Increasing the number of points did reduce this, however, it caused the train to slow down significantly and the visual stuttering was not completely eliminated.

I then tried using a modified version of the approach described in this DevForum post, where the next position the train should be at is calculated as a position on the bezier curve. However, I noticed that the train would move despite me not adding speed * dt or utilizing the progress system the DevForum post used, and the train’s speed was significant faster in the middle of the curve than the ends.

My current approach is based on the code from Roblox’s old Bezier curve documentation on Arc-Length Parameterization, in an attempt to get the train to move at a fixed speed I can control. The current issue (which has also been an issue in some previous attempts) is that the train gets stuck and constantly flips around a single point, instead of moving on to the next point. It’s also not entirely clear how I would implement traveling at a fixed speed with this code, as it expects a time input, not a distance input. Although, I might be able to do something like distanceTraveled/totalDistance.

CFrame Movement Code

local deltaTime = 0

-- Simplified version of the train's movement code on curves.
while true do
	-- Calculate train's next position
	local nextPosition = bezierCurve.fixed(10, deltaTime, bezierCurve.quad, prevNode.Position, controlNode.Position, nextNode.Position)
	-- Calculate CFrame for train's next position, including rotation
	local newCFrame = CFrame.new(nextPosition, nextPosition + CFrame.lookAt(Train.CFrame.Position, nextPosition).LookVector)
	Train.CFrame = newCFrame -- Sets train's position and rotation to new CFrame
	deltaTime = task.wait() -- Ensures code runs ASAP and provides the waiting time to the code to determine speed
end

Arc-Length Parameterization Code

function module.length(n: number, func, ...)
	local totalDistance = 0
	local ranges = {}
	local distances = {}

	for i = 0, n - 1 do
		local p1 = func(i/n, ...) -- Calculate the current point
		local p2 = func((i+1)/n, ...) -- Calculate the next point

		local distance = (p2 - p1).Magnitude -- Get the distance between them
		
		ranges[totalDistance] = {distance, p1, p2}
		
		table.insert(distances, distance)

		totalDistance = totalDistance + distance
	end

	return totalDistance, ranges, distances
end

function module.fixed(n: number, t: number, func, ...)

	-- Gather values from length function
	local length, ranges, distances = module.length(n, func, ...)

	-- Get distance along the length
	local S = t * length
	local near = 0

	-- Get the nearest point calculated
	for _, n in next, distances do
		if (S - n) < 0 then
			break
		end

		near = n
	end

	local set = ranges[near]

	-- Linearly interpolate between that point and its neighbor
	local percent = (S - near)/set[1]
	return module.linear(percent, set[2], set[3])
end
3 Likes

How does your bezier curve module work? In my own code, I used this function I wrote

local function Bezier(Start, Control, Goal, Alpha)
	local CF1 = Start:Lerp(Control, Alpha)
	local CF2 = Control:Lerp(Goal, Alpha)
	return CF1:Lerp(CF2, Alpha)
end

This is a 3 point bezier curbe, which returns a CFrame calculated with lerps (given Start, Control, and Goal, are CFrames), like this

I assume this function should be close to your bezierCurve.quad function, but you are using Position instead of CFrame (using CFrames here is really nice as the rotation of the wheels is dealt with very easily. The rotation of the control points become important)

The module.fixed function is, suboptimal, because it divides the bezier curve into multiple points, and does linear interpolation (aka lerp) between those, to return the position. That’s a lot more work than just using the bezier curve function

There is also an issue in your code, you are sending deltaTime to the bezierCurve.fixed, while it should instead be an alpha value, (from 0 to 1, 0 is the beginning, 1 is the end of the curve), which can be calculated by keeping track of where the train is along the track

-- On each frame, add by how much the train move
local TotalLength = 10 -- I will get into how to calculate this later
local CurrentDistance = 0

while true do
	
	local dt = task.wait()
	CurrentDistance += Speed*dt

	local alpha = math.clamp(CurrentDistance/TotalLength, 0, 1) -- I like to clamp it
end

The t argument is not time, it’s an alpha value (I like to use a instead of t, less confusing), what gives it up is this line

-- Get distance along the length
local S = t * length

Which makes it obvious t is a value between 0 and 1


Now, how to calculate the length of a bezier curve. The annoying thing is that the best way to do so is the brute force approach, aka, separate the bezier curve into a lot of points, and calculate the distance between all those points. The nice thing is that, this step can be done once (when the server starts for example), and then never again, so performance is not a concern. The more points you divide the curve into, the more precise the length is (the length doesn’t have to be that precise, it just leads to small inaccuracies in the speed of the train)

In the code you are using, that’s what the module.length function does (although only the first return value is of interest to me, the rest is used for the suboptimal linear interpolation). How you’ve set it up, it divides the curve into 10 points, and calculates the length between each (I would probably do more than 10 points though). However, this implementation calculates the length every time you want to get the position along the curve, which is unnecessary


Odd


Hope this helps. If you have any more questions or want more code examples, I can provide some

3 Likes

It sounds like I had the right idea originally to directly use the bezierCurve.quad function (which is the same as the code you shared above except I was calculating with CFrame.Position and not CFrame itself) instead of bezierCurve.fixed. I had switched approaches because I was having issues the train not navigating the curve properly as you referenced at the end of your reply. I also suspected that the t value was supposed to be alpha based on the way it was used in the code, but the way it was described in the documentation was as a time value for some reason.

I updated the code to the following (provided in case anyone else reads this thread and wonders how it works) and the train now navigates the curve at a controlled speed.

local deltaTime = 0
local progress = 0

-- Simplified version of the train's movement code on curves.
while progress < distance do
	local currentMovement = (speed * deltaTime)
	local distance = curveDistance --curveDistance is pre-calculated elsewhere in the code
	progress += currentMovement
	local alpha = math.clamp(progress / distance, 0, 1)

	-- Calculate train's next position
	local nextPosition = bezierCurve.quad(alpha, prevNode.Position, controlNode.Position, nextNode.Position)
	-- Calculate CFrame for train's next position, including rotation
	local newCFrame = CFrame.new(nextPosition, nextPosition + CFrame.lookAt(Train.CFrame.Position, nextPosition).LookVector)
	Train.CFrame = newCFrame -- Sets train's position and rotation to new CFrame
	deltaTime = task.wait() -- Ensures code runs ASAP and provides the waiting time to the code to determine speed
end

I had tried to implement something similar before, but I had been trying a lot of things and might have messed up the implementation.

Thanks for helping me get this working!

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.