Inf Train Track Script

I’m having some issues with a train track system I’m working on in Roblox Studio and could use some guidance.

The problem:

  1. The tracks do not despawn after a certain distance, which is causing performance issues.
  2. The tracks sometimes overlap on top of each other instead of staying neatly in a line.
  3. The tracks do not stay on the floor (Y = 0.75), so they sometimes float above or sink below the intended height.

I’ve tried setting up a pool system and moving the tracks backward to simulate train movement, but these issues persist. I want the tracks to:

  • Stay at a fixed floor height (Y = 0.75)
  • Do not overlap
  • Properly despawn once out of view or after a certain distance

Does anyone have suggestions on how to make a robust system for infinite or long train tracks that fixes these problems?

Any help would be greatly appreciated!

I use two scripts in ServerScriptService a module and a script

Module Script

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local TrackManager = {}
TrackManager.__index = TrackManager

-- UTILITIES
local function getViewerRoot(viewerOverride)
	if viewerOverride and viewerOverride:IsA("BasePart") then
		return viewerOverride
	elseif viewerOverride and viewerOverride:IsA("Model") and viewerOverride.PrimaryPart then
		return viewerOverride.PrimaryPart
	end

	local t = workspace:FindFirstChild("Train")
	if t and t.PrimaryPart then return t.PrimaryPart end

	for _,p in ipairs(Players:GetPlayers()) do
		if p.Character and p.Character:FindFirstChild("HumanoidRootPart") then
			return p.Character.HumanoidRootPart
		end
	end
	return nil
end

local function safeClone(model)
	local clone = model:Clone()
	for _,d in ipairs(clone:GetDescendants()) do
		if d:IsA("Script") or d:IsA("LocalScript") then
			d:Destroy()
		end
	end
	return clone
end

-- CONSTRUCTOR
function TrackManager.new(config)
	local self = setmetatable({}, TrackManager)

	self.templateModel = config.templateModel
	self.offset = config.offsetDistance or 109.5
	self.poolSize = config.poolSize or 50
	self.moveSpeed = config.moveSpeed or 80
	self.despawnDistance = config.despawnDistance or 400
	self.segmentsAhead = config.segmentsAhead or 20
	self.segmentsBehind = config.segmentsBehind or 10
	self.viewerOverride = config.viewerOverride or nil
	self.startPosition = config.startPosition or Vector3.new(44.022,0.75,-237.25)
	self.maxDistance = config.maxDistance or math.huge -- stop spawning after this distance

	-- TRACK DIRECTION: +Z
	self.forward = Vector3.new(0,0,1)

	-- pool/container
	self._container = Instance.new("Folder")
	self._container.Name = "TrackSegments"
	self._container.Parent = workspace

	self._pool = {}
	self._active = {}
	self._running = false
	self._connection = nil
	self._lastSpawnDistance = 0

	for i=1,self.poolSize do
		local seg = safeClone(self.templateModel)
		seg.Parent = self._container
		seg:MoveTo(Vector3.new(0,-5000,0))
		table.insert(self._pool, seg)
	end

	return self
end

-- POOL
function TrackManager:_acquire()
	if #self._pool>0 then
		return table.remove(self._pool)
	end
	local seg = safeClone(self.templateModel)
	seg.Parent = self._container
	return seg
end

function TrackManager:_release(seg)
	if not seg then return end
	seg:MoveTo(Vector3.new(0,-5000,0))
	table.insert(self._pool, seg)
end

-- SPAWN SEGMENT AT POSITION
function TrackManager:_spawnAt(pos)
	local seg = self:_acquire()
	seg.Parent = workspace
	pos = Vector3.new(pos.X, 0.75, pos.Z) -- ensure floor height
	if seg.PrimaryPart then
		seg:SetPrimaryPartCFrame(CFrame.new(pos, pos + self.forward))
	else
		seg:MoveTo(pos)
	end
	return seg
end

-- INITIAL POPULATE
function TrackManager:_initialPopulate()
	for _,seg in ipairs(self._active) do
		self:_release(seg)
	end
	self._active = {}

	for i = 0, self.segmentsAhead do
		local pos = self.startPosition + self.forward * (i * self.offset)
		local seg = self:_spawnAt(pos)
		table.insert(self._active, seg)
		self._lastSpawnDistance = i * self.offset
	end
end

-- START LOOP
function TrackManager:Start()
	if self._running then return end
	self._running = true
	self:_initialPopulate()

	self._connection = RunService.Heartbeat:Connect(function(dt)
		if not self._running then return end

		-- move segments backward to simulate train
		local moveVec = -self.forward * (self.moveSpeed * dt)
		for _,seg in ipairs(self._active) do
			if seg.PrimaryPart then
				seg:SetPrimaryPartCFrame(seg.PrimaryPart.CFrame + moveVec)
			else
				seg:MoveTo(seg:GetModelCFrame().p + moveVec)
			end
		end

		local viewer = getViewerRoot(self.viewerOverride)
		local vpos = viewer and viewer.Position or self.startPosition

		-- despawn segments behind
		for i=#self._active,1,-1 do
			local seg = self._active[i]
			local pd = (seg.PrimaryPart.Position - vpos):Dot(self.forward)
			if pd < -self.despawnDistance then
				table.remove(self._active,i)
				self:_release(seg)
			end
		end

		-- spawn ahead until max distance
		while self._lastSpawnDistance < self.maxDistance do
			local spawnDist = self._lastSpawnDistance + self.offset
			local pos = self.startPosition + self.forward * spawnDist
			local seg = self:_spawnAt(pos)
			table.insert(self._active, seg)
			self._lastSpawnDistance = spawnDist
		end
	end)
end

function TrackManager:Stop()
	if not self._running then return end
	self._running = false
	if self._connection then
		self._connection:Disconnect()
		self._connection = nil
	end
	for _,seg in ipairs(self._active) do
		self:_release(seg)
	end
	self._active = {}
end

return TrackManager

Script

local TrackManager = require(game.ServerScriptService:WaitForChild("TrackManager"))

local template = workspace:FindFirstChild("Track")
if not template then
	error("Place your Track model in Workspace and name it 'Track'")
end

local config = {
	templateModel = template,
	offsetDistance = 109.5,
	poolSize = 80,
	segmentsAhead = 25,
	segmentsBehind = 10,
	moveSpeed = 50,
	despawnDistance = 350,
	viewerOverride = workspace:FindFirstChild("Train"), -- optional
	startPosition = Vector3.new(44.022,0.75,-237.25),
	maxDistance = 100 -- stops spawning after this distance (adjust as needed)
}

local mgr = TrackManager.new(config)
mgr:Start()
print("Track system started")

If there is a better way on doing this please let me know!

https://www.youtube.com/watch?v=0f_f3aZlYYA

1 Like

wrong category, should be in scripting help

2 Likes

I ended up solving the issue!
I rewrote my system and removed the pooling setup, which fixed the main problem. The height issue was resolved by making the parts non-collidable.

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