понедельник, 13 июня 2011 г.

Разработка игр: Игровая логика и ИИ

Принцип работы алгоритма поиска пути Астар (A*).

Автор: Alexey Naumov
Есть матрица узлов (сетка клеток), каждый узел имеет линки на своих соседей. Алгоритм оперирует двумя списками: сортированным (далее open), в котором хранятся узлы, линки которых еще не были обработаны (узлы сортируются по оценке расстояния до конечного узла от наиболее близкого), и не сортированный (далее close) с обработанными узлами. Принцип работы следующий:
1. Помещаем в open список стартовый узел.
2. Основной цикл (пока open список не пуст).
3. Берм из open списка верхний узел.
4. Если этот узел равен конечному, то строим путь и выходим из основного цикла.
5. Обрабатываем линки выбранного узла. Для каждого соседа:
- проверяем его на проходимость, для конкретного юнита, если непроходим, то, понятное дело, переходим к следующему соседу;
- вычисляем оценку пути от стартового узла через текущий до соседа;
- ищем соседа в open и close списках, если найден, то сравниваем его оценку пути от стартового узла с вычисленной, если его оценка лучше то идем к следующему соседу, в противном случае удаляем узел из соответствующего списка, пересчитываем оценку пути до конечного узла и помещаем этого соседа в open список, в соответствующее место;
6. Помещаем текущий узел в close список.
7. Переходим к п.3 (конец основного цикла).
Если путь найден, его можно обработать, сделать астаровский путь более плавным, если это нужно, конечно.
Самые тяжелые по быстродействию места в алгоритме - это поиск узла в списках. Если использовать стандартные списки из stl, то затраты составляют примерно 75%, от времени работы всего алгоритма, это верно для достаточно сложной карты и поиске пути на значительное расстояние. Вставка узла в open список примерно 15% и проверка проходимости (учитывая размеры юнита) - порядка 10%.
Способы оптимизации.
Использование хэш таблиц.
Самым быстрым поиском обладают хэш таблицы. Отсюда мое желание прикрутить их к алгоритму астара. К сожалению стандартные списки (stl) использовать с хэш таблицей не представляется возможным, поэтому придется написать свои.
Основная идея хранить в хэш таблице указатель на итератор списка.
const int c_HeapSizeForAstarList = 8192;

typedef struct AstarListIt
{
  int        idx;  // узел, в данном случае его индекс
  AstarListIt*  prev; // предыдущий итератор
  AstarListIt*  next; // следующий итератор
} AstarListIt;

class AstarList
{
public:
  AstarList()
  {
    iSize = 0;
    iHeap = 0;
    heap.push_back(new AstarListIt[c_HeapSizeForAstarList]);
    head = tail = heap[iHeap]; tail->prev = head;
  }
  ~AstarList()
  {
      clear();
    delete[] heap[0];
  }
  AstarListIt* begin() {return head;}  // получить головной итератор
  AstarListIt* end() {return tail;}    // получить хвостовой итератор
  AstarListIt* push_back(int& idx);    // добавить элемент в конец списка
  int  front(); // pop                  // взять элемент из головы списка
  void erase(AstarListIt* it);         // удалить итератор
  bool empty() { return tail == head; } // проверить пустой ли список
  AstarListIt* insert(AstarListIt* itNext, int idx); // вставить элемент
  void clear();                                      // очистить список

protected:
    void resize();                        // увеличить размер

  AstarListIt*        head;     // головной итератор (начало списка)
  AstarListIt*        tail;     // хвостовой итератор (конец списка)
  std::vector<AstarListIt*>  heap;     // куча зарезервированной памяти
  int              iSize;    // размер текущей кучи
  int              iHeap;    // индекс текущей кучи
};

class AstarHash
{
public:
  typedef struct PathHashTable{ // элемент таблицы
    AstarListIt*  it;       // указатель на итератор
    int        value;    // проверочное значение
    bool      closed;   // относится к закрытому или открытому списку
  } PathHashTable;

  PathHashTable*  Table;        // таблица
  int        Size;         // размер
  int        Power;        // кол-во идентификационных бит
  int        Mask;         // маска

  AstarHash(int power): Power(power), Size(1<<power)
  { Table = new PathHashTable[Size]; Clear(); }
  ~AstarHash(){delete [] Table;}
  void Clear() { ZeroMemory(Table, Size*sizeof(PathHashTable)); } // очистка
  void Add(int value, AstarListIt* it, bool closed);   // добавить элемент
  void Resize();                                       // увеличить размер
  AstarListIt* Find(int value, bool closed);        // найти элемент
  void Erase(int value);                            // удалить
  void Check();                                    // проверка (для отладки)
};
В итоге скорость увеличилась примерно в 15 раз.
smartLOD
Второй способ оптимизации - дополнительные линки, или, так называемый, "смартЛОД". Идея в том чтобы добавить в узел помимо основных линков к непосредственным соседям дополнительные. Если узел находится в некоторой свободной зоне, в которой отсутствуют препятствия, то из этого узла можно сделать линки до краев области. Размерами области можно варьировать, в зависимости от сложности карты. Например, для карт типа лабиринта, можно искать области в радиусе от 8 до 4 клеток. Эта оптимизация дала дополнительно к первой еще 10-15% прироста производительности.
Пример.
Простой пример функции поиска, используются следующие обозначения:
m_pCells - матрица узлов;
m_lOpen - сортированный список;
m_lClose - список обработанных;
m_pAstarHash - хэш таблица;
m_fMaxSmartArea - наибольшая область, использовавшаяся при построении смартЛОДа;
узел содержит следующую информацию:
m_pLinks[8] - основные линки;
m_pSmartLinks[8] - смартлинки;
m_fCost - оценка всего пути через узел;
m_fFromStart - оценка пути от старта до узла;
m_fToFinish - оценка пути от узла до финиша;
m_ParentIdx - индекс предыдущего узла для построения пути;
линки:
fCost - стоимость перехода от узла к узлу;
fLength - расстояние между узлами;
idxTo - индекс узла соединение, с которым линк описывает;
SmartSize - размер для смартЛОДа;
// поиск пути
bool FindPath(int idxFrom, int idxTo, float fUnitSize, int iUnitType)
{
  // лист узлов для найденого пути
  list<int> lPath;
  int idxCur = idxFrom;

    // вычислить и запомнить оценку расстояния до конечного узла
  m_pCells[idxCur].CalcCost(0, m_pCells[idxTo]);
    // пометить как начало цепочки, для последующего построения пути
  m_pCells[idxCur].m_ParentIdx = -1;

    // поместить начальный узел в сортированный список
  m_lOpen.push_back(idxCur);
    // добавить запись в хэш таблицу
  m_pAstarHash->Add(idxCur, m_lOpen.begin(), false);

    // получение текущего времени для последующей проверки на критическое время
  DWORD dwTime = timeGetTime();
  DWORD dwCriticalTime = 200;

    // начало главного цикла
  while(!m_lOpen.empty())
  {
      // получение текущего узла для обработки
    idxCur = m_lOpen.front();
        // удаление записи из хэш таблицы
    m_pAstarHash->Erase(idxCur);

        // проверка на достижение конечного узла
    if (idxCur == idxTo)
    {
            // построение пути с конца в начало
      for (int p = idxTo; p >= 0; p = m_pCells[p].m_ParentIdx)
        lPath.push_front(p);
      break;
    }
        // использовать смартЛОД, если до конечного узла достаточно большое 
        // расстояние, больше чем максимальный размер области при 
        // построении смарт линков
        bool useSmart = m_pCells[idxCur].m_fToFinish > m_fMaxSmartArea;
    int iSize;
        // обработка восьми соседей
    for (int iDir = 0; iDir < 8; iDir++)
    {
          // получить дистанцию до соседа, если нет смартлинков, то - 1
      if (useSmart)
        iSize = m_pCells[idxCur].m_pSmartLinks[iDir].SmartSize;
      else
        iSize = 1;
            // получить индекс соседа
      int idx = m_pCells.GetNeighborIdx(idxCur, iDir, iSize);
            // а есть ли сосед?
      if (idx < 0)
        continue;

            // расчет нового оценочного расстояния для текущего соседа 
            // от стартового узла
      float fFromStart;
      if (useSmart)
        fFromStart = m_pCells[idxCur].m_fFromStart + 
          m_pCells[idxCur].m_pSmartLinks[iDir].fCost;
      else
        fFromStart = m_pCells[idxCur].m_fFromStart + 
          m_pCells[idxCur].m_pLinks[iDir].fCost;

            // поиск узла в списках при помощи хэш таблицы
      AstarListIt* pItO = m_pAstarHash->Find(idx, false);
      AstarListIt* pItC = m_pAstarHash->Find(idx, true);

            // если нашли то сравнить старое оценочное расстояние с новым
      if (pItO || pItC)
      {
        if (m_pCells[idx].m_fFromStart <= fFromStart)
          continue;

              // очистка списков и записей в хэш таблице
              if (pItC)
              {
                  m_lClose.erase(pItC);
                  m_pAstarHash->Erase(idx);
              }
              if (pItO)
              {
                  m_lOpen.erase(pItO);
                  m_pAstarHash->Erase(idx);
              }
      }
            else
            {
                // проверка узла на проходимость (занятость)
              if (CheckBusyWayable(idx, iUnitType, fUnitSize))
                  continue;
            }

            // обновление оценочного расстояния до конца пути
      m_pCells[idx].CalcCost(fFromStart, m_pCells[idxTo]);
      m_pCells[idx].m_ParentIdx = idxCur;

            // вставка узла в сортированный список
            AstarListIt* itOpen = m_lOpen.begin();
      for (; itOpen != m_lOpen.end(); itOpen = itOpen->next)
      {
        if ( m_pCells[itOpen->idx].m_fCost >= m_pCells[idx].m_fCost)
          break;
      }
      m_pAstarHash->Add(idx, m_lOpen.insert(itOpen, idx), false);
    }

        // поместить обработанный узел в close список
    m_pAstarHash->Add(idxCur, m_lClose.push_back(idxCur), true);

        // проверка времени (дабы пусть лучше путь не будет найден,
        // чем будет лаг из-за его поиска
    if (timeGetTime() - dwTime > dwCriticalTime)
      break;
  }
    // очистка списков и хэш таблицы
  m_lOpen.clear();
  m_lClose.clear();
  m_pAstarHash->Clear();

    // вызов функции сглаживания пути
  OptimizeWay(lPath, unitPath);

  // вернуть результат работы
  return !unitPath.m_lWayPoints.empty();
}
Принимаются предложения по оптимизации вставки элемента в сортированный список. После всех оптимизаций данная реализация вставки съедает порядка 30% времени поиска.
Исходый код к статье: 20040117.h

Комментариев нет:

Отправить комментарий