Рекурсивная генерация подземелий на Godot 4.1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В данной статье рассмотрим способ процедурной генерации подземелий, с помощью рекурсивную функцию. Сам проект будет 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 комнатами
4 варианта генерации подземелья 5х5 с 10 комнатами

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

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

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

Подземелье 20x20 с 300 комнат
Подземелье 20x20 с 300 комнат
50х50 1500 комнат
50х50 1500 комнат
100х100 5000 комнат
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))

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

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

Источник: https://habr.com/ru/articles/747660/


Интересные статьи

Интересные статьи

Алгоритм Эллера - это алгоритм генерации идеального лабиринта. Лабиринт считается идеальным, если у него нет замкнутых и зацикленных участков, и от любой точки до любой другой точки существует ровно о...
Генерация пещер. Генерация на языке программирования С++, на основе клеточных автоматов с регулируемыми настройками.
Основная идеяИдея достаточно простая: в определенной директории задаётся API функция в виде файла php которая возвращает анонимную функцию. Функции могут быть четырех типов: Put (изменение значений), ...
Terraformer — консольный инструмент для генерации кода и стейта в форматах HCL и json для уже существующей инфраструктуры. Читать д...
Сегодня разберем выступление Джеймса Вуттона из IBM Quantum на конференции FDG 2020. Речь пойдет о квантовых вычислениях — потенциально многообещающей технологии, для которой, одн...