В данной статье рассмотрим способ процедурной генерации подземелий, с помощью рекурсивную функцию. Сам проект будет 2D с видом сверху. Комната будет занимать всё окно игры.

У меня комната занимаем весь дисплей и имеет размеры 800x800, display соответственно тоже.

Генерация подземелья

Какой вообще алгоритм, сначала создадим двумерный массив состоящий из 0(как написано в документации к Godot, лучше использовать словарь векторов, так и сделаем). Дальше в центр этого массива ставим 1-ку(если 0 комнаты нет, если 1, то комната есть) и для всех соседних элементов массива вызываем функцию которая возможно поставит 1-ку(создаст комнату), а возможно нет. Дальше для всех элементов в которых функция поставила 1-ку, опять вызываем эту функцию(рекурсивно).

Сама генерация

Создайте сцену главным узлом выберите Node2D. И добавьте скрипт.

Для начала объявим несколько переменных, куда без них:

#Массив хранящий прелоады комнат
var room_array =[]

#Сам генерируемый лабиринт
var labirint_array ={}

#размер лабиринта 5x5
@export var labirint_size = 5
#Количество комнат
@export var room_count = 5

#Переменная потребуется, для увеличение максимально сгенерированного числа
#Если вдруг мы не смогли расставить все комнаты, при первом цикле
var random_max = 1

Пояснять что-то не вижу смысла.

Для начала напишем функцию, которая будет создавать комнату в заданных координатах массива, если это возможно:

#функция создания одной комнаты
#Аргументы - координаты ячейки массива labirint_array
func add_one_room(j,i):
	#Проверили нужны-ли ещё комнаты
	if room_count > 0:
		#Генерируем случайное число
		var room = randi_range(0,random_max)
		#Если сгенерировали не 0
		if room >= 1:
			#То делаем сложную проверку:
			#Проверяем не выходят ли переменные, за границы
			#Проверяем нет ли уже комнаты
			if ((j >= 0) && (j < labirint_size) && (i >= 0) && (i < labirint_size) && (labirint_array[Vector2(j,i)] != 1)):
				#и добавляем комнату в массив
				labirint_array[Vector2(j,i)] = 1
				#не забыли про счётчик
				room_count -= 1
				#возвращаем вектор, если создали
				return Vector2(j,i)
	#Если вылетили и какого-то if, то возвращаем другой ветор
	return Vector2(-1,-1)

Теперь у нас есть функция, которая создаёт в заданных координатах комнату и возвращает их( в виде вектора, тк используем словарь), либо возвращает вектор (-1,-1), именно такой вектор, потому-что мы сами не будет создавать вектора с координатами меньше, чем (0,0).

Дальше напишем функцию, которая будет вызывать функцию создания комнаты для 4 соседних координат, в качестве аргумента будем принимать координаты. После будет вызывать сама себя, для созданных комнат:

#Рекурсивная функция добавления комнат
#Аргументы - координаты ячейки массива labirint_array
func add_rooms(j,i):
	var add:Vector2
	#Сначала пробуем сгенерировать комнату слева от уже созданной 
	add = add_one_room(j-1,i)
	if add != Vector2(-1,-1):
		add_rooms(add.x,add.y)
	#пробуем сгенерировать комнату справа от созданной
	add = add_one_room(j+1,i)
	if add != Vector2(-1,-1):
		add_rooms(add.x,add.y)
	#пробуем сгенерировать комнату сверху от созданной
	add = add_one_room(j,i-1)
	if add != Vector2(-1,-1):
		add_rooms(add.x,add.y)
	#пробуем сгенерировать комнату снизу от созданной
	add = add_one_room(j,i+1)
	if add != Vector2(-1,-1):
		add_rooms(add.x,add.y)
		#Рекурсивно вызываем функции
		#Поэтому нужно обращаться конкретно к х или у

Теперь давайте создадим функцию, которая будет генерировать изначальный массив( состоящий из 0), ставить первую комнату и запускать рекурсию:

#Функция создания лабиринта
func generate_labirint():
	#Сначала заполняем массив нулями
	for i in range(labirint_size):
		for j in range(labirint_size):
			labirint_array[Vector2(j,i)] = 0
	#Если вдруг каким-то образом должно быть больше комнат,
	#Чем всего на карте, то меняем это, во избежание бесконечной рекурсии
	if labirint_array.size() < room_count:
		room_count = labirint_array.size()
	#Точкой начала выбираем центр лабиринта
	labirint_array[Vector2(round(labirint_size/2),round(labirint_size/2))] = 1
	#Не забываем про кол-во комнат
	room_count -=1
	#Вызываем в цикле, потому-что есть вероятность, что ниодной комнаты не добавится
	while room_count > 0:
		#Вызываем функцию добавления комнаты
		add_rooms(round(labirint_size/2),round(labirint_size/2))
		#Функция рекурсивная, поэтому закончится, 
		#когда отработают все вызванные функции
		#Увеличиваем счётчик, чтобы быстрее растыкать комнаты
		random_max +=1
		#Если мы такие невезучие, что счётчик дошёл до 10, 
		#То хватит с нас этого рандома
		if random_max > 10:
			break

На этом генерация подземелья закончена. Дальше мы создадим наши комнаты на сцене игры и добавим комнатам двери.

Немного заготовок

Сцена комнаты

Для начала создайте несколько сцен комнат, примерно, как должно быть:

4 маркера: Door1...Door4 - это местоположения возможных дверей, так-же в маркерах стоит выставить угол поворота двери. StaticBody2D и CollisionShape2D, нужны чтобы будущий игровой персонаж не ходил по стенам.

Скрипт комнаты:

extends Node2D
#если кто-то наступил на стену,
#то больше не может идти
func _on_area_2d_body_entered(body):
	body.velocity = Vector2(0,0)

Скрипт самый базовый, нам просто нужна комната и ничего большего.

Таких комнат следует сделать несколько.

Сцена Двери

Дверь должна выглядеть примерно так:

Когда игрок соприкасается с collisionShape2D, то он переходит в следующую комнату.

Скрипт Двери:

extends StaticBody2D
#куда дверь телепортирует игрока
@export var next_pos:Vector2
#Задаём переменную телепорта
func set_next_pos(vector):
next_pos = vector
#Возвращаем эту переменную
func get_next_pos():
return next_pos

Тут тоже всё понятно. Всё наконец-то с небольшими приготовлениями закончили.

Создание карты

Для начала напишем пару функция для создания массива с прелоадами сцен:

#Папка с комнатами, у меня все комнаты Room + ID
const MAP_ROOT = "res://Room/Rooms/Room"

#Небольшой хелпер возвращающий уже полный путь сцены
func get_room_path(index):
	return MAP_ROOT + str(index) + ".tscn"
	
#Заполним массив с прелоадами комнат
func get_room_array():
	#Счётчик
	var i = 1
	while true:
		#Если такая сцена есть, то добавляем в массив
		if load(get_room_path(i)) != null:
			room_array.append(load(get_room_path(i)))
		#Иначе заканчиваем while
		#У меня все комнаты идут по порядку(Room1,Room2...)
		#Можно сделать чуть иначе, но так проще...
		else:
			break
		i+=1

Теперь у нас есть массив с прелоадами сцен. Дальше напишем функции для полного создания подземелья.

#Функция создания лабиранта в дерево объектов
func build_labirint():
	for i in range(labirint_size):
		for j in range(labirint_size):
			#Если в массиве 1, то рисум комнату
			if labirint_array[Vector2(j,i)] == 1:
				draw_room(j,i)

#Функция добавления комнат в дерево объектов
#Аргументы - координаты ячейки массива labirint_array
func draw_room(j,i):
	#Взяли случайную комнату из массива с комнатами
	var s = room_array[randi_range(0,room_array.size()-1)].instantiate()
	#Задали местоположение
	#Умножаем на размер комнаты
	s.position = Vector2(j*800,i*800)
	#Добавили в дерево объектов
	add_child(s)
	#Добавляем все двери
	#При перемещении на 200 в моем случае пирсонаж окажется у двери
	add_one_door(j-1,i,200,0,s,4)
	add_one_door(j+1,i,-200,0,s,2)
	add_one_door(j,i-1,0,200,s,1)
	add_one_door(j,i+1,0,-200,s,3)

#Функция добавления двери
#Аргументы - координаты ячейки массива labirint_array,
#Смещение по х и по у, мы должны появляться возле двери через которую пришли
#Наша сцена комнаты, к которой добавляем дверь
#Порядковый номер нужного маркера в дереве объектов сцены комнаты
func add_one_door(j,i,add_x,add_y,s,n):
	#Делаем сложную проверку:
	#Проверяем не выходят ли переменные, за границы
	#Проверяем есть ли уже комнаты
	#Не путайте с условием из add_one_room - ЭТО ДРУГОЕ
	if ((j >= 0) && (j < labirint_size) && (i >= 0) && (i < labirint_size) && (labirint_array[Vector2(j,i)] == 1)):
		var d = preload("res://Room/Door/Door.tscn").instantiate()
		#Перенимаем трансформ с маркера, который за это ответственен
		d.transform = s.get_child(n).transform
		#Задали положение для телепорта
		#Умножаем на размер комнаты
		d.set_next_pos(Vector2(j*800+add_x,i*800+add_y))
		s.add_child(d)

Давайте немного обсудим функцию add_one_door, конкретно аргумент n, что это вообще такое. Это порядковый номер потомка дерева сцены. Как это выглядит у меня:

потомок с индексом 0- Sprite2D, следующие 4 потомка как раз наши маркеры, в которых записано местоположение и поворот двери, Door1 - потомок с индексом 1, указываем на положение верхней двери. Door2 - потомок с индексом 2, указывает на положение правой двери и т.д. по часовой. У всех сцен комнат должны быть одинаковые индексы у Marker.

В функции _ready() следует вызывать функции в следующем порядке:

func _ready():
	#Подключаем рандомайзер
	randomize()
	#Создаём лабиринт
	generate_labirint()
	#Создаём массив комнат
	get_room_array()
	#Строим либиринта
	build_labirint()

Теперь у нас генерируется подземелье, но на него никак не посмотреть. Добавьте к дереву сцены игры Camera2D. В InputMap добавьте clickright - ПКМ и clickleft - ЛКМ.

добавьте функцию _process к имеющемуся скрипту:

#играемся с зумом камеры
func _process(delta):
	if Input.is_action_just_pressed("clickright"):
		$Camera2D.zoom /= 2
	if Input.is_action_just_pressed("clickleft"):
		$Camera2D.zoom *= 2

При ПКМ камера отдаляется, при ЛКМ приближается.

и задаём изначальное местоположение камеры в центре карты:

$Camera2D.position = Vector2(800 * round(labirint_size/2),800 * round(labirint_size/2))

Результаты

Я для наглядности сделал сцены просто разным цветом, вместо комнат с какими-то декорациями.

Маленькие подземелья:

4 варианта генерации подземелья 5х5 с 10 комнатами

Средние подземелья:

9 вариантов подземелья 10х10 с 30-ю комнатами

СЛИШКОМ огромные подземелья:

Подземелье 20x20 с 300 комнат
50х50 1500 комнат
100х100 5000 комнат

Как можно заметить, чем больше подземелья, тем интереснее его форма. По времени работы не сказал бы, что генерация занимаем много времени, то-есть пустые комнаты генерируются крайне быстро, но лучше загружать какой-нибудь загрузочный экран, при входе в подземелье сгенерированное таким образом. Но точно не самый быстрый и лучший способ, но как вариант для какого-нибудь проекта будучи сильно ограниченным во времени его реализации, подойдёт.

Персонаж и перемещение между комнатами

Давайте на последок создадим простого персонажа, который будет ходить по нашему подземелью.

Дерево элементов:

Самое стандартное дерево, ничего большего не надо. Скрипт персонажа:

extends CharacterBody2D

#Скорость
const SPEED = 300.0
#Сигнал выхода из двери
signal out

func _physics_process(delta):
	#Получаем направление движения
	var input_direction = Input.get_vector("left", "right", "up", "down")
	#Нормализуем и умножаем на скорость
	velocity = input_direction.normalized() * SPEED
	#Пошли
	move_and_slide()
	
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		#Если столкнулись и есть у объекта столкновения
		#Метод get_next_pos,вызываем его и посылаем сигнал
		if collision.get_collider().has_method("get_next_pos"):
			position = collision.get_collider().get_next_pos()
			emit_signal("out")

Тоже ничего не обычного, просто перемещаемся в 8-ми направлениях и обрабатываем коллизию с дверью(у двери есть эта функция).

Теперь на камеру главной сцены тоже добавьте скрипт, немного изменим поведение камеры, чтобы она следовала за персонажем:

extends Camera2D

#Переменная хранящая игрока
var player: Node2D

#Присоединяем сигнал игрока
func connect_player():
	player.connect("out",_on_player_out)

#Перемещаем камеру
func _on_player_out():
	if player != null:
		#Используем обычное округления - это важно
		var x = round(player.position.x / 800)
		var y = round(player.position.y / 800)
		
		position = Vector2(x*800 ,y*800)

при переходе в другую комнату камера будет тоже перемещаться в центр этой комнаты.

Измени функцию _ready сцены с картой:

func _ready():
	#Подключаем рандомайзер
	randomize()
	#Создаём лабиринт
	generate_labirint()
	#Создаём массив комнат
	get_room_array()
	#Строим либиринта
	build_labirint()
	#Добавляем игрока в центр, центральной комнаты
	var h = preload("res://Character/character.tscn").instantiate()
	#Умножаем на размер комнаты
	h.position = Vector2(800 * round(labirint_size/2),800 * round(labirint_size/2))
	add_child(h)
	#Привязали к камере игрока
	$Camera2D.player = h
	#Заделали коннект
	$Camera2D.connect_player()
	#определили изначальное положение
	#Умножаем на размер комнаты
	$Camera2D.position = Vector2(800 * round(labirint_size/2),800 * round(labirint_size/2))

Теперь можно побегать по этой карте.

Ну вот, примерно такой алгоритм можно использовать для генерации подземелья, конечно чтобы получались более интересные подземелья алгоритм нужно дорабатывать под конкретные нужды.