В данной статье рассмотрим способ процедурной генерации подземелий, с помощью рекурсивную функцию. Сам проект будет 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))
Результаты
Я для наглядности сделал сцены просто разным цветом, вместо комнат с какими-то декорациями.
Маленькие подземелья:

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

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



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

Самое стандартное дерево, ничего большего не надо. Скрипт персонажа:
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))
Теперь можно побегать по этой карте.


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