前言 由于目前正在做的项目需要找到最短路径,而说到最短路径,A星寻路算法 是个最常听到的词,在加上这是面试常问(之前也面试确实问这个了),这里就对A星做个简单总结
A星寻路算法是用来解决什么问题的 我们在玩英雄联盟时,通过鼠标右键点击地图上某个点时,玩家操作的英雄就会以最短路径 ,避开障碍物 ,行进至目标点。而A星寻路正是这个用途:计算玩家或者NPC的行进路径,通过A星寻路算法可以计算出避开障碍物,到达目标点的最短路径
A星寻路的基本原理 A星算法基本逻辑是找到可以靠近目标点方向,一步步记录并行进,直到到达目标点,从而找到路径。A星寻路算法是一个基于网格地图来实现寻路的算法,无论是六边形网格还是四边形网格都可以,但是其实在大多数与A星相关的文章中,更多讨论的是节点 ,其原因就在于,如果我们仅仅将我们的寻路区域划分为四边形网格,显然是无法满足在我们游戏开发中的各种需求,我们可能还会将寻路区域划分成六边形,八边形,甚至是任意不规则的图形。而节点可以放在任意多边形的中心,或者放在多边形的顶点或者边上。所以即使我下面将使用简单的四边形网格讲述A星寻路的基本原理,我们依然用节点作为基准,使得我们讨论起这个系统更为简单。
A星算法的几个基本概念 寻路消耗公式 A星算法通过下面这个公式来计算每个节点的优先级
F(寻路消耗) = G(离起点的消耗) + H(离终点的预计消耗)
F是当前节点的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
G是当前节点距离起点的消耗。
H是当前节点距离终点的预计消耗 (预计的原因:忽略了障碍物)
寻路消耗公式是A星算法的核心
开启列表和关闭列表 开启列表和关闭列表是一个容器,用于存储我们的路径节点
开启列表 开启列表类似一个购物清单,你去到超市可能会买清单上的东西,也可能不买,这个清单只是一个计划。开启列表里面节点可能是最终寻路结果会经过的点,也可能不经过,这是一个待定列表
关闭列表 放入关闭列表中的节点,在寻路过程中加入过入关闭列表中的节点我们就不会再关注,防止重复搜索
节点对象的父对象 在我们的寻路判断结束后,从终点开始,沿着每个节点的父节点至起点(无父节点)回溯 ,这便是寻路的路径
A星算法的步骤
寻路初始状态
进入主循环
在开启列表中找到F值最低的点作为当前节点
获取当前节点周围的八个非关闭节点和非障碍物邻居节点 ,并遍历这些节点
判断每个节点如果在开启列表中
将当前点设置为该邻居节点的父节点
计算该邻居节点的G值
计算该邻居节点的H值
计算该邻居节点的F值(F = G + H)
将当前节点加入到开放列表中
否则
计算邻居节点的G值
如果G值比该邻居节点的G值小
将当前点设置为该邻居节点的父节点
更新该邻居节点的G值(这里不更改H值是因为H与父节点无关)
更新该邻居节点的F值
当终点在开启列表中时跳出循环
主循环结束,从终点回溯父节点,生成寻路的最终路径
A星算法的具体实现 在实现A星算法前,我们需要先实现一个简单的四边形网格节点地图,本文的核心不在网格地图上,所以这里代码就写的不太严谨,仅供参考,包含以下脚本:
Node类:挂载在每个单个节点脚本
Map类:负责生成管理各种Node
网格地图参考代码 Node类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 using UnityEngine; public class Node : MonoBehaviour { public float h = 0 ; public float g = 0 ; public float f = 0 ; public Node parentNode; private bool _isObstacle; private Map _map; public bool IsObstacle { get => _isObstacle; set { ChangeColor(value == false ? Color.white : Color.red); _isObstacle = value ; } } private MeshRenderer _meshRenderer; private void Awake () { _meshRenderer = GetComponent<MeshRenderer>(); _map = GameObject.FindObjectOfType<Map>(); } public void ChangeColor (Color color ) { _meshRenderer.material.color = color; } private void OnMouseDown () { if (_map.selectedOption == 0 &&_map.startNode==null ) { _map.startNode = this ; ChangeColor(Color.blue); } else if (_map.selectedOption == 1 &&_map.endNode==null ) { _map.endNode = this ; ChangeColor(Color.yellow); } else if (_map.selectedOption == 2 ) { IsObstacle = !IsObstacle; } } }
Map类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 using System; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; public class Map : MonoBehaviour { public GameObject nodePrefab; private GameObject[,] _nodes; public GameObject[,] Nodes => _nodes; public Vector2Int mapSize; [Range(0,1) ] public float probability; private Transform _selfTrans; public Node startNode; public Node endNode; private void Awake () { _selfTrans = transform; CreateMap(); } private void Update () { if (Input.GetKeyDown(KeyCode.Space)) { CreateMap(); } } private void CreateMap () { ClearMap(); _nodes = new GameObject[mapSize.x, mapSize.y]; for (int i = 0 ; i < mapSize.x; i++) { for (int j = 0 ; j < mapSize.y; j++) { _nodes[i, j] = Instantiate(nodePrefab, new Vector3(i, 0 , j), Quaternion.identity); _nodes[i, j].transform.SetParent(_selfTrans); float f = Random.Range(0 , 1.0f ); _nodes[i,j].GetComponent<Node>().IsObstacle = f <= probability; } } } private void ClearMap () { if (_nodes == null || _nodes.Length == 0 ) return ; startNode = null ; endNode = null ; for (int i = 0 ; i < mapSize.x; i++) { for (int j = 0 ; j < mapSize.y; j++) { Destroy(_nodes[i, j].gameObject); } } Array.Clear(_nodes, 0 , _nodes.Length); } public int selectedOption = 0 ; private List<string > _options = new List<string >() { "设置起点" ,"设置终点" ,"绘制障碍" }; private void OnGUI () { GUILayout.BeginArea(new Rect(Screen.width -50 , 0 , 50 , 1000 )); GUILayout.BeginVertical(); GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); buttonStyle.fontSize = 10 ; GUIStyle labelStyle = new GUIStyle(GUI.skin.label); labelStyle.fontSize = 10 ; for (int i = 0 ; i < _options.Count; i++) { bool isSelected = i == selectedOption; GUI.enabled = !isSelected; if (GUILayout.Toggle(isSelected, _options[i], buttonStyle)) { selectedOption = i; } GUI.enabled = true ; } GUILayout.EndVertical(); GUILayout.EndArea(); } }
A星寻路算法代码 首先声明两个List表示我们的开启列表和关闭列表
1 2 public List<Node> openList; public List<Node> closeList;
以下是A星的核心算法FindPath
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public void FindPath (Node startNode, Node endNode ) { openList.Clear(); closeList.Clear(); openList.Add(startNode); while (openList.Count > 0 ) { var currentNode = GetMinFNodeInOpenList(); openList.Remove(currentNode); closeList.Add(currentNode); if (currentNode == endNode) { break ; } foreach (var surroundNode in GetSurroundNodes (currentNode )) { if (surroundNode.IsObstacle || closeList.Contains(surroundNode)) continue ; float tempG = CalculateG(startNode ,surroundNode); if (!openList.Contains(surroundNode)) { surroundNode.parentNode = currentNode; surroundNode.g = tempG; surroundNode.h = CalculateH(surroundNode,endNode); surroundNode.f = surroundNode.g + surroundNode.h; openList.Add(surroundNode); } else if (tempG < surroundNode.g) { surroundNode.parentNode = currentNode; surroundNode.g = tempG; surroundNode.f = surroundNode.g + surroundNode.h; } } } GeneratePath(startNode,endNode); }
全代码(核心算法全注释) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Serialization;public class AStar { public List<Node> openList; public List<Node> closeList; public Stack<Node> path; private Map _map; public AStar (Map map ) { openList = new List<Node>(); closeList = new List<Node>(); path = new Stack<Node>(); _map = map; } public void FindPath (Node startNode, Node endNode ) { openList.Clear(); closeList.Clear(); openList.Add(startNode); while (openList.Count > 0 ) { var currentNode = GetMinFNodeInOpenList(); openList.Remove(currentNode); closeList.Add(currentNode); if (currentNode == endNode) { break ; } foreach (var surroundNode in GetSurroundNodes (currentNode )) { if (surroundNode.IsObstacle || closeList.Contains(surroundNode)) continue ; float tempG = CalculateG(startNode, surroundNode); if (!openList.Contains(surroundNode)) { surroundNode.parentNode = currentNode; surroundNode.g = tempG; surroundNode.h = CalculateH(surroundNode, endNode); surroundNode.f = surroundNode.g + surroundNode.h; openList.Add(surroundNode); } else if (tempG < surroundNode.g) { surroundNode.parentNode = currentNode; surroundNode.g = tempG; surroundNode.f = surroundNode.g + surroundNode.h; } } } GeneratePath(startNode, endNode); } private void GeneratePath (Node startNode, Node endNode ) { path.Clear(); Node node = endNode; while (node.parentNode != null ) { path.Push(node); node = node.parentNode; } foreach (var pathNode in path) { pathNode.ChangeColor(Color.green); } foreach (var pathRoad in openList) { pathRoad.ChangeColor(Color.gray); } foreach (var pathRoad in closeList) { if (!path.Contains(pathRoad)) { pathRoad.ChangeColor(Color.gray); } } startNode.ChangeColor(Color.yellow); endNode.ChangeColor(Color.yellow); } private float CalculateG (Node startNode, Node targetNode ) { float tempG = Vector2.Distance(new Vector2(startNode.transform.position.x, startNode.transform.position.z), new Vector2(targetNode.transform.position.x, targetNode.transform.position.z)); float parentG = targetNode.parentNode == null ? 0 : targetNode.parentNode.g; return tempG + parentG; } private float CalculateH (Node targetNode, Node endNode ) { return Mathf.Abs(endNode.transform.position.x - targetNode.transform.position.x) + Mathf.Abs(endNode.transform.position.z - targetNode.transform.position.z); } private List<Node> GetSurroundNodes (Node currentNode ) { var directions = new List<Vector2> { new Vector2(0 , -1 ), new Vector2(-1 , 0 ), new Vector2(1 , 0 ), new Vector2(0 , 1 ), }; List<Node> surroundNodes = new List<Node>(); foreach (Vector2 direction in directions) { var neighborX = currentNode.transform.position.x + direction.x; var neighborY = currentNode.transform.position.z + direction.y; if (neighborX >= 0 && neighborX < _map.mapSize.x && neighborY >= 0 && neighborY < _map.mapSize.y) { Node neighborNode = _map._nodes[(int )neighborX, (int )neighborY].GetComponent<Node>(); surroundNodes.Add(neighborNode); } } return surroundNodes; } private Node GetMinFNodeInOpenList () { Node minNode = openList[0 ]; foreach (Node node in openList) { if (node.f < minNode.f) { minNode = node; } } return minNode; } }