Dungeon Generation без коридоров и комнатных зависимостей

15

Я делаю игру с процедурно сгенерированным миром, созданным в начале игры, состоящим из нескольких областей, представленных сетками (скажем, 8x8, 9x6, размеры в идеале были бы произвольными). Эти области должны быть связаны друг с другом через список зависимостей.

Соединение существует, когда по крайней мере 3 пространства этой сетки открыты между этими двумя областями. В средней ячейке этой 3 области пространственного соединения находится дверной проем между областями:

Я пытался найти способ соединить их, но он становится все более сложным, чем больше областей вы должны рассмотреть одновременно.

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

Вот «простой» пример, с которым я сейчас борюсь:

  • Область 'a' должна быть связана с 'b' и 'c'
  • Область 'b' должна быть связана с 'a' и 'd'
  • Область 'c' должна быть связана с 'a' и 'd'
  • Область 'd' должна быть связана с 'b' и 'c'

Для простоты рассмотрим, как мы размещаем комнаты в порядке их появления в списке (я пробовал другие). Поэтому я рассматриваю это как ваш стандартный процедурный алгоритм Dungeon Generation.

Мы ставим «а» в любом месте на доске, так как это первая область. Затем мы выбираем случайную стену и, поскольку с этой стеной ничего не связано, мы можем разместить там «b»:

Теперь нам нужно разместить «c», но «a» уже находится на доске и имеет занятую стену, поэтому мы решили разместить ее на другой стене. Но не все места размещения подойдут, потому что появляется «d», и его тоже нужно соединить с «b» и «c»:

Я пробовал возможное ограничение, что 2 комнаты с одинаковым набором зависимостей не могут находиться на противоположных стенах, но даже это не гарантирует успеха:

А в других случаях, когда области имеют разные размеры, может оказаться, что они находятся на противоположных стенах:

Кроме того, игнорирование используемой стены является ошибочным допущением, поскольку оно исключает допустимые решения:

Я пытался найти исследование других алгоритмов процедурного генерирования или аналогичных им, таких как алгоритмы оптимальной упаковки прямоугольников и компоновки графиков, но обычно эти алгоритмы не учитывают все ограничения этой проблемы и их трудно совместить друг с другом.

Я думал о множестве подходов, включая размещение области и возврата, пока не будет найдено подходящее размещение, но они кажутся очень зависимыми от проб и ошибок и дорогостоящими с точки зрения вычислений. Но, учитывая обширные исследования последних двух проблем, которые я упомянул, это может быть единственным / лучшим решением?

Я просто хотел посмотреть, сталкивался ли кто-то с подобными проблемами в прошлом или хочет помочь мне разобраться в этом и дать несколько советов о том, с чего мне начать с алгоритма. Или, если это не удастся, мне придется ослабить установленные мной ограничения.

Джоана Алмейда
источник
Комнаты должны быть полностью квадратными?
волчий рассвет
Если вы имеете в виду, должны ли они иметь 4 стены и не больше, то да, но я сделал это, чтобы упростить мировое пространство. Мне нужно легко рассчитать пространство, которое занимает каждая область, чтобы я знал, смогу ли я разместить на ней все, что захочу.
Джоана Алмейда

Ответы:

6

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

Определение государства мира следующим образом :

//State:
//    A list of room states.
//    Room state:
//      - Does room exist?
//      - Where is room's location?
//      - What is the room's size?

Определите ограничение как:

 // Constraint(<1>, <2>):
 //  - If room <1> and <2> exist, Room <1> is adjacent to Room <2>

Где «смежный», как вы описали (разделяя как минимум 3 соседей)

Constraint называется недействительным всякий раз , когда две комнаты не смежные, и существуют обе комнаты.

Определите состояние, чтобы быть действительным, когда:

// foreach Constraint:
//        The Constraint is "not invalidated".
// foreach Room:
//       The room does not intersect another room.

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

// Returns a list of valid actions from the current state
function List<Action> GetValidActions(State current, List<Constraint> constraints):
    List<Action> actions = new List<Action>();
    // For each non-existent room..
    foreach Room room in current.Rooms:
        if(!room.Exists)
            // Try to put it at every possible location
            foreach Position position in Dungeon:
                 State next = current.PlaceRoom(room, position)
                 // If the next state is valid, we add that action to the list.
                 if(next.IsValid(constraints))
                     actions.Add(new Action(room, position));

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

// Given a start state (with all rooms set to *not exist*), and a set of
// constraints, finds a valid end state where all the constraints are met,
// using a depth-first search.
// Notice that this gives you the power to pre-define the starting conditions
// of the search, to for instance define some key areas of your dungeon by hand.
function State GetFinalState(State start, List<Constraint> constraints)
    Stack<State> stateStack = new Stack<State>();
    State current = start;
    stateStack.push(start);
    while not stateStack.IsEmpty():
        current = stateStack.pop();
        // Consider a new state to expand from.
        if not current.checked:
            current.checked = true;
            // If the state meets all the constraints, we found a solution!
            if(current.IsValid(constraints) and current.AllRoomsExist()):
                return current;

            // Otherwise, get all the valid actions
            List<Action> actions = GetValidActions(current, constraints);

            // Add the resulting state to the stack.
            foreach Action action in actions:
                State next = current.PlaceRoom(action.room, action.position);
                stateStack.push(next);

    // If the stack is completely empty, there is no solution!
    return NO_SOLUTION;

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

mklingen
источник
Забавно, что вы упомянули это решение. Я разговаривал с другом ранее, и он упомянул, что мне, вероятно, следует изучить алгоритмы поиска на основе дерева, но я не был уверен, как использовать их в этом контексте. Ваш пост был сенсационным! Это, безусловно, кажется возможным решением, если вы управляете генерацией веток и выполняете несколько оптимизаций, чтобы как можно скорее обрезать плохие ветки.
Джоана Алмейда
7

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

Общий алгоритм

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

  2. Разбросайте точки случайным образом по этому пустому пространству. Вы можете использовать простую случайную проверку каждой плитки, чтобы увидеть, получает ли она точку, или использовать стандартное / гауссово распределение для разброса точек. Назначьте уникальный цвет / числовое значение для каждой точки. Это идентификаторы. (PS Если после этого шага вы чувствуете, что вам нужно увеличить свое пространство, обязательно сделайте.)

  3. Для каждой такой сгенерированной точки последовательно увеличивайте круг или прямоугольник границ на один шаг (обычно со скоростью 0,5-1,0 ячеек / пикселей на шаг) в xиy . Вы можете либо вырастить все границы параллельно, начиная с нулевого размера на одном и том же шаге, или вы можете начать выращивать их в разное время и с разными скоростями, отдавая предпочтение размеру тех, которые начинаются раньше (представьте, что сеянцы растут, где некоторые начать поздно). Под «ростом» я подразумеваю заполнение вновь увеличенных границ цветом / идентификатором, уникальным для начальной точки этих границ. Метафора для этого - держать маркеры на обратной стороне листа бумаги и наблюдать, как чернильные пятна разного цвета растут, пока они не встретятся.

  4. В какой-то момент границы некоторой точки и другой точки будут сталкиваться на этапе роста. Это точка, в которой вы должны прекратить увеличивать границы для этих двух точек - по крайней мере, в едином смысле, описанном в шаге 3.

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

Пост-процесс Заполнение пространства

Для заполнения оставшихся пустых / пробелов на шаге 5 можно использовать различные методы:

  • Сделайте так, чтобы одна соседняя, ​​уже окрашенная область требовала пространства, заливая его таким цветом, чтобы все это соединилось.
  • Залить новыми, пока неиспользованными цветами / номерами / идентификаторами, чтобы они образовывали совершенно новые области.
  • Круговой подход подходит таким образом, что каждая уже заполненная соседняя область «врастает» немного в пустое пространство. Подумайте о животных, пьющих вокруг водопоя: они все получают немного воды.
  • Не полностью заполняйте пустое пространство, просто пересеките его, чтобы связать существующие области, используя прямые переходы.

возмущение

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

Теория, ради интереса

Это похоже на подход, использованный в диаграммах Вороного / триангуляции Делоне , за исключением того, что в приведенном выше примере вы явно не создаете ребра - вместо этого, когда сталкиваются ограничивающие области, рост прекращается. Вы заметите, что диаграммы Вороного заполняют пространство; это потому, что они не прекращают рост только при прикосновении, а скорее при некоторой номинальной степени совпадения. Вы можете попробовать подобное.

инженер
источник