介绍
AStar算法,是一种在静态路网中求最短路径的一种算法,是一种启发式的算法.其中,算法当中起点与终点的距离,与算法当中的预估距离越接近,则搜索速度越快.
其实,在AStar算法当中,他的搜索路径的特点与迪杰斯特拉那种毫无章法四处搜索的不同,他是在向着离终点比较近的方向发起的搜索,因此,他的效率自然也就一般的寻路算法要高效了.
当然,这也可以说是启发式算法的优点了
实现思路
AStar算法的实现思路比较简单,就是,向人物当前的节点的邻居节点(可移动的)发起计算,然后选择比较近的那个点,然后以那个点为中心继续开始下一轮的计算,直到到达终点为止
当然,在实现AStar算法之前,首先要掌握几个概念
代价:在计算距离的时候,可以理解为距离
fCost : 起点进过当前点到目标的代价的估计
gCost:起点到当前点的代价
hCost:当前点到终点的估计的代价
fCost = gCost + hCost
OpenSet:开放集合,可以进行计算(但没有被选中)的节点
CloseSet:封闭集合,所有已经计算过而且也被算中的节点
实现
Init
得到StartNode与TargetNode
初始化OpenSet与CloseSet
将初始节点加入到OpenSet当中
Loop (OpenSet.Count > 0)
//找出OpenSet当中fCost最小的点,这个点就是被选中的将要行走的下一个点
currentNode = OpenSet当中fCost最小的点
//将这个点从OpenSet当中移除
OpenSet.Remove(currentNode);
//将这个点加入到ClostSet当中
ClosrSet.Add(currentNode);
if(currentNode == targetNode)
路径已经找到
//若路径没有找到,则查找当前选中的节点的邻居节点,
//计算他们的fCost并加入到OpenSet当中方便下一轮计算
foreach neighbour of the currentNode
//若这个节点是不可以行走,或者已经被算中过了,则跳过这个节点
if (!neighbour.walkable || closeSet.Contains(neighbour))
continue;
//计算这个这个邻居节点的gCost,若gCost更小则说明
//通过currentNode到这个节点的距离更加的短(代价更小)
newGCostToNeighbour = currentNode.gCost +
GetDistance(currentNode,neighbour);
//gCost若更小的话则需要重新计算这个点的fCost和他的父亲节点
//若这个节点不在OpenSet的话,则计算这个节点的fCost与设置父亲节点
//通过设置父亲节点,那么在之后找到路径之后可以通过父亲节点找到整条路径
if(newGCostToNeighbour < neighbour.gCost ||
!openSet.Contans(neighbour)){
neighour.fCost = evaluateFCost(neighour);
neighour.parentNode = currentNode;
if(!OpenSet.Contains(neighour)){
OpenSet.Add(neighbour);
}
}
在Unity当中的实现
首先需要记录Node节点的信息
public class Node {
public Vector3 worldPos;
//对应的路的GameObject
public GameObject road;
public bool walkable;
public int gridX;
public int gridY;
//起点到当前点的代价
public int gCost;
//当前点到终点的预估代价(无视障碍物)
public int hCost;
//当前点到终点的代价
public int fCost
{
get
{
return gCost + hCost;
}
}
public Node(int gridX,int gridY,bool walkable)
{
this.gridX = gridX;
this.gridY = gridY;
this.walkable = walkable;
}
public Node parent;
}
然后是需要一个地图,也就是Grid
这个类主要在于实现地图的建立,与算法关系不大
在这个类当中与算法的唯一关联在于提供了计算节点的邻居节点的方式
public struct GridPosition
{
public int x;
public int y;
}
public class Grid : MonoBehaviour {
enum ClickGridState
{
SetStartPos,
SetEndPos,
Cancel
}
public GameObject cube;
private Dictionary<GridPosition, Node> nodeDic = new Dictionary<GridPosition, Node>();
public List<Node> path;
private GameObject startGO;
private GameObject endGO;
private AStartPathFinding aStartPath;
private Color roadColor;
private ClickGridState currentState;
public float cubeSize = 1f;
readonly int[,] map =
{
{0,1,0,0,0,0,0,0,0,0,1,0,0,0,0},
{0,1,0,1,1,1,0,1,0,0,0,0,0,0,0},
{0,1,0,1,0,1,0,1,0,0,1,1,1,1,0 },
{0,1,0,1,0,1,0,1,0,0,0,0,0,0,0 },
{0,1,0,1,0,1,0,1,0,0,0,0,0,0,0},
{0,1,0,1,0,1,0,1,0,0,0,0,0,0,0},
{0,1,0,0,0,0,0,1,0,0,0,0,0,0,0},
{0,1,1,1,1,1,1,1,0,0,0,0,0,0,0 },
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },
{0,0,0,0,0,0,0,0,1,1,1,1,1,0,0 },
{0,0,1,1,1,0,1,0,0,0,0,0,1,0,0 },
{0,0,1,0,0,0,1,0,1,1,1,0,1,0,0 },
{0,0,1,0,0,0,1,0,0,0,0,0,1,0,0 },
{1,1,1,0,0,0,1,1,1,1,1,1,1,0,0},
{0,0,0,0,1,0,0,0,0,0,0,0,0,0,0 },
};
const int gridSizeX = 15;
const int gridSizeY = 15;
// Use this for initialization
void Start () {
currentState = ClickGridState.SetStartPos;
roadColor = Color.white;
InitMap();
aStartPath = GetComponent<AStartPathFinding>();
}
// Update is called once per frame
void Update () {
OnMouseClick();
}
public List<Node> GetNeighbours(Node node)
{
List<Node> neighbours = new List<Node>();
//四联通的搜索方式
for(int i = -1;i <= 1; i++)
{
if (i == 0)
{
continue;
}
int checkX = node.gridX + i;
if(checkX >=0 && checkX < gridSizeX)
{
GridPosition pos = new GridPosition
{
x = checkX,
y = node.gridY
};
neighbours.Add(nodeDic[pos]);
}
int checkY = node.gridY + i;
if(checkY >= 0 && checkY < gridSizeY)
{
GridPosition pos = new GridPosition
{
x = node.gridX,
y = checkY
};
neighbours.Add(nodeDic[pos]);
}
}
//八连通的搜索方式
//for (int i = -1; i <= 1; i++)
//{
// for (int j = -1; j <= 1; j++)
// {
// if (i == 0 && j == 0)
// {
// continue;
// }
// int checkX = node.gridX + i;
// int checkY = node.gridY + j;
// if (checkX >= 0 && checkX < gridSizeX
// && checkY >= 0 && checkY < gridSizeY)
// {
// GridPosition nodePos = new GridPosition
// {
// x = checkX,
// y = checkY
// };
// neighbours.Add(nodeDic[nodePos]);
// }
// }
//}
return neighbours;
}
private void OnMouseClick()
{
if (Input.GetMouseButtonDown(0))
{
GameObject node;
switch (currentState)
{
case ClickGridState.SetStartPos:
node = RayCheck();
if (node == null)
return;
startGO = node;
node.GetComponent<MeshRenderer>().material.color = Color.red;
currentState = ClickGridState.SetEndPos;
break;
case ClickGridState.SetEndPos:
node = RayCheck();
if (node == null || node == startGO)
return;
endGO = node;
node.GetComponent<MeshRenderer>().material.color = Color.cyan;
FindPath();
if (path != null)
ColourationPath(Color.green);
currentState = ClickGridState.Cancel;
break;
case ClickGridState.Cancel:
startGO.GetComponent<MeshRenderer>().material.color = roadColor;
endGO.GetComponent<MeshRenderer>().material.color = roadColor;
startGO = null;
endGO = null;
//为路径绘色
ColourationPath(roadColor);
path = null;
currentState = ClickGridState.SetStartPos;
break;
}
}
}
private void ColourationPath(Color color)
{
for(int i = 0;i < path.Count; i++)
{
GridPosition position = new GridPosition
{
x = path[i].gridX,
y = path[i].gridY
};
nodeDic[position].road.GetComponent<MeshRenderer>().material.color = color;
}
}
private void FindPath()
{
GridPosition startPos = WorldPostionToGridPosition(startGO.transform.position);
GridPosition endPos = WorldPostionToGridPosition(endGO.transform.position);
if(startPos.x == -1 || endPos.x == -1)
{
return;
}
Node startNode = nodeDic[startPos];
Node endNode = nodeDic[endPos];
aStartPath.FindPath(startNode, endNode);
}
private GridPosition WorldPostionToGridPosition(Vector3 worldPos)
{
foreach(Node node in nodeDic.Values)
{
if(node.worldPos == worldPos)
{
return new GridPosition
{
x = node.gridX,
y = node.gridY
};
}
}
return new GridPosition
{
x = -1,
y = -1
};
}
private GameObject RayCheck()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit info;
if(Physics.Raycast(ray,out info))
{
if (info.transform.tag.Equals("Road"))
{
return info.transform.gameObject;
}
}
return null;
}
private void InitMap()
{
for (int i = 0; i < gridSizeX; i++)
{
for (int j = 0; j < gridSizeY; j++)
{
Vector3 pos = Vector3.zero + new Vector3(i * cubeSize * 1.1f, 0, j * cubeSize * 1.1f);
GameObject obj = GameObject.Instantiate(cube, pos, Quaternion.identity);
bool walkable = (map[i, j] == 0);
Node node = new Node(i, j, walkable)
{
road = obj,
worldPos = pos
};
if (!walkable)
{
var p = GameObject.Instantiate(cube, pos + new Vector3(0, cubeSize, 0), Quaternion.identity);
p.GetComponent<MeshRenderer>().material.color = Color.blue;
//将代表障碍物的道路清楚开来
p.tag = "Untagged";
obj.tag = "Untagged";
}
GridPosition nodePos = new GridPosition
{
x = i,
y = j
};
nodeDic.Add(nodePos, node);
}
}
}
}
最后是AStar的实现类
public class AStartPath : MonoBehaviour
{
Grid grid;
private void Awake()
{
grid = GetComponent<Grid>();
}
public void FindPath(Node startNode, Node targetNode)
{
//初始化
List<Node> openSet = new List<Node>(); //要去查看的点
HashSet<Node> closeSet = new HashSet<Node>(); //已经走过的路
//将初始节点加入到OpenSet当中
openSet.Add(startNode);
while(openSet.Count > 0)
{
//将第一个点看作是起始点
Node currentNode = openSet[0];
//遍历,查看有没有代价更少的点
// 若有的点代价相同,则选择移动到终点的代价更少的点
for(int i = 1;i < openSet.Count; i++)
{
if(currentNode.fCost > openSet[i].fCost ||
(currentNode.fCost == openSet[i].fCost && openSet[i].hCost < currentNode.hCost))
{
currentNode = openSet[i];
}
}
//将得到的点从openSet移除,加入到closeSet
openSet.Remove(currentNode);
closeSet.Add(currentNode);
//到达的情况
if(targetNode == currentNode)
{
//根据节点的父亲节点还原路径信息
RetracePath(startNode, targetNode);
return;
}
//向当前节点的邻居节点搜索信息
foreach(Node neighbour in grid.GetNeighbours(currentNode))
{
if(!neighbour.walkable || closeSet.Contains(neighbour))
{
continue;
}
//查找到的邻居,若比现在更加接近目标,或者该邻居还没有进入到openSet的检索范围,增加他到OpenSet当中
//并且,计算出他的fCost,以及将他的父节点设置为当前的node
int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour);
if(newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour))
{
neighbour.gCost = newMovementCostToNeighbour;
neighbour.hCost = GetDistance(neighbour, targetNode);
neighbour.parent = currentNode;
if (!openSet.Contains(neighbour))
{
openSet.Add(neighbour);
}
}
}
}
}
private void RetracePath(Node startNode,Node endNode)
{
List<Node> path = new List<Node>();
Node currentNode = endNode;
while(currentNode != startNode)
{
path.Add(currentNode);
currentNode = currentNode.parent;
}
path.Reverse();
grid.path = path;
}
//获取两个节点之间的距离的代价
int GetDistance(Node nodeA,Node nodeB)
{
int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX);
int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY);
//若x大于y则说明向上的路径是走等腰直角三角形的斜边的
//代价的计算,斜边为14,直角边为10(其实斜边精确来说是14.14...)
if(dstX > dstY)
{
return 14 * dstY + 10 * (dstX - dstY);
}
return 14 * dstX + 10 * (dstY - dstX);
}
}