How I can optimize terrain generation?

Good day everyone! Recently I started make a terrain generation system using perlin noise, but I ran into a problem that took a long time to generate the terrain itself. My terrain is 250 x 250 in size.
Is there any option to optimize my code and make it faster?
I’m writing here out of despair because I’ve been sitting on this for 4 days now.
Code

local Players = game:GetService("Players")
local perlinFolder = workspace:WaitForChild("PerlinTest")
local terrain = workspace:WaitForChild("Terrain")
local periodicity = 60
local size = 250
local chunck = {}
local seed = math.random()*30
print(seed)
local amp = 80
local materials = {
	[1] = Enum.Material.Ground,
	[2] = Enum.Material.Grass,
	[3] = Enum.Material.Granite,
	[4] = Enum.Material.Sand,
	[5] = Enum.Material.Snow,
	[6] = Enum.Material.Mud,
}



function SelectMaterial(x,z)
	local xNoise = x/50
	local zNoise = z/50
	local noise = math.noise(xNoise,zNoise,seed)+1
	local delta = #materials/2
	local num = math.floor(noise*delta)
	if num < 1 then
		num = 1
	elseif num > #materials then
		num = #materials
	end


	return materials[num]
end



function NoizeCount(x,z)
	local xNoise = x/50
	local zNoise = z/50
	local noise = math.noise(xNoise,zNoise,seed)*periodicity+amp 
	return noise
end

function CheckChunk(x,z)
	local ret = nil
	if chunck[x] == nil then
		chunck[x] = {}
	end
	ret = chunck[x][z]
	return ret
end

function WorldGenerate(posX,posZ)
	print("Start generation")
	warn(posX,posZ)
	for x=posX-size,posX+size do

		for z=posZ-size, posZ+size do
			if CheckChunk(x,z) == nil then
				chunck[x][z] = true
				local part = Instance.new("Part",perlinFolder)
				part.Shape = Enum.PartType.Block
				part.Anchored = true
				local y = NoizeCount(x,z)
				local y1 = y - 3*4
				part.Size = Vector3.new(4,(y-y1)/2,4)
				part.Position = Vector3.new(x,(y+y1)/2,z)

				terrain:FillBlock(part.CFrame,part.Size,SelectMaterial(x,z))
				part:Destroy()
			end
		end
		wait()
	end
	print("End generation")
	task.wait()
end



function CharacterMoved(character)
	local oldX = nil
	local oldZ = nil
	repeat
		if character and character.Parent then
			local coordinates = character:WaitForChild("HumanoidRootPart"):GetPivot()
			local posX = math.floor(coordinates.Position.X)
			local posZ = math.floor(coordinates.Position.Z)
			if oldX ~= posX or oldZ ~= posZ then
				warn("Генерация началась в ", posX,posZ)
				task.spawn(WorldGenerate,posX,posZ)
				oldX = posX
				oldZ = posZ
			else
				task.wait()
			end
		else
			return
		end
	until not (character and character.Parent)

end

function CharacterAdded(character)
	task.spawn(CharacterMoved,character)
end

function PlayerAdded(player:Player)
	player.CharacterAdded:Connect(CharacterAdded)
end

Players.PlayerAdded:Connect(PlayerAdded)
4 Likes

I have not read anything in your post, but I am assuming you’re not using parallel lua or multi-threading. Try them out, and the difference will be night and day.

If I am wrong, please correct me.

1 Like

A simple optimisation you could do is remove the need of creating Parts then destroying them. Instead, just focus on the terrain. Another thing is that using wait() will make the code run slower than task.wait(), so you should use the latter. Here’s the updated code for the WorldGenerate function:

function WorldGenerate(posX,posZ)
	print("Start generation")
	warn(posX,posZ)
	for x=posX-size,posX+size do

		for z=posZ-size, posZ+size do
			if CheckChunk(x,z) == nil then
				chunck[x][z] = true
				local y = NoizeCount(x,z)
				local y1 = y - 3*4
				terrain:FillBlock(CFrame.new(x,(y+y1)/2,z),Vector3.new(4,(y-y1)/2,4),SelectMaterial(x,z))
			end
		end
		task.wait()
	end
	print("End generation")
	task.wait()
end

Also, prints and warns can slow down your code. If you remove those 2 print statements and that 1 warn statement, the code would run faster too.

Edit 2: Actually prints and warns don’t make that much of an impact on performance if the function isn’t ran frequently, however right now the function is ran every time the player moves a stud. You should probably make the player call the WorldGenerate after travelling every 16 studs or something. You could achieve that using this code:

local posX = math.floor(coordinates.Position.X/16)*16 -- generate world every 16 studs the player moves
local posZ = math.floor(coordinates.Position.Z/16)*16

Use local functions not global functions. They are much faster and can get inlined

Also worth noting that as long there’s no lag you can batch multiple loop iterations together and only wait on every Nth iteration(for example every 2 iterations):

if x%2 == 0 then task.wait() end --only wait every time x is even
1 Like

The best way to make the generation fast is to do it all client-side. Of course, that leads to other problems. If you want all the functionality of server-side generation … stall. Once generated a new player joining will not have to wait for anything but the first to join will need a stall. This is when you show two logo screens and possibly even more, like a story text or something. Pretty much anything to buy you time to finish the generation. You’ve been seeing this technique for years.

I decided to try to make a parallel luau using an actor, but this did not increase the drawing speed

1 Like

And as you can see, for some reason the generation doesn’t draw where I’m standing…

1 Like

Parallel lua is kinda hard to implement. There’s no clear tutorial on how many actors you should use, or if you should use one. I know you are doing it wrong as the example they give is much faster, but no one properly knows how to do it.

So, what I should do in this situation?

Ah sorry, I should’ve contributed in that comment.

I didn’t notice when re-reading the documentation, but Actors actually have a bindtomessage function that allows you to communicate between different actors. Basically, you create multiple workers (actors) which will handle repetitive creation (such as chunk loading in different areas). I found this code example from the documentation:

-- Parallel execution requires the use of actors
-- This script clones itself; the original initiates the process, while the clones act as workers

local actor = script:GetActor()
if actor == nil then
	local workers = {}
	for i = 1, 32 do
		local actor = Instance.new("Actor")
		script:Clone().Parent = actor
		table.insert(workers, actor)
	end

	-- Parent all actors under self
	for _, actor in workers do
		actor.Parent = script
	end

	-- Instruct the actors to generate terrain by sending messages
	-- In this example, actors are chosen randomly
	task.defer(function()
		local rand = Random.new()
		local seed = rand:NextNumber()

		local sz = 10
		for x = -sz, sz do
			for y = -sz, sz do
				for z = -sz, sz do
					workers[rand:NextInteger(1, #workers)]:SendMessage("GenerateChunk", x, y, z, seed)
				end
			end
		end
	end)

	-- Exit from the original script; the rest of the code runs in each actor
	return
end

function makeNdArray(numDim, size, elemValue)
	if numDim == 0 then
		return elemValue
	end
	local result = {}
	for i = 1, size do
		result[i] = makeNdArray(numDim - 1, size, elemValue)
	end
	return result
end

function generateVoxelsWithSeed(xd, yd, zd, seed)
	local matEnums = {Enum.Material.CrackedLava, Enum.Material.Basalt, Enum.Material.Asphalt}
	local materials = makeNdArray(3, 4, Enum.Material.CrackedLava)
	local occupancy = makeNdArray(3, 4, 1)

	local rand = Random.new()

	for x = 0, 3 do
		for y = 0, 3 do
			for z = 0, 3 do
				occupancy[x + 1][y + 1][z + 1] = math.noise(xd + 0.25 * x, yd + 0.25 * y, zd + 0.25 * z)
				materials[x + 1][y + 1][z + 1] = matEnums[rand:NextInteger(1, #matEnums)]
			end
		end
	end

	return {materials = materials, occupancy = occupancy}
end

-- Bind the callback to be called in parallel execution context
actor:BindToMessageParallel("GenerateChunk", function(x, y, z, seed)
	local voxels = generateVoxelsWithSeed(x, y, z, seed)
	local corner = Vector3.new(x * 16, y * 16, z * 16)

	-- Currently, WriteVoxels() must be called in the serial phase
	task.synchronize()
	workspace.Terrain:WriteVoxels(
		Region3.new(corner, corner + Vector3.new(16, 16, 16)),
		4,
		voxels.materials,
		voxels.occupancy
	)
end)
5 Likes

Did you use one singular actor or did you use multiple and split the task of generating the terrain between them?

That’s exactly what I did with my code. I found this example in the documentation and made this code using it

local Players = game:GetService("Players")
local perlinFolder = workspace:WaitForChild("PerlinTest")
local terrain = workspace:WaitForChild("Terrain")
local periodicity = 30
local size = 250
local chunck = {}
local seed = math.random()*30
print(seed)
local amp = 80
local materials = {
	[1] = Enum.Material.Ground,
	[2] = Enum.Material.Grass,
	[3] = Enum.Material.Granite,
	[4] = Enum.Material.Sand,
	[5] = Enum.Material.Snow,
	[6] = Enum.Material.Mud,
}

local actor = script:GetActor()
local workers = {}
if actor == nil then
	local actor = Instance.new("Actor")
	script:Clone().Parent = actor
	table.insert(workers,actor)
end

for _, actor in workers do
	actor.Parent = script
end

function SelectMaterial(x,z)
	local xNoise = x/50
	local zNoise = z/50
	local noise = math.noise(xNoise,zNoise,seed)+1
	local delta = #materials/2
	local num = math.floor(noise*delta)
	if num < 1 then
		num = 1
	elseif num > #materials then
		num = #materials
	end


	return materials[num]
end



function NoizeCount(x,z)
	local xNoise = x/50
	local zNoise = z/50
	local noise = math.noise(xNoise,zNoise,seed)*periodicity+amp 
	return noise
end

function CheckChunk(x,z)
	local ret = nil
	if chunck[x] == nil then
		chunck[x] = {}
	end
	ret = chunck[x][z]
	return ret
end

local function WorldGenerate(posX,posZ)
	print("Start generation")
	warn(posX,posZ)
	for x=posX-size,posX+size do

		for z=posZ-size, posZ+size do
			if CheckChunk(x,z) == nil then
				chunck[x][z] = true
				local y = NoizeCount(x,z)
				local y1 = y - 3*4
				terrain:FillBlock(CFrame.new(x,(y+y1)/2,z),Vector3.new(4,(y-y1)/2,4),SelectMaterial(x,z))
			end
		end
		wait()
	end
	print("End generation")
	task.wait()
end



function CharacterMoved(character)
	local oldX = nil
	local oldZ = nil
	repeat
		if character and character.Parent then
			local coordinates = character:WaitForChild("HumanoidRootPart"):GetPivot()
			local posX = math.floor(coordinates.Position.X)
			local posZ = math.floor(coordinates.Position.Z)
			if oldX ~= posX or oldZ ~= posZ then
				warn("Генерация началась в ", posX,posZ)
				actor:SendMessage("GenerateChunck",posX,posZ)
				oldX = posX
				oldZ = posZ
			else
				task.wait()
			end
		else
			return
		end
	until not (character and character.Parent)

end

function CharacterAdded(character)
	task.spawn(CharacterMoved,character)
end

function PlayerAdded(player:Player)
	player.CharacterAdded:Connect(CharacterAdded)
end

Players.PlayerAdded:Connect(PlayerAdded)


actor:BindToMessageParallel("GenerateChunck",function(posX,posZ)
	print("Start generation")
	warn(posX,posZ)
	task.synchronize()
	for x=posX-size,posX+size do

		for z=posZ-size, posZ+size do
			if CheckChunk(x,z) == nil then
				chunck[x][z] = true
				local y = NoizeCount(x,z)
				local y1 = y - 3*4
				terrain:FillBlock(CFrame.new(x,(y+y1)/2,z),Vector3.new(4,(y-y1)/2,4),SelectMaterial(x,z))
			end
		end
		wait()
	end
	print("End generation")
	task.wait()
end)
1 Like

This is only creating one actor… meaning your script still runs on one single core

delta is constant so you can define it outside the function

replace with num = math.clamp(num, 1, materials)

and maybe use bit shifting for dividing by 2 and you can change the /50 to /64 and then replace with bit shift

So, how i can make a more Actor’s?
I would be very grateful if you could provide me with an example

The example is in my code. I don’t think you understand what you’re even reading in that example.

local workers = {}
	for i = 1, 32 do
		local actor = Instance.new("Actor")
		script:Clone().Parent = actor
		table.insert(workers, actor)
	end

32 is the amount of workers, the amount of actors, the amount of threads that the code is ran in parallel.

Here you made one actor, this is the same as just running the code in serial. Make more actors.

I know you’ve started working with actors, but the real problem still remains. You are telling your code to wait 1/60th of a second every step you take in the loop which will of course make your code slow. A quick fix is to add this code near the top of your script (this will only wait if it actually NEEDS to wait to keep the game running) And note the bigger the EXEC_TIME_LIMIT is the faster your code runs, but also the more time it takes from other systems that need time to run. I wouldn’t set it above 1/10th

local lastWait = tick()
local EXEC_TIME_LIMIT = 1/60
local smartWait = function()
	if tick() - lastWait > EXEC_TIME_LIMIT then
		task.wait()
		lastWait = tick()
	end
end

Then instead of calling wait in your loops

--End of WorldGenerate()
    wait()
  end
  print("End generation")
  task.wait()
end

Do

--End of WorldGenerate()
    smartWait()
  end
  print("End generation")
  task.wait()
end

The actor thing too is worth implementing if you want it. But if you are doing actors, this complicates things. You still want to wait as little as possible, but my smartWait approach wouldn’t work as it is (it’s not thread safe). You would need a version of it per task so they can each track how long they have ran. Or you need to schedule tasks such that they only run when allowed and are small enough the individual tasks don’t need to wait, which would allow you to use smartwait in the task scheduler.

2 Likes

Yesterday I tried to do it according to this principle and it turned out that the initial generation is going on
1 Long
2 This generation is multiplying. Those. one chunk is processed 32 times, which eats up performance.

1 Like

Yes, it works, but it’s a shame that it didn’t solve the problem that the generation is not happening where I’m standing…