Infinite tile generation around player

Hello

I am trying to create infinite tiles a player can walk over.
Some keypoints:

  • 9 tiles must always be around the character.
  • The tiles need to stay stationary, they can only change their position on creation.
  • Tiles that don’t need to be recreated need to be reused.

I’m having trouble figuring out whenever the parts need to get updated. I currently have a threshold if a player walks to far from the center, it finds the closest part and builds 9 new parts around that closest part.
However, this can lead to flickering if the code doesn’t exactly find the closest part.

The reusing of tiles is also difficult for me.

Here's my (messy) code:
local floatingPointCorrection = 0.25
local partTable = {}
local updateInProgress = false
-- Standard configuration (indexes of table):
-- 1 = TopLeft
-- 2 = TopMiddle
-- 3 = TopRight
-- 4 = MiddleLeft
-- 5 = MiddleMiddle (reference)
-- 6 = MiddleRight
-- 7 = BottomLeft
-- 8 = BottomMiddle
-- 9 = BottomRight

-- Compute positions around referencePosition (only for squares)
local function positionsAroundReference(refPos, length)
	return {
		[1] = refPos + Vector3.new(-length, 0, length),
		[2] = refPos + Vector3.new(0, 0, length),
		[3] = refPos + Vector3.new(length, 0, length),
		[4] = refPos + Vector3.new(-length, 0, 0),
		[5] = refPos,
		[6] = refPos + Vector3.new(length, 0, 0),
		[7] = refPos + Vector3.new(-length, 0, -length),
		[8] = refPos + Vector3.new(0, 0, -length),
		[9] = refPos + Vector3.new(length, 0, -length),
	}
end

-- Create a part at the specified position
local function createPart(pos)
	local part = Instance.new("Part")
	part.Size = Vector3.new(25, 1, 25)
	part.Anchored = true
	part.CanCollide = true
	part.Position = pos
	part.Parent = workspace
	return part
end

local function cloneTable(src)
	local newTable = {}
	for i, v in pairs(src) do
		newTable[i] = v
	end
	return newTable
end

local function updatePartTable(sourcePart)
	if updateInProgress then
		return
	end
	print("Update")
	updateInProgress = true

	for _, v in pairs(partTable) do
		v:Destroy()
	end
	if sourcePart then
		sourcePart:Destroy()
	end

	for i, newPosition in pairs(positionsAroundReference(sourcePart.Position, sourcePart.Size.X)) do
		partTable[i] = createPart(newPosition)
	end

	-- local newTable = {}
	-- for i, newPosition in pairs(positionsAroundReference(sourcePart.Position, sourcePart.Size.X)) do
	-- 	-- Check if other already created part has the same position
	-- 	for _, v in pairs(partTable) do
	-- 		if (v.Position - newPosition).Magnitude <= floatingPointCorrection then
	-- 			print("Use already created part")
	-- 			table.insert(newTable, i, v)
	-- 			continue -- Jump to next iteration
	-- 		end
	-- 	end
	-- 	-- Create a new part
	-- 	table.insert(newTable, i, createPart(newPosition))
	-- end

	-- -- Remove unused parts
	-- for _, oldPart in pairs(partTable) do
	-- 	local found = table.find(newTable, oldPart)
	-- 	if not found then
	-- 		print("Destroy")
	-- 		oldPart:Destroy()
	-- 	end
	-- end
	-- partTable = cloneTable(newTable)

	updateInProgress = false
end

local sourcePart = createPart(Vector3.new(0, 200, 0))

updatePartTable(sourcePart)

game:GetService("RunService").Heartbeat:Connect(function()
	local char = game:GetService("Players").LocalPlayer.Character
	if char then
		local rootPart = char:FindFirstChild("HumanoidRootPart")
		if rootPart and partTable[5] then
			-- Vector pointing from middle part to HumanoidRootPart (Vector2)
			local rootVector = Vector2.new(rootPart.Position.X, rootPart.Position.Z)
				- Vector2.new(partTable[5].Position.X, partTable[5].Position.Z)
			local distance = rootVector.Magnitude

			if distance > (partTable[5].Size.X * (2 / 3)) then
				-- Player has walked further than max distance --> update parts
				-- local closestPart
				-- local closestDistance = partTable[5].Size.X * 2
				-- for i, part in pairs(partTable) do
				-- 	if i ~= 5 then -- Don't use middle part
				-- 		local newDistance =
				-- 			(Vector2.new(part.Position.X, part.Position.Z) - Vector2.new(rootPart.Position.X, rootPart.Position.Z)).Magnitude
				-- 		if newDistance < closestDistance + (part.Size.X / 12) then -- If part is closer (by a margin)
				-- 			closestPart = part
				-- 			closestDistance = newDistance
				-- 		end
				-- 	end
				-- end
				-- updatePartTable(closestPart)

				-- Get part --> HumanoidRootPart vector with the least angle (get part player is moving towards)
				local directionPart
				local smallestAngle = math.huge
				for i, part in pairs(partTable) do
					if i ~= 5 then -- Don't use middle part
						-- |v1||v2|cos(a) = v1*v2
						-- a=acos((v1*v2)/(|v1||v2|))
						local distVector =
							(
								Vector2.new(part.Position.X, part.Position.Z)
								- Vector2.new(rootPart.Position.X, rootPart.Position.Z)
							)
						local angle =
							math.acos(
								rootVector:Dot(distVector) / (rootVector.Magnitude * distVector.Magnitude)
							)
						-- Always get sharp angle
						if angle > 90 then
							angle = 180 - angle
						end
						-- Check if smaller
						if math.abs(angle) < smallestAngle then
							smallestAngle = angle
                            directionPart = part
						end
					end
				end
                updatePartTable(directionPart)
			end
		end
	end
end)

1 Like

Sorry for the bump, but does anyone have an idea on this? I’m still stuck :upside_down_face:.

Notice that it moves when you’re off the middle

All I did was add a cooldown so you can see

local cooldown = false
local function updatePartTable(sourcePart)
	if updateInProgress or cooldown then
		return
	end
	cooldown = true
	delay(0.5, function()
		cooldown = false
	end)

Hi, thanks for your reply.

The flickering back and forth you see at around 6 seconds in the video you provided is what I want to prevent. I want it to change the parts as little as possible, whilst still achieving an infinite effect.

So I got the reusing of parts working but I can’t seem to fix the flickering issue. Whenever you walk exactly to the corner, the script gets confused about what part is the closest and jumps back and forth very quickly. Causing a lag spike.

Here’s my code now:

local floatingPointCorrection = 0.25
local debounceCorrection = 5
local partTable = {}
local updateInProgress = false
-- Standard configuration (indexes of table):
-- 1 = TopLeft
-- 2 = TopMiddle
-- 3 = TopRight
-- 4 = MiddleLeft
-- 5 = MiddleMiddle (reference)
-- 6 = MiddleRight
-- 7 = BottomLeft
-- 8 = BottomMiddle
-- 9 = BottomRight

-- Compute positions around referencePosition (only for squares)
local function positionsAroundReference(refPart)
	local refPos = refPart.Position
	local length = refPart.Size.X
	return {
		[1] = refPos + Vector3.new(-length, 0, length),
		[2] = refPos + Vector3.new(0, 0, length),
		[3] = refPos + Vector3.new(length, 0, length),
		[4] = refPos + Vector3.new(-length, 0, 0),
		[5] = refPos,
		[6] = refPos + Vector3.new(length, 0, 0),
		[7] = refPos + Vector3.new(-length, 0, -length),
		[8] = refPos + Vector3.new(0, 0, -length),
		[9] = refPos + Vector3.new(length, 0, -length),
	}
end

-- Create a part at the specified position
local function createPart(pos)
	local part = Instance.new("Part")
	part.Size = Vector3.new(25, 1, 25)
	part.Anchored = true
	part.CanCollide = true
	part.Position = pos
	part.Parent = workspace

	-- local part = workspace.Ocean.Plane:Clone()
	-- part.Position += Vector3.new(0, 200, 0)
	-- part.Parent = workspace
	return part
end

local function updatePartTable(sourcePart)
	if updateInProgress then
		return
	end
	print("Update")
	updateInProgress = true

	-- Get new positions
	local newPositions = positionsAroundReference(sourcePart)

	local newTable = {}
	for index, newPos in pairs(newPositions) do
		if index == 5 then
			-- Use sourcePart in the middle
			newTable[index] = sourcePart
		else
			for _, part in pairs(partTable) do
				if (part.Position - newPos).Magnitude <= floatingPointCorrection then
					-- This already created part has the position we want!
					newTable[index] = part
				end
			end

			if not newTable[index] then
				-- Create a new part
				newTable[index] = createPart(newPos)
			end
		end
	end

	-- Remove unused parts
	for _, v in pairs(partTable) do
		local found = table.find(newTable, v)
		if not found then
			v:Destroy()
		end
	end

	-- Update partsTable
	for i, v in pairs(newTable) do
		partTable[i] = v
	end

	updateInProgress = false
end

local sourcePart = createPart(Vector3.new(0, 200, 0))

updatePartTable(sourcePart)

game:GetService("RunService").Heartbeat:Connect(function()
	local char = game:GetService("Players").LocalPlayer.Character
	if char then
		local rootPart = char:FindFirstChild("HumanoidRootPart")
		if rootPart and partTable[5] then
			-- Vector pointing from middle part to HumanoidRootPart (Vector2)
			local rootVector = Vector2.new(rootPart.Position.X, rootPart.Position.Z)
				- Vector2.new(partTable[5].Position.X, partTable[5].Position.Z)
			local distance = rootVector.Magnitude

			if distance > (partTable[5].Size.X * (2 / 3)) then
				-- Player has walked further than max distance --> update parts
				local closestPart
				local closestDistance = partTable[5].Size.X * 2
				for i, part in pairs(partTable) do
					if i ~= 5 then -- Don't use middle part
						local newDistance =
							(Vector2.new(part.Position.X, part.Position.Z) - Vector2.new(rootPart.Position.X, rootPart.Position.Z)).Magnitude
						if newDistance + debounceCorrection < closestDistance then -- If part is closer (by a margin)
							closestPart = part
							closestDistance = newDistance
						end
					end
				end
				updatePartTable(closestPart)
			end
		end
	end
end)