Algorithm for finding the path A * in a voxel 3d game on Unity
Introduction
When developing my game, I got to the point of creation of the first NPCs. And the question was how to make the NPC go around the wall and not “go into it”.
Climbing on the Internet, I found these algorithms:
- Search Wide (BFS, Breadth-First Search)
- Dijkstra algorithm (Dijkstra)
- And Star "A with an asterisk"
- Search for the first best match (Best-First Search)
- IDA (A with iterative deepening)
- Jump Point Search
And I decided to try to implement my A * on the voxel 3d grid.
Algorithm Description
A * step through all the paths leading from the initial vertex to the final, until it finds the minimum. Like all informed search algorithms, he first looks at those routes that “seem” to lead to the goal. From the greedy algorithm, which is also the search algorithm for the first best match, it is distinguished by the fact that when choosing a vertex it takes into account, among other things, the entire path traveled to it.
Implementation
Since the algorithm needs "nodes" - points to determine the path, we write the class structure of the node:
publicenum EMoveAction { walk, jump, fall, swim };
publicclassPathPoint
{// текущая точкаpublic Vector3 point { get; set; }
// расстояние от стартаpublicfloat pathLenghtFromStart { get; set; }
// примерное расстояние до целиpublicfloat heuristicEstimatePathLenght { get; set; }
// еврестическое расстояние до целиpublicfloat estimateFullPathLenght
{
get
{
returnthis.heuristicEstimatePathLenght + this.pathLenghtFromStart;
}
}
// способ движенияpublic EMoveAction moveAction = EMoveAction.walk;
// точка из которой пришли сюдаpublic PathPoint cameFrom;
}
private PathPoint NewPathPoint(Vector3 point, float pathLenghtFromStart, float heuristicEstimatePathLenght, EMoveAction moveAction){
PathPoint a = new PathPoint();
a.point = point;
a.pathLenghtFromStart = pathLenghtFromStart;
a.heuristicEstimatePathLenght = heuristicEstimatePathLenght;
a.moveAction = moveAction;
return a;
}
private PathPoint NewPathPoint(Vector3 point, float pathLenghtFromStart, float heuristicEstimatePathLenght, EMoveAction moveAction, PathPoint ppoint){
PathPoint a = new PathPoint();
a.point = point;
a.pathLenghtFromStart = pathLenghtFromStart;
a.heuristicEstimatePathLenght = heuristicEstimatePathLenght;
a.moveAction = moveAction;
a.cameFrom = ppoint;
return a;
}
Next, we need the structure of the path search settings:
publicstructSPathFinderType
{// Возможность персонажа ходить, прыгать, падать, и плаватьpublicbool walk, jump, fall, swim;
// Максимальная высота падения, прыжкаpublicint maxFallDistance, jumpHeight, jumpDistance;
// Высота персонажаpublicint characterHeight;
// Создаём стандартные настройкиpublicstatic SPathFinderType normal(){
SPathFinderType n = new SPathFinderType();
n.walk = true;
n.jump = true;
n.fall = true;
n.swim = false;
n.maxFallDistance = 1;
n.jumpHeight = 1;
n.jumpDistance = 0;
n.characterHeight = 1;
return n;
}
}
Further, "World" is a class of a unique database for storing information about map blocks. You may have implemented differently.
public List<PathPoint> GetPathToTarget(Vector3 beginPoint, Vector3 targetPoint, World worldData, SPathFinderType pfType)
{
List<PathPoint> path = new List<PathPoint>();
// Список не проверенных нодов
List<PathPoint> openPoints = new List<PathPoint>();
// Список проверенных нодов
List<PathPoint> closedPoints = new List<PathPoint>();
// Добавляем к открытым начальную точку
openPoints.Add(NewPathPoint(beginPoint, 0, GameLogic.Distance(beginPoint, targetPoint), EMoveAction.walk));
// закрываем точку
closedPoints.Add(openPoints[0]);
// открываем новые точки и удаляем закрытую
openPoints = ClosePoint(0, openPoints, closedPoints, worldData, pfType, targetPoint);
// "Стоп сигнал" для нашего поискаbool stopFlag = true;
// Устанавливаем ограничители чтобы избежать проверки всей картыfloat maxEstimatePath = 1500;
int maxNodes = 6000;
while (stopFlag)
{
// Находим индекс точки с минимальным евристическим расстояниемint minIndex = GetMinEstimate(openPoints);
if (openPoints.Count > 0)
if (openPoints[minIndex].estimateFullPathLenght < maxEstimatePath)
{
// закрываем точку
closedPoints.Add(openPoints[minIndex]);
// добавляем новые если есть и удаляем minIndex
openPoints = ClosePoint(minIndex, openPoints, closedPoints, worldData, pfType, targetPoint);
}
else
{
// если расстояние достигло пределов поиска// просто закрываем точку без добавления новых
closedPoints.Add(openPoints[minIndex]);
openPoints.RemoveAt(minIndex);
}
// Функция проверяет найден ли финишif (FinishFounded(closedPoints))
{
Debug.Log("Финиш найден!");
path = GetPathToTarget(closedPoints);
stopFlag = false; // остановка цикла если найдена цель
}
if (openPoints.Count <= 0)
stopFlag = false; // остановка цикла если открытых точек нетif ((openPoints.Count>= maxNodes) ||(closedPoints.Count>= maxNodes))
stopFlag = false; // остановка цикла слишком много нодов
}
Debug.Log("Nodes created "+ closedPoints.Count.ToString());
// Рисуем полученные пути
DrawPath(openPoints, Color.green, 6f);
DrawPath(closedPoints, Color.blue, 6f);
DrawPath(path, Color.red, 6f);
return path;
}
// находит индекс точки ближайшей к точке назначенияprivateintGetMinEstimate(List<PathPoint> points){
int min = 0;
for (int i = 0; i < points.Count; i++)
{
if (points[i].estimateFullPathLenght < points[min].estimateFullPathLenght)
min = i;
}
return min;
}
// Рисует линии из точек путиpublicvoidDrawPath(List<PathPoint> points, Color c, float time){
for (int i = 0; i < points.Count; i++)
{
if (points[i].cameFrom != null)
Debug.DrawLine(points[i].point, points[i].cameFrom.point, c, time);
}
}
// проверяет найдена ли цельprivateboolFinishFounded(List<PathPoint> points){
for (int i = 0; i < points.Count; i++)
{
if (points[i].heuristicEstimatePathLenght <= 0)
returntrue;
}
returnfalse;
}
// Возвращает список точек от старта до финишаprivate List<PathPoint> GetPathToTarget(List<PathPoint> points)
{
List<PathPoint> path = new List<PathPoint>();
int targetIndex = 0;
for (int i = 0; i < points.Count; i++)
{
if (points[i].heuristicEstimatePathLenght <= 0)
targetIndex = i;
}
PathPoint ppoint = new PathPoint();
ppoint = points[targetIndex];
while (ppoint.pathLenghtFromStart > 0)
{
path.Add(ppoint);
ppoint = ppoint.cameFrom;
}
path.Reverse();
return path;
}
Closepoint
The ClosePoint function depends entirely on the implementation of the World class, it adds all possible paths from it to the list of open points and removes the current point from this list (closes it). I will give an example of my "closing point" in the first four directions.
private List<PathPoint> ClosePoint(int index, List<PathPoint> openPoints, List<PathPoint> closedPoints, World worldData, SPathFinderType pfType, Vector3 targetPoint)
{
List<PathPoint> newOpenPoints = openPoints;
PathPoint lastPoint = openPoints[index];
// если персонаж может идтиif (pfType.walk)
// если персонаж может стоять на текущем блокеif (CanStand(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z), pfType.characterHeight, worldData))
{
// ---------------------------------------------------------------//north// /|\// |// если не в списке закрытыхif (!InList(closedPoints, new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z)))
// и уже не добавленаif (!InList(newOpenPoints, new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z)))
// если может там стоять if (CanStand(new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z), pfType.characterHeight, worldData))
{
newOpenPoints.Add(NewPathPoint(new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z)
, lastPoint.pathLenghtFromStart + GetTravelCost(new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z), worldData, pfType.characterHeight)
, GameLogic.Distance(new Vector3(lastPoint.point.x + 1, lastPoint.point.y, lastPoint.point.z), targetPoint)
, EMoveAction.walk
, lastPoint));
}
// south// |// \|/// если не в списке закрытыхif (!InList(closedPoints, new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z)))
// и уже не добавленаif (!InList(newOpenPoints, new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z)))
// если может там стоять if (CanStand(new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z), pfType.characterHeight, worldData))
{
newOpenPoints.Add(NewPathPoint(new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z)
, lastPoint.pathLenghtFromStart + GetTravelCost(new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z), worldData, pfType.characterHeight)
, GameLogic.Distance(new Vector3(lastPoint.point.x - 1, lastPoint.point.y, lastPoint.point.z), targetPoint)
, EMoveAction.walk
, lastPoint));
}
// east// ---->// // если не в списке закрытыхif (!InList(closedPoints, new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1)))
// и уже не добавленаif (!InList(newOpenPoints, new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1)))
// если может там стоять if (CanStand(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1), pfType.characterHeight, worldData))
{
newOpenPoints.Add(NewPathPoint(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1)
, lastPoint.pathLenghtFromStart + GetTravelCost(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1), worldData, pfType.characterHeight)
, GameLogic.Distance(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z + 1), targetPoint)
, EMoveAction.walk
, lastPoint));
}
// west// <----// // если не в списке закрытыхif (!InList(closedPoints, new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1)))
// и уже не добавленаif (!InList(newOpenPoints, new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1)))
//если может стоять тамif (CanStand(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1), pfType.characterHeight, worldData))
{
newOpenPoints.Add(NewPathPoint(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1)
, lastPoint.pathLenghtFromStart + GetTravelCost(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1), worldData, pfType.characterHeight)
, GameLogic.Distance(new Vector3(lastPoint.point.x, lastPoint.point.y, lastPoint.point.z - 1), targetPoint)
, EMoveAction.walk
, lastPoint));
}
}
newOpenPoints.RemoveAt(index);
return newOpenPoints;
}
Optimization
By simply dividing the path from the start to the current point, we reduce the number of nodes many times and make it more "greedy."
returnthis.heuristicEstimatePathLenght + this.pathLenghtFromStart /2;
Results
Pros:
- Quick search in open spaces.
- Universality of the algorithm
Minuses:
- It requires a lot of memory to calculate the route.
Green is the open list of nodes, red is the way to the target, blue is closed nodes.
Literature
https://tproger.ru/articles/pathfindings/
https://ru.wikipedia.org/wiki/A*