Roguelike сейчас в моде на сцене инди-игр, и я знаю, что у меня и у других есть множество отличных идей, которые они хотят добавить в этот жанр. Но один из ключевых принципов жанра, случайно генерируемые уровни, может быть сложным для понимания. Мой движок — Game Maker Studio 2, и ему особенно не хватает функций, необходимых для безболезненного процесса. Хорошая новость! Я решил это для вас. Следуйте этому руководству, и у вас будет элементарная система генерации уровней в кратчайшие сроки.

Для начала нам нужно обрисовать в общих чертах, что представляет собой это решение для генерации уровней, а что нет. Это решение не является системой генерации на основе шума, т.е. Minecraft или Terraria. Вместо этого мы используем метод генерации уровней Spelunky. Вы можете посмотреть это фантастическое видео Марка Брауна, чтобы глубже погрузиться в то, что Spelunky делает хорошо, но я дам вам суть. Уровни генерируются на сетке из разработанных шаблонов. Эти шаблоны гарантируют, что игрок наткнется на интересный кураторский контент, а также гарантируют, что контент отличается от того, когда он последний раз играл. Мы собираемся настроить базовые инструменты, чтобы сделать это возможным. Я оставлю вам индивидуальный дизайн. Это будет промежуточный урок; установка будет направляться, но на самом деле использовать эти инструменты будет сложнее. А теперь к делу.

Общая теория здесь заключается в том, что мы создадим пустую сетку 8x8 и дадим ей информацию о том, где игрок может перемещаться между комнатами. Давайте создадим объект с именем obj_initialize_new_room и назначим ему несколько переменных. Определите следующее в определениях переменных для вашего объекта.

new_room_width = 8
new_room_height = 8

Далее нам нужно сделать этот объект постоянным. Это скоро пригодится. Теперь давайте добавим некоторый код в его событие создания.

room_list = []
var max_width = new_room_width - 1
var max_height = new_room_height - 1
for (var i = 0; i < new_room_width; i ++)
{
    room_list[i] = []
    for (var j = 0; j < new_room_height; j ++
    {
        room_list[i][j] = [false, false, false, false]
    }
}

Теперь мы создали массив массивов массивов. Самые глубокие вложенные массивы имеют четыре логических значения, которые сообщают нам, каким образом игрок может перемещаться по комнатам. В соответствии со стандартами Game Maker каждый Bool соответствует следующему: 0 = вправо, 1 = вверх, 2 = влево, 3 = вниз.

Теперь мы собираемся пройти через этот массив и создать пути через массивы. Под кодом, который у вас уже есть в событии создания, добавьте следующее.

sx = random(new_room_width)
sy = random(new_room_height)
do {
    ex = random(new_room_width)
    ey = random(new_room_height)
} until ex !== sx || ey !== sy
var xx = sx
var yy = sy

Теперь мы определили начальную и конечную комнаты в случайных местах массива. Мы также задали начальные координаты временному набору x и y. Мы собираемся переместить эти x и y в конечные координаты, открывая пути по пути.

Давайте сделаем это сейчас, снова прямо под нашим последним фрагментом кода.

count = 4
var room_count = 0
while count > 0
{
   dir = floor(random(4))   
//Note 1
   while xx !== ex || yy !== ey
      {
       var px = xx
       var py = yy
       xx += (dir == 0) - (dir == 2)
       yy += (dir == 3) - (dir == 1)
       if xx > max_width
       {
          xx = max_width
       }
       if xx < 0
       {
          xx = 0
       }
       if yy > max_height
       {
          yy = max_height
       }
       if yy < 0
       {
          yy = 0
       }
//Note 2    
    if (xx == px and yy == py) or (random(100) < 50)
       {
           dir = floor(random(4))
       }
       else
       {
           if xx < ex
           {
               dir = 0
           }
           if xx > ex
           {
               dir = 2
           }
           if yy < ey
           {
               dir = 3
           }
           if yy > ey
           {
               dir = 1
           }
      }
// Note 3
    var empty = true
    for (var i = 0; i < 4; i ++)
    {
         if room_list[xx][yy][i]
         {
             empty = false
         }
    }
    if empty
    {
        room_count ++
    }
//Note 4
    if xx > px
    {
        room_list[px][py][0] = true
        room_list[xx][yy][2] = true
    }
    if xx < px
    {
        room_list[px][py][2] = true
        room_list[xx][yy][0] = true
    }
    if yy > py
    {
        room_list[px][py][3] = true
        room_list[xx][yy][1] = true
    }
    if yy < py
    {
        room_list[px][py][1] = true
        room_list[xx][yy][3] = true
    }
}
//Note 5
var empty = true
var against = []
var random_count = 0
do {
    sx = floor(random(new_room_width))
    sy = floor(random(new_room_height))
    against = room_list[sx][sy]
    if against[0] or against[1] or against[2] or against[3]
    {
        empty = false
    }
    random_count ++
} until empty = true or random_count > 20
empty = true
against = []
random_count = 0
do {
    ex = floor(random(new_room_width))
    ey = floor(random(new_room_height))
    against = room_list[ex][ey]
    if against[0] or against[1] or against[2] or against[3]
    {
        empty = false
    }
    random_count ++
} until empty = false or random_count > 20
xx = sx
yy = sy
//Note 6
if count = 1 and room_count < (new_room_width * new_room_height) * 0.75
{
    count ++
}
count -= 1
}

Хорошо, это было много. Но давайте пройдемся по этой ерунде, используя примечания, которые я оставил, начиная с примечания 1. Здесь мы начинаем зацикливаться, пока xx и yy не сравняются с end_x и end_y. Мы используем dir, чтобы задать xx и yy направление. Мы также сохраняем xx и yy в px и py для последующего использования. Если xx или yy меньше или больше установленных нами границ, мы помещаем их обратно в границы.

Переходим к примечанию 2. Мы выбираем следующее направление для xx и yy. В этом случае мы выбираем его разумно, перемещая его в сторону end_x и end_y с уклоном в сторону вертикального движения. Если вы делаете платформер, вы можете подумать о перестановке чеков, чтобы игра смещала горизонтальное движение. Также есть вероятность 50/50, что направление будет выбрано случайным образом, просто чтобы добавить немного шума. Наконец, если по какой-либо причине xx и yy не сдвинулись, направление выбирается случайным образом.

Теперь переходим к примечанию 3. Здесь мы проверяем, пуста ли в данный момент комната, в которую мы переместились. Если это так, мы добавляем один к количеству номеров. Это пригодится позже.

В Note 4 мы открываем выходы в каждой комнате, через которую проходим. Сравнив наши предыдущие x и предыдущие y, мы можем узнать, какие выходы открывать в обеих комнатах.

Далее, в примечании 5, мы находим новое начало x и начало y при следующих условиях: мы хотим начать в пустой комнате и закончить в непустой комнате. Поскольку эти уровни генерируются случайным образом, мы не можем просто начинать и заканчивать их в совершенно случайных местах. Пути уровня могут никогда не пересекаться, и вы, по сути, заблокируете доступ игрока к областям карты.

Наконец, в Note 6 мы проверяем, сколько комнат мы создали. Если мы создали менее 75 процентов всех потенциальных комнат для карты, мы гарантируем, что не выйдем из цикла, увеличив значение переменной count.

Если бы вы провели линию от центра каждой непустой комнаты к ее выходам, вы могли бы получить что-то вроде следующего.

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

Двигаясь вперед, нам нужно освоиться с несколькими функциями GML, а именно layer_set_target_room() и room_add(). Рекомендую ознакомиться с документацией здесь и здесь. По сути, эти функции позволяют нам создавать комнату на лету и записывать в нее информацию. Мы собираемся изучить эти концепции в следующей части этого руководства. Теперь, когда у вас есть случайно сгенерированная сетка, вы можете сами попробовать сгенерировать реальный уровень, а затем посмотреть, как я соберу свой. Больше идей — это обычно хорошо! Если вы хотите сразу перейти к проекту, использующему это решение, ознакомьтесь с альфа-версией моего проекта здесь. Спасибо за прочтение и до встречи в следующей части!