How I can improve this map generation system?

Already, this terrain system is very optimized and fast. But when it comes to generating trees and structures, thats when the slowness and lag starts to come.

I am not sure why, but at the start of loops for placing plants and structures, you get a lag spike. And also I want a way to speed up going through all the available structures without waiting like 10 seconds for structures that aren’t even going to be in the map most of the time.

I’ve attempted to do this my self by adding waits or even my “Advanced wait” functions to it. But its still slow and laggy when getting to the models.

I’d also want to overall improve this system as well.

Some help would be greatly appreciated

local TerrainTemplatesModule = require(script:WaitForChild("TerrainTemplates"))

local Terrain = workspace:WaitForChild("Terrain")

local ServerStorage = game:GetService("ServerStorage")
local Structures = ServerStorage:WaitForChild("Structures")

local PlantsWorkspaceFolder = workspace:WaitForChild("Plants")
local StructuresWorkspaceFolder = workspace:WaitForChild("Structures")
local SpawnPointsWorkspaceFolder = workspace:WaitForChild("SpawnPoints")

local ChosenTerrain = TerrainTemplatesModule.Snowy

local NoiseScale = ChosenTerrain.NoiseScale
local NoiseHeightLevel = ChosenTerrain.NoiseHeightLevel
local ExtraMapHeight = ChosenTerrain.ExtraMapHeight
local WaterEnabled = ChosenTerrain.WaterEnabled
local CavesEnabled = ChosenTerrain.CavesEnabled
local PlantsChance = ChosenTerrain.PlantsChance
local Plants = ChosenTerrain.Plants

local PartSize = 10

local MapSize = 100
local MapLengthY = 200

local WaterThickness = 20

local CaveInterval = 4
local CaveSize = 15
local CaveLength = 100
local Caves = 50

local Seed = math.random(-50000, 50000)
--local Seed = 8000

local PerlinNoiseAPI = require(script:WaitForChild("PerlinNoise"))
-- PerlinNoiseAPI.new(coords,amplitude,octaves,persistence)

print("Seed: " .. Seed)

local GenerationTimer = os.time()

local function CreateWaterPart(PosY)
	local WaterPart = Instance.new("Part")
	WaterPart.Color = Color3.fromRGB(0, 179, 255)
	WaterPart.Material = Enum.Material.Granite
	WaterPart.Transparency = 0.8
	WaterPart.Size = Vector3.new(MapSize * PartSize - 10, MapLengthY/2 + PartSize + WaterThickness, MapSize * PartSize - 10)
	WaterPart.Position = Vector3.new((MapSize * PartSize) / 2, (PosY + (MapSize / 2) - (WaterThickness / 2)) / PartSize, (MapSize * PartSize) / 2)
	return WaterPart
end

local WaitCounter = 0
local function AdvancedWait(WaitTime)
	if WaitCounter >= WaitTime then
		WaitCounter = 0
		wait()
	else
		WaitCounter = WaitCounter + 1
	end
end

if WaterEnabled then -- Generate water
	local WaterPart = CreateWaterPart(-30)
	Terrain:FillBlock(WaterPart.CFrame, WaterPart.Size, Enum.Material.Water)
end

for x = 1, MapSize do -- Generate perlin noise terrain
	wait()
	for z = 1, MapSize do
		local Height = PerlinNoiseAPI.new({x / NoiseScale, z / NoiseScale, Seed}, 10, 50, 0.5) * NoiseHeightLevel
		
		local Part = Instance.new("Part")
		Part.Size = Vector3.new(PartSize + 3, MapLengthY + PartSize, PartSize + 3)
		Part.TopSurface = Enum.SurfaceType.Smooth
		Part.BottomSurface = Enum.SurfaceType.Smooth
		--Part.Color = Color3.new(Height, Height, Height)
		Part.Position = Vector3.new(x, Height + ExtraMapHeight, z) * Vector3.new(PartSize, PartSize, PartSize)
		Part.Anchored = true
		Part.Parent = workspace
		
		if Part.Position.Y > 150 then -- snow
			Terrain:FillBlock(Part.CFrame, Part.Size, Enum.Material.Snow)
		elseif Part.Position.Y > 50 then -- stone
			Terrain:FillBlock(Part.CFrame, Part.Size, Enum.Material.Rock)
		elseif Part.Position.Y > -25 then -- grass
			Terrain:FillBlock(Part.CFrame, Part.Size, Enum.Material.Ground)
		elseif Part.Position.Y < -25 then -- sand
			Terrain:FillBlock(Part.CFrame, Part.Size, Enum.Material.Sand)
		end
		Part:Destroy()
		
	end
end

local NumWorm = 0

if CavesEnabled then -- Create perlin worm caves
	for i = 1, Caves do
		wait()
		NumWorm = NumWorm + 1
		local sX = math.noise(NumWorm / CaveInterval + 0.1, Seed)
		local sY = math.noise(NumWorm / CaveInterval + sX + 0.1, Seed)
		local sZ = math.noise(NumWorm / CaveInterval + sY + 0.1, Seed)
		local WormCF = CFrame.new(math.random(0, MapSize * PartSize), math.random(0, MapSize * PartSize), math.random(0, MapSize * PartSize)) * CFrame.new(sX * CaveLength, (sY * CaveLength), sZ * CaveLength)
		print("Worm "..NumWorm.." spawning at "..WormCF.X..", "..WormCF.Y..", "..WormCF.Z)
		local Dist = (math.noise(NumWorm / CaveInterval + WormCF.p.magnitude,Seed) + 0.5) * CaveLength
		
		for i = 1, CaveLength do
			local X = math.noise(WormCF.X / CaveInterval + 0.1, Seed)
			local Y = math.noise(WormCF.Y / CaveInterval + 0.1, Seed)
			local Z = math.noise(WormCF.Z / CaveInterval + 0.1, Seed)
			WormCF = WormCF * CFrame.Angles(X * 2,Y * 2,Z * 2) * CFrame.new(0, 0, -CaveInterval)
			local Part = Instance.new("Part")
			Part.Anchored = true
			Part.CFrame = CFrame.new(WormCF.p)
			Part.Parent = workspace
			Terrain:FillBall(Part.CFrame.p, CaveSize, Enum.Material.Air)
			Part:Destroy()
		end
	end
end

local GrassParams = RaycastParams.new()
local StructureParams = RaycastParams.new()

function PaintMaterial(MinPos, MaxPos, MaterialToReplace, ReplacementMaterial)
	local Reg = Region3.new(MinPos, MaxPos)
	local materialToReplace = Enum.Material.Ground
	local replacementMaterial = Enum.Material.Grass
	
	local NewReg = Reg:ExpandToGrid(4)
	workspace.Terrain:ReplaceMaterial(NewReg, 4, materialToReplace, replacementMaterial)
end

for x = 1, MapSize do -- Add grass
	x = x * PartSize
	AdvancedWait(10)
	for z = 1, MapSize do
		z = z * PartSize
		local GrassRay = workspace:Raycast(Vector3.new(x, 1000, z), Vector3.new(x, -1000, z), GrassParams)
		if GrassRay then
			if GrassRay.Instance == Terrain then
				PaintMaterial(GrassRay.Position - Vector3.new(15, 15, 15),  GrassRay.Position + Vector3.new(15, 15, 15), Enum.Material.Ground, Enum.Material.Grass)
			end
		end
	end
end

-- V V V Starts to lag and slow down here  V V V--

local function PlaceStructure(ModelToClone) -- Structures
	print("Adding structures...")
	for x = 1, MapSize do
		x = x * PartSize
		for z = 1, MapSize do
			z = z * PartSize
			local StructureRay = workspace:Raycast(Vector3.new(x, 1000, z), Vector3.new(x, -2000, z), StructureParams)
			if StructureRay then
				if StructureRay.Instance == Terrain then
					if ModelToClone.Configuration.MaterialsToBePlacedOn:FindFirstChild(StructureRay.Material) then
						local AngleOffset = ModelToClone.Configuration.SpawnAngle.Value
						local Chance = math.random(0, 1000000)
						if Chance <= ModelToClone.Configuration.SpawnChance.Value then
							local NewModel = ModelToClone:Clone()
							local NewModelCFrame = CFrame.new(StructureRay.Position) * CFrame.Angles(math.rad(math.random(-AngleOffset.X, AngleOffset.X)), math.rad(math.random(-AngleOffset.Y, AngleOffset.Y)), math.rad(math.random(-AngleOffset.Z, AngleOffset.Z)))
							NewModel:SetPrimaryPartCFrame(NewModelCFrame)
							NewModel.Parent = StructuresWorkspaceFolder
							
							local Parts = NewModel:GetChildren()
							for i, PartToConvert in ipairs(Parts) do
								if PartToConvert:FindFirstChild("ConvertToMaterial") and PartToConvert:IsA("BasePart") then
									if PartToConvert.Shape == Enum.PartType.Ball then
										Terrain:FillBall(PartToConvert.Position, PartToConvert.Size.Y / 2, PartToConvert.ConvertToMaterial.Value)
									elseif PartToConvert.Shape == Enum.PartType.Block then
										Terrain:FillBlock(PartToConvert.CFrame, PartToConvert.Size, PartToConvert.ConvertToMaterial.Value)
									end
									PartToConvert:Destroy()
								end
							end
						end
						AdvancedWait(150)
					end
				end
			end
		end
	end
end

local function PlacePlant(PlantToClone) -- Plants and trees
	print("Adding plants and trees...")
	for x = 1, MapSize do
		x = x * PartSize
		for z = 1, MapSize do
			z = z * PartSize
			local PlantRay = workspace:Raycast(Vector3.new(x, 500, z), Vector3.new(x, -500, z), GrassParams)
			if PlantRay then
				if PlantRay.Instance == Terrain then
					if PlantToClone.Configuration.MaterialsToBePlacedOn:FindFirstChild(PlantRay.Material) then
						local AngleOffset = PlantToClone.Configuration.SpawnAngle.Value
						local Chance = math.random(0, 1000000)
						if Chance <= ChosenTerrain.PlantsChance then
							local NewModel = PlantToClone:Clone()
							local NewModelCFrame = CFrame.new(PlantRay.Position) * CFrame.Angles(math.rad(math.random(-AngleOffset.X, AngleOffset.X)), math.rad(math.random(-AngleOffset.Y, AngleOffset.Y)), math.rad(math.random(-AngleOffset.Z, AngleOffset.Z)))
							NewModel:SetPrimaryPartCFrame(NewModelCFrame)
							NewModel.Parent = PlantsWorkspaceFolder
						end
						AdvancedWait(150)
					end
				end
			end
		end
	end
end

for i = 1, 25 do -- Add spawn points
	wait()
	local RandomX = math.random(0, MapSize) * PartSize
	local RandomZ = math.random(0, MapSize) * PartSize
	local SpawnRay = workspace:Raycast(Vector3.new(RandomX, 1500, MapSize / 2), Vector3.new(RandomX, -2000, RandomZ), GrassParams)
	if SpawnRay then
		if SpawnRay.Instance == Terrain then
			local SpawnPart = Instance.new("SpawnLocation")
			SpawnPart.Position = SpawnRay.Position
			SpawnPart.CanCollide = false
			SpawnPart.Transparency = 1
			SpawnPart.Anchored = true
			SpawnPart.Parent = SpawnPointsWorkspaceFolder
		end
	end	
end


for i, Structure in ipairs(Structures:GetChildren()) do -- Get structures to add to the world
	local ConfigurationFolder = Structure:FindFirstChild("Configuration")
	if ConfigurationFolder then
		if ConfigurationFolder.CanBeUsed.Value == true then
			PlaceStructure(Structure)
		end
	end
	wait()
end

for i, Object in ipairs(Plants) do -- Get plants to add to the world
	local ConfigurationFolder = Object:FindFirstChild("Configuration")
	if ConfigurationFolder then
		if ConfigurationFolder.CanBeUsed.Value == true then
			PlacePlant(Object)
		end
	end
	wait()
end

print("Finished generating! | Time: " .. os.time() - GenerationTimer .. "s")
1 Like

If you’re looking to improve your code, you should put this in #help-and-feedback:code-review. As for the lag, make it a seperate topic.

2 Likes