Random map generation

it is the backrooms level zero and canonically that is basically endless. Most backrooms devs don’t make it endless, they just have this one exit you can memorize.

making it endless would just make it impossible to escape giving it no use

Which is why you randomly put exits and things

1 Like

The canonical backrooms has no exit, this entire game was just to see how long somebody can try to find an exit if they were told one exists (but it doesn’t actually exist).

Are there any reliable ways to make infinite generation with a preset of tiles though?

It’s basically a mental test, in other words.

1 Like

There are a few methods you could use to do this.

For multiple room models, using a tile system might be best to start off with.

  • First, mark exits/doorways for each room model using parts.
  • For each room exit point, generate another Room model and set its PrimaryPart to an exit part that you want to connect to the previous room exit. You can then use SetPrimaryPartCFrame() to align the new room model to the exit part CFrame of the old room to connect the two rooms.
  • You should also make sure to check if that room is intersecting any room in that same area, and if so don’t generate that room.
  • Finally, make sure to only generate the rooms that are within a certain distance of your camera or player, and to remove the ones that are too far away. Storing every room model within a table could probably help you organize their distances and positions.

Another method where you could use seeds is a perlin noise chunk algorithm. This system though is easier to deal with when just using single tiles representing the ground, floor, and ceiling.

  • First, you can use perlin noise to generate specific heights for each chunk/tile. Also, make sure to only generate chunks around your camera/player by checking distance and storing chunks accordingly.
  • Additionally, you could add randomly generated models or props to each chunk to give more depth to your environments, such as adding doorways, pillars, and more.

As of now, I suggest using the perlin noise method since it opens a lot more doors for making your environments look better, and is also a more documented approach.
okeanskiy has a wonderful series that focuses on implementing infinite perlin noise generation.
You can also take a look at my own infinite backrooms implementation here.

4 Likes

How exactly would you be able to find all the chunks within a certain radius around the player, then see if the chunk is occupied, then if not, place a random tile?

Here’s my simplified implementation of the Perlin noise method I described. You can find visual and more detailed representations of the systems described in my implementation link I posted earlier.

If you want the place file you can find it here:
ChunkTutorial.rbxlx (57.5 KB)

  • Before adding the scripts, make three folders in ReplicatedStorage called Floors, Walls, and Ceilings.

  • You can then add different models in those folders for their specific tiles.

  • Make sure the sizes of the models reflect the Grid Size that you want. By default, it’s 12, meaning a floor tile for example should be 12x1x12 in size. (you can change the grid size in the script below as you please by changing gen.gridScale).

  • Make sure that each model has a PrimaryPart set that also aligns with the Grid Scale.
    parts

  • Place this inside a ModuleScript named “Generation” in ReplicatedStorage:

--# Sevices
local ReplicatedStorage = game:GetService("ReplicatedStorage")


--# Point
local gen = {}


--# Storage
local Floors = ReplicatedStorage:WaitForChild("Floors"):GetChildren()
local Walls = ReplicatedStorage:WaitForChild("Walls"):GetChildren()
local Ceilings = ReplicatedStorage:WaitForChild("Ceilings"):GetChildren()

gen.chunkStorage = {}


--# Placement Variables
gen.gridScale = 12 -- The grid size in studs.
gen.renderDistance = 5 -- How far we can render relative to a position.
gen.originHeight = 5 -- Height of the ground.
gen.ceilingHeight = 10 -- How high ceilings are placed (offset from the origin height).
gen.wallDensity = 2 -- The amount of walls that get generated.


--# Generation Variables
gen.terrainSmoothness = 3 -- How noisy the generation will be.
gen.wallHeight = 20 -- What is considered the height at which a wall will be placed instead of a floor tile.
gen.seed = 300


--# Functions
local function checkForOverlappingChunk(x, z)
	for _, chunk in pairs(gen.chunkStorage) do
		if chunk.PrimaryPart.Position.X == x and chunk.PrimaryPart.Position.Z == z then
			return true
		end
	end
end

function gen.placeChunk(x, z, chunkType)
	-- Don't place this chunk if a chunk already exists here.
	if checkForOverlappingChunk(x, z) == true then
		return true
	end
	
	-- We're placing either a floor or a roof depending on the type of chunk.
	local chunkModel = nil
	local ceilingModel = nil
	
	if chunkType == "Floor" then
		chunkModel = Floors[math.random(1, #Floors)]:Clone()
		
		-- Place the ceiling above ground tiles.
		ceilingModel = Ceilings[math.random(1, #Ceilings)]:Clone()
		ceilingModel:SetPrimaryPartCFrame(CFrame.new(x, gen.originHeight + gen.ceilingHeight, z))
		ceilingModel.Parent = workspace
	else
		chunkModel = Walls[math.random(1, #Walls)]:Clone()
	end
	
	-- We're setting the chunks position.
	chunkModel:SetPrimaryPartCFrame(CFrame.new(x, gen.originHeight, z))
	
	-- Saving the chunk in storage.
	chunkModel.Parent = workspace
	table.insert(gen.chunkStorage, chunkModel)
	table.insert(gen.chunkStorage, ceilingModel)
end

function gen.removeChunk(x, z)
	for i, chunk in pairs(gen.chunkStorage) do
		if chunk.PrimaryPart.Position.X == x and chunk.PrimaryPart.Position.Z == z then
			chunk:Destroy()
			table.remove(gen.chunkStorage, i)
			return nil
		end
	end
end

function gen.removeDistantChunks(x, z)
	-- Delete all the chunks which are too far away.
	for _, chunk in pairs(gen.chunkStorage) do
		if (Vector2.new(chunk.PrimaryPart.Position.X, chunk.PrimaryPart.Position.Z) - Vector2.new(x, z)).Magnitude > (gen.renderDistance*gen.gridScale) then
			gen.removeChunk(chunk.PrimaryPart.Position.X, chunk.PrimaryPart.Position.Z)
		end
	end
end

local function snapToGrid(x, z)
	return (math.floor(x / gen.gridScale +.5 ) * gen.gridScale), (math.floor(z / gen.gridScale +.5 ) * gen.gridScale) 
end

function gen.placeSurroundingChunks(x, z)
	-- Get the values we'll need.
	local scaledRenderDistance = gen.renderDistance*gen.gridScale
	local halfScaledRenderDistance = (scaledRenderDistance)*0.5
	
	for posX = 0, (gen.renderDistance) do
		for posZ = 0, (gen.renderDistance) do
			local scaledPosX = posX*gen.gridScale
			local scaledPosZ = posZ*gen.gridScale
			
			-- Make the chunks centered to our position.
			local actualPos = Vector2.new(
				x + (scaledPosX - halfScaledRenderDistance ),
				z + (scaledPosZ - halfScaledRenderDistance )
			)
			actualPos = Vector2.new(snapToGrid(actualPos.X, actualPos.Y))
			
			-- Generate the chunks within the render distance.
			if (actualPos - Vector2.new(x, z)).Magnitude <= (gen.renderDistance*gen.gridScale) then
				local chunkType = "Floor"
				
				-- Generate a random height based on the chunks position.
				local dividedX = actualPos.X/gen.gridScale
				local dividedZ = actualPos.Y/gen.gridScale
				local noise = math.noise( (posX + dividedX)/gen.gridScale, (posZ + dividedZ)/gen.gridScale, gen.seed)
				
				if math.abs(noise) > (gen.wallDensity*0.1) then
					chunkType = "Wall"
				end
				print(noise)
				gen.placeChunk(actualPos.X, actualPos.Y, chunkType)
			end
		end
	end
end


--# Finalize
return gen
  • Place this inside a LocalScript in PlayerGui or StarterPlayerScripts:
--# Sevices
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")


--# Include
local gen = require(ReplicatedStorage:WaitForChild("Generation"))


--# Plr
local plr = Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local HRP = char:WaitForChild("HumanoidRootPart")


--# Loop

-- Process chunks every 0.2 seconds.
while wait(0.2) do
	gen.placeSurroundingChunks(HRP.Position.X, HRP.Position.Z)
	gen.removeDistantChunks(HRP.Position.X, HRP.Position.Z)
end
  • The placeSurroundingChunks method in the module script describes how to generate chunks within a certain radius around the Player.
  • Checking if a chunk is occupied is in the checkForOverlappingChunk method in the module script as well.
  • Placing random tiles is in the placeChunk method of the same script as well.
13 Likes

Thank you for your help! Although, I want the height of the rooms to be at a certain height and not vary, any way to remove that specific part of it?

And once again, I will definitely be using this (and I will credit you!)

No problem! You can remove the height offset on line 55 of the module script by changing

ceilingModel:SetPrimaryPartCFrame(CFrame.new(x, gen.originHeight + gen.ceilingHeight, z))

to

ceilingModel:SetPrimaryPartCFrame(CFrame.new(x, gen.ceilingHeight, z))

As for changing any other height settings, you can modify the values in the module script.

Right now, it looks good! One problem I have is when you go into an empty space and see outside the map. Changing the renderDistance only causes more lag. An example of my issue here:


Is there any way to reliably reduce lag, or to have the hallways generate closer together?
(This also brings me to my wallDensity problem, if I set it to 1 then the hallways are way too close together, but if I set it to 2 they are too far apart.)

You could cover up the render distance limitation by changing the fog settings in Lighting, as well as changing the skybox to the fog color you chose.

The best way to reduce lag is just changing the Grid Scale to something larger than 12 (and also your model sizes to reflect this), as well as making the render distance smaller.

Regarding wall density, keep in mind that it can be any number you want, including decimals, which should hopefully help fix your issue. If you want more variation you can divide the x and y values of the noise function by a smoothing value by changing line 117 of the module script:

local smoothingValue = 3
local noise = math.noise( ((posX + dividedX)/gen.gridScale)/smoothingValue , ((posZ + dividedZ)/gen.gridScale)/smoothingValue, gen.seed)

Creating randomly generated exits could lead to chaos because the exit could either be very common or very rare

Simply lower fogend to the stud radius you want, adjusting the fog color and stuff to make a sort of vision blocker, to never see the end.

No offense, but I think people would get bored after walking for 2 minutes straight looking for an exit that doesn’t exist. A game needs a goal. I don’t want to bring you down. I think it’s very great that you are trying to make something cool, but maybe put it towards another project.

It’s not your business to judge some random person’s game, and the game will have places where you can exit (aka, go to level 1) so it will have some sort of goal in the end.

And @Razorboots, is there a way to make certain items more rare (like have a rare wall that spawns that takes you to the manilla room or something)

Alright, after some testing, the lag is coming from the script checking each tile to see if it is occupied. The process is not very efficient, so my idea is to make the process more efficient by having the script only check the furthest out.

I turned up the render distance to around 40 and there is 0 lag if checking is paused.

Yep! I just updated the place file to support any number of tile types.
You can find and add new ones in ReplicatedStorage and also block 118 of the module script.

You can change the chance for an exit to spawn in line 33 of the module script. You could also add a method to check if there are any other exits that are too close nearby, but that would require having to loop through the entire chunk list again.

ChunkTutorial.rbxlx (63.6 KB)

Also, the checkForOverlappingChunk() method is, unfortunately, necessary for the generation to function properly.

Alrighty then, I think I’m gonna leave this topic off here then for now. Good luck!

3 Likes

This can help you for what you want to do

https://devforum.roblox.com/t/dungeon-generation-a-procedural-generation-guide/342413&ved=2ahUKEwiDyrL-sob3AhWjxIUKHZsrCoYQFnoECEMQAQ&usg=AOvVaw3C43GQ53Da3Sf1qVqlPkbc

The reason backrooms games work is because not only are you trying to find an exit, but you are also trying not to die, adding an aspect of realism (like camera bobbing or a running animation on a viewmodel) can immerse you in the experience and make it enjoyable even if you aren’t finding an exit.

Anyway back to the main point, I think the reason it might be lagging is because the remove function is checking the radial magnitude of the ending rooms while the add function seems to be in a double for loop (essentially searching the x and y distances separately). since the edges of the circle are being removed by the remove thing and added by the add thing every 0.2 seconds, that could be your issue.

(what i mean)


function gen.removeDistantChunks(x, z)
	-- Delete all the chunks which are too far away.
	for _, chunk in pairs(gen.chunkStorage) do
--right here check the x and y distances separately instead of grouping them together (i'm on a tablet so i can't format code lol)
		if (Vector2.new(chunk.PrimaryPart.Position.X, chunk.PrimaryPart.Position.Z) - Vector2.new(x, z)).Magnitude > (gen.renderDistance*gen.gridScale) then
			gen.removeChunk(chunk.PrimaryPart.Position.X, chunk.PrimaryPart.Position.Z)
		end
	end
end

edit: usually a simple distance check wouldn’t make any lag, accedentially deleting and cloning a bunch of instances would though, it also isn’t apparent in low render distances like 1-3 because the middle of the room isn’t being cut off by the edge of the circle radius

edit2: I’m really bad at explaining stuff by words so here’s a diagram

anything outside of the red circle is getting detected by remove checker and anything inside of the yellow square is being added, so all the blue area would be removed on the remove sweep and re-instanced on the rebuild sweep (causing a lot of a
lag)