Models being deleted for no reason?

Hello, I am trying to make a voxel building game with procedurally generated terrain (a.k.a. Minecraft Classic). The world is made out of 4x4x4 blocks, which means that there are going to be a lot of blocks. So I split each 16x128x16 section of the world into chunks. And I am making it so that the client can only see nearby chunks, so I can make the world bigger without making it too laggy.

How it works:

  1. Chunks on the client farther than the threshold get deleted
  2. The client asks the server for the nearby chunks
  3. Each request is put into a queue that gets interpreted every 0.1 seconds. The latest request is prioritized over others.
  4. The server gets the chunk model for the requested chunk. If it doesn’t exist, then the chunk model is created and parented to the server camera (so it doesn’t replicate). A clone of the chunk is created for the player.
  5. The cloned chunk gets parented to the player’s StarterGear (so it only replicates to the targeted client).
  6. The client knows that it has received the chunk and gets parented to the Workspace.

But for some reason when I test it, some of the server chunk models get deleted, even though there are no :Destroy() or :Remove() calls done on a chunk.

Server

replicatedStorage.GetChunk.OnServerInvoke = function(player, cx, cy)
	table.insert(chunkProviderQueue, {player, cx, cy})
	
	while chunkProviderReturn[player] == nil do wait() end
	
	local model = chunkProviderReturn[player]
	
	if model == false then
		chunkProviderReturn[player] = nil
		return nil
	else
		model.Parent = player.StarterGear --kinda hacky lol
		chunkProviderReturn[player] = nil
		
		return model.Name
	end
end

--provide chunks for client
while wait(0.1) do
	if #chunkProviderQueue > 0 then
		local chunkPos = chunkProviderQueue[#chunkProviderQueue] --prioritize clients who asked last
		local player = chunkPos[1]
		local chunkX = chunkPos[2]
		local chunkY = chunkPos[3]
		
		table.remove(chunkProviderQueue, #chunkProviderQueue)
		
		if (
			chunkX < 0 or chunkX > globals.WORLD_WIDTH - 1 or
			chunkY < 0 or chunkY > globals.WORLD_HEIGHT - 1
		)
		then
			chunkProviderReturn[player] = false
			continue
		end
		
		--print("chunk " .. chunkPos[2] .. " " .. chunkPos[3])
		local chunkModel = chunkModels:FindFirstChild("chunk " .. chunkX .. " " .. chunkY)
		
		if chunkModel == nil then
			createChunk(chunkX, chunkY)
				
			--craete neighbor chunks to fix chunk edges
			if chunkX > 0 then createChunk(chunkX - 1, chunkY) end
			if chunkX < globals.WORLD_WIDTH - 1 then createChunk(chunkX + 1, chunkY) end
			if chunkY > 0 then createChunk(chunkX, chunkY - 1) end
			if chunkY < globals.WORLD_HEIGHT - 1 then createChunk(chunkX, chunkY + 1) end
			
			local serverChunkModel = createChunkModel(chunkX, chunkY)
			serverChunkModel.Parent = chunkModels
			
			chunkModel = serverChunkModel:Clone()
		end
		
		chunkProviderReturn[player] = chunkModel
	end
end

Client:

while wait(0.5) do
	if character ~= nil and character.Parent == workspace and character:FindFirstChild("Head") ~= nil then
		local head = character.Head
		local charPosX = math.floor(head.Position.X / (globals.CHUNK_SIZE_X * globals.BLOCK_SIZE))
		local charPosY = math.floor(head.Position.Z / (globals.CHUNK_SIZE_Z * globals.BLOCK_SIZE))
		
		print(charPosX, charPosY)
		
		--delete chunks farther than render dist
        --for now, find/create chunks in a square area. will make it a circle area later.
		for x in pairs(loadedChunks) do
			for y in pairs(loadedChunks) do
				local newx = math.abs(x - charPosX)
				local newy = math.abs(y - charPosY)
				local v = loadedChunks[x][y]
				
				if newx < -RENDER_DIST / 2 and newx > RENDER_DIST / 2 and newy < -RENDER_DIST / 2 and newy > RENDER_DIST / 2 then
					--v:Destroy()
					--loadedChunks[x][y] = nil
				end
			end
		end
		--create chunks inside render dist
		for x=-RENDER_DIST / 2, RENDER_DIST / 2 do
			for y=-RENDER_DIST / 2, RENDER_DIST / 2 do
				if loadedChunks[x] == nil or loadedChunks[x][y] == nil then
					local rx = math.floor(x) + charPosX
					local ry = math.floor(y) + charPosY
					--if outside world, do not create chunk
					if rx < 0 or rx > globals.WORLD_WIDTH - 1 or ry < 0 or ry > globals.WORLD_HEIGHT - 1 then
						continue
					end
					
					local modelName = replicatedStorage.GetChunk:InvokeServer(rx, ry)
					
					if modelName ~= nil then
						local model = player.StarterGear:WaitForChild(modelName) --it takes some time to replicate
						
						model.Parent = workspace
						
						if loadedChunks[x] == nil then
							loadedChunks[x] = {}
						end
						
						loadedChunks[x][y] = model
					end
				end
			end
		end
	end
end

EDIT:
MinecraftClone.rbxl (60.5 KB)

What I would recommend doing is creating folders using instance.new and then naming them by number in order of generation (for instance, the 3rd chunk folder would be named 3.) Then, use the player’s render distance to determine the distance the player needs to be before it disappears. Then, either move that folder to ServerStorage so it isn’t being rendered, or, delete everything inside (note, deleting them will make it to where a different chunk is generated unless it is seed-based.)

Edit: The distance calculations can be done with something having to do with magnitude or something. Don’t take my word for it, I’m not much help with the Roblox engine.

That’s just what I did. I created models for the chunks named “chunk {x} {y}” and I use the player’s render distance to determine the distance the player needs to be before it disappears (except for now it uses a square area instead of a circular. You would get a circular area by using the distance between the two). Then I delete the chunk. The chunk is not deleted on the server, only on the client. But the problem is that the chunks on the server are being deleted for no reason.

The chunk should not deleted on the server so if the player comes across the same chunk again then it will be the same.

Ugh, nevermind. I found out the culprit of the problem. When the model already exists I accidentally made it so it gives the server chunk model instead of a clone of the server chunk model. I guess I didn’t realize this at the time because I was a bit tired.

		local chunkModel = chunkModels:FindFirstChild("chunk " .. chunkX .. " " .. chunkY) --server chunk model
		
		if chunkModel == nil then
			--if chunk model already does exist, this is not important
		end
		
		chunkProviderReturn[player] = chunkModel --chunkModel is server chunk model. uh-oh.
		--instead should be chunkProviderReturn[player] = chunkModel:Clone()
		--and if the chunk model doesn't exist, in the chunkModel == nil statement, chunkModel should just be the server chunk model and not a clone of it, for this change

I also changed the code so that the cloned chunks models are deleted after the requests. But the client clones the model before it gets deleted so it still has it.