I’m having some issues with a train track system I’m working on in Roblox Studio and could use some guidance.
The problem:
- The tracks do not despawn after a certain distance, which is causing performance issues.
- The tracks sometimes overlap on top of each other instead of staying neatly in a line.
- 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!