Алгоритм поиска пути A*

Алгоритм поиска пути

В интернете существует множество статей и видео по данной теме. Однако для многих поиск пути кажется сложным и не понятным. В этой статье я попытаюсь объяснить на пальцах как он работает.

Код во всех случаях будет отличаться, поэтому самое главное понять логику.

Итак. Поиск пути - это очень распространенная задача. Заключается она в поиске оптимального маршрута от точки А к точке Б. Однако алгоритм можно адаптировать под схожие задачи.

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

  • Однонаправленными
  • Разной сложности проходимости
  • Непроходимыми
  • Могут быть связаны не только с рядом стоящими клетками ( Телепорты, порталы... )

Суть алгоритма поиска пути

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

Каждая клетка должна знать:

  • С какими клетками она связана
  • На сколько тяжело проходить по ней самой.

Псевдокод клетки

Клетка{
     массив связей,
     коэффициент проходимости,
     ссылка_на_предыдущую_клетку,
     пройдено,
     Примерно_до_финиша.
}

Так же нам необходимы 2 массива. В одном мы будем хранить клетки, которые в данный момент нужно посмотреть. В другом клетки, которые уже посмотрены и больше не учувствуют в расчётах.

Считаем на сколько все связанные клетки далеко от нашей текущей позиции, а так-же на сколько далеко эти клетки от конечной точки.

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

Проводим те же расчёты дальше, пока не дойдем до финиша.

Звучит очень просто, на деле все так-же, но немного сложнее в плане написания кода. Главное тут, это понять логику как оно вообще работает, после этого написание кода не должно вызывать проблем.

Ниже я приведу псевдокод алгоритма. Он более полный, но возможно не всем будет понятен, а также приложу несколько видео на эту тему.

массив В_ОБРАБОТКЕ
массив ОБРАБОТАННЫЕ

В_ОБРАБОТКЕ.Добавить( НАЧАЛЬНАЯ_КЛЕТКА )

цикл ( пока не нашли финиш ){
	цикл ( Пока В_ОБРАБОТКЕ не пуст ){
		КЛЕТКА = В_ОБРАБОТКЕ.клетка с наименьшей суммой ( пройдено + Примерно_до_финиша )

		цикл ( КЛЕТКА.связи ){

			если (СВЯЗЬ находиться в массиве ОБРАБОТАННЫЕ) Пропускаем СВЯЗЬ и смотрим слудующую

			СВЯЗЬ.ссылка_на_предыдущую_клетку = КЛЕТКА
			СВЯЗЬ.пройдено = КЛЕТКА.пройдено + расстояние_между_клетками
			СВЯЗЬ.Примерно_до_финиша = расстояние_напрямую (от СВЯЗЬ до КОНЕЧНАЯ_КЛЕТКА)

			если ( СВЯЗЬ = КОНЕЧНАЯ_КЛЕТКА ) {

				ВРЕМЕННАЯ_КЛЕТКА = СВЯЗЬ
				массив ПУТЬ = пуст

				цикл( пока ВРЕМЕННАЯ_КЛЕТКА не равна НАЧАЛЬНАЯ_КЛЕТКА){
					ПУТЬ.Добавить( ВРЕМЕННАЯ_КЛЕТКА )
					ВРЕМЕННАЯ_КЛЕТКА = ВРЕМЕННАЯ_КЛЕТКА.ссылка_на_предыдущую_клетку
				}

				Заканчиваем алгоритм и выдает ПУТЬ

			}иначе{
				В_ОБРАБОТКЕ.Добавить( СВЯЗЬ )
				В_ОБРАБОТКЕ.Удалить( КЛЕТКА )
				ОБРАБОТАННЫЕ.Добавить( КЛЕТКА )
			}
		}
	}
}

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

// переменные для расчёта пути
private List<TileStats> check = new List<TileStats>(); // тайлы которые мы проверяем
private List<TileStats> is_check = new List<TileStats>(); // тайлы которые мы проверили
public List<TileStats> result = new List<TileStats>(); // тропинка
public bool theEndCheck = false;

public List<TileStats> CheckTileTo(TileStats to, TileStats _activeTile)
{
    for (int i = 0; i < tiles.Count; i++)
    { // для безопасности очищаем информацию хранящуюся на тайлах
        tiles[i].GetComponent<TileStats>().prevTile = null; // очищаем предыдущий объект с которого мы пришли
        tiles[i].GetComponent<TileStats>().distance = 0; // очищаем дистанцию пройденную
        tiles[i].GetComponent<TileStats>().distanceEnd = 0; // очищаем дистанцию до поиского объекта
    }
    // функция поиска пути
    theEndCheck = false; // обнуляем переменную конца
    check.Clear(); // очищаем список проверяемых тайлов
    is_check.Clear(); // очищаем список проверенных тайлов 
    result.Clear(); // очищаем список результата

    check.Add(_activeTile); // добавляем в проверку ячейку на которой стоим
    _activeTile.prevTile = _activeTile; // добавляем ячейе саму себя

    if (to == _activeTile)
        return null;

    int i_step = 10; // переменная для незацикливания цикла
    while (!theEndCheck)
    {
        int j_step = 10; // переменная для незакикливания цикла
        while (check.Count > 0)
        {
            TileStats item = SelectMinTile(); // ищем элемент с минимальной дистанцией
            int step_tile = item.Tiles.Count;

            for (int i = 0; i < item.Tiles.Count; i++) // перебираем соседние тайлы
            {
                bool noSelect = false; // переменная которая исключает проверку соседнего тайла

                float distantion = item.distance + Vector2.Distance(item.transform.position, item.Tiles[i].transform.position); // считаем пройденную дистанцию от тайла котоый проверем, до соседнего тайла
                float distanceEnd = Vector2.Distance(to.transform.position, item.Tiles[i].transform.position); // считаем дистанцию до конечной точки
                if (SelectCheck(item.Tiles[i], is_check)) // провереряем есть ли у нас элемент в списке проверенных
                {
                    if (distantion + distanceEnd > item.Tiles[i].distance + item.Tiles[i].distanceEnd) // проверяем этот элемент на изменение наименьшей дистанции
                        noSelect = true; // запрещаем проверку
                }
                if (item.Tiles[i].Hardcore > selectedWarriror.GetComponent<PlayerStats>().type) noSelect = true; // проверяем на тип перемещения

                if (!noSelect) // если у нас проверка разрешена
                {
                    if (SelectCheck(item.Tiles[i], check)) // проверяем существует ли у нас объект в списке провяемых
                    {
                        if (distantion + distanceEnd < item.Tiles[i].distance + item.Tiles[i].distanceEnd) // проверяем дистанция меньше чем была до этого
                        {
                            item.Tiles[i].prevTile = item; // меняем предыдущий тайл
                            item.Tiles[i].distance = distantion; // меняем пройденную дистанцию
                            item.Tiles[i].distanceEnd = distanceEnd; // меняем конечную дистанцию
                        }
                    }
                    else
                    {
                        item.Tiles[i].prevTile = item; // задаём предыдущий тайл
                        item.Tiles[i].distance = distantion; // записываем пройденную дистанцию
                        item.Tiles[i].distanceEnd = distanceEnd; // записываем конечную дистанцию
                    }
                    if (item.Tiles[i] == to) // проверяем если наш соседний тайл конец
                    {
                        TileStats timeTile = item.Tiles[i]; // временный тайл
                        while (timeTile != _activeTile) // запускаем цикл, пока наш временный тайл не является началом
                        {
                            result.Add(timeTile); // добавляем временный тайл в результат
                            timeTile = timeTile.prevTile; // временный тайл становится предыдущим тайлом
                        }

                        theEndCheck = true; // говорим что нашли конец

                        return result; // выходим из функции
                    }

                    if (item.prevTile != item.Tiles[i] && !SelectCheck(item.Tiles[i], check)) // проверяем что тайл который мы проверяем не является предыдущим и проверяем что наш проверяем тайл уже не находится в массиве проверяемых
                        check.Add(item.Tiles[i]); // мы добавляем тайл в проверяемые

                    check.Remove(item); // исключаем свой тайл и проверки
                    if (!SelectCheck(item, is_check)) is_check.Add(item); // если проверяемого тайла нет в списке проверенных, то добавляем проверяемый тайл в список проверенных
                }
                else
                {
                    step_tile--;
                }
            }
            // првоверить что у соседних тайлов нет выходов и удалить проверяемы тайл из списка проверки
            if (step_tile == 0)
            {
                check.Remove(item); // убираем из списка проверяемых
                if (!SelectCheck(item, is_check)) is_check.Add(item); // проверяем есть ли она в списке проверенных если нет то добавляем
            }
            j_step--;
        }
        i_step--;
        if (check.Count == 0) theEndCheck = true;
    }
    return result;
}
public bool SelectCheck(TileStats selectTile, List<TileStats> list)
{
    bool select = false; // переменная проверки
    for (int i = 0; i < list.Count; i++) // цикл по всему списку
    {
        if (list[i].Equals(selectTile)) // проверяем есть ли у нас элемент в списке
        {
            select = true; // говорим что есть
            break; // останавливаем цикл
        }
    }

    return select; // возвращаем результат
}

public TileStats SelectMinTile()
{
    TileStats ret = null;
    float min = float.PositiveInfinity;

    for (int i = 0; i < check.Count; i++)
    {

        if (check[i].distance + check[i].distanceEnd < min)
        {
            min = check[i].distance + check[i].distanceEnd;
            ret = check[i];
        }
    }
    return ret;
}
4.5/5 (1)

Оцените