Terrain Destruction/Regeneration

trying to make a terrain destruction/regeneration system, but the issue comes at regeneration. I assume the issue is data is being overwritten for intersecting chunks, and so the destroyed data is the one being used for regeneration aswell, not entirely sure how to fix it.

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local RunService = game:GetService("RunService")
local Terrain = workspace.Terrain

local destructionRadius = 5
local gridResolution = 4
local destructionDelay = 0.05
local regenerationDelay = 5
local regenerationStep = 0.1 

local destroying = false
local activeRegions = {} 

local function getRegionId(region)
	return tostring(region.CFrame) .. tostring(region.Size)
end

local function regenerateTerrain(regionId)
	local data = activeRegions[regionId]
	if not data or data.state ~= "destroying" or data.regenerating ~= true then
		return
	end

	local region = data.region
	local material = data.material
	local originalOccupancy = data.occupancy

	local currentOccupancy = {}
	for x = 1, #originalOccupancy do
		currentOccupancy[x] = {}
		for y = 1, #originalOccupancy[x] do
			currentOccupancy[x][y] = {}
			for z = 1, #originalOccupancy[x][y] do
				currentOccupancy[x][y][z] = 0
			end
		end
	end

	while true do
		local allDone = true
		for x = 1, #currentOccupancy do
			for y = 1, #currentOccupancy[x] do
				for z = 1, #currentOccupancy[x][y] do
					local target = originalOccupancy[x][y][z]
					local diff = target - currentOccupancy[x][y][z]
					if math.abs(diff) > 0.01 then
						currentOccupancy[x][y][z] = currentOccupancy[x][y][z] + regenerationStep * diff
						allDone = false
					else
						currentOccupancy[x][y][z] = target
					end
				end
			end
		end

		Terrain:WriteVoxels(region, gridResolution, material, currentOccupancy)

		if allDone then break end
		task.wait(0.05)
	end
	
	activeRegions[regionId] = nil
end

local function destroyTerrain(position, radius)
	local region = Region3.new(
		position - Vector3.new(radius, radius, radius),
		position + Vector3.new(radius, radius, radius)
	):ExpandToGrid(gridResolution)

	local regionId = getRegionId(region)

	if activeRegions[regionId] and activeRegions[regionId].state == "destroying" then
		return
	end

	local material, occupancy = Terrain:ReadVoxels(region, gridResolution)

	activeRegions[regionId] = {
		region = region,
		material = material,
		occupancy = occupancy,
		state = "destroying",
		regenerating = false 
	}

	local airMaterials = {}
	local airOccupancy = {}
	for x = 1, #occupancy do
		airMaterials[x] = {}
		airOccupancy[x] = {}
		for y = 1, #occupancy[x] do
			airMaterials[x][y] = {}
			airOccupancy[x][y] = {}
			for z = 1, #occupancy[x][y] do
				airMaterials[x][y][z] = Enum.Material.Air
				airOccupancy[x][y][z] = 0
			end
		end
	end

	Terrain:WriteVoxels(region, gridResolution, airMaterials, airOccupancy)

	task.delay(regenerationDelay, function()
		if activeRegions[regionId] and not activeRegions[regionId].regenerating then
			activeRegions[regionId].regenerating = true
			regenerateTerrain(regionId)
		end
	end)
end


mouse.Button1Down:Connect(function()
	destroying = true
	while destroying do
		local targetPosition = mouse.Hit.Position
		destroyTerrain(targetPosition, destructionRadius)
		task.wait(destructionDelay)
	end
end)

mouse.Button1Up:Connect(function()
	destroying = false
end)

Isn’t the potential problem in how you’re making the regions?

1 Like

can you elaborate?

If you have an issue that you suspect is due to overlapping regions then what you’d need to check is the logic that divides the terrain into regions, you could have logic that checks for overlap and handles 2 regions that have “ownership” of a volume of terrain.

well that’s the issue, the overlapping regions. how am I meant to identify they’re overlapping and after I’ve done that, how would I determine which one has the correct (undestroyed) data?

I don’t know this would be implemented, but I feel like the only solution is to save the initial state of the terrain when the game starts, and somehow use that saved data for regeneration. But again, not entirely of how to do so.

As for identifying overlapping you can keep the original regions stored and write a simple utility function which you should probably memoise.

-- this is parallelisable if you want to use actors, and you can memoise it or just store its result in `data`
local function doRegionsIntersect(a: Region3, b: Region3): boolean
    local displacement = a.CFrame.Position - b.CFrame.Position
    local distanceLimit = (a.Size + b.Size) / 2
    return math.abs(displacement.X) <= distanceLimit.X
        and math.abs(displacement.Y) <= distanceLimit.Y
        and math.abs(displacement.Z) <= distanceLimit.Z
end

Then in your data you have an intersecting key that contains a list of all the intersecting regions for a given region. I think you could probably resolve conflicts by timestamping.

1 Like

I don’t understand how this would work, for a theoretical implementation:

I save all to-be regenerated regions in a table, I loop through and check them all for intersections, and I find the earliest timestamp data of the intersection, here’s the questions:

  1. How do I fill in ONLY the place of intersection?
  2. How do I then proceed to handle the rest of the chunks which are partially intersecting? how do I apply their data only to the remaining area away from the intersection?

I think there has to be a better implementation for this

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local RunService = game:GetService("RunService")
local Terrain = workspace.Terrain

local destructionRadius = 5
local gridResolution = 4
local destructionDelay = 0.05
local regenerationDelay = 5
local regenerationStep = 0.1

local destroying = false
local activeRegions = {}

local function getRegionId(region)
	return tostring(region.CFrame) .. tostring(region.Size)
end

local function intersectRegions(region1, region2)
	local min1, max1 = region1.CFrame.Position - region1.Size / 2, region1.CFrame.Position + region1.Size / 2
	local min2, max2 = region2.CFrame.Position - region2.Size / 2, region2.CFrame.Position + region2.Size / 2

	local minIntersect = Vector3.new(
		math.max(min1.X, min2.X),
		math.max(min1.Y, min2.Y),
		math.max(min1.Z, min2.Z)
	)

	local maxIntersect = Vector3.new(
		math.min(max1.X, max2.X),
		math.min(max1.Y, max2.Y),
		math.min(max1.Z, max2.Z)
	)

	if minIntersect.X <= maxIntersect.X and minIntersect.Y <= maxIntersect.Y and minIntersect.Z <= maxIntersect.Z then
		local intersectionSize = maxIntersect - minIntersect
		local intersectionCFrame = CFrame.new((minIntersect + maxIntersect) / 2)
		return Region3.new(minIntersect, maxIntersect):ExpandToGrid(gridResolution)
	else
		return nil -- No intersection
	end
end

local function fakeRegion3(min,max)
	return {
		Min = min,
		Max = max,
	}
end
local function regenerateTerrain(regionId)
	local data = activeRegions[regionId]
	if not data or data.state ~= "destroying" or data.regenerating ~= true then
		return
	end

	local region = data.region
	local material = data.material
	local originalOccupancy = data.occupancy

	local currentOccupancy = {}
	for x = 1, #originalOccupancy do
		currentOccupancy[x] = {}
		for y = 1, #originalOccupancy[x] do
			currentOccupancy[x][y] = {}
			for z = 1, #originalOccupancy[x][y] do
				currentOccupancy[x][y][z] = 0
			end
		end
	end

	while true do
		local allDone = true
		for x = 1, #currentOccupancy do
			for y = 1, #currentOccupancy[x] do
				for z = 1, #currentOccupancy[x][y] do
					local target = originalOccupancy[x][y][z]
					local diff = target - currentOccupancy[x][y][z]
					if math.abs(diff) > 0.01 then
						currentOccupancy[x][y][z] = currentOccupancy[x][y][z] + regenerationStep * diff
						allDone = false
					else
						currentOccupancy[x][y][z] = target
					end
				end
			end
		end

		Terrain:WriteVoxels(region, gridResolution, material, currentOccupancy)

		if allDone then break end
		task.wait(0.05)
	end

	activeRegions[regionId] = nil
end

local function subtractIntersection(region, excludedRegion)
	local min1, max1 = region.CFrame.Position - region.Size / 2, region.CFrame.Position + region.Size / 2
	local min2, max2 = excludedRegion.CFrame.Position - excludedRegion.Size / 2, excludedRegion.CFrame.Position + excludedRegion.Size / 2

	-- Calculate the overlapping area
	local newMin = Vector3.new(
		math.max(min1.X, min2.X),
		math.max(min1.Y, min2.Y),
		math.max(min1.Z, min2.Z)
	)

	local newMax = Vector3.new(
		math.min(max1.X, max2.X),
		math.min(max1.Y, max2.Y),
		math.min(max1.Z, max2.Z)
	)

	-- If there’s no overlap, return the original region
	if newMin.X > newMax.X or newMin.Y > newMax.Y or newMin.Z > newMax.Z then
		return region
	end

	-- Calculate the remaining parts of the region
	local remainingRegions = {}

	-- Split the original region into sub-regions excluding the intersection
	for _, corner in ipairs({
		{Vector3.new(min1.X, min1.Y, min1.Z), Vector3.new(max1.X, max1.Y, newMin.Z)}, -- Front slice
		{Vector3.new(min1.X, min1.Y, newMax.Z), Vector3.new(max1.X, max1.Y, max1.Z)}, -- Back slice
		{Vector3.new(min1.X, min1.Y, newMin.Z), Vector3.new(newMin.X, max1.Y, newMax.Z)}, -- Left slice
		{Vector3.new(newMax.X, min1.Y, newMin.Z), Vector3.new(max1.X, max1.Y, newMax.Z)}, -- Right slice
		{Vector3.new(min1.X, newMax.Y, newMin.Z), Vector3.new(max1.X, max1.Y, newMax.Z)}, -- Top slice
		{Vector3.new(min1.X, min1.Y, newMin.Z), Vector3.new(max1.X, newMin.Y, newMax.Z)}, -- Bottom slice
		}) do
		local cornerMin, cornerMax = corner[1], corner[2]
		if cornerMin.X <= cornerMax.X and cornerMin.Y <= cornerMax.Y and cornerMin.Z <= cornerMax.Z then
			table.insert(remainingRegions, Region3.new(cornerMin, cornerMax))
		end
	end

	-- Return the remaining regions that don’t overlap
	return remainingRegions
end

local function destroyTerrain(position, radius)
	local region = Region3.new(
		position - Vector3.new(radius, radius, radius),
		position + Vector3.new(radius, radius, radius)
	):ExpandToGrid(gridResolution)

	local remainingRegions = {region}

	-- Check for intersecting regions and subtract their intersections
	for existingRegionId, data in pairs(activeRegions) do
		if data.state == "destroying" then
			local excludedRegion = data.region
			local newRemainingRegions = {}

			for _, r in ipairs(remainingRegions) do
				local subtracted = subtractIntersection(r, excludedRegion)
				
				if type(subtracted) == "table" then
					for _, subRegion in ipairs(subtracted) do
						table.insert(newRemainingRegions, subRegion)
					end
				else
					table.insert(newRemainingRegions, subtracted)
				end
			end

			remainingRegions = newRemainingRegions
			task.wait()
		end
	end

	-- If there's no region left to destroy, return
	if #remainingRegions == 0 then return end

	-- Process each remaining region
	for _, r in ipairs(remainingRegions) do
		local regionId = getRegionId(r)

		if activeRegions[regionId] and activeRegions[regionId].state == "destroying" then
			return
		end

		local material, occupancy = Terrain:ReadVoxels(r, gridResolution)

		activeRegions[regionId] = {
			region = r,
			material = material,
			occupancy = occupancy,
			state = "destroying",
			regenerating = false
		}

		-- Prepare air materials and occupancy for destruction
		print(occupancy)
		
		
		local airMaterials = {}
		local airOccupancy = {}
		for x = 1, #occupancy do
			airMaterials[x] = {}
			airOccupancy[x] = {}
			for y = 1, #occupancy[x] do
				airMaterials[x][y] = {}
				airOccupancy[x][y] = {}
				for z = 1, #occupancy[x][y] do
					airMaterials[x][y][z] = Enum.Material.Air
					airOccupancy[x][y][z] = 0
				end
			end
		end

		-- Destroy terrain by writing air voxels
		Terrain:WriteVoxels(r, gridResolution, airMaterials, airOccupancy)

		-- Schedule regeneration
		task.delay(regenerationDelay, function()
			if activeRegions[regionId] and not activeRegions[regionId].regenerating then
				activeRegions[regionId].regenerating = true
				regenerateTerrain(regionId)
			end
		end)
	end
end


mouse.Button1Down:Connect(function()
	destroying = true
	while destroying do
		local targetPosition = mouse.Hit.Position
		destroyTerrain(targetPosition, destructionRadius)
		task.wait(destructionDelay)
	end
end)

mouse.Button1Up:Connect(function()
	destroying = false
end)

I ended up implementing this, where basically before I destroy I check all intersections and end up creating a few regions which don’t intersect. But this lags like CRAZY because ofcourse it’s creating a million region3s, any ideas I can optimize this?

Something like an octree can reduce ur search IDK.