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