A * Path Finding Algorithm em um jogo Voxel 3d Unity

1. Introdução


Ao desenvolver meu jogo, cheguei ao ponto de criar os primeiros NPCs. E surgiu a questão de como fazer o NPC contornar o muro e não "entrar nele".


Subindo pela Internet, encontrei os seguintes algoritmos:


  • Pesquisa ampla (BFS, pesquisa pela primeira vez)
  • Algoritmo de Dijkstra (Dijkstra)
  • Uma estrela "A com um asterisco"
  • Pesquise pela primeira melhor correspondência (pesquisa pela primeira vez)
  • IDA (A com aprofundamento iterativo)
  • Pesquisa de ponto de salto

E decidi tentar implementar meu A * em uma grade 3D voxel.



Exemplo de mapa do meu jogo

imagem


Descrição do algoritmo


Um * passo a passo examina todos os caminhos que vão do vértice inicial ao final, até encontrar o mínimo. Como todos os algoritmos de pesquisa informados, ele primeiro analisa as rotas que "parecem" levar ao objetivo. Do algoritmo ganancioso, que também é o algoritmo de busca da primeira melhor correspondência, ele se distingue pelo fato de que, ao escolher um vértice, leva em consideração, entre outras coisas, todo o caminho percorrido até ele.


Visualização do trabalho de A * na Wikipedia



Implementação


Como o algoritmo precisa de "nós" - pontos para determinar o caminho, escrevemos a estrutura de classes do nó:


Código do nó:
public enum EMoveAction { walk, jump, fall, swim }; public class PathPoint { //   public Vector3 point { get; set; } //    public float pathLenghtFromStart { get; set; } //     public float heuristicEstimatePathLenght { get; set; } //     public float estimateFullPathLenght { get { return this.heuristicEstimatePathLenght + this.pathLenghtFromStart; } } //   public EMoveAction moveAction = EMoveAction.walk; //      public PathPoint cameFrom; } 

Construtores de classe pequena:
  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; } 

Em seguida, a estrutura das configurações de pesquisa de caminho será útil:


Código de configurações da pesquisa de caminho:
  public struct SPathFinderType { //   , , ,   public bool walk, jump, fall, swim; //   ,  public int maxFallDistance, jumpHeight, jumpDistance; //   public int characterHeight; //    public static 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; } } 

Além disso, "World" é um tipo de classe de banco de dados para armazenar informações sobre blocos de mapas. O seu pode ser implementado de maneira diferente.


O resultado da função de pesquisa de caminho de obter a rota:
  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; } 

GetMinEstimate
  //        private int GetMinEstimate(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; } 

Drawpath
  //      public void DrawPath(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); } } 

Finishfounded
  //     private bool FinishFounded(List<PathPoint> points) { for (int i = 0; i < points.Count; i++) { if (points[i].heuristicEstimatePathLenght <= 0) return true; } return false; } 

GetPathToTarget
  //        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; } 

Ponto próximo


A função ClosePoint depende apenas da implementação da classe World, adiciona todos os caminhos possíveis à lista de pontos em aberto e remove o ponto atual dessa lista (fecha-a). Vou dar um exemplo do meu "ponto de fechamento" nas quatro primeiras direções.


Aviso grande pilha de código
  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; } 

Otimização


Simplesmente dividindo o caminho do ponto inicial ao ponto atual, reduzimos o número de nós muitas vezes e o tornamos mais ganancioso.


 return this.heuristicEstimatePathLenght + this.pathLenghtFromStart /2; 

Sumário


Prós:


  • Pesquisa rápida em espaços abertos.
  • Versatilidade do algoritmo

Contras:


  • Requer muita memória para calcular a rota.

Verde mostra uma lista aberta de nós, caminho vermelho para o destino, nós fechados azuis.


Rotas recebidas antes da otimização:



Rotas recebidas após otimização:



Literatura


https://tproger.ru/articles/pathfindings/
https://ru.wikipedia.org/wiki/A*

Source: https://habr.com/ru/post/pt416737/


All Articles