Я делаю простую 2D-игру на основе тайлов, в которой используется алгоритм поиска пути A * («Звезда»). У меня все работает правильно, но у меня проблемы с производительностью поиска. Проще говоря, когда я щелкаю непроходимую плитку, алгоритм, очевидно, проходит по всей карте, чтобы найти маршрут к непроходимой плитке, даже если я стою рядом с ней.
Как я могу обойти это? Я думаю, я мог бы уменьшить поиск пути к области экрана, но, может быть, я упускаю что-то очевидное здесь?
algorithm
performance
path-finding
user2499946
источник
источник
Ответы:
Некоторые идеи об избежании поисков, которые приводят к ошибочным путям в целом:
ID острова
Один из самых дешевых способов эффективного завершения поиска A * - вообще не выполнять поиск. Если районы действительно недоступны для всех агентов, заполните каждую область уникальным идентификатором острова под нагрузкой (или в трубопроводе). При поиске пути проверьте, соответствует ли идентификатор острова источника пути идентификатору острова пункта назначения. Если они не совпадают, поиск не имеет смысла - две точки находятся на разных, не связанных между собой островах. Это помогает только при наличии действительно непроходимых узлов для всех агентов.
Ограничить верхнюю границу
Я ограничиваю верхнюю границу максимального количества узлов, которые можно искать. Это помогает непроходимым поискам работать вечно, но это означает, что некоторые проходимые поиски, которые очень долго могут быть потеряны. Это число необходимо настроить, и оно не решает проблему, но снижает затраты, связанные с длительным поиском.
Если вы обнаружите, что это занимает слишком много времени, тогда полезны следующие методы:
Сделайте это асинхронным и ограничить итерации
Пусть поиск запускается в отдельном потоке или немного в каждом кадре, чтобы игра не застыла в ожидании поиска. Отобразите анимацию персонажа, царапающего голову или топающего ноги, или чего-либо подходящего в ожидании окончания поиска. Чтобы сделать это эффективно, я бы сохранил состояние поиска как отдельный объект и учел бы существование нескольких состояний. Когда запрашивается путь, возьмите объект свободного состояния и добавьте его в очередь активных объектов состояния. В своем обновлении поиска пути вытяните активный элемент из передней части очереди и запускайте A *, пока не будет завершено либо A., либо B. Выполнено некоторое ограничение итераций. Если завершено, поместите объект состояния обратно в список свободных объектов состояния. Если он еще не завершен, поместите его в конец «активных поисков» и переходите к следующему.
Выберите правильные структуры данных
Убедитесь, что вы используете правильные структуры данных. Вот как работает мой StateObject. Все мои узлы предварительно выделены для конечного числа - скажем, 1024 или 2048 - по соображениям производительности. Я использую пул узлов, который ускоряет распределение узлов, и это также позволяет мне хранить индексы вместо указателей в моих структурах данных, которые являются u16s (или u8, если у меня есть максимум 255 узлов, что я делаю в некоторых играх). Для поиска пути я использую приоритетную очередь для открытого списка, сохраняя указатели на объекты Node. Он реализован в виде двоичной кучи, и я сортирую значения с плавающей запятой в виде целых чисел, поскольку они всегда положительны, а моя платформа имеет медленные сравнения с плавающей запятой. Я использую хеш-таблицу для своей закрытой карты, чтобы отслеживать узлы, которые я посетил. Он сохраняет NodeID, а не Nodes, чтобы сэкономить на размерах кэша.
Кеш что можешь
Когда вы впервые посещаете узел и вычисляете расстояние до места назначения, кэшируйте его в узле, хранящемся в объекте состояния. При повторном посещении узла используйте кэшированный результат вместо его повторного вычисления. В моем случае это помогает не делать квадратный корень на повторных узлах. Вы можете обнаружить, что есть другие значения, которые вы можете рассчитать и кэшировать.
Другие области, которые вы можете исследовать: используйте двусторонний поиск пути для поиска с любого конца. Я не сделал этого, но, как другие заметили, это может помочь, но не без предостережений. Другая вещь в моем списке, чтобы попробовать это иерархический поиск пути или поиск пути кластеризации. Существует интересное описание в документации HavokAI Здесь описания их кластерную концепцию, которая отличается от HPA * реализации описанного здесь .
Удачи, и дайте нам знать, что вы найдете.
источник
AStar - это полный алгоритм планирования, означающий, что если существует путь к узлу, AStar гарантированно найдет его. Следовательно, он должен проверить каждый путь из начального узла, прежде чем он сможет решить, что целевой узел недоступен. Это очень нежелательно, когда у вас слишком много узлов.
Способы смягчения этого:
Если вы априори знаете, что узел недоступен (например, у него нет соседей или он помечен
UnPassable
), вернитесьNo Path
без вызова AStar.Ограничение количества узлов AStar будет расширяться до завершения. Проверьте открытый набор. Если это когда-нибудь станет слишком большим, прекратите и вернитесь
No Path
. Однако это ограничит полноту Астара; поэтому он может планировать только пути максимальной длины.Ограничьте время, которое Астар занимает, чтобы найти путь. Если время истекло, выйдите и вернитесь
No Path
. Это ограничивает полноту, как предыдущая стратегия, но масштабируется со скоростью компьютера. Обратите внимание, что для многих игр это нежелательно, поскольку игроки с более быстрыми или медленными компьютерами будут по-разному воспринимать игру.источник
Если у цели есть только 6 плиток, доступных вокруг нее, а у источника есть 1002 плитки, поиск останавливается на 6 (двойных) итерациях.
Как только один поиск находит посещенные узлы другого, вы также можете ограничить область поиска посещенными узлами другого и завершить работу быстрее.
источник
Предполагая, что проблема в том, что пункт назначения недоступен. И что сетка навигации не динамична. Самый простой способ сделать это - иметь гораздо более разреженный навигационный граф (достаточно разреженный, чтобы полный прогон был относительно быстрым), и использовать детальный график, только если путь возможен.
источник
Используйте несколько алгоритмов с разными характеристиками
А * имеет некоторые прекрасные характеристики. В частности, он всегда находит кратчайший путь, если он существует. К сожалению, вы также обнаружили некоторые плохие характеристики. В этом случае он должен провести тщательный поиск всех возможных путей, прежде чем допустить, что решения не существует.
«Недостаток», который вы обнаруживаете в A *, заключается в том, что он не знает о топологии. У вас может быть двумерный мир, но он этого не знает. Насколько он знает, в дальнем углу вашего мира находится лестница, которая доставляет его прямо под мир к месту назначения.
Попробуйте создать второй алгоритм, который знает топологию. В качестве первого прохода вы можете заполнить мир «узлами» каждые 10 или 100 пробелов, а затем поддерживать график связности между этими узлами. Этот алгоритм будет искать пути, находя доступные узлы рядом с началом и концом, а затем пытаясь найти путь между ними на графе, если таковой существует.
Один из простых способов сделать это - назначить каждую плитку узлу. Нетрудно показать, что вам нужно назначить только один узел для каждой плитки (вы никогда не сможете получить доступ к двум узлам, которые не связаны на графике). Тогда ребра графа определены так, что две плитки с разными узлами смежны.
Этот график имеет недостаток: он не находит оптимальный путь. Он просто находит в путь. Однако теперь он показал вам, что A * может найти оптимальный путь.
Он также предоставляет эвристику для улучшения ваших недооценок, необходимых для выполнения функции A *, потому что теперь вы знаете больше о своем ландшафте. У вас меньше шансов полностью исследовать тупик, прежде чем вы поймете, что вам нужно сделать шаг назад, чтобы идти вперед.
источник
Еще несколько идей в дополнение к ответам выше:
Кэшируйте результаты поиска A *. Сохраните данные пути из ячейки A в ячейку B и, если возможно, используйте повторно. Это более применимо к статическим картам, и вам придется больше работать с динамическими картами.
Кэшируйте соседей каждой ячейки. Реализация * должна расширять каждый узел и добавлять его соседей в открытый набор для поиска. Если эти соседи вычисляются каждый раз, а не кешируются, это может значительно замедлить поиск. И если вы уже сделали это, используйте приоритетную очередь для A *.
источник
Если ваша карта статична, вы можете просто иметь каждый отдельный раздел с собственным кодом и проверить это перед запуском A *. Это можно сделать при создании карты или даже закодировать на карте.
У непроходимых плиток должен быть флажок, и при переходе к такой плитке вы можете отказаться от запуска A * или выбрать рядом с ней плитку, которая является достижимой.
Если у вас есть динамические карты, которые часто меняются, вам не повезло. Вы должны сделать так, чтобы ваше решение, заставляющее ваш алгоритм остановиться до завершения или проверять разделы, часто закрывалось.
источник
Профилируйте свою
Node.IsPassable()
функцию, выясняйте самые медленные части, ускоряйте их.Решая, является ли узел проходимым, ставьте наиболее вероятные ситуации наверху, чтобы большую часть времени функция возвращалась сразу, не удосуживаясь проверить более неясные возможности.
Но это для ускорения проверки одного узла. Вы можете профилировать, чтобы увидеть, сколько времени тратится на запросы узлов, но похоже, что ваша проблема в том, что слишком много узлов проверяется.
Если сама мозаика назначения непроходима, алгоритм не должен проверять никакие плитки вообще. Прежде чем даже начать поиск пути, он должен запросить плитку назначения, чтобы проверить, возможно ли это, а если нет, вернуть результат без пути.
Если вы имеете в виду, что само место назначения проходимо, но окружено непроходимыми плитками, так что пути нет, то для A * нормально проверить всю карту. Как еще он узнает, что пути нет?
Если последнее имеет место, вы можете ускорить его, выполнив двунаправленный поиск - таким образом, поиск, начинающийся с места назначения, может быстро обнаружить отсутствие пути и остановить поиск. Посмотрите этот пример , окружите пункт назначения стенами и сравните двунаправленное и единичное направление.
источник
Делайте поиск пути назад.
Если только ваша карта не имеет больших непрерывных областей недоступных тайлов, это будет работать. Вместо поиска по всей карте достижимости, поиск пути будет искать только в закрытой недоступной области.
источник
Если области, к которым подключен плеер (нет телепорта и т. Д.), И недоступные области, как правило, не очень хорошо связаны, вы можете просто выполнить A *, начиная с узла, к которому вы хотите добраться. Таким образом, вы все равно можете найти любой возможный маршрут к месту назначения, и A * прекратит быстрый поиск недоступных районов.
источник
Другие ответы хороши, но я должен указать на очевидное: вам вообще не следует искать пути к непроходимой плитке.
Это должен быть ранний выход из алгоритма:
источник
Чтобы проверить самое длинное расстояние на графике между двумя узлами:
(при условии, что все ребра имеют одинаковый вес)
v
.v
, мы назовем ееd
.u
.u
, мы ее назовемw
.u
иw
является самым длинным расстоянием на графике.Доказательство:
y
иx
больше!D2 + R < D3
D2 < R + D3
v
иx
больше, чемv
иu
?u
бы не было выбрано на первом этапе.Кредит проф. Шломи Рубинштейн
Если вы используете взвешенные ребра, вы можете выполнить то же самое за полиномиальное время, запустив Dijkstra вместо BFS, чтобы найти самую дальнюю вершину.
Пожалуйста, обратите внимание, я предполагаю, что это связанный граф. Я также предполагаю, что это не направлено.
A * не очень полезен для простой игры на основе 2d плиток, потому что, если я правильно понимаю, если предположить, что существа движутся в 4 направлениях, BFS достигнет тех же результатов. Даже если существа могут двигаться в 8 направлениях, ленивая BFS, которая предпочитает узлы ближе к цели, все равно достигнет тех же результатов. A * - это модификация Dijkstra, которая намного дороже в вычислительном отношении, чем использование BFS.
BFS = O (| V |) предположительно O (| V | + | E |), но не совсем в случае отображения сверху вниз. A * = O (| V | log | V |)
Если у нас есть карта с 32 x 32 тайлами, BFS будет стоить не более 1024, а истинный A * может стоить вам колоссальных 10000. Это разница между 0,5 и 5 секундами, возможно, больше, если принять во внимание кэш. Поэтому убедитесь, что ваш A * ведет себя как ленивый BFS, который предпочитает плитки, которые находятся ближе к желаемой цели.
* Полезно для навигационных карт, где стоимость краев важна в процессе принятия решений. В простых играх на основе тайловых издержек стоимость краев, вероятно, не является важным фактором. В случае, если это так (разные плитки стоят по-разному), вы можете запустить модифицированную версию BFS, которая откладывает и штрафует пути, проходящие через плитки, которые замедляют персонажа.
Так что да, BFS> A * во многих случаях, когда речь идет о плитках.
источник
log|V|
в А * сложности 's действительно происходит от поддержания этого открытого набора, или размера полосы, и для сетки карты это крайне мало - о журнале (SQRT (| V |)) с помощью нотации. Журнал | V | отображается только в гипер-связанных графах. Это пример, где наивное применение сложности наихудшего случая дает неверное заключение.