Алгоритм A* для новичков.

advertisement
Алгоритм A* для новичков.
Автор: Патрик Лестер (Patrick Lester) (2 апреля 2004) Почта:
Перевод: Morpher (26 июля 2004) Почта: Morpher_KKnD@mail.ru
Данная статья переведена на Испанский и Французкий. Другие переводы приветствуются.
Алгоритм A* (произносится как "А-звездочка"), возможно, немного трудноват для новичков. И хотя в сети существует
множество материалов, обьясняющих A*, большинство из них написаны для людей, которые уже понимают основы. Эта же
статья действительно написана для новичков.
Статья не пытается быть исчерпывающим материалом по данной теме. Вместо этого она описывает фундаментальные
понятия и подготавливает вас к тому, чтобы прочитать все те материалы и понять о чем в них идет речь. Ссылки на
некоторые из них прилагаются в конце статьи (под заголовком "Дальнейшее изучение").
Также эта статья не направлена на определенный язык программирования - вы сможете адаптировать ее к любому из них.
Как вы, должно быть, и ожидали, я добавил ссылку на пример реализации в конце статьи. Архив примера содержит две
версии: на C++ и на Blitz Basic. Он также содержит скомпилированную версию, если вы просто хотите увидеть A* в действии.
Но пора приступать к делу. Давайте начнем с самого начала...
Введение: Область Поиска
Давайте представим себе, что у нас есть кто-то, кто хочет попасть из точки А в точку B. Эти две точки разделены стеной. На
иллюстрации ниже зеленый квадрат это стартовая точка A, красный квадрат - целевая точка B, а несколько синих квадратов стена между ними.
[Рис. 1]
Первое, на что вы должны обратить внимание, то, что мы разделили нашу область поиска на сетку с квадратными ячейками.
Упрощение области поиска это первый шаг в поиске пути. Этот метод упрощает нашу область поиска до простого двумерного
массива. Каждый элемент масссива представляет одну из клеток сетки, а его значением будет проходимость этой клетки
(проходима и непроходима). Для нахождения пути нам необходимо определить какие нам нужны клетки для перемещения из
точки A в точку B. Как только путь будет найден, наш путник начнет двигаться с центра одной клетки на центр следующей до
тех пор, пока не достигнет целевой клетки.
Эти центральные точки называют "вершинами". Когда вы что-нибудь читаете про поиск пути, то часто можно столкнуться с
обсуждением вершин. Почему бы просто не назвать их клетками? Потому что всегда возможно разделить вашу область
поиска на что-то отличное от квадратов. Например, на прямоугольники, шестиугольники, треугольники, или любую другую
фигуру. И вершины могут располагаться где-угодно - в центре, вдоль граней или еще где-нибудь. Мы используем эту систему,
поскольку она самая простая.
Начало Поиска
Как только мы упростили нашу область поиска до некоторого числа вершин, нам нужно начать поиск для нахождения
кратчайшего пути. Начнем с точки A, проверяя соседние клетки и двигаясь дальше до тех пор, пока не найдем целевую точку.
Начинаем поиск пути выполняя следующее:
Начинаем со стартовой точки A и добавляем ее в "открытый список" клеток, которые нужно обработать.
Открытый список это что-то наподобие списка покупок. В данный момент есть только один элемент в списке, но позже мы
добавим еще. Список содержит клетки, которые может быть находятся вдоль пути, который вы выберете, а может и нет.
Проще говоря, это список клеток, которые нужно проверить.
2.
Ищем доступные или проходимые клетки, граничащие со стартовой точкой, игнорируя клетки со
стенами, водой или другой непроходимой областью. И также добавляем их в открытый список. Для каждой из этих клеток
сохраняем точку A, как "родительскую клетку". Эта родительская клетка важна, когда мы будем прослеживать наш путь. Это
будет описано намного позже.
3.
Удаляем стартовую точку A с вашего открытого списка и добавляем ее в "закрытый список" клеток,
которые вам больше не нужно проверять.
1.
Теперь у вас должно быть что-то похожее на следующую иллюстрацию. На этой иллюстрации темно-зеленый квадрат в
центре - ваша стартовая точка. Она выделена голубым цветом для отображения того, что она находится в закрытом списке.
Все соседние клетки в данный момент находятся в открытом списке. Они выделены светло-зеленым. Каждая имеет серый
указатель, направленный на родительскую клетку, которая в нашем случае является стартовой точкой.
[Рис. 2]
Дальше мы выберем одну из соседних клеток в открытом списке и практически повторим вышеописанный процесс . Но какую
клетку мы выберем? Ту, у которой меньше стоимость F.
Оценка пути
Способом определения того, какую же клетку использовать, является следующие выражение:
F=G+H
где

G = стоимость передвижения из стартовой точки A к данной клетке, следуя найденному пути к этой
клетке.

H = примерная стоимость передвижения от данной клетки до целевой, то есть точки B. Она обычно
является эвристической функцией. Причина по которой она так называется в том, что это предположение. Мы действительно
не узнаем длину пути, пока не найдем сам путь, потому что в процессе поиска нам может встретиться множество вещей
(например, стены и вода). В этой статье вам предложили один способ вычислить H, но существует множество способов,
которые можно найти в других статьях.
Наш путь генерируется путем повторного прохода через открытый список и выбора клетки с наименьшей стоимостью F. Этот
процесс будет описан в статье более подробно, но немного позже. Прежде всего давайте внимательно рассмотрим
вычисление стоимости F.
Как описано выше, G является стоимостью передвижения со стартовой клетки до текущей, используя найденный к ней путь. В
этом примере мы присвоим стоимость 10 к горизонтальным и вертикальным передвижениям, а к диагональным - 14. Мы
используем эти числа потому, что пройденное по диагонали расстояние примерно в 1,414 раз (корень с 2) больше стоимости
передвижения по горизонтали или вертикали. Для простоты мы используем 10 и 14. Соотношение соблюдается и мы
избегаем вычисления квадратных корней и десятичной дроби. Это не просто потому, что мы дураки и не любим математику.
Использование целых чисел вроде этих, намного быстрее для компьютера. Как вы скоро узнаете, поиск пути может быть
очень медленным если вы не используете упрощения наподобие этих.
Так как мы вычисляем стоимость G вдоль пути к текущей точке, способ ее установить состоит в том, чтобы взять G
родительской клетки и прибавить 10 или 14, в зависимости от диагонального или ортогонального (не диагонального)
расположения текущей клетки относительно родительской клетки. Необходимость использования этого метода станет
очевидной немного позже, когда мы отдалимся от стартовой точки более чем на одну клетку.
Стоимость H может быть вычислена множеством способов. Метод, который мы используем, называется методом Манхеттена
(Manhattan method), где вы считаете общее количество клеток, необходимых для достижения целевой точки от текущей, по
горизонтали и вертикали, игнорируя диагональные перемещения и любые препятствия, которые могут оказаться на пути.
Затем мы умножаем общее количество полученных клеток на 10.
Читая это описание вы, должно быть, решили, что эвристика - просто приблизительное определение оставшегося расстояния
между текущей клеткой и целью по прямой. Но это не так. Мы пытаемся установить оставшееся расстояние вдоль пути
(который обычно идет не по прямой), но алгоритм требует от нас не переоценить это расстояние, иначе он может найти не
верный путь. Использованный здесь метод гарантирует предоставить нам правильный путь. Хотите узнать про эвристику
больше? Вы найдете выражения и дополнительные сведенья здесь.
Стоимость F вычисляется путем сложения стоимостей G и H. Результаты первого шага нашого поиска пути
проиллюстрированы ниже. Значения F, G и H записаны в каждой клетке. Как видим по клетке справа от стартовой точки, F
выводится в верхнем левом углу, G выводится в нижнем левом углу, а H выводится в нижнем правом углу.
[Рис. 3]
Давайте посмотрим на некоторые из этих клеток. В клетке с буквами G = 10. Это потому, что она находится на расстоянии в
одну клетку от стартовой точки, при том по горизонтали. Также G = 10 у клеток прямо сверху, снизу и слева от стартовой
точки. У диагональных клеток G = 14.
Стоимость H посчитана с помощью вычисления Манхеттенского расстояния к красной целевой клетке, двигаясь только по
горизонтали и вертикали, игнорируя все стены на пути. У клетки, прямо справа от стартовой, расстояние до цели 3 клетки.
Используя этот метод видим, что H = 30. У клетки прямо над ней, расстояние уже 4 клетки (помните, что надо двигаться
только по горизонтали и вертикали). И ее значение стоимости H будет равно 40. Вы, вероятно, можете догаться, как
вычисляются стоимости H для других клеток.
Стоимость F для каждой клетки вычисляется простым суммированием G и H.
Продолжаем поиск
Для продолжения поиска мы просто выбираем клетку с наименьшей стоимостью F из всех клеток, находящихся в открытом
списке. Затем с выбранной клеткой мы производим такие действия:
4) Удаляем ее из открытого списка и добавляем в закрытый список.
5) Проверяем все соседние клетки. Игнорируем те, которые находятся в закрытом списке или непроходимы (поверхность со
стенами, водой), остальные добавляем в открытый список, если они там еще не находятся. Делаем выбранную клетку
"родительской" для всех этих клеток.
6) Если соседняя клетка уже находится в открытом списке, проверяем, а не короче ли путь по этой клетке? Иными словами,
сравниваем значения стоимости G этих двух клеток. Если при использовании этой клетки стоимость G выше, чем при
использовании текущей клетки, то не предпринимаем ничего.
Если же она ниже, то меням "родителя" этой клетки на выбранную клетку. Затем вычисляем стоимости F и G этой клетки.
Если это выглядит для вас немного запутанным, далее вы можете увидеть это на иллюстрации.
Хорошо, давайте посмотрим, как это работает. С наших начальных 9 клеток, осталось 8 в открытом списке, а стартовая
клетка была внесена в закрытый список. Клетка с наименьшей стоимостью F находится прямо справа от стартовой клетки, и
ее стоимость F = 40. Поэтому мы выбираем эту клетку как нашу следующую клетку. Она выделена голубым цветом на этой
иллюстрации.
[Рис. 4]
Сначала мы удаляем ее из открытого списка и добавляем в закрытый список (вот почему она выделена голубым цветом).
Затем мы проверяем соседние клетки. Клетки, сразу справа от этой клетки - стены, поэтому мы их игнорируем. Клетка, прямо
слева - стартовая клетка. Она находится в закрытом списке, поэтому мы ее тоже игнорируем.
Оставшиеся 4 клетки уже находятся в открытом списке, поэтому мы должны проверить, не короче ли пути по этим клеткам,
используя текущую клетку. Сравнивать будем по соимости G. Давайте посмотрим на клетку, прямо под нашей выбранной
клеткой. Ее стоимость G равна 14. Если мы будем двигаться по этой клетке, стоимость G будет равна 20 (10, соимость G
чтобы добраться к текущей клетке плюс 10 для движения вертикально вверх, к соседней клетке). Стоимость G = 20 больше,
чем G = 14, потому это будет не лучший путь. Это станет понятным, если взглянуть на диаграму. Более целесообразным
будет движение по диагонали на одну клетку, чем движение на одну клетку по горизонтали, а потом одну по вертикали.
Когда мы повторим этот процесс для всех 4-х соседних клеток, которые находятся в открытом списке, то узнаем, что ни один
из путей не улучшится при движении по этим клеткам через выбранную, потому ничего не меняем. Теперь, когда мы
осмотрели все соседние клетки, то закончили с текущей клеткой и готовы двигаться к следующей.
Теперь мы проходим весь открытый список, который уменьшился до 7-ми клеток, и выбираем клетку с наименьшей
стоимостью F. Интересно, что в этом случае существует 2 клетки со стоимостью 54. Так какую мы выберем? Это не имеет
никакого значения. В целях увеличения скорости поиска можно выбрать последнюю клетку, которую мы добавили в открытый
список. Это предупредит поиск в выборе клеток, к которым можно будет обратиться позже, когда мы подберемся ближе к
цели. Но в действительности это не так уж важно. (Вот почему две версии A* могут найти разные пути с одинаковой длиной.)
Так что давайте выберем клетку, прямо внизу, спава от стартовой, как показано на рисунке.
[Figure 5]
В этот раз, когда мы проверяем соседние клетки, видим, что клетка, прямо справа - стена и мы ее пропускаем. Так же
поступаем и с клеткой, которая находится прямо над ней. Так же мы игнорируем клетку, которая находится прямо под ней.
Почему? Потому, что вы не можете добраться до той клетки без среза угла ближайшей стены. Сначала вы должны
спуститься вниз, а только потом двигаться на эту клетку. (Замечание: Это правило среза углов необязательно. Его
использование зависит от расположения ваших вершин.)
Остается еще 5 клеток. 2 клетки, находящиеся под текущей, еще не в открытом списке, потому мы их добавляем в открытый
список и назначаем текущую клетку их "родителем". Из 3-х других клеток 2 уже находятся в закрытом списке (стартовая
клетка и клетка, прямо над ней, на диаграмме обе подсвечены голубым цветом) и мы их игнорируем. Последняя клетка,
которая находится прямо слева от текущей, проверяется на длину пути по текущей клетке через эту клетку по стоимости G.
Нет, путь будет не короче. Так что мы здесь закончили и готовы проверить следующую клетку в открытом списке.
Повторяем этот процесс до тех пор, пока не добавим целевую клетку в открытый список. К этому времени у вас получится
что-то похожее на иллюстрацию ниже.
[Рис. 6]
Заметьте, что родительская клетка для клетки, находящейся в 2-х клетках под стартовой изменилась по сравнению с
предидущей иллюстрацией. Преред этим у нее стоимость G была равна 28 и указатель был направлен вверх и влево. Теперь
стоимость G равна 20, а указатель направлен прямо вверх. Это произошло где-то в процессе нашего поиска, когда была
проверена стоимость G и оказалось, что путь через эту клетку будет более коротким. Поэтому поменялась ее родительская
клетка и были пересчитаны стоимости G и F. И хотя в этом примере это не кажется очень важным, существует множестиво
ситуаций, когда такая проверка
будет сильно влиять на выбор более короткого пути к цели.
Так как же мы определим сам путь? Очень просто. Начнем с красной целевой клетки и будем двигаться назад с клетки на ее
родителя, следуя указателям. Это доставит вас к стартовой клетке и это и будет ваш путь. Получится как показано на
иллюстрации ниже. Движение от стартовой точки A к целевой точке B будет просто передвижением от центра каждой клетки
(вершины) к центру следующей клетки до тех пор, пока вы не достигните цели.
[Рис. 7]
Итоги метода A*
Хорошо, вы прошли все обьяснение, давайте посмотрим на пошаговое представление этого метода:
1) Добавляем стартовую клетку в открытый список.
2) Повторяем следующее:
a) Ищем в открытом списке клетку с наименьшей стоимостью F. Делаем ее текущей клеткой.
b) Помещаем ее в закрытый список. (И удаляем с открытого)
c) Для каждой из соседних 8-ми клеток ...

Если клетка непроходимая или она находится в закрытом списке, игнорируем ее. В противном
случае делаем следующее.

Если клетка еще не в открытом списке, то добавляем ее туда. Делаем текущую клетку родительской
для это клетки. Расчитываем стоимости F, G и H клетки.

Если клетка уже в открытом списке, то проверяем, не дешевле ли будет путь через эту клетку. Для
сравнения используем стоимость G. Более низкая стоимость G указывает на то, что путь будет дешевле. Эсли это так, то
меняем родителя клетки на текущую клетку и пересчитываем для нее стоимости G и F. Если вы сортируете открытый список
по стоимости F, то вам надо отсортировать свесь список в соответствии с изменениями.
d) Останавливаемся если:


Добавили целевую клетку в открытый список, в этом случае путь найден.
Или открытый список пуст и мы не дошли до целевой клетки. В этом случае путь отсутствует.
3) Сохраняем путь. Двигаясь назад от целевой точки, проходя от каждой точки к ее родителю до тех пор, пока не дойдем до
стартовой точки. Это и будет наш путь.
Послесловие
Для ипользования алгоритма A*, вам необходимо включить элементы, описанные выше -- открытый и закрытый списки,
стоимости F, G, и H. Существует множество других алгоритмов поиска пути, но эти методы не A*, который считается лучшим
из многих. Брайан Стаут (Bryan Stout) обсуждает многие из них в статье, ссылка на которую расположена чуть дальше.
Некоторые альтернативные методы будут лучше при некотрых обстоятельствах, но вы должны понимать, что вам надо.
Ладно, хватит разглагольствований. Вернемся к нашей статье.
Заметки к реализации
Теперь, когда вы понимаете основы метода, вот несколько тем к размышлению, когда вы начнете писать свою программу.
Некоторые из этих материалов ссылаются на программу, которую я написал на C++ и Blitz Basic, но это будет актуально и для
других языков.
1. Другие юниты (уклонение от столкновений) Если вы присмотритесь внимательней к моему примеру реализации
алгоритма, вы заметите, что он полностью игнорирует все остальные юниты на экране. Юниты спокойно проходят сквозь друг
друга. В зависимости от игры, это может быть приемлимым, но может и не быть. Если вы хотите чтобы юниты обходили друг
друга, то я предлагаю вам несколько методов. Можно обратбатывать юниты, которые не двигаються или находятся близко к
юниту, для которого ищется путь, а остальные просто игнорировать. Если же вы хотите обрбатывать юниты, которые
двигаются и находятся на расстоянии более чем в одну вершину, то вам нужно будет разработать метод для установления их
положения в пространстве в любое время, чтобы они могли должным образом уклониться. Иначе в дальнейшем вы можете
столкнуться со странными путями, где юниты двигаються по зигзагообразной траектории, пытаясь обьехать другие юниты,
которых там давно нет. Вам также потребуется система определения столкновений, так как не важно насколько хорош ваш
путь, со временем многие вещи могут измениться. Когда происходит столкновение, для юнита надо просчитать новый путь
или, если это не лобовое столкновение, подождать пока другой юнит отойдет с дороги, перед тем, как двигаться дальше.
Вот некоторые ссылки, которые могут вам пригодиться:

Steering Behavior for Autonomous Characters: по движению автономных устройств немного отходит от
задачи поиска пути, но она может быть интегрирована с ним для более совершенного перемещения и определения
столкновений.

The Long and Short of Steering in Computer Games: Интересный обзор литературы по передвижению
и поиску пути. Это pdf документ.

Coordinated Unit Movement: Первая статья из двухсерийного цикла про формацию и сгруппированое
передвижение юнитов от дизайнера Эпохи Империй (Age of Empires) Дейва Поттингера (Dave Pottinger).

Implementing Coordinated Movement: Вторая статья из этого цикла.
2. Различная стоимость передвижения: В этой статье и в моей прилагающейся программе поверхность может быть
только двух типов - проходимая и непроходимая. Но что, если у вас есть территория, которая проходимая, но имеет большую
стоимость передвижения? Болота, возвышенности, лестницы в пещерах, и т.д. - это примеры поверхности, котрую можно
пройти, но стоимость передвижения у нее выше, чем стоимость передвижения по ровной, открытой поверхности. Также
стоимость передвижения по дорогам может быть немного ниже стоимости передвижения по другим видам поверхности.
Эта проблема легко решается путем добавления стоимости поверхности при вычислении стоимости G любой вершины.
Просто добавьте дополнительную стоимость к таким вершинам. Алгоритм поиска пути A* написан для нахождения пути с
наименьшей стоимостью и легко справится с такой задачей. В простом примере, который я описал, когда поверхность может
быть или проходимой или нет, A* будет искать кратчайший, более прямолинейный путь. Но в случае с различной стоимостью
поверхности, юнит может пойти более длинным путем - как, например, при движении по дороге через болото, а не движении
напрямик по самому болоту.
Существует еще одно интересное решение, которое проффесионалы называют "influence mapping" (что-то вроде карты с
областями влияния). Так же как и с различной стоимостью поверхности, вы можете создавать дополнительные системы и
применять их для целей ИИ (искуственного интеллекта). Представьте, что у вас есть карта, на которой находятся толпы
юнитов, охраняющих проезд через горный регион. Каждый раз, когда компьютер посылает свой юнит через этот проход, он
уничтожается. Если вы захотите, то можете создать "карту влияний", которая будет "штрафовать" вершины, возле которых
множество вражеских единиц. Это научит компьютер планировать более безопасные пути и поможет избежать глупых
ситуаций, когда компьютер продолжит посылать юниты через более опасный район только потому, что он более короткий.
3. Обработка неизведанных территорий: Вы когда-нибудь играли в игры, в которых компьютер всегда знает точно какой
путь выбрать, даже если карта еще не полностью исследована? В зависимости от игры, поиск пути это то, что может быть
очень нереалистичным. К сожалению, это та проблема, которую не так-то просто решить.
Ответ заключается в создании массива "известная Проходимость" для каждого из игроков и компьютерных оппонентов
("каждый игрок" не значит "каждый юнит", это потребовало бы очень много памяти). Каждый такой массив должен содержать
информацию про области, которые игрок уже исследовал, остальные же области должны оставаться непроходимыми до
исследования. Используя такое решение, юниты будут попадать в тупики и искать неверный путь до тех пор, пока они не
откроют всю карту. Как только карта будет исследована, поиск пути станет всегда находить верные пути.
4. Сглаженные пути: A* даст вам кратчайший, с наименьшей стоимостью путь, но он не даст вам визуально сглаженного
пути. Давайте посмотрим на окончательный путь, просчитанный в примере (рис. 7). На этом пути первый шаг находится
справа внизу от стартовой клетки. Не казался бы наш путь более плавным, если бы первый шаг находился прямо под
стартовой клеткой?
Есть несколько способов решить эту проблему. В процессе поиска вы можете "штрафовать" вершины, где есть смена
направления движения, увеличивая их стоимости G. Так же вы можете пройти по всему пути после его вычисления и
выискивать вершины, в которых вы бы хотели изменить направление для более сглаженного пути. Для более обширного
описания проблемы, проверьте , Toward More Realistic Pathfinding, бесплатную (но требующую регистрации) статью Марка
Пинтера (Marco Pinter) на Gamasutra.com.
5. Неквадратные области поиска: В нашем примере мы используем простые плоские квадратные ячейки. Необязательно
всегда выбирать такое решение. Вы можете использовать области с неправильной формой. Вспомните про игру Risk (Риск :-)
и страны в ней. Вы можете захоть использовать такой сценарий поиска пути. Для того, чтобы это сделать вам необходимо
создать таблицу для хранения информации о том, какие страны с какими граничат и стоимости G для передвижения от одной
страны к другой. Вы также должны выбрать метод определения стоимости H. Все остальное остается таки же, как и в нашем
примере. При добавлении элементов в открытый список, вместо просмотра соседних клеток просто просматривайте соседние
страны в таблице.
Также вы можете создать систему вэйпоинтов для путей нафиксированной карте. Вэйпоинты представляют собой несколько
связанных точек пути, например, на дороге или в тунелле подземелья. Как гейм-дизайнер вы можете указывать эти
вэйпоинты вручную. Два вэйпоинта считаются соседними, если на линии между ними нет никаких препятствий. В случае с
игрой Риск (Risk), вы сохраняете эту информацию о соседстве в какой-нибудь таблице и используете эту информацию при
генерации элементов открытого списка. Затем вы сохраняеете присвоенные стоимости G (возможно, используя длины
отрезков, соеденяющих вершины) и стоимости H (возможно, используя длины отрезков, соеденяющих вершины и цель). Все
остальное выполняется как обычно.
Амит Пател (Amit Patel) написал статью предлагающую некоторые альтернативы. Пример по поиску на изометрической RPG
карте, используя неквадратные области поиска, лежит здесь: Two-Tiered A* Pathfinding.
6. Некоторые советы по увеличению скорости поиска: Как только вы разработаете свою собственную программу A*,
или адаптируете ту, которую написал я, то обнаружите, что поиск пути использует львиную долю вашего процессорного
времени, особенно если у вас огромное количесво юнитов и большая карта. Если вы прочитаете в сети различные
материалы, то узнаете, что это распостраняется даже на программистов таких игр, как Starcraft или Age of Empires. Если вы
заметили, что поиск пути стал медленным, то вот несколько советов о том, как ускорить этот процесс:

Используйте карты поменьше и небольшое количество юнитов.

Никогда не ищите путь одновременно для нескольких юнитов. Вместо этого поставьте их в очередь
и рассчитывайте за несколько игровых циклов. Если ваша игра работает на скорости, скажем, в 40 циклов за кадр, никто
никогда этого не заметит. Но все заметят, если игра начнет работать очень медленно каждый раз, когда путь вычисляется
одновременно для множества юнитов.

Используйте для вашей карты как можно большие квадраты (или любую другую фигуру). Это
уменьшит общее количество вершин просмотренных при поиске пути. Если вы очень амбитный человек, то можете
использовать две или больше систем поиска пути, которые будут использоваться в разных ситуациях в зависимости от длины
пути. Вот что делают проффесионалы: используют большие области для длинных путей, а затем переходят на более точный
поиск, используя меньшие квадраты/области при приближении к цели. Если вы заинтересованы в таком подходе, прочтите
мою статью Two-Tiered A* Pathfinding.

Для длинных путей можно использовать предварительно просчитанные пути.

Обрабатывайте вашу карту на наличие недоступных из других частей карты участков. Я называю
эти участки "острова." В действительности они могут быть островами или любой другой поверхностью, которая отгорожена
стенами или недоступна. Один из недостатков алгоритма A* состоит в том, для нахождения путей к таким участкам, он будет
искать путь на всей карте, останавливаясь только когда каждый квадрат/вершина прошли через открытый и закрытый списки.
Это может потратить очень много процессорного времени. Этого можно избежать если просчитывать какие области
недостижими (с помощью flood-fill заливки или похожего метода), сохраняя эту информацию в любом массиве и проверяя ее
каждый раз перед началом поиска.

В запутанном, лабиринтоподобном окружении, стоит отметить вершины, которые не приводят
никуда, кроме тупиков. Такие области можно указывать на вашей карте вручную или, если вы очень амбитны, разработать
алгоритм, который будет опряделять такие области автоматически. Любой группе таких вершин можно присвоить уникальный
идентификационный номер. Затем вы можете спокойно игнорировать все тупики при поиске пути, задумывая сь только о том,
чтобы стартовая или челевая точки не попали в такую "тупиковую" область.
7. Реализация открытого списка: Это один из наиболее важных элементов в алгоритме A*. Каждый раз при обращении к
открытому списку вам необходимо найти вершину с наименьшей стоимостью F. Существует несколько способов это сделать.
Вы можете сохранять элементы пути когда потребуется и просто проматривать весь открытый список каждый раз, когда надо
извлечь элемент с наименьшей стоимостью F. Это простой метод, но очень медленный для длинных путей. Он может быть
улучшен использованием сортированного списка и тогда нуждно будет просто выбрать первый элемент списка - это и будет
элемент с наименьшей стоимостью F. Когда я писал свою программу этот метод был первым, который я использовал.
Это будет быстро работать на маленьких картах, но это не оптимальное решение. Серьезные программисты A*, которые
стремятся действительно к впечатляющей скорости, используют что-то, что называются "двоичными кучами" (binary heaps) и
это то, что я использовал в своей программе. Это будет по крайней мере в 2-3 раза быстрее в большинстве случаев и
намного быстрее (в 10 и больше раз) на длинных путях. Если вы заинтересовались, прочтите мою статью Using Binary Heaps
in A* Pathfinding.
8. Алгоритм Дийкстры: В то время, как считается, что A* самый лучший алгоритм поиска (смотрите выше), сущетвует по
крайней мере еще один алгоритм, который активно используется - алгоритм Дийкстры (Dijkstra's algorithm). Этот алгоритм
точно такой же как и A*, только без эвристики (H всегда равна 0). Так как в нем нет эвристики, он ищет путь одновременно во
все стороны. Перед тем, как найти путь, алгоритм исследует намного больше территории. Это делает его медленнее A*.
Так зачем его использовать? Иногда мы не знаем где находится наша цель. Например, у вас есть добывающий юнит,
которому надо собрать каких-нибудь ресурсов. Он может знать, что на этой территории есть несколько областей с ресурсами,
но он хочет добраться к ближайшим. Здесь использовать алгоритм Дийкстры будет более целесообразно, чем использовать
A* потому, что мы не знаем, какие из ресурсов находятся ближе. Альтернатива есть только в повторном использовании A*
для нахождения расстояния до каждого из ресурсов, а затем использовании этого пути. Существует множество таких
ситуаций.
Дальнейшее изучение
Хорошо, теперь вы знаете основы и смысл некоторых углубленных решений. На этом этапе я советую заглянуть в мой
исходный код. Архив содержит две версии: на C++ и на Blitz Basic. Обе версии содержат множество комментариев и должны
быть довольно простыми в пониманииd. Вот ссылка.

Sample Code: A* Pathfinder (2D) Version 1.9
Если у вас нет возможности работать на C++ или в Blitz Basic, два небольших exe-файла находятся в папке с версией на C++.
Версия на Blitz Basic может быть скомпилирована с помощью бесплатной демо-версии Blitz Basic 3D (не Blitz Plus) на
оффициальном сайте Blitz Basic. Сетевая демонстрация Бена О'Нилла (Ben O'Neill) находится тут.
Вы также можете заинтересоваться некоторыми сайтами. После прочтения этой статьи они будут простыми для понимания.

Amit’s A* Pages: Это очень обширная страничка Амита Патела (Amit Patel), но она может быть
немного непонятной для тех, кто не читал эту статью. Но все же стоит глянуть. Советую прочитать личные рассуждения
Амита про статью.

Smart Moves: Intelligent Path Finding: Эта статья написана Брайаном Стаутом (Bryan Stout) для
портала Gamasutra.com и требует регистрации для чтения. Регистраци бесплатна и позволяет прочитать не только эту статью
но и множество других ресурсов, дуступных на сайте. Программа, написанная Брайаном на Delphi помогла мне изучить A* и
вдохновила на написание моей программы A*. Также она описывает некоторые альтернативные возможности A*.

Terrain Analysis: Это сложная, но интересная статья Дейва Поттингера (Dave Pottinger),
проффесионала из Ensemble Studios. Этот парень координировал разработку Эпохи Империй (Age of Empires) и ее второй
части. Не ожидайте, что вы поймете все, написанное в ней, но это очень интересная статья, которая может натолкнуть вас на
некоторые идеи. Она включает в себя обсуждение мип-маппинга, карты с влияниями и некоторыми другими углубленными
решениями ИИ/поиска пути. Обсуждение "заливки" (“flood filling”) послужило идеей моих собственных "тупиков"и
"островов"(“dead ends” and “islands”) , которые прилагаются к Blitz версии моей программы..
Вот некоторые интересные сайты, которые следует просмотреть:



aiGuru: Pathfinding
Game AI Resource: Pathfinding
GameDev.net: Pathfinding
Хорошо, вот и все. Если вы напишите программу, которая использует любые из этих принципов, я с удовольствием на нее
взгляну. Пишите письма. :)
До встречи и удачи!
Глава 12 - Поиск пути по алгоритму A*
Поиск пути по алгоритму A*
Существует большое количество легкодоступных алгоритмов поиска пути, хотя моим
индивидуальным фаворитом считается алгоритм с названием A*. Данное прекрасный
алгоритм, позволяющий находить путь обхода преград и характеризовать лучший путь на
изменяющемся ландшафте. Из этого можно сделать вывод, что этот способ не столько
обнаружит путь из точки Ну а в точку В, ведь и что обнаруженный путь из точки Ну а в
точку В станет лучшим.
Чтобы оказать вам помощь, я прописал программу, показывающую алгоритм А* в
действии. Загрузите план D3D_PathFinding и запустите его, дабы увидеть работу
алгоритма А*. Коль скоро все сделано адекватно, вы увидите окошко, подобное на
изображенное на рис. 12.5.
Рис. 12.5. Окошко программы D3D_PathFinding
Запустите программу и щелкните по расположенной на панели команд кнопке Go. В
следствии увидит свет алгоритм поиска пути. Как заметно на рис. 12.5 программа
выискивает путь из изначальной точки в конечную и отражает решение повторяющий вид
стрелок. У вас есть возможность загружать разные ландшафты, оказавшиеся в
сопроводительных файлах, и наблюдать, как алгоритм справляется с ними. Лучшее из
лучших, что алгоритм A* практически постоянно находит гораздо лучший путь с учетом
наличествующего времени и ресурсов.
Как действует программа D3D_PathFinding? Не тревожьтесь; на данный раз я не буду
незамедлительно переходить к описанию исходного кода. Взамен данного я вначале
приведу теоретическое описание работы алгоритма A*.
Основы A*
Давайте познакомимся с терминами, кои применяются при описании алгоритма A*.
Узел — Позиция на карте.
Открытый список — Перечень узлов в кои имеет возможность перенестись
инвестор и кои считаются смежными с перекрытыми узлами.
Закрытый список — Спиок узлов, в кои имеет возможность перенестись инвестор
и кои уже были пройдены им.
Чтобы взять в толк, как данные термины используются, посмотрите на рис. 12.6.
Рис. 12.6. Терминология в алгоритме A*
На рис 12.6 изображены узлы, компоненты карту. Практически, узлом считается любой
квадрат карты. Я знаю, что термин «узел» имеет возможность звучать странно, хотя он
подойдет более, нежели «квадрат» либо «клетка». Оказывается, алгоритм A* имеет
возможность применяться и тем карт, где форма блоков выделяется от квадрата.
На карте как обыкновенно замечены изначальная и конечная позиции. Кроме этого,
изначальная позиция обведена деликатной рамкой. Данное проявляет, что этот узел
присутствует в перекрытом перечне. Потому что изначальная позиция практически
постоянно станет считаться частью выискиваемого пути, она механически срабатывает в
перекрытый перечень пройденных узлов.
Узлы, соседствующие с единственным узлом из перекрытого перечня станут помещены в
открытый перечень. В следствии у вас станет 1 узел в перекрытом перечне и 8 узлов в
открытом. Данное показано на рис. 12.7.
Рис. 12.7. Добавление узлов в открытый список
На рис. 12.7 изображены 8 узлов входящих в открытый перечень и 1 узел, входящий в
перекрытый перечень. Узлы, входящие в открытый перечень довольно просто вычислить
по нарисованным в них стрелкам. Данные стрелки демонстрируют направление движения
из того перекрытого узла, к коему относятся эти открытые узлы. Перекрытый узел в такой
ситуации называется родительским узлом любого из открытых узлов.
Начало поиска
Вот вы и узнали о терминологии, использующейся в алгоритме А*, хотя как принимать на
вооружение сам алгоритм? Для начала делает алгоритм А* — данное добавление
изначального узла в перекрытый перечень. Данное делается поскольку изначальный узел
практически постоянно станет первым узлом полученного пути. Сделав данное вы
обязаны обнаружить все узлы, кои считаются смежными с изначальным и в кои имеет
возможность перенестись инвестор. Коль скоро смежный узел доступен, он прибавляется
в открытый перечень. Т.к. на заре нет практически никаких открытых узлов, перед
началом работы алгоритма открытый перечень пуст.
Итак, вот рубежи поиска:
1. Поместить изначальный узел в перекрытый перечень.
2. Поместить легкодоступные соседние узлы в открытый перечень.
На рис. 12.7 я сделал данные 2 шага и и уже у меня 1 узел в перекрытом перечне и 8 узлов
в открытом. Что дальше?
Вычисление цены узлов
В мире А* узлы не равны друг от друга. Одни из них гораздо лучше подходят для
существа пути, нежели иные. Дабы проверить, какой узел считается лучшим из лучших,
нужно было любому узлу в перекрытом и открытом перечнях назначить его цена. Когда
всем узлам назначена цена, довольно несложной сортировки, дабы проверить какой узел
считается наиболее доступным. Для вычисления цены узлов в алгоритме А* нужны
последующие значения:



Базовая цена узла.
Стоимость возврата к изначальному узлу.
Стоимость достижения цели.
Базовая цена узла
Базовой ценой узла называется цена манёвра через этот узел. В обычнейшем случае
базовая цена всех легкодоступных узлов одинаковая. Впрочем, коль скоро вы пытаетесь
осложнить игру, возможно назначить различным узлам разную цена, исходя из их вида
ландшафта. Посмотрите, к примеру, на грядущий перечень узлов с их стоимостью:
Таблица 12.1. Базовая цена узлов
Тип узла
Стоимость
Трава
1
Грязь
2
Песок
3
Скалы
4
Болото
5
В таблице 12.1 перечислены 5 типов узлов и их базовая цена. Назначив любому виду узла
свою базовую цена у вас появится возможность находить лучший путь на карте. Дабы не
усложнять образчик, я назначаю всем узлам карты одну и ту же базовую цена. Вам ведь
ничто не вредит назначать в настоящей игре разные цены разным узлам.
Стоимость что же касается изначального узла
Следующая цена разрешает отследить во какое количество ограничится инвестору
возвращение из этого узла к изначальному. Она нужна чтобы вы знали как труден путь из
изначального узла до этого. Вычисляется данная цена довольно просто — довольно взять
цена что же касается изначального узла для родительского узла и добавить к ней базовую
цена текущего узла. В следствии вы получите единую цена текущего узла что же касается
изначального.
Стоимость что же касается цели
Последний составляющих стоимости — данное цена достижения цели из этого узла. Она
вычисляется путем сложения численности строчек и столбцов на кои нынешний узел
отстоит от цели. Допустим, нынешний узел находится на 1 ряд ниже и на 10 столбцов
левее цели. Цена данного узла что же касается цели станет 10 + 1 = 11. Истина, просто?
Общая стоимость
Как лишь все 3 упомянутых повыше цены вычислены, вам нужно подобрать их воедино и
обрести единую цена узла. Такое может звучать не ясно, и посмотрите на рис. 12.8, где
показаны цены всех рассматриваемых в случае узлов из открытого перечня.
Рис. 12.8. Цена узлов из открытого списка
На рис. 12.8 показаны узлы из открытого перечня с их ценой. Из чего оформляется цена
любого узла показано на рис. 12.9.
Рис. 12.9. Компоненты цены узла
Как заметно на рис. 12.8 и рис. 12.9, единая цена узла показана в левом верхнем углу. В
правом верхнем углу приводится базовая цена, в левом нижнем — цена что же касается
изначального узла и в правом нижнем — цена что же касается цели.
Поиск лучшего узла
Вооружившись единой ценой любого узла, довольно просто обнаружить лучший узел, для
добавления его в перекрытый перечень. Отсортируйте узлы по значению единой цены и
подберите тот из них у которого она менее всего. На рис. 12.8 кратчайшая единая цена у
узла, расположенного справа от стартовой точки. Она равна 10 и прочих настолько же
доступных узлов нет. Я в том числе и обвел на рисунке данный узел рамкой, дабы
продемонстрировать, что непосредственно его надлежит подобрать.
После того, как узел с кратчайшей единой ценой обнаружен, добавьте его в перекрытый
перечень в виде кандидата на участие в итоговом пути. Не позабудьте удалить данный
узел из открытого перечня, дабы он не был подвергнут обработке вновь. Наконец, давайте
подытожим пройденные шаги:
1.
2.
3.
4.
Поместить изначальный узел в перекрытый перечень.
Поместить легкодоступные соседние узлы в открытый перечень.
Найти узел с кратчайшей единой ценой и прибавить его в перекрытый перечень.
Удалить узел с кратчайшей единой ценой из открытого перечня.
Продолжение поиска
Если в открытом перечне отсутствует конечный пункт маршрута, надлежит продолжать
поиск пути добавив в открытый перечень те узлы, кои пребывают около узла только вот
добавленного в перекрытый перечень. Потом вы вновь обнаружите открытый узел с
кратчайшей ценой и добавите его в перекрытый перечень. Данные воздействия станут
повторяться до того времени, покуда конечный пункт маршрута не окажется в открытом
перечне.
Обратная трассировка для нахождения пути
Как лишь конечный пункт маршрута окажется в открытом перечне, нужно было станет
составить путь обратно к исходной точке. Чтобы достичь желаемого результата мы берем
опекуна открытого узла в котором находится конечный пункт. Далее берем опекуна
опекуна и т.п. до того времени, покуда не вернемся к исходной позиции. В следствии вы
получите путь от конечного пункта до изначального. И уже вам довольно инвертировать
полученный путь, дабы обрести маршрут от исходной точки до цели. На рис. 12.10
показан путь, сформированный алгоритмом для рассматриваемого примера.
Рис. 12.10. Обнаруженный путь
На рисунке у вас есть возможность обнаружить, что в сформированный путь попали
некоторое количество избыточных узлов. Данное вызвано тем, что некоторое количество
узлов имеют одну и ту же единую цена. Вы не имеет возможности подобрать
незамедлительно 2 узла, и лишь берется тот узел, коей присутствует в перечне первым.
Такое может привести к увеличению размера работы, хотя в конце путь станет
скорректирован.
Алгоритм А*
Наилучшим алгоритмом для поиска оптимальных путей в различных пространствах
является A* (читается как "А-звездочка"). Этот эвристический поиск сортирует все узлы
по приближению наилучшего маршрута идущего через этот узел. Типичная формула
эвристики выражается в виде:
f(n) = g(n) + h(n)
где:
f(n) значение оценки, назначенное узлу n
g(n) наименьшая стоимость прибытия в узел n из точки старта
h(n) эвристическое приближение стоимости пути к цели от узла n
Таким образом, этот алгоритм сочетает в себе учет длины предыдущего пути из алгоритма
Дийкстры с эвристикой из алгоритма "лучший-первый". Алгоритм хорошо отражен в
Листинге 3. Так как некоторые узлы могут обрабатываться повторно (для поиска
оптимальных путей к ним позднее) необходимо ввести новый список Closed для их
отслеживания.A* имеет множество интересных свойств. Он гарантированно находит
кратчайший путь, до тех пор пока эвристическое приближение h(n) является допустимым,
то есть он никогда не превышает действительного оставшегося расстояния до цели. Этот
алгоритм наилучшим образом использует эвристику: ни один другой алгоритм не
раскроет меньшее число узлов, не учитывая узлов с одинаковой стоимостью. На рисунках
9A - 9C, можно увидеть как A* справляется с ситуациями проблемными для других
алгоритмов.
Гибкость А*.
На практике A* оказывается очень гибким. Рассмотрим различные части алгоритма.
Состоянием зачастую является ячейка или позиция, которую занимает объект. Но при
необходимости, состоянием с таким же успехом может быть ориентация и скорость
(например, при поиске пути для танка или любой другой машины - их радиус поворота
становится хуже при большей скорости).
Соседние состояния могут изменяться в зависимости от игры и локальной ситуации.
Смежные позиции могут быть исключены, потому что они непроходимы. Некоторые типы
местности могут быть непроходимыми для некоторых объектов, но проходимыми для
других; объекты, которые медленно разворачиваются, не могут перемещаться во все
соседние клетки.
Стоимость перехода из одной позиции в другую может представлять множество вещей:
обычное расстояние между позициями; стоимость по времени или топливу на
перемещение; штрафы за проезд через нежелательную область (например, точки в
пределах вражеской артиллерии); бонусы за проезд через предпочтительные области
(например, исследование новой области или захват контроля над неконтролируемыми
зонами); и эстетические соображения (например, если диагональные перемещения такие
же по стоимости, как и ортогональные, все равно лучше сделать их стоимость больше, для
того чтобы пути выглядели более прямыми и естественными).
Приближение обычно равняется минимальному расстоянию между текущим узлом и
целью умноженному на минимальную стоимость передвижения между узлами. Это
гарантирует допустимость эвристики h(n). (На карте с квадратными клетками, где объекты
могут занимать только точки на сетке, минимальным расстоянием будет не
геометрическое расстояние, а минимальное число ортогональных и диагональных
перемещений между двумя точками.)
Цель не обязательно должна быть единственной позицией, но может состоять из
множества позиций. Тогда приближение должно равняться минимуму приближений ко
всем возможным целям. Указание пределов по стоимости пути или по расстоянию, или по
обоим признакам может быть легко реализовано. Из моего непосредственного опыта я
знаю, что А* хорошо работает для большого числа типов путей в военных играх
(wargames) и стратегиях.
Ограничения A*.
Существуют ситуации, в которых A* не может работать хорошо по различным причинам.
Требования работы в реальном масштабе времени для некоторых игр, плюс ограничения
по памяти и процессорному времени в некоторых из них, создают проблемы для хорошей
работы даже для А*. Большая карта может требовать тысячи ячеек в списках Open и
Closed, для чего может оказаться недостаточно места. Даже если для них окажется
достаточно памяти, алгоритмы для работы с этими списками могут оказаться
неэффективными.
Качество работы алгоритма сильно зависит от качества эвристического приближения h(n).
Если h близко к истинной стоимости оставшегося пути, то эффективность будет очень
высокой; с другой стороны, если h будет слишком низким, то это отразится на
эффективности в худшую сторону. На самом деле, алгоритм Дийкстры - это A* с h
установленной в ноль для всех узлов -это конечно будет допустимым приближением и
этот алгоритм найдет путь, но это будет очень медленно. На рисунке 10A, можно увидеть
что при поиске в зоне с повышенной стоимостью (затененная зона), фронт просмотренных
узлов похож на фронт в алгоритме Дийкстры; на рисунке 10Б при увеличении эвристики
стал более сфокусированным.
Иерархический поиск.
Рассмотрим способы, как добиться от A* большей эффективности в рассматриваемых
областях.
Возможно самой главной оптимизацией, которую только можно сделать, является
реструктуризация проблемы в более простую. Макрооператоры - это последовательности
шагов, которые принадлежат друг другу и могут быть объединены в один шаг, заставляя
поиск делать большие шаги за один раз. Например, самолеты делают серию шагов для
того, чтобы изменить свои положение и высоту. Общая последовательность может быть
использована как одно изменение состояния, вместо множества маленьких отдельных
шагов. В дополнение к сказанному, поиск и общие методы решения задач могут быть
значительно упрощены, если они будут разбиты на подзадачи, индивидуальное решение
каждой из которых довольно простое. В случае поиска пути, карта разбивается на
большие непрерывные области, чья связность известна. Выбирается одна или две
граничных ячейки, которые находятся между двумя смежными областями; затем поиск
производится между смежными областями, в каждой из которых путь находится для
граничных ячеек.
Например, в стратегической карте Европы, планировщик пути прокладывающий путь из
Мадрида в Афины может с большой вероятностью потратить кучу времени на обработку
итальянского "сапога". Используя страны в качестве областей, иерархический
планировщик сначала определит, что путь должен следовать из Испании во Францию,
затем в Италию, и из Югославии (если смотреть на старую карту) в Грецию; и затем путь
через Италию должен вести от границей между Италией и Францией до границы между
Италией и Югославией. В качестве другого примера можно привести путь из одной части
здания в другое, который может быть разбит на пути между комнатами и коридорами и
пути между дверьми в каждой комнате.
Гораздо проще выбирать области в предопределенных картах, чем заставлять делать это
компьютер на случайно сгенерированных картах. Необходимо отметить, что обсуждаемые
примеры работают в основном с обходом препятствий; для взвешенных областей
желательно назначать полезные области, особенно для компьютера (они могут быть и не
совсем полезные).
Реализация А* в играх.
Хотя поиск А* сам по себе достаточно эффективен, он может быть замедлен
неэффективными алгоритмами работы с внутренними структурами данных. Для поиска
используются две основных структуры данных.
Первая - это представление игровой области. Тут возникает много вопросов. Как игровая
область должна быть представлена? Будут ли области доступны из каждой точки?
Стоимости перемещения будут храниться непосредственно в карте или будут вычисляться
по необходимости? Как будут представлены различные особенные точки в областях на
карте? Будут ли они представлены непосредственно на карте или в отдельных структурах?
Как алгоритм поиска может быстро получить необходимую информацию? Ответы на эти
вопросы зависят от типа игры и используемого программного и аппаратного обеспечения.
Другая основная используемая структура - это узел или состояние поиска, который можно
рассмотреть более детально. На нижнем уровне находится структура состояния поиска, в
которую разработчик может захотеть включить следующие поля:









Место (координаты) на карте, рассматриваемое в этом состоянии.
Другие существенные атрибуты объекта, например, ориентация и скорость.
Стоимость лучшего пути к этой позиции из исходной позиции.
Длина пути к этой позиции.
Эвристическое приближение h(n) (стоимости оставшегося пути к цели).
Оценка для этого состояния f(n), используемая для выбора следующего состояния
извлекаемого из Open.
Предел длины пути, или его стоимости, или того и другого вместе, если
необходимо.
Ссылка (указатель или индекс) на родителя этого узла, то есть узла который привел
к этому состоянию.
Дополнительные ссылки на другие узлы, как это требуют структуры хранения
данных, используемые для хранения списков Open и Closed; например, "next" и
возможно "previous" указатели для связанных списков, "right", "left" и "parent"
указатели для двоичных деревьев.
Другим вопросом является решение - когда распределять память под эти структуры?
Ответ зависит от требований и ограничений игры, аппаратного обеспечения и
операционной системы.
На верхнем уровне находятся более сложные структуры данных - списки Open и Closed.
Хотя их раздельное хранение типично, можно хранить их вместе, используя флажок в узле
для определения открыт этот узел или нет. Вот виды операций, которые необходимы для
работы со списком Closed:




Добавить новый узел.
Удалить произвольный узел.
Искать узел с заданными атрибутами (положение, скорость, направление).
Очистить список в конце поиска.
Для списка Open нужны такие же операции, как для списка Close, плюс:


Извлечь узел с лучшей оценкой f(n).
Изменить оценку узла.
Список Open можно рассматривать как приоритетную очередь, где следующий
извлеченный узел является узлом с высшим приоритетом - в нашем случае с наилучшей
оценкой. В соответствии с указанными выше операциями есть несколько возможных
представлений: линейный, неупорядоченный массив; неупорядоченный связанный
список; сортированный массив; сортированный связанный список; куча (структура
используемая в heapsort); сбалансированные бинарные деревья. Существует несколько
типов бинарных деревьев: 2-3-4 деревья, красно-черные деревья, сбалансированные по
глубине (AVL trees), и сбалансированные по весу деревья. Кучи и сбалансированные
деревья имеют преимущество логарифмической зависимости времени вставки, удаления и
поиска; однако, если количество узлов очень велико, затраты на их хранение могут
превысить это преимущество.
Оптимизация А*.
Существует несколько способов изменить алгоритм поиска, чтобы получить хорошие
результаты при использовании ограниченных ресурсов:
Лучевой поиск. Одним из способов борьбы с ограничением по памяти является
наложение ограничений на количество узлов в списке Open; когда список полон и
необходимо добавить новый узел, просто выбрасывается узел с наихудшим значением.
Список Closed также может быть уничтожен, если каждая ячейка хранит в себе длину
наилучшего пути и обратный указатель. Этот алгоритм не гарантирует оптимальности
пути, так как узел ведущий к нему может быть выброшен, но все равно может позволить
найти разумный путь.
Алгоритм последовательных приближений для A* (IDA*). Алгоритм
последовательных приближений, использованный для IDDFS, как упоминалось ранее,
может быть использован и для A*. Это полностью избавляет от необходимости хранить
списки Open и Closed. Делается простой рекурсивный поиск, собирается наколенная
стоимость пути g(n), и поиск прекращается при достижении значением f(n) = g(n) + h(n)
заданных пределов. Начинать нужно с пределом остановки равным h(start), и в каждой
последовательной итерации, устанавливать новый предел остановки равным
минимальному значению f(n), которое превысило прежнюю границу. Аналогично для
IDDFS среди методов полного перебора, IDA* - асимптотически оптимален в расходе
времени и памяти среди эвристических методов.
Недопустимая эвристика h(n). Как обсуждалось выше, если эвристическое приближение
оставшейся стоимости пути слишком мало, то A* может быть очень неэффективным. Но
если приближение слишком высоко, то для найденного пути не гарантируется
оптимальность. В играх в которых диапазон изменения стоимостей ландшафта широк - от
болот до автострад - можно поэкспериментировать с различными промежуточными
значениями, чтобы найти точный баланс между эффективностью поиска и качеством
найденного пути.
Существуют и другие модификации A*, однако они не подтвердили свою полезность для
поиска пути в геометрическом пространстве.
Алгоритм A* для новичков.
Автор: Патрик Лестер (Patrick Lester) (2 апреля 2004) Почта:
Перевод: Morpher (26 июля 2004) Почта: Morpher_KKnD@mail.ru
Данная статья переведена на Испанский и Французкий. Другие переводы приветствуются.
Алгоритм A* (произносится как "А-звездочка"), возможно, немного трудноват для новичков. И хотя
в сети существует множество материалов, обьясняющих A*, большинство из них написаны для
людей, которые уже понимают основы. Эта же статья действительно написана для новичков.
Статья не пытается быть исчерпывающим материалом по данной теме. Вместо этого она
описывает фундаментальные понятия и подготавливает вас к тому, чтобы прочитать все те
материалы и понять о чем в них идет речь. Ссылки на некоторые из них прилагаются в конце
статьи (под заголовком "Дальнейшее изучение").
Также эта статья не направлена на определенный язык программирования - вы сможете
адаптировать ее к любому из них. Как вы, должно быть, и ожидали, я добавил ссылку на пример
реализации в конце статьи. Архив примера содержит две версии: на C++ и на Blitz Basic. Он также
содержит скомпилированную версию, если вы просто хотите увидеть A* в действии.
Но пора приступать к делу. Давайте начнем с самого начала...
Введение: Область Поиска
Давайте представим себе, что у нас есть кто-то, кто хочет попасть из точки А в точку B. Эти две
точки разделены стеной. На иллюстрации ниже зеленый квадрат это стартовая точка A, красный
квадрат - целевая точка B, а несколько синих квадратов - стена между ними.
[Рис. 1]
Первое, на что вы должны обратить внимание, то, что мы разделили нашу область поиска на сетку
с квадратными ячейками. Упрощение области поиска это первый шаг в поиске пути. Этот метод
упрощает нашу область поиска до простого двумерного массива. Каждый элемент масссива
представляет одну из клеток сетки, а его значением будет проходимость этой клетки (проходима и
непроходима). Для нахождения пути нам необходимо определить какие нам нужны клетки для
перемещения из точки A в точку B. Как только путь будет найден, наш путник начнет двигаться с
центра одной клетки на центр следующей до тех пор, пока не достигнет целевой клетки.
Эти центральные точки называют "вершинами". Когда вы что-нибудь читаете про поиск пути, то
часто можно столкнуться с обсуждением вершин. Почему бы просто не назвать их клетками?
Потому что всегда возможно разделить вашу область поиска на что-то отличное от квадратов.
Например, на прямоугольники, шестиугольники, треугольники, или любую другую фигуру. И
вершины могут располагаться где-угодно - в центре, вдоль граней или еще где-нибудь. Мы
используем эту систему, поскольку она самая простая.
Начало Поиска
Как только мы упростили нашу область поиска до некоторого числа вершин, нам нужно начать
поиск для нахождения кратчайшего пути. Начнем с точки A, проверяя соседние клетки и двигаясь
дальше до тех пор, пока не найдем целевую точку.
Начинаем поиск пути выполняя следующее:
1. Начинаем со стартовой точки A и добавляем ее в "открытый список" клеток, которые нужно
обработать. Открытый список это что-то наподобие списка покупок. В данный момент есть
только один элемент в списке, но позже мы добавим еще. Список содержит клетки,
которые может быть находятся вдоль пути, который вы выберете, а может и нет. Проще
говоря, это список клеток, которые нужно проверить.
2. Ищем доступные или проходимые клетки, граничащие со стартовой точкой, игнорируя
клетки со стенами, водой или другой непроходимой областью. И также добавляем их в
открытый список. Для каждой из этих клеток сохраняем точку A, как "родительскую клетку".
Эта родительская клетка важна, когда мы будем прослеживать наш путь. Это будет
описано намного позже.
3. Удаляем стартовую точку A с вашего открытого списка и добавляем ее в "закрытый список"
клеток, которые вам больше не нужно проверять.
Теперь у вас должно быть что-то похожее на следующую иллюстрацию. На этой иллюстрации
темно-зеленый квадрат в центре - ваша стартовая точка. Она выделена голубым цветом для
отображения того, что она находится в закрытом списке. Все соседние клетки в данный момент
находятся в открытом списке. Они выделены светло-зеленым. Каждая имеет серый указатель,
направленный на родительскую клетку, которая в нашем случае является стартовой точкой.
[Рис. 2]
Дальше мы выберем одну из соседних клеток в открытом списке и практически повторим
вышеописанный процесс . Но какую клетку мы выберем? Ту, у которой меньше стоимость F.
Оценка пути
Способом определения того, какую же клетку использовать, является следующие выражение:
F=G+H
где


G = стоимость передвижения из стартовой точки A к данной клетке, следуя найденному
пути к этой клетке.
H = примерная стоимость передвижения от данной клетки до целевой, то есть точки B. Она
обычно является эвристической функцией. Причина по которой она так называется в том,
что это предположение. Мы действительно не узнаем длину пути, пока не найдем сам путь,
потому что в процессе поиска нам может встретиться множество вещей (например, стены и
вода). В этой статье вам предложили один способ вычислить H, но существует множество
способов, которые можно найти в других статьях.
Наш путь генерируется путем повторного прохода через открытый список и выбора клетки с
наименьшей стоимостью F. Этот процесс будет описан в статье более подробно, но немного
позже. Прежде всего давайте внимательно рассмотрим вычисление стоимости F.
Как описано выше, G является стоимостью передвижения со стартовой клетки до текущей,
используя найденный к ней путь. В этом примере мы присвоим стоимость 10 к горизонтальным и
вертикальным передвижениям, а к диагональным - 14. Мы используем эти числа потому, что
пройденное по диагонали расстояние примерно в 1,414 раз (корень с 2) больше стоимости
передвижения по горизонтали или вертикали. Для простоты мы используем 10 и 14. Соотношение
соблюдается и мы избегаем вычисления квадратных корней и десятичной дроби. Это не просто
потому, что мы дураки и не любим математику. Использование целых чисел вроде этих, намного
быстрее для компьютера. Как вы скоро узнаете, поиск пути может быть очень медленным если вы
не используете упрощения наподобие этих.
Так как мы вычисляем стоимость G вдоль пути к текущей точке, способ ее установить состоит в
том, чтобы взять G родительской клетки и прибавить 10 или 14, в зависимости от диагонального
или ортогонального (не диагонального) расположения текущей клетки относительно родительской
клетки. Необходимость использования этого метода станет очевидной немного позже, когда мы
отдалимся от стартовой точки более чем на одну клетку.
Стоимость H может быть вычислена множеством способов. Метод, который мы используем,
называется методом Манхеттена (Manhattan method), где вы считаете общее количество клеток,
необходимых для достижения целевой точки от текущей, по горизонтали и вертикали, игнорируя
диагональные перемещения и любые препятствия, которые могут оказаться на пути. Затем мы
умножаем общее количество полученных клеток на 10.
Читая это описание вы, должно быть, решили, что эвристика - просто приблизительное
определение оставшегося расстояния между текущей клеткой и целью по прямой. Но это не так.
Мы пытаемся установить оставшееся расстояние вдоль пути (который обычно идет не по прямой),
но алгоритм требует от нас не переоценить это расстояние, иначе он может найти не верный путь.
Использованный здесь метод гарантирует предоставить нам правильный путь. Хотите узнать про
эвристику больше? Вы найдете выражения и дополнительные сведенья здесь.
Стоимость F вычисляется путем сложения стоимостей G и H. Результаты первого шага нашого
поиска пути проиллюстрированы ниже. Значения F, G и H записаны в каждой клетке. Как видим по
клетке справа от стартовой точки, F выводится в верхнем левом углу, G выводится в нижнем
левом углу, а H выводится в нижнем правом углу.
[Рис. 3]
Давайте посмотрим на некоторые из этих клеток. В клетке с буквами G = 10. Это потому, что она
находится на расстоянии в одну клетку от стартовой точки, при том по горизонтали. Также G = 10 у
клеток прямо сверху, снизу и слева от стартовой точки. У диагональных клеток G = 14.
Стоимость H посчитана с помощью вычисления Манхеттенского расстояния к красной целевой
клетке, двигаясь только по горизонтали и вертикали, игнорируя все стены на пути. У клетки, прямо
справа от стартовой, расстояние до цели 3 клетки. Используя этот метод видим, что H = 30. У
клетки прямо над ней, расстояние уже 4 клетки (помните, что надо двигаться только по
горизонтали и вертикали). И ее значение стоимости H будет равно 40. Вы, вероятно, можете
догаться, как вычисляются стоимости H для других клеток.
Стоимость F для каждой клетки вычисляется простым суммированием G и H.
Продолжаем поиск
Для продолжения поиска мы просто выбираем клетку с наименьшей стоимостью F из всех клеток,
находящихся в открытом списке. Затем с выбранной клеткой мы производим такие действия:
4) Удаляем ее из открытого списка и добавляем в закрытый список.
5) Проверяем все соседние клетки. Игнорируем те, которые находятся в закрытом списке или
непроходимы (поверхность со стенами, водой), остальные добавляем в открытый список,
если они там еще не находятся. Делаем выбранную клетку "родительской" для всех этих
клеток.
6) Если соседняя клетка уже находится в открытом списке, проверяем, а не короче ли путь по
этой клетке? Иными словами, сравниваем значения стоимости G этих двух клеток. Если
при использовании этой клетки стоимость G выше, чем при использовании текущей клетки,
то не предпринимаем ничего.
Если же она ниже, то меням "родителя" этой клетки на выбранную клетку. Затем
вычисляем стоимости F и G этой клетки. Если это выглядит для вас немного запутанным,
далее вы можете увидеть это на иллюстрации.
Хорошо, давайте посмотрим, как это работает. С наших начальных 9 клеток, осталось 8 в
открытом списке, а стартовая клетка была внесена в закрытый список. Клетка с наименьшей
стоимостью F находится прямо справа от стартовой клетки, и ее стоимость F = 40. Поэтому мы
выбираем эту клетку как нашу следующую клетку. Она выделена голубым цветом на этой
иллюстрации.
[Рис. 4]
Сначала мы удаляем ее из открытого списка и добавляем в закрытый список (вот почему она
выделена голубым цветом). Затем мы проверяем соседние клетки. Клетки, сразу справа от этой
клетки - стены, поэтому мы их игнорируем. Клетка, прямо слева - стартовая клетка. Она находится
в закрытом списке, поэтому мы ее тоже игнорируем.
Оставшиеся 4 клетки уже находятся в открытом списке, поэтому мы должны проверить, не короче
ли пути по этим клеткам, используя текущую клетку. Сравнивать будем по соимости G. Давайте
посмотрим на клетку, прямо под нашей выбранной клеткой. Ее стоимость G равна 14. Если мы
будем двигаться по этой клетке, стоимость G будет равна 20 (10, соимость G чтобы добраться к
текущей клетке плюс 10 для движения вертикально вверх, к соседней клетке). Стоимость G = 20
больше, чем G = 14, потому это будет не лучший путь. Это станет понятным, если взглянуть на
диаграму. Более целесообразным будет движение по диагонали на одну клетку, чем движение на
одну клетку по горизонтали, а потом одну по вертикали.
Когда мы повторим этот процесс для всех 4-х соседних клеток, которые находятся в открытом
списке, то узнаем, что ни один из путей не улучшится при движении по этим клеткам через
выбранную, потому ничего не меняем. Теперь, когда мы осмотрели все соседние клетки, то
закончили с текущей клеткой и готовы двигаться к следующей.
Теперь мы проходим весь открытый список, который уменьшился до 7-ми клеток, и выбираем
клетку с наименьшей стоимостью F. Интересно, что в этом случае существует 2 клетки со
стоимостью 54. Так какую мы выберем? Это не имеет никакого значения. В целях увеличения
скорости поиска можно выбрать последнюю клетку, которую мы добавили в открытый список. Это
предупредит поиск в выборе клеток, к которым можно будет обратиться позже, когда мы
подберемся ближе к цели. Но в действительности это не так уж важно. (Вот почему две версии A*
могут найти разные пути с одинаковой длиной.)
Так что давайте выберем клетку, прямо внизу, спава от стартовой, как показано на рисунке.
[Figure 5]
В этот раз, когда мы проверяем соседние клетки, видим, что клетка, прямо справа - стена и мы ее
пропускаем. Так же поступаем и с клеткой, которая находится прямо над ней. Так же мы
игнорируем клетку, которая находится прямо под ней. Почему? Потому, что вы не можете
добраться до той клетки без среза угла ближайшей стены. Сначала вы должны спуститься вниз, а
только потом двигаться на эту клетку. (Замечание: Это правило среза углов необязательно. Его
использование зависит от расположения ваших вершин.)
Остается еще 5 клеток. 2 клетки, находящиеся под текущей, еще не в открытом списке, потому мы
их добавляем в открытый список и назначаем текущую клетку их "родителем". Из 3-х других клеток
2 уже находятся в закрытом списке (стартовая клетка и клетка, прямо над ней, на диаграмме обе
подсвечены голубым цветом) и мы их игнорируем. Последняя клетка, которая находится прямо
слева от текущей, проверяется на длину пути по текущей клетке через эту клетку по стоимости G.
Нет, путь будет не короче. Так что мы здесь закончили и готовы проверить следующую клетку в
открытом списке.
Повторяем этот процесс до тех пор, пока не добавим целевую клетку в открытый список. К этому
времени у вас получится что-то похожее на иллюстрацию ниже.
[Рис. 6]
Заметьте, что родительская клетка для клетки, находящейся в 2-х клетках под стартовой
изменилась по сравнению с предидущей иллюстрацией. Преред этим у нее стоимость G была
равна 28 и указатель был направлен вверх и влево. Теперь стоимость G равна 20, а указатель
направлен прямо вверх. Это произошло где-то в процессе нашего поиска, когда была проверена
стоимость G и оказалось, что путь через эту клетку будет более коротким. Поэтому поменялась ее
родительская клетка и были пересчитаны стоимости G и F. И хотя в этом примере это не кажется
очень важным, существует множестиво ситуаций, когда такая проверка
будет сильно влиять на выбор более короткого пути к цели.
Так как же мы определим сам путь? Очень просто. Начнем с красной целевой клетки и будем
двигаться назад с клетки на ее родителя, следуя указателям. Это доставит вас к стартовой клетке
и это и будет ваш путь. Получится как показано на иллюстрации ниже. Движение от стартовой
точки A к целевой точке B будет просто передвижением от центра каждой клетки (вершины) к
центру следующей клетки до тех пор, пока вы не достигните цели.
[Рис. 7]
Итоги метода A*
Хорошо, вы прошли все обьяснение, давайте посмотрим на пошаговое представление этого
метода:
1) Добавляем стартовую клетку в открытый список.
2) Повторяем следующее:
a) Ищем в открытом списке клетку с наименьшей стоимостью F. Делаем ее текущей клеткой.
b) Помещаем ее в закрытый список. (И удаляем с открытого)
c) Для каждой из соседних 8-ми клеток ...



Если клетка непроходимая или она находится в закрытом списке, игнорируем ее. В
противном случае делаем следующее.
Если клетка еще не в открытом списке, то добавляем ее туда. Делаем текущую клетку
родительской для это клетки. Расчитываем стоимости F, G и H клетки.
Если клетка уже в открытом списке, то проверяем, не дешевле ли будет путь через эту
клетку. Для сравнения используем стоимость G. Более низкая стоимость G указывает
на то, что путь будет дешевле. Эсли это так, то меняем родителя клетки на текущую
клетку и пересчитываем для нее стоимости G и F. Если вы сортируете открытый список
по стоимости F, то вам надо отсортировать свесь список в соответствии с
изменениями.
d) Останавливаемся если:


Добавили целевую клетку в открытый список, в этом случае путь найден.
Или открытый список пуст и мы не дошли до целевой клетки. В этом случае путь
отсутствует.
3) Сохраняем путь. Двигаясь назад от целевой точки, проходя от каждой точки к ее родителю до
тех пор, пока не дойдем до стартовой точки. Это и будет наш путь.
Послесловие
Для ипользования алгоритма A*, вам необходимо включить элементы, описанные выше -открытый и закрытый списки, стоимости F, G, и H. Существует множество других алгоритмов
поиска пути, но эти методы не A*, который считается лучшим из многих. Брайан Стаут (Bryan Stout)
обсуждает многие из них в статье, ссылка на которую расположена чуть дальше. Некоторые
альтернативные методы будут лучше при некотрых обстоятельствах, но вы должны понимать, что
вам надо. Ладно, хватит разглагольствований. Вернемся к нашей статье.
Заметки к реализации
Теперь, когда вы понимаете основы метода, вот несколько тем к размышлению, когда вы начнете
писать свою программу. Некоторые из этих материалов ссылаются на программу, которую я
написал на C++ и Blitz Basic, но это будет актуально и для других языков.
1. Другие юниты (уклонение от столкновений) Если вы присмотритесь внимательней к
моему примеру реализации алгоритма, вы заметите, что он полностью игнорирует все остальные
юниты на экране. Юниты спокойно проходят сквозь друг друга. В зависимости от игры, это может
быть приемлимым, но может и не быть. Если вы хотите чтобы юниты обходили друг друга, то я
предлагаю вам несколько методов. Можно обратбатывать юниты, которые не двигаються или
находятся близко к юниту, для которого ищется путь, а остальные просто игнорировать. Если же
вы хотите обрбатывать юниты, которые двигаются и находятся на расстоянии более чем в одну
вершину, то вам нужно будет разработать метод для установления их положения в пространстве в
любое время, чтобы они могли должным образом уклониться. Иначе в дальнейшем вы можете
столкнуться со странными путями, где юниты двигаються по зигзагообразной траектории, пытаясь
обьехать другие юниты, которых там давно нет. Вам также потребуется система определения
столкновений, так как не важно насколько хорош ваш путь, со временем многие вещи могут
измениться. Когда происходит столкновение, для юнита надо просчитать новый путь или, если это
не лобовое столкновение, подождать пока другой юнит отойдет с дороги, перед тем, как двигаться
дальше.
Вот некоторые ссылки, которые могут вам пригодиться:




Steering Behavior for Autonomous Characters: по движению автономных устройств немного
отходит от задачи поиска пути, но она может быть интегрирована с ним для более
совершенного перемещения и определения столкновений.
The Long and Short of Steering in Computer Games: Интересный обзор литературы по
передвижению и поиску пути. Это pdf документ.
Coordinated Unit Movement: Первая статья из двухсерийного цикла про формацию и
сгруппированое передвижение юнитов от дизайнера Эпохи Империй (Age of Empires)
Дейва Поттингера (Dave Pottinger).
Implementing Coordinated Movement: Вторая статья из этого цикла.
2. Различная стоимость передвижения: В этой статье и в моей прилагающейся программе
поверхность может быть только двух типов - проходимая и непроходимая. Но что, если у вас есть
территория, которая проходимая, но имеет большую стоимость передвижения? Болота,
возвышенности, лестницы в пещерах, и т.д. - это примеры поверхности, котрую можно пройти, но
стоимость передвижения у нее выше, чем стоимость передвижения по ровной, открытой
поверхности. Также стоимость передвижения по дорогам может быть немного ниже стоимости
передвижения по другим видам поверхности.
Эта проблема легко решается путем добавления стоимости поверхности при вычислении
стоимости G любой вершины. Просто добавьте дополнительную стоимость к таким вершинам.
Алгоритм поиска пути A* написан для нахождения пути с наименьшей стоимостью и легко
справится с такой задачей. В простом примере, который я описал, когда поверхность может быть
или проходимой или нет, A* будет искать кратчайший, более прямолинейный путь. Но в случае с
различной стоимостью поверхности, юнит может пойти более длинным путем - как, например, при
движении по дороге через болото, а не движении напрямик по самому болоту.
Существует еще одно интересное решение, которое проффесионалы называют "influence
mapping" (что-то вроде карты с областями влияния). Так же как и с различной стоимостью
поверхности, вы можете создавать дополнительные системы и применять их для целей ИИ
(искуственного интеллекта). Представьте, что у вас есть карта, на которой находятся толпы
юнитов, охраняющих проезд через горный регион. Каждый раз, когда компьютер посылает свой
юнит через этот проход, он уничтожается. Если вы захотите, то можете создать "карту влияний",
которая будет "штрафовать" вершины, возле которых множество вражеских единиц. Это научит
компьютер планировать более безопасные пути и поможет избежать глупых ситуаций, когда
компьютер продолжит посылать юниты через более опасный район только потому, что он более
короткий.
3. Обработка неизведанных территорий: Вы когда-нибудь играли в игры, в которых
компьютер всегда знает точно какой путь выбрать, даже если карта еще не полностью
исследована? В зависимости от игры, поиск пути это то, что может быть очень нереалистичным. К
сожалению, это та проблема, которую не так-то просто решить.
Ответ заключается в создании массива "известнаяПроходимость" для каждого из игроков и
компьютерных оппонентов ("каждый игрок" не значит "каждый юнит", это потребовало бы очень
много памяти). Каждый такой массив должен содержать информацию про области, которые игрок
уже исследовал, остальные же области должны оставаться непроходимыми до исследования.
Используя такое решение, юниты будут попадать в тупики и искать неверный путь до тех пор, пока
они не откроют всю карту. Как только карта будет исследована, поиск пути станет всегда находить
верные пути.
4. Сглаженные пути: A* даст вам кратчайший, с наименьшей стоимостью путь, но он не даст вам
визуально сглаженного пути. Давайте посмотрим на окончательный путь, просчитанный в примере
(рис. 7). На этом пути первый шаг находится справа внизу от стартовой клетки. Не казался бы наш
путь более плавным, если бы первый шаг находился прямо под стартовой клеткой?
Есть несколько способов решить эту проблему. В процессе поиска вы можете "штрафовать"
вершины, где есть смена направления движения, увеличивая их стоимости G. Так же вы можете
пройти по всему пути после его вычисления и выискивать вершины, в которых вы бы хотели
изменить направление для более сглаженного пути. Для более обширного описания проблемы,
проверьте , Toward More Realistic Pathfinding, бесплатную (но требующую регистрации) статью
Марка Пинтера (Marco Pinter) на Gamasutra.com.
5. Неквадратные области поиска: В нашем примере мы используем простые плоские
квадратные ячейки. Необязательно всегда выбирать такое решение. Вы можете использовать
области с неправильной формой. Вспомните про игру Risk (Риск :-) и страны в ней. Вы можете
захоть использовать такой сценарий поиска пути. Для того, чтобы это сделать вам необходимо
создать таблицу для хранения информации о том, какие страны с какими граничат и стоимости G
для передвижения от одной страны к другой. Вы также должны выбрать метод определения
стоимости H. Все остальное остается таки же, как и в нашем примере. При добавлении элементов
в открытый список, вместо просмотра соседних клеток просто просматривайте соседние страны в
таблице.
Также вы можете создать систему вэйпоинтов для путей нафиксированной карте. Вэйпоинты
представляют собой несколько связанных точек пути, например, на дороге или в тунелле
подземелья. Как гейм-дизайнер вы можете указывать эти вэйпоинты вручную. Два вэйпоинта
считаются соседними, если на линии между ними нет никаких препятствий. В случае с игрой Риск
(Risk), вы сохраняете эту информацию о соседстве в какой-нибудь таблице и используете эту
информацию при генерации элементов открытого списка. Затем вы сохраняеете присвоенные
стоимости G (возможно, используя длины отрезков, соеденяющих вершины) и стоимости H
(возможно, используя длины отрезков, соеденяющих вершины и цель). Все остальное
выполняется как обычно.
Амит Пател (Amit Patel) написал статью предлагающую некоторые альтернативы. Пример по
поиску на изометрической RPG карте, используя неквадратные области поиска, лежит здесь: TwoTiered A* Pathfinding.
6. Некоторые советы по увеличению скорости поиска: Как только вы разработаете свою
собственную программу A*, или адаптируете ту, которую написал я, то обнаружите, что поиск пути
использует львиную долю вашего процессорного времени, особенно если у вас огромное
количесво юнитов и большая карта. Если вы прочитаете в сети различные материалы, то узнаете,
что это распостраняется даже на программистов таких игр, как Starcraft или Age of Empires. Если
вы заметили, что поиск пути стал медленным, то вот несколько советов о том, как ускорить этот
процесс:






Используйте карты поменьше и небольшое количество юнитов.
Никогда не ищите путь одновременно для нескольких юнитов. Вместо этого поставьте их в
очередь и рассчитывайте за несколько игровых циклов. Если ваша игра работает на
скорости, скажем, в 40 циклов за кадр, никто никогда этого не заметит. Но все заметят,
если игра начнет работать очень медленно каждый раз, когда путь вычисляется
одновременно для множества юнитов.
Используйте для вашей карты как можно большие квадраты (или любую другую фигуру).
Это уменьшит общее количество вершин просмотренных при поиске пути. Если вы очень
амбитный человек, то можете использовать две или больше систем поиска пути, которые
будут использоваться в разных ситуациях в зависимости от длины пути. Вот что делают
проффесионалы: используют большие области для длинных путей, а затем переходят на
более точный поиск, используя меньшие квадраты/области при приближении к цели. Если
вы заинтересованы в таком подходе, прочтите мою статью Two-Tiered A* Pathfinding.
Для длинных путей можно использовать предварительно просчитанные пути.
Обрабатывайте вашу карту на наличие недоступных из других частей карты участков. Я
называю эти участки "острова." В действительности они могут быть островами или любой
другой поверхностью, которая отгорожена стенами или недоступна. Один из недостатков
алгоритма A* состоит в том, для нахождения путей к таким участкам, он будет искать путь
на всей карте, останавливаясь только когда каждый квадрат/вершина прошли через
открытый и закрытый списки. Это может потратить очень много процессорного времени.
Этого можно избежать если просчитывать какие области недостижими (с помощью flood-fill
заливки или похожего метода), сохраняя эту информацию в любом массиве и проверяя ее
каждый раз перед началом поиска.
В запутанном, лабиринтоподобном окружении, стоит отметить вершины, которые не
приводят никуда, кроме тупиков. Такие области можно указывать на вашей карте вручную
или, если вы очень амбитны, разработать алгоритм, который будет опряделять такие
области автоматически. Любой группе таких вершин можно присвоить уникальный
идентификационный номер. Затем вы можете спокойно игнорировать все тупики при
поиске пути, задумывая сь только о том, чтобы стартовая или челевая точки не попали в
такую "тупиковую" область.
7. Реализация открытого списка: Это один из наиболее важных элементов в алгоритме A*.
Каждый раз при обращении к открытому списку вам необходимо найти вершину с наименьшей
стоимостью F. Существует несколько способов это сделать. Вы можете сохранять элементы пути
когда потребуется и просто проматривать весь открытый список каждый раз, когда надо извлечь
элемент с наименьшей стоимостью F. Это простой метод, но очень медленный для длинных путей.
Он может быть улучшен использованием сортированного списка и тогда нуждно будет просто
выбрать первый элемент списка - это и будет элемент с наименьшей стоимостью F. Когда я писал
свою программу этот метод был первым, который я использовал.
Это будет быстро работать на маленьких картах, но это не оптимальное решение. Серьезные
программисты A*, которые стремятся действительно к впечатляющей скорости, используют что-то,
что называются "двоичными кучами" (binary heaps) и это то, что я использовал в своей программе.
Это будет по крайней мере в 2-3 раза быстрее в большинстве случаев и намного быстрее (в 10 и
больше раз) на длинных путях. Если вы заинтересовались, прочтите мою статью Using Binary
Heaps in A* Pathfinding.
8. Алгоритм Дийкстры: В то время, как считается, что A* самый лучший алгоритм поиска
(смотрите выше), сущетвует по крайней мере еще один алгоритм, который активно используется алгоритм Дийкстры (Dijkstra's algorithm). Этот алгоритм точно такой же как и A*, только без
эвристики (H всегда равна 0). Так как в нем нет эвристики, он ищет путь одновременно во все
стороны. Перед тем, как найти путь, алгоритм исследует намного больше территории. Это делает
его медленнее A*.
Так зачем его использовать? Иногда мы не знаем где находится наша цель. Например, у вас есть
добывающий юнит, которому надо собрать каких-нибудь ресурсов. Он может знать, что на этой
территории есть несколько областей с ресурсами, но он хочет добраться к ближайшим. Здесь
использовать алгоритм Дийкстры будет более целесообразно, чем использовать A* потому, что мы
не знаем, какие из ресурсов находятся ближе. Альтернатива есть только в повторном
использовании A* для нахождения расстояния до каждого из ресурсов, а затем использовании
этого пути. Существует множество таких ситуаций.
Дальнейшее изучение
Хорошо, теперь вы знаете основы и смысл некоторых углубленных решений. На этом этапе я
советую заглянуть в мой исходный код. Архив содержит две версии: на C++ и на Blitz Basic. Обе
версии содержат множество комментариев и должны быть довольно простыми в пониманииd. Вот
ссылка.

Sample Code: A* Pathfinder (2D) Version 1.9
Если у вас нет возможности работать на C++ или в Blitz Basic, два небольших exe-файла
находятся в папке с версией на C++. Версия на Blitz Basic может быть скомпилирована с помощью
бесплатной демо-версии Blitz Basic 3D (не Blitz Plus) на оффициальном сайте Blitz Basic. Сетевая
демонстрация Бена О'Нилла (Ben O'Neill) находится тут.
Вы также можете заинтересоваться некоторыми сайтами. После прочтения этой статьи они будут
простыми для понимания.



Amit’s A* Pages: Это очень обширная страничка Амита Патела (Amit Patel), но она может
быть немного непонятной для тех, кто не читал эту статью. Но все же стоит глянуть.
Советую прочитать личные рассуждения Амита про статью.
Smart Moves: Intelligent Path Finding: Эта статья написана Брайаном Стаутом (Bryan Stout)
для портала Gamasutra.com и требует регистрации для чтения. Регистраци бесплатна и
позволяет прочитать не только эту статью но и множество других ресурсов, дуступных на
сайте. Программа, написанная Брайаном на Delphi помогла мне изучить A* и вдохновила
на написание моей программы A*. Также она описывает некоторые альтернативные
возможности A*.
Terrain Analysis: Это сложная, но интересная статья Дейва Поттингера (Dave Pottinger),
проффесионала из Ensemble Studios. Этот парень координировал разработку Эпохи
Империй (Age of Empires) и ее второй части. Не ожидайте, что вы поймете все, написанное
в ней, но это очень интересная статья, которая может натолкнуть вас на некоторые идеи.
Она включает в себя обсуждение мип-маппинга, карты с влияниями и некоторыми другими
углубленными решениями ИИ/поиска пути. Обсуждение "заливки" (“flood filling”) послужило
идеей моих собственных "тупиков"и "островов"(“dead ends” and “islands”) , которые
прилагаются к Blitz версии моей программы..
Вот некоторые интересные сайты, которые следует просмотреть:



aiGuru: Pathfinding
Game AI Resource: Pathfinding
GameDev.net: Pathfinding
Хорошо, вот и все. Если вы напишите программу, которая использует любые из этих принципов, я
с удовольствием на нее взгляну. Пишите письма. :)
Download