Need help stop train stuttering movement?

Hello. Quick question, So I have a train here, I just got it from toolbox. It works fine and everything. I have a train system to make it move through the map.

So today It’s been working fine, its all smooth cuz’ I made the train system client sided. But it is almost smooth until I noticed that there are stuttering movement between the train cars. When they are turning through the curve rails. I do not understand why it does this, it’s supposed to only stutter when its moving on the server but no, I already made my train client sided. I don’t know what’s causing the stuttering on the train car movement.

This is what happens when the train cars are turning on the curve:
https://streamable.com/9rxupr << Click that link to watch the vid

As you can see, when the train cars are turning through the curves, you can notice a stuttering movement in each of the train cars, including the front train cars. I don’t know what to do to smooth it out. You got any ideas to smooth out this entire train movement? I will be showing the code for the train system:

This code is a module. It’s in a module script required in ReplicatedStorage by a client script:

local trainModule = {}
math.randomseed(tick())
-- \\ Settings // --
local waitBeforeSpawn = 2.5 + math.random()
local trainSpawnTime = 30 -- Cooldown of train spawning
local rearFacingTrain = true
local minCarriages = 5
local maxCarriages = 8
local loop = true
local stop = false
local speed = script.Parent.Speed
--------------------
local serverStorage = game:GetService("ReplicatedStorage")
local trains = serverStorage.TrainObjects.Trains
local carriages = serverStorage.TrainObjects.Carriages
local train = trains.Train
local workspaceTrains = workspace.Trains
local connections = {}
local runService = game:GetService("RunService")


function trainModule:Sounds(Train)
	print("SOUNDS")
	Train.PrimaryPart.Engine:Play()
	Train.PrimaryPart.Horn:Play()
	Train.PrimaryPart.Bell:Play()
end

function trainModule:GetTime(distance, speed)
	return distance / speed
end

function trainModule:MoveModel(model,start,End,AddBy)
    for i = 0, 1, AddBy do
		if not model.PrimaryPart then break end
        model:SetPrimaryPartCFrame(start:Lerp(End,i))
		runService.Heartbeat:Wait()
    end
end


function trainModule:MoveCarriages()
	for i, v in pairs(self.Carriages) do
		coroutine.wrap(function()
			self:MoveCarriage(v, self.Node)
		end)()
	end
end

function trainModule:MoveCarriage(carriage, nodeNum)
	local node = self.Nodes:FindFirstChild("Node"..nodeNum)
	if not node then carriage:Destroy() return end
	
	local cf1 = node.CFrame * CFrame.new(0, carriage.PrimaryPart.Size.Y / 2, 0)
	local cf2 = carriage.PrimaryPart.CFrame
	local distance = (cf1.p - cf2.p).Magnitude
	local Time = self:GetTime(distance, speed.Value)
	local addBy = 1 / Time
	
	self:MoveModel(carriage, carriage.PrimaryPart.CFrame, cf1, addBy)
	self:MoveCarriage(carriage, nodeNum + 1)
end

function trainModule:Move(nodeNum)
	local node = self.Nodes:FindFirstChild("Node"..nodeNum)
	if not node then self.Train:Destroy() return end
	
	if nodeNum == 1 then
		self:MoveCarriages()
	end
	
	
	local cf1 = node.CFrame * CFrame.new(0, self.Train.PrimaryPart.Size.Y / 2, 0)
	local cf2 = self.Train.PrimaryPart.CFrame
	local distance = (cf1.p - cf2.p).Magnitude
	local Time = self:GetTime(distance, speed.Value)
	local addBy = 1 / Time

	--if nodeNum == 4 then stop = true if stop == true then return end wait(5) print("SHYDytasfd") end	

	self:MoveModel(self.Train, self.Train.PrimaryPart.CFrame, cf1, addBy)
	self:Move(nodeNum + 1)
end

function trainModule:Spawn()
	self.Train = train:Clone()
	self.Train.Parent = workspaceTrains
	self.Nodes = workspace.Nodes
	self.Node = 1
	self.Carriages = {}
	for i, v in pairs(connections) do
		v:Disconnect()
	end
	local node = self.Nodes:FindFirstChild("Node"..self.Node)
	if not node then error("Could not find Node"..self.Node) return end
	
	for i, v in pairs(self.Train:GetDescendants()) do
		if v:IsA("BasePart") then
			if v ~= self.Train.PrimaryPart then
				local Weld = Instance.new("Weld")
				Weld.Part0 = v
				Weld.Part1 = self.Train.PrimaryPart
				Weld.C0 = v.CFrame:inverse()
				Weld.C1 = self.Train.PrimaryPart.CFrame:inverse()
				Weld.Parent = self.Train.PrimaryPart
			end
		end
	end
	self.Train:SetPrimaryPartCFrame(node.CFrame * CFrame.new(0, self.Train.PrimaryPart.Size.Y / 2, 0))
	self.LastCarriage = self.Train
	self:Sounds(self.Train)
	
	if rearFacingTrain then
		self.BackWardsTrain = train.RearTrain.Value:Clone()
		self.BackWardsTrain.Parent = workspaceTrains
		
		for i, v in pairs(self.BackWardsTrain:GetDescendants()) do
			if v:IsA("BasePart") then
				if v ~= self.BackWardsTrain.PrimaryPart then
					local Weld = Instance.new("Weld")
					Weld.Part0 = v
					Weld.Part1 = self.BackWardsTrain.PrimaryPart
					Weld.C0 = v.CFrame:inverse()
					Weld.C1 = self.BackWardsTrain.PrimaryPart.CFrame:inverse()
					Weld.Parent = self.BackWardsTrain.PrimaryPart
				end
			end
		end
		
		self.BackWardsTrain:SetPrimaryPartCFrame(self.Train.PrimaryPart.CFrame * CFrame.new(0, -(self.Train.PrimaryPart.Size.Y - self.BackWardsTrain.PrimaryPart.Size.Y) / 2, (self.Train.PrimaryPart.Size.Z / 1.90) + (self.BackWardsTrain.PrimaryPart.Size.Z / 1.90)))
		self.LastCarriage = self.BackWardsTrain
		table.insert(self.Carriages, self.BackWardsTrain)
	end
	local availibleCarriages = {}
	for i, v in pairs(carriages:GetChildren()) do
		if v.Train.Value == train then
			table.insert(availibleCarriages, v)
		end
	end
	local carriagesToMake = math.random(minCarriages, maxCarriages)
	
	for i = 1, carriagesToMake do
		self.CurrentCarriage = availibleCarriages[math.random(1, #availibleCarriages)]:Clone()
		self.CurrentCarriage.Parent = workspaceTrains
		
		for i, v in pairs(self.CurrentCarriage:GetDescendants()) do
			if v:IsA("BasePart") then
				if v ~= self.CurrentCarriage.PrimaryPart then
					local Weld = Instance.new("Weld")
					Weld.Part0 = v
					Weld.Part1 = self.CurrentCarriage.PrimaryPart
					Weld.C0 = v.CFrame:inverse()
					Weld.C1 = self.CurrentCarriage.PrimaryPart.CFrame:inverse()
					Weld.Parent = self.CurrentCarriage.PrimaryPart
				end
			end
		end
		
		self.CurrentCarriage:SetPrimaryPartCFrame(self.LastCarriage.PrimaryPart.CFrame * CFrame.new(0, -(self.LastCarriage.PrimaryPart.Size.Y - self.CurrentCarriage.PrimaryPart.Size.Y) / 2, (self.LastCarriage.PrimaryPart.Size.Z / 1.92) + (self.CurrentCarriage.PrimaryPart.Size.Z / 1.92)))

		self.LastCarriage = self.CurrentCarriage
		table.insert(self.Carriages, self.CurrentCarriage)
	end

	if self.Train:FindFirstChild("KillerBrick") then
		local killConnection = self.Train:FindFirstChild("KillerBrick").Touched:Connect(function(hit)
			if hit.Parent:FindFirstChild("Humanoid") then
				hit.Parent:FindFirstChild("Humanoid").Health = 0
			elseif hit.Parent.Parent:FindFirstChild("Humanoid") then
				hit.Parent.Parent:FindFirstChild("Humanoid").Health = 0
			end
		end)
	end
	self:Move(self.Node)
	wait(trainSpawnTime)
	if loop then
		self:Spawn()
	else
		return
	end
end

function trainModule:Start()
	wait(waitBeforeSpawn)
	print("Train Packet Creation")
	for i, v in pairs(workspace.Nodes:GetDescendants()) do
		if v:IsA("BasePart") then
			v.Transparency = 1
		end
	end
	self:Spawn()
end


return trainModule

This script will use a remoteEvent in order to replicate for everyone. It will use a remoteEvent and fire it to all clients with a string value acting as a “Request Type” for security:

wait(5)
print("Remote event fired!")
game.ReplicatedStorage.CreateTrain:FireAllClients({reason = "SpawnTrain"}) -- this table contains a string value. The string value acts as a "Request Type" so when a script picks it up, it will check if the request is valid, otherwise it will not pickup.

The client script will pickup the event:

local ReplicatedStorage = game.ReplicatedStorage

ReplicatedStorage.CreateTrain.OnClientEvent:Connect(function(RequestType)
	if RequestType.reason == "SpawnTrain" then
		for i, v in next, game.ReplicatedStorage.TrainSystem.Modules:GetChildren() do
			if v:IsA("ModuleScript") then
				local thread = coroutine.create(function()
					local module = require(v)
					module:Start()
				end)
				coroutine.resume(thread)
			end
		end
	else
		print("Invalid request")
	end
end)

EXTRA DETAILS
I use strings on remote events just for organized stuff and security as well. So the scripts that will be picking up events will not just accept it unless the reason for firing an event is valid.

So that’s all about it. If you got any more questions, ask me below. Got any ideas, suggestions, examples, anything to share to me? tell me in the replies below

All help is appreciated!
Thanks in advance :slight_smile:

2 Likes

It’s possible that, if the train is a physics object, it could be a conflict of network ownership. Probably not, but worth a shot, right?

Try setting it to nil and see if that works.

1 Like

@Tommytherox The train is actually not using roblox’s physics like BodyMovers. The train is actaully cframed if you looked through the entire module code, you will see that the train is being cframed.

1 Like

@Tommytherox I just read the Network Ownership info from the link you sent me. So I cannot set the network owner ship of the train cars to nil since their primarypart is anchored, the rest are unanchored. The primarypart moving the train is only part that it anchored. Can’t move/lerp through the rails properly if the primarypart moving the train cars are unanchored.

Its been weeks and there’s still no answer :slightly_frowning_face:

1 Like