Help with Procedural Generation's Dead Ends

Hello! I’ve been working on a procedural generation module for my upcoming game, but I’ve noticed that it generates too many dead ends, as it’s unable to link up with other generated rooms. I’m in need of assistance to solve this issue - since I believe it’ll affect gameplay a lot if not resolved.

Some background on the way rooms are generated:
The rooms are chosen randomly from a folder under ServerStorage and check if it’s able to place it, if not it fires the function again in search of a different room that can fill the gap. If no rooms are able to do this - a “dead end” part is added and positioned onto a connector point.

If any of you need more information on how it works, feel free to ask. I’ve been encountering this issue for the past few days and I have NO absolute idea of how to solve it, any help will be appreciated.

1 Like

I think we are going to need a snippet of the code, if not as much as possible. There are things like connector points (where rooms connect), rules by which rooms spawn, and how the dead ends are handled that we need to know.

Hey, sorry for the late response. Here’s a snippet of the main generation function:

RoomModule.GenerateRoom = function(TargetPoint: Part, PreviousRoom: Model, RetryCount: number)
	if RetryCount == nil then RetryCount = 0 end

	if (RoomModule.Database.GeneratedRooms < RoomModule.RoomLimit) and (TargetPoint) then
		local TargetCFrame = TargetPoint.CFrame
		TargetPoint:Destroy()

		local Room = GetRandomRoom(PreviousRoom)
		Room.Parent = GeneratedRooms

		local TouchConnection = Room.Hitbox.Touched:Connect(function() end)

		local SelectedPointIndex = RoomModule.Random:NextInteger(1, #Room.Points:GetChildren())
		local SelectedPoint = Room.Points:GetChildren()[SelectedPointIndex]
		local SelectedPoints = Room.Points:GetChildren()

		RoomModule.Database.GeneratedRooms += 1
		Room.PrimaryPart = SelectedPoint
		Room:PivotTo(TargetCFrame * CFrame.new(0, 0, -1) * CFrame.Angles(0, math.rad(180), 0))
		Room.PrimaryPart:Destroy()

		for _, Part in pairs(Room.Hitbox:GetTouchingParts()) do
			task.wait()

			if not Part:IsDescendantOf(Room) then
				Room:Destroy()
				Room = nil

				-- If the room placement fails, retry with a different room
				if RetryCount < 5 then
					RoomModule.GenerateRoom(TargetPoint, PreviousRoom, RetryCount + 1)
					return
				else
					AddDeadEnd(TargetPoint.CFrame)
					break
				end
			end
		end

		TouchConnection:Disconnect()
		PreviousRoom = Room

		RoomModule.Furniture.new(Room)

		table.remove(SelectedPoints, SelectedPointIndex)

		task.wait(0.1)

		task.spawn(function()
			if (Room) then
				for Index, Point in pairs(SelectedPoints) do
					RoomModule.GenerateRoom(Point, PreviousRoom)
				end
			end
		end)
	elseif (TargetPoint) then
		AddDeadEnd(TargetPoint.CFrame)
	end
end

The functions used within this function should be pretty self-explanatory, if you want me to elaborate on them feel free to ask.

1 Like

Could you tell me more about how targetpoint is selected? Any other details that can be mentioned?

Ill take a look at this later today, its a bit complex and even though I have a rough idea of how it works, I would need to analyze this code for longer to fully understand it. Ill get back to you in about a couple of hours.

1 Like

Alright. Each room has a folder called “Points”, the “TargetPoint” is one of the parts under that folder. The generation function chooses one of these points (the function is originally given a starting point to continue from), and generates a room to add onto that point (one way you could look at this is like a Lego brick, it continues to stack until it’s told to stop). The script then uses a hitbox to determine if it’s not intersecting with another room, if not it’ll generate ANOTHER room from the generated room’s points. However, if it’s intersecting then it’ll destroy the room and add a dead end.

I’ve also attached a video of the rooms generating as a visual example.

image

1 Like

Thank you for providing the visual example, it helped me immediately understand how your code works.

Before I proceed with a more formal proposal, theres one thing that I noticed in the video which I believe needs to be resolved or clarified. In your video, from 0:20 to 0:22, there was a room trying to spawn with no visible collisions blocking its path. However, none of the rooms spawned. Is this an error or intended behavior?

Moving onto the main topic: Unfortunately, solving the problem of dead ends as you want it will take a lot of ingenuity and work, as far as I can tell. I don’t think it’s impossible. However, this may only work for about half of dead end cases you want to fix.

I think it would involve:

  1. adding as many connectionpoint places on your rooms as possible (even hallways). You can make two types of connectionpoints, (1) a dual type connection point, that works for the actual connection of rooms like you have currently and also for connecting a “dead end” connection point to an unused connection point in another room, and (2) a connection point SOLELY for connecting a “dead end” connection point to an unused connection point in another room.

  2. Make the connection point areas (even for both types of connection points as described above) to initially have “open” or “exposed” walls, like you are currently doing for connection points. Make sure these gaps are being filled in with walls at the very end of everything if they were not used to connect rooms.

  3. Do not resolve dead ends (do not close the wall off) immediately. Leave that for either what im about to say nedt will happen (connecting a dead end! :slight_smile:), or for the italicized sentence in point 2 to happen (dead end will happen :frowning: ).

  4. Implement something along the lines of this, right before the step of closing off the walls and right after all rooms have been placed:

local connectedPoints = {}

local function isConnected(point)
    return connectedPoints[point] == true
end

function processConnectionPoint(TargetPoint)
	if not isConnected(TargetPoint) then -- You need to identify these connected target points each time you spawn a room
		local nearbyPoints = FindNearbyPoints(TargetPoint)

		for _, point in pairs(nearbyPoints) do
			ConnectRooms(point.Room, TargetPoint.Room) -- Algorithm to connect both points. This will be challenging to implement and will require some knowledge of geometry.

			if noCollisions() then -- Do a collision check for this newly created room
				connectedPoints[TargetPoint] = true
				connectedPoints[point] = true
				return true 
			end
		end
	end

	return false 
end

for _, room in pairs(AllRooms) do -- AllRooms should be every single room spawned
	for _, TargetPoint in pairs(room.Points:GetChildren()) do
		processConnectionPoint(TargetPoint) -- Call the function for each connection point
	end
end

I may have implemented or wrote this incorrectly, but it should look something like that. The FindNearbyPoints(TargetPoint) function could look something like this:

function FindNearbyPoints(TargetPoint)
    local nearbyPoints = {}
    -- Iterate through all rooms or connection points, identifying ones within a certain range
    for _, room in pairs(AllRooms) do
        for _, point in pairs(room.Points:GetChildren()) do
            if (point.Position - TargetPoint.Position).Magnitude < CONNECTION_RANGE then
                table.insert(nearbyPoints, point)
            end
        end
    end
    return nearbyPoints
end

As for ConnectRooms(point.Room, TargetPoint.Room), this will require a great deal of geometry and mathematics to size a room to connect to a part. I think having a room that can dynamically change its sizes (like an xz length that can change sizes in any direction to be able to connect to another connection point) would be the way to go. Its walls would also have to size as well. If theres a connection a floor above or below (which the differences in Y between the connection parts would tell as well), perhaps the first step of this function would be to spawn the stair room you have, and then go from there to connect it to the open connection point of another room via x and z sizings of an additional fledible room part. I have a rough idea of how I could implement it in my mind, but it would take a lot of time to pan it out. Maybe if I have time in the net couple of days, I might provide something that may complete this function.

EDIT: In the FindNearbyPoints function, you may want to do a range check for the rooms (a slightly bigger range than the connecting parts check) as well as a range check for the connecting parts. This is done to increase the performance and speed of the code.

Thats all for now : )

1 Like

Thank you so much! I haven’t fully digested everything you’ve mentioned, but I do appreciate the detailed reply! I’m going to give this a read and update you if I manage to solve it.

As for this, the stairs “room” has a really big hitbox compared to other rooms. None of the rooms spawned in as the room generation function detected that it would be intersecting with the stairs. It might not look like it’s intersecting any of the visual parts, but it IS intersecting the hitbox.

For the time being, I’ll mark your reply as the solution.

1 Like

I did not think of that, makes sense since hitboxes have to be a rectangular prism. That being said, you might want to make a special “room” with only just the stairs part (no extra length attached to it) for connecting a dead end to a connecting part on a different Y level to have more space to add on the xz part to align it by the xz axes as well. I know its really confusing what Im trying to say because me myself I am struggling to explain it. Ill try to clarify when I have more time and perhaps add to that connectrooms function.

Also as a sidenote, my final implemented dead end fix may double the total time it takes to render your map (I am not entirely sure but its possible). Hopefully not, but lets see.

How did it go? Did you manage to make this work, or got stuck at a certain point?

Haven’t tested it out yet, busy with some other important parts for my game at the moment. I’ll have to give it a test later on.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.