[SOLVED] Help with my path generation system. (click me)

this post is solved

Hello, I have a path generation system. I have 3 path so far:

  • Straight Path
  • Left Turn Path
  • Right Turn Path

How my thing works is it randomly selects a path to generate that will connect with the previous path.

The issue is like, imagine this: You’re playing snake, and you run into yourself. Basically 1 path intersects another path (which I don’t like)

Generation Module:

local ss = game.ServerStorage
local pieces = ss.Pieces:GetChildren()

function generate(cframe : CFrame)
	local randomPiece : Model = pieces[math.random(1, #pieces)]:Clone()
	randomPiece.Parent = workspace.Pieces
	randomPiece:PivotTo(cframe)
	return randomPiece
end

return generate

Piece Creation Function (Server Script):

-- Create Piece function --
local function createPiece()
	piecesMade += 1
	local piece : Model = generate(nextCFrame)
	piece.Name = piecesMade
	local endWall : Part = piece.EndWall
	nextCFrame = endWall.CFrame
	
	local connection : RBXScriptConnection
	connection = endWall.Touched:Connect(function(hit)
		if hit.Parent:FindFirstChildOfClass("Humanoid") then
			print(`{hit.Parent} made it through piece #{piece.Name}`)
			createPiece()
			clearOldPieces()
			connection:Disconnect()
		end
	end)
	return piece
end

If anyone knows how to fix I would greatly appreciate it. I’ve been waiting for 2 hours.

Screenshot 2025-09-30 at 8.11.00 PM

1 Like

Can’t you simply try to see if a generated path will overlap? if so then choose another option from the possible options? (of course you can head into a dead end and be stuck but…)
https://create.roblox.com/docs/reference/engine/classes/WorldRoot#GetPartsInPart

1 Like

This does not work for a variety of reasons including but not limited to:

  • Why does your road have humanoid in them?
  • Touched only works on unanchored parts
  • This can be triggered if a player walks on the path

instead do

local partsOverlappingWithRoad = Workspace:GetPartsInPart(part)
for _, part in ipairs(partsOverlappingWithRoad) do
    if part:HasTag("tag your road templates something and paste it here") then
        print(`{hit.Parent} made it through piece #{piece.Name}`)
		createPiece()
		clearOldPieces()
		connection:Disconnect()
    end
end

I would say that your code is the one of the most readable I have seen so far, good job, keep it up!

2 Likes

From experience with my procedural generation logic, you can simply keep track of double turns and remove that direction from the room options if the last turn was the 2nd turn. Ex. two snippets from my room generation module:

	if self.LastTurn == "Left" and self.DoubleLeft then
		--print("Left", self.LastTurn)
		allowedRooms = recursiveRemove(allowedRooms, "Left")
	elseif self.LastTurn == "Right" and self.DoubleRight then
		--print("Right", self.LastTurn)
		allowedRooms = recursiveRemove(allowedRooms, "Right")
	end
	if randomRoom.Name:find("Left") then
		if self.LastTurn == "Left" then
			self.DoubleLeft = true
			self.DoubleRight = false
		end
		
		self.LastTurn = "Left"
	elseif randomRoom.Name:find("Right") then
		if self.LastTurn == "Right" then
			self.DoubleRight = true
			self.DoubleLeft = false
		end
		
		self.LastTurn = "Right"
	end
1 Like

oh i think you misunderstood what that snippet of code does:

what is does is when the player reaches the end of the road it will generate a new road and destroy very old roads

however the code you provided might work so ill try it out, ty

ah ok that makes sense

god i hate the fact that i cant comfortably use self and metatables

@ig1oo1 I tried making something like yours but I’m still getting those annoying loops:

repeat
		task.wait()
		piece = generate(nextCFrame)
		if piece.Name:find("Right") and lastTurn == "Right" then
			piece:Destroy()
		else
			lastTurn = "Right"
		end
		
		if piece.Name:find("Left") and lastTurn == "Left" then
			piece:Destroy()
		else
			lastTurn = "Left"
		end
		
	until (piece ~= nil)

@Jeremylin080114 I tried your strategy, it works however

.this happens.

repeat
		task.wait()
		piece = generate(nextCFrame)
		local floors = piece.Floors
		for _, floor in floors:GetChildren() do
			for _, hit in workspace:GetPartsInPart(floor, overlapParams) do
				if not hit then continue end
				if hit.Parent.Parent:HasTag("Piece") and hit.Parent.Parent ~= piece and hit.Parent.Parent.Name ~= tostring(piecesMade) then
					print(piece:GetFullName())
					print(hit:GetFullName())
					piece:Destroy()
					return
				end
			end
		end
	until (piece ~= nil)
	piecesMade += 1
1 Like

Sorry about that, I think my code was slightly unclear. Also, forgive me if I get something incorrect, I made the generation module a while ago.


I believe this is the problem. If you look at my code, I was tracking the last turn that was made - not just the current turn. So what I did was check if the last turn was right and DoubleRight is true, then I would remove all right turns from the list of room options. Same thing for left turns. Now, for the double turn detection, all I did was check if the current room is a right turn and the last turn was also a right turn. If both conditions are true, I set DoubleRight to true, and DoubleLeft to false. Again, same thing with left turns.

1 Like

I personally believe this is a very crude and a bad way of solving this problem. While it should technically work in theory (aside from the dead-ends), it still doesn’t guarantee that there will be no problems. In something like this dealing with procedural generation, you should almost always rely on solid logic to ensure that everything works fine.

1 Like

If your connectors could be placed in uniform bounding boxes, you could possibly discretize your “possible placement grid” into cells of uniform size. Then, you could just check if the corresponding cells of the 3 possible connector placements are occupied and avoid them from your available connectors list.

1 Like

Yes, like ABizzare_Dummy pointed out, using collision is inefficient, however, in this case if grid works then this should work too.

I would also like to point out that just like in the snake game, it is possible to get yourself into a position where it would be impossible to not hit one’s own tail (e.g. by drawing a square with only 1 opening, and entering through that opening)

Example

Is it a hard requirement that the paths must not de-spawn? How are the paths stored? linked list? perhaps if a collision is inevitable, cut off the tail?

1 Like

I don’t understand. What my code is doing is it is detecting if the new piece is a right turn and if the last turning piece was a right turn. If both of these are true, it resets, generating a different piece until the criteria are met. To me, it seems like both of our scripts do the same thing, detect double turns.

Sorry if I didn’t understand that well, I’m having to deal with some overly authorative parents right now.

1 Like

I have a folder in ServerStorage with all the pieces inside

Well, how the game works is you run through a series of generated tunnels from a wall that will kill you. With a bit of tweaking, I could probably make it so that the tunnels are able to despawn quickly without any issues.

Tell me about it.

Alright. I understand the problem now.

It’s the way your if statements are organized. You are resetting the last turn to left (or right) even if the piece.Name doesn’t contain “Left” (or “Right”). Try this code;

repeat
    task.wait()
    piece = generate(nextCFrame)

    local isRight = piece.Name:find("Right")
    local isLeft = piece.Name:find("Left")

    if isRight then
        if lastTurn == "Right" then
            piece:Destroy()
            continue
        end
        lastTurn = "Right"
    elseif isLeft then
        if lastTurn == "Left" then
            piece:Destroy()
            continue
        end
        lastTurn = "Left"
    end

until piece ~= nil

Let me know if you need more help understanding it.

Edit: Also, double turns are fine, it’s just the triple turns that we want to avoid

2 Likes

Yo, how does my solution work?

1 Like

I don’t understand how preventing triple turns is a full solution for self-avoiding

1 Like

Can’t you also prune the parts of the path where the wall has already passed? This might make for more interesting play

1 Like

It prevents looping in on itself. I can provide an example of this logic working if you would like.

1 Like

AHA

now i get it

now i get it (x2)

ohhh now i get it (x3)

1 Like

EDIT: I was able to fix the bug by setting the piece variable to nil after the piece:Destroy()

I set it to generate 500 times and I did not find a single crossover. Additionally, I like how it makes the path a lot more straight :slight_smile:

Previous Post

@ig1oo1 I found a bug and I was wondering if you could help me fix

So basically a few lines below the repeat segment I have a variable that is assigned to the piece’s endPart. However, it seems like the contents of the piece are being destroyed before it reaches that part.

I made a small segment that makes a clone of the piece before that variable assignment for testing,

Edit: I added a print statement before the piece:Destroy() and it prints very shortly before the error occurs.
Screenshot 2025-10-05 at 8.08.10 PM

Code:

repeat
		task.wait()
		piece = generate(nextCFrame)

		local isRight = piece.Name:find("Right")
		local isLeft = piece.Name:find("Left")

		if isRight then
			if lastTurn == "Right" then
				piece:Destroy()
				continue
			end
			lastTurn = "Right"
		elseif isLeft then
			if lastTurn == "Left" then
				piece:Destroy()
				continue
			end
			lastTurn = "Left"
		end

	until (piece)
	piecesMade += 1
	piece.Name = piecesMade
	piece:Clone().Parent = workspace
	print(piece)
	local endWall : Part = piece.EndWall -- Errors here that EndWall doesn't exist


As you can see

  1. There is no 5th model in the main folder (idk why)
  2. The 5th model that was seperately cloned is empty (idk why)

If you can figure out why this is happening i would appreciate it