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
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.
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.
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
calledFloors
,Walls
, andCeilings
. -
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.
-
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.
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!
This can help you for what you want to do
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)