Hi there. So, here’s the situation: I’m trying to create a game featuring procedurally generated rooms, but with a slight twist—they operate in quadrants. Specifically, a “start” room generates a chain of rooms leading to a “finish” room, but in a branching pattern. Naturally, I’m using a 32x32 base grid, with a 512x512 grid surrounding the “start” (green) room. The generation is seed-based. I plan to tweak things later to get the exact result I want, but first, I need to fix the generation foundation. Basically, I want to generate three separate paths that eventually merge into a single path, while allowing for branches that can spawn dead-ends and loops. I wrote the scripts myself based on my knowledge of Studio. Also, the system is similar to the one used in the Roblox game Pressure during the “Operation Heartburn” event.
Here are the reference images and a video showing how the room generation works—both mine and theirs. (Note: I’m also having issues with rooms generating backwards and not aligning with the grid; I suspect I might need a grid underneath as well, though I recall seeing in some dev streams that they might not be doing it that way anymore. I also have some images showing the folder hierarchy.)
Just one more thing: the BoundingBoxes would act like a hitbox to prevent the rooms from overlapping. Also, from what I saw in their dev streams, there is a part called “BoundrySpawner” behind the “exit” part of the “finish” room—it seems to serve as a boundary or limiter, though I’m not sure exactly what for. Anyway, here is my code
local RoomGenerator = {}
local RNG = Random.new()
local Workspace = game:GetService("Workspace")
local PlacedRooms = {}
local GeneratedCount = 0
local FinishModel = nil
local FAIL_LIMIT = 10
local function GetBoundingBoxes(model)
local boxes = {}
for _, v in ipairs(model:GetDescendants()) do
if v:IsA("BasePart") and v.Name == "BoundingBox" then
boxes[#boxes + 1] = v
end
end
return boxes
end
local function GetExits(model)
local exits = {}
for _, v in ipairs(model:GetDescendants()) do
if v:IsA("BasePart") and v.Name == "Exit" then
exits[#exits + 1] = v
end
end
local more = model:FindFirstChild("MoreExits", true)
if more then
for _, v in ipairs(more:GetDescendants()) do
if v:IsA("BasePart") then
exits[#exits + 1] = v
end
end
end
return exits
end
local function ResetExits(model)
for _, exit in ipairs(GetExits(model)) do
exit:SetAttribute("Used", false)
exit:SetAttribute("Closed", false)
exit.Transparency = 0
exit.CanCollide = true
end
end
local function HideExitVisual(exit)
exit.Transparency = 1
exit.CanCollide = false
for _, obj in ipairs(exit:GetDescendants()) do
if obj:IsA("Decal") or obj:IsA("Texture") then
obj.Transparency = 1
elseif obj:IsA("SurfaceGui") then
obj.Enabled = false
elseif obj:IsA("BasePart") then
obj.Transparency = 1
obj.CanCollide = false
end
end
end
local function UpdateExitVisual(exit)
if exit:GetAttribute("Used") or exit:GetAttribute("Closed") then
HideExitVisual(exit)
end
end
local function PickRoom(folder, onlyFirst)
local candidates = {}
local total = 0
for _, room in ipairs(folder:GetChildren()) do
if onlyFirst and room:GetAttribute("FirstRoom") ~= true then
continue
end
local w = math.clamp(room:GetAttribute("SpawnChance") or 100, 0, 100)
if w <= 0 then
continue
end
total += w
candidates[#candidates + 1] = { room = room, weight = w }
end
if #candidates == 0 then
return nil
end
local roll = RNG:NextNumber(0, total)
local acc = 0
for _, c in ipairs(candidates) do
acc += c.weight
if roll <= acc then
return c.room
end
end
return candidates[#candidates].room
end
local function GetRoomTemplates(folder)
local templates = {}
for _, room in ipairs(folder:GetChildren()) do
if room:IsA("Model") then
local weight = math.clamp(room:GetAttribute("SpawnChance") or 100, 0, 100)
if weight > 0 then
templates[#templates + 1] = room
end
end
end
for i = #templates, 2, -1 do
local j = RNG:NextInteger(1, i)
templates[i], templates[j] = templates[j], templates[i]
end
return templates
end
local function IsColliding(boxes, room)
local overlap = OverlapParams.new()
overlap.FilterType = Enum.RaycastFilterType.Exclude
overlap.FilterDescendantsInstances = { room }
for _, b in ipairs(boxes) do
local touching = Workspace:GetPartsInPart(b, overlap)
for _, p in ipairs(touching) do
if p.Name == "BoundingBox" then
return true
end
end
end
return false
end
local function Align(room, targetExit)
local exits = GetExits(room)
if #exits == 0 then
return false
end
local chosen = exits[RNG:NextInteger(1, #exits)]
local modelCF = room:GetPivot()
local offset = modelCF:ToObjectSpace(chosen.CFrame)
local targetCF = targetExit.CFrame * CFrame.Angles(0, math.rad(180), 0)
room:PivotTo(targetCF * offset:Inverse())
return true
end
local function TryGenerate(roomTemplate, targetExit, roomFolder)
local clone = roomTemplate:Clone()
local boxes = GetBoundingBoxes(clone)
if #boxes == 0 and not clone.PrimaryPart then
clone:Destroy()
return nil
end
if #boxes == 0 then
clone:Destroy()
return nil
end
if not Align(clone, targetExit) then
clone:Destroy()
return nil
end
if IsColliding(boxes, clone) then
clone:Destroy()
return nil
end
clone.Parent = roomFolder
PlacedRooms[#PlacedRooms + 1] = { Model = clone }
GeneratedCount += 1
return clone
end
local function CloseExit(exit)
exit.Transparency = 1
exit.CanCollide = false
exit:SetAttribute("Closed", true)
HideExitVisual(exit)
end
local function SpawnDeadEnd(exit, roomFolder, deadFolder)
if exit:GetAttribute("Used") or exit:GetAttribute("Closed") then
return
end
local template = PickRoom(deadFolder, false)
if not template then
CloseExit(exit)
return
end
local result = TryGenerate(template, exit, roomFolder)
if result then
exit:SetAttribute("Used", true)
UpdateExitVisual(exit)
else
CloseExit(exit)
end
end
local function CloseAllUnusedExits(roomFolder, deadFolder, startModel)
for _, data in ipairs(PlacedRooms) do
local model = data.Model
if model ~= startModel and model ~= FinishModel then
for _, exit in ipairs(GetExits(model)) do
if not exit:GetAttribute("Used") then
SpawnDeadEnd(exit, roomFolder, deadFolder)
end
UpdateExitVisual(exit)
end
end
end
end
local function TryForceGenerateFinish(roomTemplate, targetExit, roomFolder)
local clone = roomTemplate:Clone()
local boxes = GetBoundingBoxes(clone)
if #boxes == 0 then
clone:Destroy()
return nil
end
if not Align(clone, targetExit) then
clone:PivotTo(targetExit.CFrame * CFrame.Angles(0, math.rad(180), 0))
end
clone.Parent = roomFolder
PlacedRooms[#PlacedRooms + 1] = { Model = clone }
GeneratedCount += 1
return clone
end
local function GetRoomsByDistanceFromStart(start)
local startPos = start:GetPivot().Position
local rooms = {}
for _, data in ipairs(PlacedRooms) do
local model = data.Model
if model ~= FinishModel then
local dist = (model:GetPivot().Position - startPos).Magnitude
rooms[#rooms + 1] = { Model = model, Distance = dist }
end
end
table.sort(rooms, function(a, b)
return a.Distance > b.Distance
end)
return rooms
end
local function TryFinishOnExit(exit, roomFolder, finishFolder, force)
local templates = GetRoomTemplates(finishFolder)
for _, finishTemplate in ipairs(templates) do
local result
if force then
result = TryForceGenerateFinish(finishTemplate, exit, roomFolder)
else
result = TryGenerate(finishTemplate, exit, roomFolder)
end
if result then
exit:SetAttribute("Used", true)
exit:SetAttribute("Closed", false)
UpdateExitVisual(exit)
FinishModel = result
return true
end
end
return false
end
local function SpawnFinish(start, roomFolder, finishFolder)
local roomsByDistance = GetRoomsByDistanceFromStart(start)
for _, roomData in ipairs(roomsByDistance) do
for _, exit in ipairs(GetExits(roomData.Model)) do
if not exit:GetAttribute("Used") and not exit:GetAttribute("Closed") then
if TryFinishOnExit(exit, roomFolder, finishFolder, false) then
return true
end
end
end
end
for _, roomData in ipairs(roomsByDistance) do
for _, exit in ipairs(GetExits(roomData.Model)) do
if not exit:GetAttribute("Used") and not exit:GetAttribute("Closed") then
warn("[RoomGenerator] Normal Finish failed. Forcing Finish spawn.")
if TryFinishOnExit(exit, roomFolder, finishFolder, true) then
return true
end
end
end
end
for _, roomData in ipairs(roomsByDistance) do
for _, exit in ipairs(GetExits(roomData.Model)) do
warn("[RoomGenerator] No free exit for Finish. Forcing on any available exit.")
if TryFinishOnExit(exit, roomFolder, finishFolder, true) then
return true
end
end
end
return false
end
local function GenerateOnce(seed, maxRooms)
local roomsFolder = Workspace:WaitForChild("Rooms")
local roomFolder = Workspace:WaitForChild("RoomFolder")
local zone = roomsFolder:WaitForChild("Zone")
local start = zone:WaitForChild("Start")
local main = roomsFolder:WaitForChild("MainRooms")
local finish = roomsFolder:WaitForChild("Finish")
local dead = roomsFolder:FindFirstChild("DeadEnds")
RNG = Random.new(seed)
for _, v in ipairs(roomFolder:GetChildren()) do
v:Destroy()
end
PlacedRooms = {}
GeneratedCount = 0
FinishModel = nil
ResetExits(start)
PlacedRooms[#PlacedRooms + 1] = { Model = start }
local queue = GetExits(start)
local fails = 0
while GeneratedCount < maxRooms and fails < FAIL_LIMIT do
if #queue == 0 then
break
end
local exit = table.remove(queue, 1)
if not exit or exit:GetAttribute("Used") or exit:GetAttribute("Closed") then
if exit then
UpdateExitVisual(exit)
end
continue
end
local template = PickRoom(main, GeneratedCount == 0)
if not template then
break
end
local newRoom = TryGenerate(template, exit, roomFolder)
if newRoom then
fails = 0
exit:SetAttribute("Used", true)
UpdateExitVisual(exit)
for _, ex in ipairs(GetExits(newRoom)) do
queue[#queue + 1] = ex
end
else
fails += 1
end
end
local okFinish = SpawnFinish(start, roomFolder, finish)
if okFinish then
if dead then
CloseAllUnusedExits(roomFolder, dead, start)
end
else
warn("[RoomGenerator] Finish could not spawn. DeadEnds were not generated.")
end
return okFinish and GeneratedCount >= math.floor(maxRooms * 0.6)
end
function RoomGenerator.Generate(seed, maxRooms)
local MAX_ATTEMPTS = 20
local attempt = 0
while attempt < MAX_ATTEMPTS do
local currentSeed = seed or (math.floor(os.clock() * 100000) + attempt * 9999)
local success = GenerateOnce(currentSeed, maxRooms)
if success then
print("[RoomGenerator] OK | Seed:", currentSeed, "Rooms:", GeneratedCount)
return
end
attempt += 1
end
warn("[RoomGenerator] Failed after multiple seed attempts")
end
return RoomGenerator
If anyone can help, I’d be very grateful; I’ve been working on this for over two months and don’t know what to do :))

