How can I fix this lag!?

I have this voxel based game with tile like generation sorta like the game 3008. As of right now I make 900 cells per generation. (one whole map). each cell is like 16 by 1 by 16 blocks. Here is the code for the generation:

--============================================================
-- MAP GENERATION SCRIPT
-- Procedurally builds a grid of "cells" (tiles) surrounded by
-- walls and a roof, using parallel Actors to speed up generation.
-- COMMENTS DONE BY CLAUDE SORRY
--============================================================

-- ==== GRID CONFIGURATION ====
local Width = 30      -- number of cells along the X axis
local Length = 30     -- number of cells along the Z axis -- must be equal to width and being greater than 20 causes problemo
local YStart = 1.5    -- base Y position (height) for the floor grid

local CellSize = 16 * 3       -- size of a single cell (48 studs)
local SpacingSize = 3 * 3     -- gap/spacing between cells (9 studs)

local Step = CellSize + SpacingSize -- distance between the centers of two adjacent cells

-- Half of the total grid width/length, used to center the grid on the origin
local HalfW = Width * Step / 2
local HalfL = Length * Step / 2

-- Bounding box (min/max X and Z) of the entire grid, including a little padding
-- for the outer edge cells
local MinX = Step - HalfW - CellSize / 2
local MaxX = Width * Step - HalfW + CellSize / 2
local MinZ = Step - HalfL - CellSize / 2
local MaxZ = Length * Step - HalfL + CellSize / 2

-- Overall grid dimensions and center point (used for walls/roof/border placement)
local GridW = MaxX - MinX
local GridL = MaxZ - MinZ
local GridCX = (MinX + MaxX) / 2
local GridCZ = (MinZ + MaxZ) / 2

-- ==== WALL / ROOF CONFIGURATION ====
local WallHeight = 600
local WallThickness = 6
-- Walls are extended slightly beyond the grid bounds so they meet
-- at the corners and include the outer spacing gap
local ExtendedW = GridW + WallThickness * 2 + SpacingSize * 2
local ExtendedL = GridL + WallThickness * 2 + SpacingSize * 2

-- ==== SERVICES & FOLDER REFERENCES ====
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local MapFolder = workspace.Map                       -- where floor/wall/roof parts get parented
local BlocksFolder = workspace.Blocks                 -- where placeable "block" parts end up
local CellPresetsFolder = ServerStorage.CellPresets   -- folder containing all possible cell templates
local CellPresets = CellPresetsFolder:GetChildren()   -- list of cell preset templates
local Preset1 = CellPresetsFolder.Preset1             -- (unused reference to a specific preset)
local Texture0Template = ServerStorage.Texture0       -- texture template applied to ground/space parts
local TextureTemplate = ServerStorage.Texture         -- texture template applied to walls

-- Names of the 6 faces of a part, used when applying textures to all sides
local FACE_NAMES = { "Front", "Back", "Left", "Right", "Top", "Bottom" }

-- ==== HELPER SIZES FOR SPACER/GAP PARTS ====
local CellHalf = CellSize / 2
local StepHalf = CellHalf + SpacingSize / 2
local RowSize  = Vector3.new(SpacingSize, 3, CellSize)   -- thin gap strip running along a row
local ColSize  = Vector3.new(CellSize, 3, SpacingSize)   -- thin gap strip running along a column
local CornerSize = Vector3.new(SpacingSize, 3, SpacingSize) -- small square filling a corner gap

-- Rounds each component of a Vector3 up to the nearest multiple of 3
-- (keeps part sizes aligned to a 3-stud grid)
local function RoundTo3(v)
	return Vector3.new(
		math.ceil(v.X / 3) * 3,
		math.ceil(v.Y / 3) * 3,
		math.ceil(v.Z / 3) * 3
	)
end

-- ==== WEIGHTED RANDOM CELL PRESET SELECTION ====

-- Sum up the "Weight" attribute of every cell preset (defaults to 1 if missing)
-- so we can pick presets with weighted probability
local TotalWeight = 0
for _, preset in pairs(CellPresets) do
	TotalWeight += preset:GetAttribute("Weight") or 1
end

-- Picks a random cell preset, favoring presets with higher Weight values
local function getRandomTemplate()
	local roll = math.random(1, TotalWeight)

	-- Step 3: Walk through the table until we find the winner
	local cumulative = 0
	for _, item in ipairs(CellPresets) do
		cumulative += item:GetAttribute("Weight") or 1
		if roll <= cumulative then
			return item
		end
	end
end

-- Folder of all possible "block" types (materials/appearances) that can be placed
local Blocks = game.ReplicatedStorage.Blocks

-- Copies the visual properties (material, color, transparency, textures, name)
-- from a "Type" block template onto an existing "Block" part.
-- Used to reskin a placed block to look like a different block type.
local function LarpBlock(Block: BasePart, Type: BasePart)
	if Block == nil or Type == nil then warn("check 4 FAILLLLLLLLLLL") return end
	
	Block.Material = Type.Material
	Block.Color = Type.Color
	Block.Transparency = Type.Transparency
	Block.Name = Type.Name
	Blocks:SetAttribute("Strength", Type:GetAttribute("Strength"))

	-- Build a lookup of the target type's textures by face
	local typeTextures = {}
	for _, texture in pairs(Type:GetChildren()) do
		if texture:IsA("Texture") then
			typeTextures[texture.Face] = texture
		end
	end

	-- Apply the matching texture (same face) from the type onto the block
	for _, texture in pairs(Block:GetChildren()) do
		if not texture:IsA("Texture") then
			continue
		end

		local matchingTexture = typeTextures[texture.Face]
		if matchingTexture then
			texture.Texture = matchingTexture.Texture
			texture.StudsPerTileU = matchingTexture.StudsPerTileU
			texture.StudsPerTileV = matchingTexture.StudsPerTileV
		end
	end
end

-- Returns a random block template from the Blocks folder.
-- If a Keyword is given, only blocks whose name contains that keyword are considered.
-- Blocks tagged "Exclude" are swapped out for a default "Tiles" block.
local function getRandomBlock(Keyword: string)
	local blocks = Blocks:GetChildren()

	if Keyword then
		local filtered = {}
		for _, block in blocks do
			if block.Name:find(Keyword) then
				table.insert(filtered, block)
			end
		end
		blocks = filtered
	end

	if #blocks == 0 then
		return Blocks.Tiles
	end

	local block = blocks[math.random(1, #blocks)]
	return if block:HasTag("Exclude") then Blocks.Tiles else block
end

-- Creates a simple decorative "ground/gap" part (used for spacing between cells,
-- borders, etc.) at the given position and size, with a grass look and texture.
local function CreateSpacePart(Pos, Size)
	local Space = Instance.new("Part")
	Space.Size = Size
	Space.Anchored = true
	Space.Material = Enum.Material.Grass
	Space.BrickColor = BrickColor.new("White")
	Space.Position = Pos
	Space:AddTag("Ground")
	Space.Parent = MapFolder
	Texture0Template:Clone().Parent = Space
end

-- Builds the four outer walls surrounding the entire grid.
local function CreateWalls()
	local WallYCenter = YStart + WallHeight / 2   -- vertical center of the walls
	local WallYOffset = Vector3.new(0, 1.5, 0)    -- small extra vertical offset

	-- Helper to create one wall part with textures applied to all 6 faces
	local function MakeWall(size, pos)
		local wall = Instance.new("Part")
		wall.Size = RoundTo3(size)
		wall.Position = pos + WallYOffset
		wall.Anchored = true
		wall.Material = Enum.Material.Concrete
		wall.Color = Color3.new(0.258824, 0.258824, 0.258824)
		wall.CastShadow = false
		wall.Name = "Wall"
		wall.Parent = MapFolder

		for i = 1, 6 do
			local texture = TextureTemplate:Clone()
			texture.Face = Enum.NormalId[FACE_NAMES[i]]
			texture.Parent = wall
		end
	end

	-- Far wall (+Z side)
	MakeWall(
		Vector3.new(ExtendedW, WallHeight, WallThickness),
		Vector3.new(GridCX, WallYCenter, MaxZ + SpacingSize + WallThickness / 2)
	)
	-- Near wall (-Z side)
	MakeWall(
		Vector3.new(ExtendedW, WallHeight, WallThickness),
		Vector3.new(GridCX, WallYCenter, MinZ - SpacingSize - WallThickness / 2)
	)
	-- Right wall (+X side)
	MakeWall(
		Vector3.new(WallThickness, WallHeight, ExtendedL),
		Vector3.new(MaxX + SpacingSize + WallThickness / 2, WallYCenter, GridCZ)
	)
	-- Left wall (-X side)
	MakeWall(
		Vector3.new(WallThickness, WallHeight, ExtendedL),
		Vector3.new(MinX - SpacingSize - WallThickness / 2, WallYCenter, GridCZ)
	)
end

-- Builds a flat roof/ceiling covering the whole walled area.
local function CreateRoof()
	local RoofThickness = 10
	local Roof = Instance.new("Part")
	Roof.Size = Vector3.new(ExtendedW, RoofThickness, ExtendedL)
	Roof.Position = Vector3.new(GridCX, YStart + WallHeight + RoofThickness / 2, GridCZ)
	Roof.Anchored = true
	Roof.Material = Enum.Material.Concrete
	Roof.Color = Color3.new(0.258824, 0.258824, 0.258824)
	Roof.CastShadow = false
	Roof.Name = "Roof"
	Roof.Parent = MapFolder
end

-- Fills in the spacing/gap parts that run around the outer edge of the grid
-- (top/bottom/left/right border strips plus the 4 corner pieces).
local function CreateBorderSpacing()
	local HalfSpacing = SpacingSize / 2
	CreateSpacePart(Vector3.new(GridCX, YStart, MaxZ + HalfSpacing), Vector3.new(GridW, 3, SpacingSize))
	CreateSpacePart(Vector3.new(GridCX, YStart, MinZ - HalfSpacing), Vector3.new(GridW, 3, SpacingSize))
	CreateSpacePart(Vector3.new(MaxX + HalfSpacing, YStart, GridCZ), Vector3.new(SpacingSize, 3, GridL))
	CreateSpacePart(Vector3.new(MinX - HalfSpacing, YStart, GridCZ), Vector3.new(SpacingSize, 3, GridL))

	local cornerSize = Vector3.new(SpacingSize, 3, SpacingSize)
	CreateSpacePart(Vector3.new(MaxX + HalfSpacing, YStart,  MaxZ + HalfSpacing), cornerSize)
	CreateSpacePart(Vector3.new(MinX - HalfSpacing, YStart,  MaxZ + HalfSpacing), cornerSize)
	CreateSpacePart(Vector3.new(MaxX + HalfSpacing, YStart,  MinZ - HalfSpacing), cornerSize)
	CreateSpacePart(Vector3.new(MinX - HalfSpacing, YStart,  MinZ - HalfSpacing), cornerSize)
end

--============================================================
-- ACTOR EXECUTION SETUP
-- This script runs both as the "main" coordinator script and,
-- when cloned into Actor instances, as a parallel "worker" script.
-- script:GetActor() tells us which mode we're in.
--============================================================

local NUM_WORKERS = 100

local actor = script:GetActor()
if actor == nil then
	-- ==== MAIN / COORDINATOR MODE ====
	-- Spin up a pool of Actor instances, each running a clone of this script,
	-- so cell generation can happen in parallel across multiple threads.
	local workers = {}
	for i = 1, NUM_WORKERS do
		local workerActor = Instance.new("Actor")
		script:Clone().Parent = workerActor
		table.insert(workers, workerActor)
	end

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

	-- Track how many cells have finished generating so we know when to stop
	local totalCells = Width * Length
	local completedCells = 0
	local doneEvent = Instance.new("BindableEvent")

	-- Fired by each worker once it finishes generating a cell
	local CountEvent = Instance.new("BindableEvent")
	CountEvent.Name = "CellDone"
	CountEvent.Parent = script
	CountEvent.Event:Connect(function()
		completedCells += 1
		if completedCells >= totalCells then
			doneEvent:Fire()
		end
	end)

	-- Dispatch a "GenerateCell" message to workers (round-robin) for every
	-- cell position in the grid
	task.defer(function()
		local workerIndex = 1
		for i = 1, Width do
			for v = 1, Length do
				task.wait(.001) -- small delay to avoid flooding all workers at once
				local X = v * Step - HalfW
				local Z = i * Step - HalfL
				workers[workerIndex]:SendMessage("GenerateCell", i, v, X, Z)
				workerIndex = (workerIndex % NUM_WORKERS) + 1
			end
		end
	end)

	-- Wait until every single cell has reported completion
	doneEvent.Event:Wait()

	-- Clean up the worker actors now that generation is done
	for _, workerActor in workers do
		workerActor:Destroy()
	end

	CountEvent:Destroy()

	-- Finish the map with borders, walls, and a roof
	CreateBorderSpacing()
	CreateWalls()
	CreateRoof()

	return
end

--============================================================
-- WORKER MODE
-- Code below only runs inside the cloned Actor instances.
--============================================================

-- Reference to the shared "CellDone" event on the coordinator script
local CountEvent = script.Parent.Parent:WaitForChild("CellDone")

-- Handles a single "GenerateCell" request: places a random cell preset
-- at the given grid position with a random rotation, skins its blocks,
-- and fills in the small gaps around it.
actor:BindToMessageParallel("GenerateCell", function(i, v, X, Z)
	local Pos = Vector3.new(X, YStart, Z)

	-- Pick a random cell preset and a random 90-degree rotation
	local template = getRandomTemplate()
	local Rotation = CFrame.Angles(0, math.random(1, 4) / 2 * math.pi, 0)

	-- Sync back to the main thread to safely clone/parent instances
	task.synchronize()

	local Cell = template:Clone()
	Cell.Parent = MapFolder

	-- Compute each child part's new world CFrame based on its offset
	-- from the cell's pivot, then move them all in one bulk operation
	local CFList = {}
	local PList = {}
	local Pivot = Cell:GetPivot()

	for idx, block in pairs(Cell:GetChildren()) do
		PList[idx] = block
		local RelativeCFrame = Pivot:ToObjectSpace(block.CFrame)
		CFList[idx] = CFrame.new(Pos) * Rotation * RelativeCFrame
	end

	workspace:BulkMoveTo(PList, CFList, Enum.BulkMoveMode.FireAllEvents)

	-- Pick a random block type to use for this cell's top texture/appearance
	local random = getRandomBlock()
	local Texture = (function()
		for _, texture in pairs(random:GetChildren()) do
			if texture:IsA("Texture") and texture.Face == Enum.NormalId.Top then
				return texture:Clone()
			end
		end

		return random.Texture or Blocks.Tiles.Texture
	end)()

	-- Apply the chosen texture/material/color to the cell's primary part
	Texture.Parent = Cell.PrimaryPart
	Texture.OffsetStudsU = 0
	Texture.OffsetStudsV = 0

	Cell.PrimaryPart.Material = random.Material
	Cell.PrimaryPart.Color = random.Color

	-- If this cell is tagged "Randomize" and contains "Acacia Planks",
	-- randomly decide whether to reskin its Log or Planks pieces to a
	-- different matching block type
	local BlockLarpTarget, Keyword = (function()
		if not Cell:HasTag("Randomize") then return nil, nil end

		if Cell:FindFirstChild("Acacia Planks") then

			local pickedKeyword = (if math.random(1, 2) == 1 then "Log" else "Planks")
			return getRandomBlock(pickedKeyword), pickedKeyword
		end

		return nil, nil
	end)()

	-- Move all "Block"-tagged parts into the shared BlocksFolder,
	-- reskinning matching pieces (Log/Planks) if applicable
	for _, block in ipairs(Cell:GetChildren()) do
		if block:HasTag("Block") then
			block.Parent = BlocksFolder
			block:SetAttribute("Owner", nil)

			if Keyword and block.Name:lower():find(Keyword:lower()) then
				LarpBlock(block, BlockLarpTarget)
			end
		end
	end

	-- (Redundant second pass ensuring all "Block"-tagged parts are
	-- moved into BlocksFolder with no owner set)
	for _, block in ipairs(Cell:GetChildren()) do
		if block:HasTag("Block") then
			block.Parent = BlocksFolder
			block:SetAttribute("Owner", nil)
		end
	end

	-- Fill in the small gap parts between this cell and its neighbors
	-- (only added on the "trailing" side so gaps aren't duplicated)
	if v < Length then
		CreateSpacePart(Vector3.new(X + StepHalf, YStart, Z), RowSize)
	end
	if i < Width then
		CreateSpacePart(Vector3.new(X, YStart, Z + StepHalf), ColSize)
	end
	if v < Length and i < Width then
		CreateSpacePart(Vector3.new(X + StepHalf, YStart, Z + StepHalf), CornerSize)
	end

	-- Report this cell as finished so the coordinator can track progress
	CountEvent:Fire()
end) 

workspace settings:

important


useless

important

ISSUE

The issue is rendering all the blocks (note on average block amount can reach 100k or more with each block having a texture). When standing still fps can reach about 170. which is awesome!!! however when moving fps tanks to like 20 and then shoots up. this is probably because of chunks. How can I fix this???

boring side note: personally the game is fun to me atlest however optimiaztion is KILLING me, generating all these parts in the begginning is laggy enough but I can barely even render them, is there like anything i can do??

game link: voxel 3008 kinda | Play on Roblox

you gotta wait like 20 seconds for it too load tho, and uses 20 by 20 cells instead fo 30

6 Likes

629282029_677162602091756_1356591237761251846_n

3 Likes

3 Likes

It’s most definitely your part count. I would recommend either toying with streaming settings more, or you could opt for dynamic chunk loading, loading chunks only when they’re near players.

I have a generation demo: World Building Algorithm | Play on Roblox click the settings and info tabs and click dev mode, and you can see how the game loads only what’s near the player while being infinite.

2 Likes

Feel like you kinda answered your own reason/question here – instead of just saying its part count, I’m thinking its something to do with how often you’re sending messages to update the blocks around you.

Normally what you’d want is to have a chunkloader system that handles priority of chunks and limits CPU usage based on thread time. If you’re having multiple async tasks (calling ipairs) you’re making a ton of work be done on a ton of parts, meaning a ton of time gets used up. Good way to check this kind of stuff also would be to add debug.profilebegin() throughout this and check the script flamegraph

To see flamegraph, go to Microprofiler and select this tab, this definitely seems like client-side code
image

My biggest concern obviously is this:

actor:BindToMessageParallel("GenerateCell", function(i, v, X, Z)
	local Pos = Vector3.new(X, YStart, Z)

	-- Pick a random cell preset and a random 90-degree rotation
	local template = getRandomTemplate()
	local Rotation = CFrame.Angles(0, math.random(1, 4) / 2 * math.pi, 0)

	-- Sync back to the main thread to safely clone/parent instances
	task.synchronize()

	local Cell = template:Clone()
	Cell.Parent = MapFolder

	-- Compute each child part's new world CFrame based on its offset
	-- from the cell's pivot, then move them all in one bulk operation
	local CFList = {}
	local PList = {}
	local Pivot = Cell:GetPivot()

	for idx, block in pairs(Cell:GetChildren()) do
		PList[idx] = block
		local RelativeCFrame = Pivot:ToObjectSpace(block.CFrame)
		CFList[idx] = CFrame.new(Pos) * Rotation * RelativeCFrame
	end

	workspace:BulkMoveTo(PList, CFList, Enum.BulkMoveMode.FireAllEvents)

	-- Pick a random block type to use for this cell's top texture/appearance
	local random = getRandomBlock()
	local Texture = (function()
		for _, texture in pairs(random:GetChildren()) do
			if texture:IsA("Texture") and texture.Face == Enum.NormalId.Top then
				return texture:Clone()
			end
		end

		return random.Texture or Blocks.Tiles.Texture
	end)()

	-- Apply the chosen texture/material/color to the cell's primary part
	Texture.Parent = Cell.PrimaryPart
	Texture.OffsetStudsU = 0
	Texture.OffsetStudsV = 0

	Cell.PrimaryPart.Material = random.Material
	Cell.PrimaryPart.Color = random.Color

	-- If this cell is tagged "Randomize" and contains "Acacia Planks",
	-- randomly decide whether to reskin its Log or Planks pieces to a
	-- different matching block type
	local BlockLarpTarget, Keyword = (function()
		if not Cell:HasTag("Randomize") then return nil, nil end

		if Cell:FindFirstChild("Acacia Planks") then

			local pickedKeyword = (if math.random(1, 2) == 1 then "Log" else "Planks")
			return getRandomBlock(pickedKeyword), pickedKeyword
		end

		return nil, nil
	end)()

	-- Move all "Block"-tagged parts into the shared BlocksFolder,
	-- reskinning matching pieces (Log/Planks) if applicable
	for _, block in ipairs(Cell:GetChildren()) do
		if block:HasTag("Block") then
			block.Parent = BlocksFolder
			block:SetAttribute("Owner", nil)

			if Keyword and block.Name:lower():find(Keyword:lower()) then
				LarpBlock(block, BlockLarpTarget)
			end
		end
	end

	-- (Redundant second pass ensuring all "Block"-tagged parts are
	-- moved into BlocksFolder with no owner set)
	for _, block in ipairs(Cell:GetChildren()) do
		if block:HasTag("Block") then
			block.Parent = BlocksFolder
			block:SetAttribute("Owner", nil)
		end
	end

	-- Fill in the small gap parts between this cell and its neighbors
	-- (only added on the "trailing" side so gaps aren't duplicated)
	if v < Length then
		CreateSpacePart(Vector3.new(X + StepHalf, YStart, Z), RowSize)
	end
	if i < Width then
		CreateSpacePart(Vector3.new(X, YStart, Z + StepHalf), ColSize)
	end
	if v < Length and i < Width then
		CreateSpacePart(Vector3.new(X + StepHalf, YStart, Z + StepHalf), CornerSize)
	end

	-- Report this cell as finished so the coordinator can track progress
	CountEvent:Fire()
end) 

you’re having it run in pairs tasks on 5-6 times each time a binded message occurs… ton of work running in order, being forced to run in pairs.

(wrote this at 4:17 AM without even checking any of the shit I just said, if anyone wants to correct me feel free)

2 Likes

ughhh i have to “unlock it” do you have a placefile? if your willing to share ofc.

this is for the generation of the blocks which happens in the beginning which you see an issue which ill look at but the issue is the after render, moving around the streaming enabled system makes it that moving around a few cells causes huge frame rate drops temporarily.

Yeah, I sent you a friend request on disc.

I don’t know how much better it makes your script run… but you can use const instead of local when you are calling a variable that does not change… for instance your services

const is identical to local when it’s compiled to bytecode, there wouldn’t be any performance difference.


Also that isn’t even half of my total moderation messages or even a fourth. The moderators are trying to prevent my intelligence and my wisdom from spreading to others. Because it’s genuinely dangerous. This is why people deny my intelligence instead of accepting it because it can genuinely cause severe irreversible brain expansion. Whereas other unintelligents will cause it “irreversible brain damage”. Although in many cases I could literally fry your brain. So I have to be careful from now on. The moderators will likely remove this post too.

1 Like