[Need more Feedback] Feedback on pregenerted room generation game

My game generates at the start with 50x50 grid room adding up to 5000 rooms. The game is not always procedural like a roguelike. Instead, the player explores a large, pregenerated world that remains consistent throughout the session. Each time rare and uncommon rooms are placed in a diffrent place.

Does this method keep exploration interesting?
What needs improvements?

Game: The rooms (alpha) - Roblox

2 Likes

I think the idea is great.

If it gets to monotonous, players will leave.

You can add excitement by making more diversity in less rare rooms, by adding coordinates, by fixing the doorways, and popular for these types of games is a looting system.

2 Likes

Thanks for your feedback! I will consider adding the looting system and other features.

1 Like

my character was too big adjust the game setting to only allow smaller avatars or increase the door size. because of that i’m not really sure but i think that each of the rooms have 4 doors if you want more of a maze then i would make it have chances of different ones my friend made this a while a go not sure how it works but he gave it to me

Replicatedstorage, Module

-- Generates a maze grid and places room prefabs rotated to match door openings.
-- Assumes 50x50 room footprint and models in ReplicatedStorage.Rooms
-- Base direction convention (unrotated prefabs):
--   North = -Z, East = +X, South = +Z, West = -X

local RoomMaze = {}

local RS = game:GetService("ReplicatedStorage")

-- Bitmask flags for door openings
local DIR = { N = 1, E = 2, S = 4, W = 8 }
local ORDER = { "N", "E", "S", "W" } -- for rotation logic

-- Opposite direction lookup
local OPP = { [DIR.N]=DIR.S, [DIR.E]=DIR.W, [DIR.S]=DIR.N, [DIR.W]=DIR.E }

local bit32 = bit32  -- shorthand

-- Rotate a door bitmask 90° clockwise 'times' times
local function rotateMask(mask, times)
	times = times % 4
	if times == 0 then return mask end
	local result = 0
	-- map N->E->S->W->N using ORDER = { "N","E","S","W" }
	for i, name in ipairs(ORDER) do
		local bit = DIR[name]
		if bit32.band(mask, bit) ~= 0 then
			-- shift index by 'times', wrap 1..4
			local newIndex = ((i - 1 + times) % 4) + 1
			local newName = ORDER[newIndex]
			result = bit32.bor(result, DIR[newName])
		end
	end
	return result
end

-- Tile set: base model + its base mask (doors before rotation)
-- Only ONE of each needed; we rotate to fit any mask variant.
local function getTileSet(roomsFolder)
	return {
		{ name = "Room_1Door",          baseMask = DIR.N },                                              -- N
		{ name = "Room_2Door_Straight", baseMask = bit32.bor(DIR.N, DIR.S) },                            -- N+S
		{ name = "Room_2Door_Corner",   baseMask = bit32.bor(DIR.N, DIR.E) },                            -- N+E
		{ name = "Room_3Door",          baseMask = bit32.bor(DIR.N, DIR.E, DIR.S) },                     -- N+E+S
		{ name = "Room_4Door",          baseMask = bit32.bor(DIR.N, DIR.E, DIR.S, DIR.W) },              -- N+E+S+W
	}, roomsFolder
end

-- Find a prefab + rotation that matches a desired door mask
local function choosePrefabForMask(mask, roomsFolder)
	local tileSet = getTileSet(roomsFolder)
	-- Try most specific to least? We can just loop; 4Door will match only 15 anyway.
	for _, entry in ipairs(tileSet) do
		local model = roomsFolder:FindFirstChild(entry.name)
		if model and model:IsA("Model") then
			for rot = 0, 3 do
				if rotateMask(entry.baseMask, rot) == mask then
					return model, rot
				end
			end
		end
	end
	-- Fallback: if mask somehow 0 (shouldn't happen), place a 1Door anyway.
	local one = roomsFolder:FindFirstChild("Room_1Door")
	if one then
		return one, 0
	end
	warn("No matching prefab found for mask: ", mask)
	return nil, 0
end

-- Shuffle helper for random traversal
local function shuffledDirs(rng)
	local dirs = { DIR.N, DIR.E, DIR.S, DIR.W }
	for i = #dirs, 2, -1 do
		local j = rng:NextInteger(1, i)
		dirs[i], dirs[j] = dirs[j], dirs[i]
	end
	return dirs
end

local bit32 = bit32

-- Generate a perfect maze: returns a grid of mask ints (open doors per cell)
local function carveMaze(width, height, rng)
	-- masks[x][y] stores door openings bitmask for that cell
	local masks = table.create(width)
	for x = 1, width do
		masks[x] = table.create(height, 0)
	end

	local visited = table.create(width)
	for x = 1, width do
		visited[x] = table.create(height, false)
	end

	local function inBounds(x, y)
		return x >= 1 and x <= width and y >= 1 and y <= height
	end

	-- Neighbor step deltas by DIR
	local DX = { [DIR.N]=0, [DIR.E]=1, [DIR.S]=0, [DIR.W]=-1 }
	local DY = { [DIR.N]=-1,[DIR.E]=0, [DIR.S]=1, [DIR.W]= 0 }

	-- Iterative DFS
	local stack = {}
	local function push(x,y) stack[#stack+1] = {x=x,y=y} end
	local function pop() local n=stack[#stack]; stack[#stack]=nil; return n end

	-- start anywhere (1,1) for consistency
	push(1,1)
	visited[1][1] = true

	while #stack > 0 do
		local node = stack[#stack]
		local x, y = node.x, node.y
		local progressed = false

		for _, dir in ipairs(shuffledDirs(rng)) do
			local nx, ny = x + DX[dir], y + DY[dir]
			if inBounds(nx, ny) and not visited[nx][ny] then
				visited[nx][ny] = true
				-- open current cell's door in 'dir'
				masks[x][y] = bit32.bor(masks[x][y], dir)
				-- open neighbor's opposite door
				masks[nx][ny] = bit32.bor(masks[nx][ny], OPP[dir])
				push(nx, ny)
				progressed = true
				break
			end
		end

		if not progressed then
			pop()
		end
	end

	return masks
end


-- Place a model clone at (gridX, gridY) with rotation rot (0..3) on Y-axis
local function placeRoom(container, prefab, originCF, roomSize, gridX, gridY, rot)
	local clone = prefab:Clone()
	if not clone.PrimaryPart then
		warn(("PrimaryPart not set on prefab %s"):format(prefab.Name))
		return nil
	end

	-- translate: cell (1,1) at origin; X grows +X, Y grows +Z
	local offset = CFrame.new((gridX-1) * roomSize, 0, (gridY-1) * roomSize)
	local rotation = CFrame.Angles(0, math.rad(90 * rot), 0)
	clone:PivotTo(originCF * offset * rotation)
	clone.Parent = container
	return clone
end

-- Public API
-- params = {
--   gridW: number,
--   gridH: number,
--   roomSize: number (default 50),
--   originCFrame: CFrame (default CFrame.new()),
--   roomsFolder: Instance (default ReplicatedStorage.Rooms),
--   seed: number? (optional, for reproducible mazes),
--   extraLoops: integer (optional, adds random extra connections)
-- }
function RoomMaze.Generate(params)
	local bit32 = bit32

	local gridW = params.gridW or 20
	local gridH = params.gridH or 20
	local roomSize = params.roomSize or 50
	local origin = params.originCFrame or CFrame.new()
	local roomsFolder = params.roomsFolder or RS:WaitForChild("Rooms")
	local seed = params.seed
	local extraLoops = math.max(0, tonumber(params.extraLoops) or 0)

	local rng = Random.new(seed or os.time())

	-- Build door masks via DFS
	local masks = carveMaze(gridW, gridH, rng)

	-- Optionally add a few extra connections to create loops (less linear)
	if extraLoops > 0 then
		for _ = 1, extraLoops do
			local x = rng:NextInteger(1, gridW)
			local y = rng:NextInteger(1, gridH)
			local dirs = shuffledDirs(rng)

			-- try to open a new neighbor edge if closed
			local DX = { [DIR.N]=0, [DIR.E]=1, [DIR.S]=0, [DIR.W]=-1 }
			local DY = { [DIR.N]=-1,[DIR.E]=0, [DIR.S]=1, [DIR.W]= 0 }

			for _, d in ipairs(dirs) do
				local nx, ny = x + DX[d], y + DY[d]
				if nx >= 1 and nx <= gridW and ny >= 1 and ny <= gridH then
					-- was: if (masks[x][y] & d) == 0 then
					if bit32.band(masks[x][y], d) == 0 then
						-- was: masks[x][y] |= d
						masks[x][y] = bit32.bor(masks[x][y], d)
						-- was: masks[nx][ny] |= OPP[d]
						masks[nx][ny] = bit32.bor(masks[nx][ny], OPP[d])
						break
					end
				end
			end
		end
	end


	-- Container for placed rooms
	local container = workspace:FindFirstChild("GeneratedRooms")
	if container then container:Destroy() end
	container = Instance.new("Folder")
	container.Name = "GeneratedRooms"
	container.Parent = workspace

	-- Place each cell's room with the correct prefab & rotation
	for y = 1, gridH do
		for x = 1, gridW do
			local mask = masks[x][y]
			local prefab, rot = choosePrefabForMask(mask, roomsFolder)
			if prefab then
				placeRoom(container, prefab, origin, roomSize, x, y, rot)
			else
				warn("No prefab for cell", x, y, "mask", mask)
			end
		end
	end

	return container
end

return RoomMaze

Server

local RS = game:GetService("ReplicatedStorage")

-- Optional: origin where (1,1) cell sits
local origin = CFrame.new(0, 0, 0)
local RoomMaze = require(RS.RoomMaze.Generator)
RoomMaze.Generate({
	gridW = 20,          -- width in rooms
	gridH = 20,          -- height in rooms
	roomSize = 50,       -- your room footprint
	originCFrame = origin,
	roomsFolder = RS:WaitForChild("Rooms"),
	seed = 12345,        -- set for reproducibility; remove for random each run
	extraLoops = 40,     -- optional: adds loops so maze feels more “dungeon”
})

I tested it it works if it doesn’t its the pathing to it or models somthing like that

2 Likes

I asked ai to add comments on it so yeah

Ill think about implementing it. btw thanks for the feedback.

1 Like
  • was struggling to fit through each door cause of my avatar/r15 animation
  • no music, no sounds, no notifications
  • no objectives to know what i should be doing
  • the monster wasnt scary which was boring, also no variety of different scary enemies
  • wasnt really anything going on so i was left bored go through each room, feeling like doing a chore instead of playing a game for fun
  • cause each room was the same thing it was getting monotonous, and cause i could easily see what the next rooms were there wasnt a sense of danger going into the next room
  • backtracking might help the game a bit cause i didnt need to go back anywhere at all

Thanks for your feedback! I’m already working on more rooms that are more common and many other improvments

I accidentally updated the game. This version is not finished though.

1 Like