Part optimization

Hello People, I have a code for a basic terrain function using a kind of Perlin noise in it, spawning chunk by chunk up to around 8 layers of blocks. This code has one flaw, which is optimization, which is a big problem since I am using parts and I cannot use terrain because I would obviously lose the blocky style of it. Does anyone have any idea on how I could improve it so I can make it at least playable? Since well, it’s even hard to load in when just a tiny part of it is built, 500 is just and indicative, but I would need much more than that, like a complete idk 10000? and that squared is way bigger than 250k parts per layer (I would need to put more layers too much, like in Minecraft if any of you played it).

local x = require(game:GetService("ReplicatedStorage"):WaitForChild("Modules").OmegaNum)
local collectionservice = game:GetService("CollectionService")
local tag = "zpawn"
local Tag = "boss_spawn"
local seed = math.random(1,1000000000)
		-- ALL IS MESUARED IN BLOCKS SO IF YSPREAD IS 400 ITS GONNA NEED 400 BLOCKS MINIMUM TO REACH THAT HIGH --
		
local Msize = 500 -- number dividable by 2 (uses base block)
local Csize = 25 -- the size of each chunk  
local peakheight = 45 -- how much the peaks can go in the y axis
local yspread = 50 -- how spread is such value (normally at least 10% more than peakheight)
local Bsize = 5 -- just the block size
local MobSpawnChance = 0.001 -- in % the spawning chance for each block in the world
		-- Blocks Layers --
local NB1Layers = 3 -- Num of layers of Block1
local NB2Layers = 4 -- Num of layers of Block2


-- Calculation Vars DONT CHANGE
local TC = math.ceil(Msize/Csize) -- just the tot amt of chunks
local world = workspace.Map -- Where the map is
local Mscale = math.floor(Msize/2)

-- Terrain Chunks Gen --

for CX = 1,TC do -- chunk x val
	for CZ = 1,TC do -- chunk z val
		local CF = Instance.new("Folder",world) -- The chunk folder
		CF.Name = "Chunk_"..CX.."_"..CZ

		for X = 1, Csize do
			for Z = 1, Csize do
				local realx = (CX - 1)*Csize+X
				local realz = (CZ - 1)*Csize+Z

				-- Generate Terrain Noise
				local noise = math.noise(realx/yspread,seed,realz/yspread)*peakheight

				-- Layer 0 Block --
				local b0 = script.Block:Clone()
				b0.Size = Vector3.new(Bsize,Bsize,Bsize)
				b0.Position = Vector3.new(realx*Bsize,(math.floor(noise)*Bsize)-Bsize*3,realz*Bsize)
				b0.Color = Color3.new(noise,noise,noise)
				b0.Parent = CF
				if x.leeq(x.rand(0,1),MobSpawnChance) then
					-- Determine if it's a boss spawner with a chance
					if x.leeq(x.rand(0,1),0.5) then
						b0.Name = "MobSpawner"
						b0.Color = Color3.fromRGB(0,0,255)
						collectionservice:AddTag(b0, tag)
						b0:SetAttribute("Mob", "0") -- Replace with the actual mob name
						b0:SetAttribute("Amount", math.random(1, 3)) -- Replace with the desired amount
						b0:SetAttribute("Cooldown", math.random(2, 5)/10) -- Replace with the desired cooldown
					else
						b0.Name = "BossSpawner"
						b0.Color = Color3.fromRGB(255,0,0)
						collectionservice:AddTag(b0, Tag)
						b0:SetAttribute("Mob", "0") -- Replace with the actual boss name
						b0:SetAttribute("Amount", 1) -- Replace with the desired amount
						b0:SetAttribute("Cooldown", math.random(50, 400)/100) -- Replace with the desired cooldown
					end
				end

				-- Layer -1 Block --
				for i = 1,NB1Layers do
					-- Layer -1 Block --
					local b1 = script["Building Blocks"].B1:Clone()
					b1.Size = Vector3.new(Bsize,Bsize,Bsize)
					b1.Position = Vector3.new(realx*Bsize,math.floor(noise)*Bsize - (Bsize*(i+NB1Layers)),realz*Bsize)
					b1.Parent = CF
				end

				-- Layer -2 Block
				for i = 1,NB2Layers do
					-- Layer -2 Block --
					local b2 = script["Building Blocks"].B2:Clone()
					b2.Size = Vector3.new(Bsize,Bsize,Bsize)
					b2.Position = Vector3.new(realx*Bsize,math.floor(noise)*Bsize - (Bsize*(i+NB1Layers)),realz*Bsize)
					b2.Parent = CF
				end
			end
			game:GetService("RunService").Heartbeat:Wait()
		end
	end
end

dont mind the if statment for the mob its just another thing for mobspawning

  • List item
1 Like

Okay so here’s a few basic tips I can give you.

  1. Don’t spawn parts where they wouldn’t be visible (e.g. part is surrounded form all 6 sides, therefor not visible).

    We also refer to this as “culling”.

  2. Do greedy meshing.
    You may ask “what on earth is greedy meshing?”, well it’s simple.

    Let’s say you have a row of parts that are all the same material?


    (Excuse my crude drawings.)

    You match their patterns and then turn it into 1 long part.

    You can easily extend this to 2 dimensions as well.
    And 3rd dimension is just the same thing but check on another axis.

    You can do this in multiple passes and iterations so you don’t have to write something horribly complex.

Hope that helped!

4 Likes

ok thx for the tips, for the first one i literally have 0 idea on how to apply it, the second one tho i made the uniasyc but idk how to get IF something besides it exist i added this script for the unionasyc but it dosent work. why? (the new script version)

local x = require(game:GetService("ReplicatedStorage"):WaitForChild("Modules").OmegaNum)
local collectionservice = game:GetService("CollectionService")
local tag = "zpawn"
local Tag = "boss_spawn"
local seed = x.toNumber(x.rand(1,1000000000))
print(seed)
		-- ALL IS MESUARED IN BLOCKS SO IF YSPREAD IS 400 ITS GONNA NEED 400 BLOCKS MINIMUM TO REACH THAT HIGH --
		
local Msize = 150 -- number dividable by 2 (uses base block)
local Csize = 25 -- the size of each chunk  
local peakheight = 120-- how much the peaks can go in the y axis
local yspread = 150 -- how spread is such value (normally at least 10% more than peakheight)
local Bsize = 5 -- just the block size
local MobSpawnChance = 0.001 -- in % the spawning chance for each block in the world
		-- Blocks Layers --
local NB1Layers = 8 -- Num of layers of Block1
local NB2Layers = 12 -- Num of layers of Block2


-- Calculation Vars DONT CHANGE
local TC = math.ceil(Msize/Csize) -- just the tot amt of chunks
local world = workspace.Map -- Where the map is
local Mscale = math.floor(Msize/2)
local LowOdds = math.random(1,5)
local Highodds = math.random(1,2)

-- Union Function

local function unifyBlocks(block1, block2)
	local success, union = pcall(function()
		return block1:UnionAsync(block2)
	end)

	if success and union then
		union.Position = block1.Position
		union.Parent = block1.Parent
		union.Name = block1.Name
	end

	block1:Destroy()
	block2:Destroy()

	return union
end

-- Terrain Chunks Gen --

for CX = 1,TC do -- chunk x val
	for CZ = 1,TC do -- chunk z val
		local CF = Instance.new("Folder",world) -- The chunk folder
		CF.Name = "Chunk_"..CX.."_"..CZ

		for X = 1, Csize do
			for Z = 1, Csize do
				local realx = (CX - 1)*Csize+X
				local realz = (CZ - 1)*Csize+Z

				-- Generate Terrain Noise
				local noise = math.noise(realx/yspread,seed,realz/yspread)*peakheight

				-- Layer 0 Block --
				local b0 = script.Block:Clone()
				b0.Size = Vector3.new(Bsize,Bsize,Bsize)
				b0.Position = Vector3.new(realx*Bsize,(math.floor(noise)*Bsize)-Bsize*NB1Layers,realz*Bsize)
				b0.Parent = CF
				b0:SetAttribute("Layer", 0)
				b0:SetAttribute("Layer", 0)
				
				b0.Touched:Connect(function(part)
					if part:IsA("Part") and part:GetAttribute("Layer") == b0:GetAttribute("Layer") then
						local union = unifyBlocks(b0, part)
						b0 = union
						return b0
					end
				end)
				
				if x.leeq(x.rand(0,1),MobSpawnChance) then
					-- Determine if it's a boss spawner with a chance
					if x.leeq(x.rand(0,1),0.5) then
						b0.Name = "MobSpawner"
						b0.Color = Color3.fromRGB(0,0,255)
						collectionservice:AddTag(b0, tag)
						b0:SetAttribute("Mob", "0") -- Replace with the actual mob name
						b0:SetAttribute("Amount", math.random(1, 3)) -- Replace with the desired amount
						b0:SetAttribute("Cooldown", math.random(2, 5)/10) -- Replace with the desired cooldown
					else
						b0.Name = "BossSpawner"
						b0.Color = Color3.fromRGB(255,0,0)
						collectionservice:AddTag(b0, Tag)
						b0:SetAttribute("Mob", "0") -- Replace with the actual boss name
						b0:SetAttribute("Amount", 1) -- Replace with the desired amount
						b0:SetAttribute("Cooldown", math.random(50, 400)/100) -- Replace with the desired cooldown
					end
				end
				-- Layer -1 Block --
				for i = 1,NB1Layers do
					-- Layer -1 Block --
					local b1 = script["Building Blocks"].B1:Clone()
					b1.Size = Vector3.new(Bsize,Bsize,Bsize)
					b1.Position = Vector3.new(realx*Bsize,math.floor(noise)*Bsize - (Bsize*(i+NB1Layers)),realz*Bsize)
					b1.Parent = CF
					b0:SetAttribute("Layer", -1)
					--print(b1.Color)
				end

				-- Layer -2 Block
				for i = 1,NB2Layers do
					-- Layer -2 Block --
					local b2 = script["Building Blocks"].B2:Clone()
					b2.Size = Vector3.new(Bsize,Bsize,Bsize)
					b2.Position = Vector3.new(realx*Bsize,math.floor(noise)*Bsize - (Bsize*(i+NB1Layers)),realz*Bsize)
					b2.Parent = CF
					b0:SetAttribute("Layer", -2)
					--print(b2.Color)
				end
			end
			game:GetService("RunService").Heartbeat:Wait()
		end
	end
end

Don’t use unions, they’re terrible for performance and rack up draw calls.
Not super good with code examples here but take a look at this.

So let’s say you have an array of parts that is just 1 straight line.

local blocks = {"dirt", "dirt", "grass", "grass", "grass"} -- Imagine there is a straight line of blocks in here.

local merged_blocks = {}

-- Now we start matching.

local current_block = nil

local length = 0

for k, v in ipairs(blocks) do
 -- Current block has not been set yet, therefor we reset it.
 if v ~= current_block then
  -- Prevent function from adding empty / zero length blocks to the list.
  if length > 0 then merged_blocks[current_block] = length end

  current_block = v

 else
  length += 1
 end

end

Now, apologies if this doesn’t work, I’m writing this like shortly after I woke up and showered.

But if the code works as intended, you should end up with an array looking like this:

merged_blocks = {["dirt"] = 2; ["grass"] = 3;}

(Anyone below me feel free to correct me.)

But why do we need the length?

You need the length of every row you merge, once you turn it into one long block, you need the length so it can have the correct size.

Setting the position of our merged block should also be fairly straight forward.
We can simply use the position of the very first block (where we started matching patterns) use that position and add the length to it, divided by 2.

Alternatively you get the position of the first and last block and average their positions to get a position that is right in the middle.

Hope that helps a bit, it’s as much as I can provide right now.

1 Like

i legit understood nothing of it, what does that merge thing do exactly, how is matching supposed to happen? and i dont understand that for loop why is it there and what do you mean by prevent the function from adding an empty/ zero lenght block? every block has a lenght pre specified wich is the Bsize, im never gonna have a block below that

It’s a greedy meshing algorithm.

After you generated the world, you check for parts that are right next to each other and turn them into 1 long part.

You basically turn a row of cubes with the same color and material into a long rectangle.
It’s as simple as I can explain it.

Voxel (or block) based games typically use this to reduce the amount of work the computer has to do to render all these blocks.

there is a problem, i need to finish generating it… considering a map size is gonna be alot, it will be hard to apply AFTER its finished

1 Like

Okay so allow me to clarify it a bit.
You first generate your map, but you do not spawn in the parts yet.

AFTER you generated the map, and BEFORE you thus start placing down parts, you loop through arrays of blocks and compare them.

Blocks that are the same type can be merged into loooong blocks that stretch across the terrain like I showed you previously.

Also if you feel like it would be too many blocks to go through then try making your chunk sizes smaller so you can perform the merging process on each chunk with some delay to prevent big lag spikes.