I am making a game where players are on an infinite train ride, and the goal is that there are Landscape/Terrain Sections that move back past the train to create the illusion of the train moving, then after a certain point delete the landscape sections and add more in front to create a sense that the train is infinitely moving forward. Train doesn’t move, landscape does.
Mostly everything is working, but for some reason the landscape sections spawn in gaps after a point, like there’s gaps in-between each section, about the size one of those sections. In my script I have a buffer that pre-spawns landscape sections, and those spawn just fine, but not the ones after.
Screenshot of problem:
the game is a bit foggy, sorry but on the right there are landscape sections that are evenly spaced but after a couple they spawn in gaps, like on the left side.
Heres the script I have for the game:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
-- Constants
local LANDSCAPE_LENGTH = 2048
local SPEED = 150
local SPAWN_BUFFER = 3
local DESPAWN_DISTANCE = (SPAWN_BUFFER + 4) * LANDSCAPE_LENGTH
local RARE_CHANCE = 0.125
local landscapesFolder = ReplicatedStorage:WaitForChild("Landscapes")
local commonFolder = landscapesFolder:WaitForChild("Common")
local rareFolder = landscapesFolder:WaitForChild("Rare")
local activeSections = Instance.new("Folder")
activeSections.Name = "ActiveSections"
activeSections.Parent = Workspace
local trainZ = 0
local sectionPool = {}
local spawnedSections = {}
local lastSpawnedIndex = -1
local function createKillZone()
local part = Instance.new("Part")
part.Name = "KillZone"
part.Anchored = true
part.CanCollide = false
part.Transparency = 1
part.Size = Vector3.new(LANDSCAPE_LENGTH, 16, LANDSCAPE_LENGTH)
part.Position = Vector3.new(0, 0, 0)
return part
end
local function setupKillZone(killZone)
killZone.Touched:Connect(function(hit)
local character = hit:FindFirstAncestorOfClass("Model")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 and not character:FindFirstChild("FellOffTrain") then
local tag = Instance.new("BoolValue")
tag.Name = "FellOffTrain"
tag.Parent = character
local conn
conn = RunService.Heartbeat:Connect(function(dt)
if not character or not character:IsDescendantOf(Workspace) then
conn:Disconnect()
return
end
local root = character.PrimaryPart or character:FindFirstChild("HumanoidRootPart")
if root then
local moveVector = Vector3.new(0, 0, -SPEED * dt)
local newCFrame = CFrame.new(root.Position + moveVector, root.Position + moveVector + root.CFrame.LookVector)
character:SetPrimaryPartCFrame(newCFrame)
else
conn:Disconnect()
end
end)
task.delay(0.001, function()
if humanoid and humanoid.Health > 0 then
humanoid.Health = 0
end
end)
end
end)
end
local function getRandomFromFolder(folder)
local items = folder:GetChildren()
if #items == 0 then return nil end
return items[math.random(1, #items)]:Clone()
end
local function chooseLandscape()
if math.random() < RARE_CHANCE then
return getRandomFromFolder(rareFolder)
else
return getRandomFromFolder(commonFolder)
end
end
local function prepareSection(section)
section.ModelStreamingMode = Enum.ModelStreamingMode.Atomic
for _, part in ipairs(section:GetDescendants()) do
if part:IsA("BasePart") then
part.Anchored = true
end
end
end
local function spawnSection(index)
local zPos = index * LANDSCAPE_LENGTH - LANDSCAPE_LENGTH
local section
if #sectionPool > 0 then
section = table.remove(sectionPool)
else
section = chooseLandscape()
if not section then return end
prepareSection(section)
end
local killZone = createKillZone()
killZone.CFrame = CFrame.new(0, 1, zPos)
killZone.Parent = activeSections
setupKillZone(killZone)
section:PivotTo(CFrame.new(0, 0, zPos))
section.Parent = activeSections
spawnedSections[index] = {
section = section,
killZone = killZone,
}
end
-- Initial spawn
for i = 0, SPAWN_BUFFER + 1 do
spawnSection(i)
lastSpawnedIndex = i
end
RunService.Heartbeat:Connect(function(dt)
local moveDist = SPEED * dt
trainZ = trainZ + moveDist
for index, data in pairs(spawnedSections) do
local section = data.section
local killZone = data.killZone
if section and section:IsA("Model") then
local sectionCFrame = section:GetPivot()
local killZoneCFrame = killZone.CFrame
local newSectionCFrame = sectionCFrame * CFrame.new(0, 0, -moveDist)
section:PivotTo(newSectionCFrame)
local newKillZoneCFrame = killZoneCFrame * CFrame.new(0, 0, -moveDist)
killZone.CFrame = newKillZoneCFrame
local frontEdgeZ = newSectionCFrame.Position.Z + (LANDSCAPE_LENGTH / 2)
if frontEdgeZ < trainZ - DESPAWN_DISTANCE then
section.Parent = nil
killZone.Parent = nil
spawnedSections[index] = nil
table.insert(sectionPool, section)
end
end
end
local maxSpawnIndex = math.floor((trainZ + (SPAWN_BUFFER + 1) * LANDSCAPE_LENGTH) / LANDSCAPE_LENGTH)
while lastSpawnedIndex < maxSpawnIndex do
lastSpawnedIndex = lastSpawnedIndex + 1
spawnSection(lastSpawnedIndex)
end
end)
I have it set up to where the models of the sections are in replicated storage, and when they need to spawn in the script copies them out into the workspace, and after a point it gets deleted. If someone could help me solve the problem of the gaps, that would be much appreciated.