Random map generation

Right now I am making a backrooms-type game, but I am trying to figure out a way to make an infinitely generated map. I want the system to have a seed so every map will be the same, and parts of the map will despawn when out of view to reduce lag.

The map will be around 7 main parts (corner, wall, pillar, etc) and they will randomly generate each time a new area is loaded.

NOTE: The roof part of it should not rotate, I’ll add that in myself.

Are there any ways to do this?

Examples of some tiles:

8 Likes

Not a complete answer but if you enable Workspace.StreamingEnabled you have options for setting the StreamingMinRadius which is 64 studs by default. Note that this is a property of Workspace and is not scriptable as far as I know.

The thing is, I want an infinite map and you cant really do that by hand.

I also know StreamingEnabled exists, but it wont really help in this situation.

you can probably build a decent map by hand that lines up with itself and use that

(you can also build different segments that all hook up together like puzzle pieces)

heck, you don’t even need them to hook up perfectly because that seems exactly like something the backrooms would end up doing

edit: maybe also make a bunch of exits and entities (and delete 99% on section spawn) so they can “randomly” appear

1 Like

Wouldn’t making the map infinite be useless? the player needs to have a exit objective

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.