找了份新工作之后,忙的要死,都沒時間鼓搗博客了,深深的感受到資本家的剝削,端午節連粽子都沒有,每天下班累得跟條咸魚一樣(可能就是)。
剛好最近忙里偷閑,就來寫寫unity在2D下的AStar尋路算法。
地圖用untiy的tilemap來貼。
大概的效果,沒有去找好看的圖片,將就弄點顏色表示:

黑色表示障礙,綠色表示路徑,開頭和結尾也是用的綠色,好懶o(╥﹏╥)o
原理和詳細解釋,還是參考的這位國外的大神:
https://www.redblobgames.com/pathfinding/a-star/introduction.html
解說如下:
A*算法其實可以理解為是貪心算法和廣度優先搜索算法的結合體。
廣度優先搜索算法,每次都可以找到最短的路徑,每走一步都會記下起點到當前點的步數,優點是絕對能找到最短的路徑,缺點就是地圖越大計算量會變得很巨大。
貪心算法,每次都是走當前點距離終點最近的格子,在沒有障礙的情況下效率很高,但是如果有障礙的話,就很繞路。
A*算法結合兩者,計算當前走過的步數 與 當前點到終點的距離 之和作為走格子的依據,優點就是當有障礙物時,能找到最短距離並且計算量沒有廣度優先搜索大,沒有障礙物時,效率和貪心算法一樣高。
其實代碼量沒多少,直接貼出來了,具體也不解釋,看注釋吧,我好懶。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class MapBehaviour : MonoBehaviour
{
public Vector2Int mapSize;//地圖尺寸
public Tilemap tilemap;
public Tile normalTile;//白色tile
public Tile obstacleTile;//黑色tile
public Tile pathTile;//綠色tile
public int obstacleCount;//要生成的障礙物數量
public Vector3Int startPos;//起點
public Vector3Int endPos;//終點
private bool hasStartPosSet;//是否設置了起點
private bool hasEndPosSet;//是否設置了終點
private Dictionary<Vector3Int, int> search = new Dictionary<Vector3Int, int>();//要進行的查找任務
private Dictionary<Vector3Int, int> cost = new Dictionary<Vector3Int, int>();//起點到當前點的消耗
private Dictionary<Vector3Int, Vector3Int> pathSave = new Dictionary<Vector3Int, Vector3Int>();//保存回溯路徑
private List<Vector3Int> hadSearch = new List<Vector3Int>();//已經查找過的坐標
private List<Vector3Int> obstacle = new List<Vector3Int>();//障礙物坐標
private void Start()
{
CreateNormalTiles();
CreateObstacleTiles();
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
if (!hasStartPosSet)//第一次點擊設置起點
{
startPos = tilemap.WorldToCell(Camera.main.ScreenToWorldPoint(Input.mousePosition));
tilemap.SetTile(startPos, pathTile);
hasStartPosSet = true;
}
else if (!hasEndPosSet)//第二次點擊設置終點
{
endPos = tilemap.WorldToCell(Camera.main.ScreenToWorldPoint(Input.mousePosition));
tilemap.SetTile(endPos, pathTile);
hasEndPosSet = true;
AStarSearchPath();
}
else//重置
{
hasStartPosSet = false;
hasEndPosSet = false;
foreach (var item in pathSave)
{
tilemap.SetTile(item.Key, normalTile);
}
search.Clear();
cost.Clear();
pathSave.Clear();
hadSearch.Clear();
}
}
}
//創建白色地圖
public void CreateNormalTiles()
{
for (int i = 0; i < mapSize.x; i++)
{
for (int j = 0; j < mapSize.y; j++)
{
Vector3Int position = new Vector3Int(i, j, 0);
tilemap.SetTile(position, normalTile);
}
}
}
//創建黑色障礙
public void CreateObstacleTiles()
{
List<Vector3Int> blankTiles = new List<Vector3Int>();
for (int i = 0; i < mapSize.x; i++)
{
for (int j = 0; j < mapSize.y; j++)
{
blankTiles.Add(new Vector3Int(i, j, 0));
}
}
for (int i = 0; i < obstacleCount; i++)
{
int index = Random.Range(0, blankTiles.Count);
Vector3Int obstaclePos = blankTiles[index];
blankTiles.RemoveAt(index);
obstacle.Add(obstaclePos);
tilemap.SetTile(obstaclePos, obstacleTile);
}
}
//AStar算法查找
public void AStarSearchPath()
{
//初始化
search.Add(startPos, GetHeuristic(startPos, endPos));
cost.Add(startPos, 0);
hadSearch.Add(startPos);
pathSave.Add(startPos, startPos);
while (search.Count > 0)
{
Vector3Int current = GetShortestPos();//獲取任務列表里的最少消耗的那個坐標
if (current.Equals(endPos))
break;
List<Vector3Int> neighbors = GetNeighbors(current);//獲取當前坐標的鄰居
foreach (var next in neighbors)
{
if (!hadSearch.Contains(next))
{
cost.Add(next, cost[current] + 1);//計算當前格子的消耗,其實就是上一個格子加1步
search.Add(next, cost[next] + GetHeuristic(next, endPos));//添加要查找的任務,消耗值為當前消耗加上當前點到終點的距離
pathSave.Add(next, current);//保存路徑
hadSearch.Add(next);//添加該點為已經查詢過
}
}
}
if (pathSave.ContainsKey(endPos))
ShowPath();
else
print("No road");
}
//獲取周圍可用的鄰居
private List<Vector3Int> GetNeighbors(Vector3Int target)
{
List<Vector3Int> neighbors = new List<Vector3Int>();
Vector3Int up = target + Vector3Int.up;
Vector3Int right = target + Vector3Int.right;
Vector3Int left = target - Vector3Int.right;
Vector3Int down = target - Vector3Int.up;
//Up
if (up.y < mapSize.y && !obstacle.Contains(up))
{
neighbors.Add(up);
}
//Right
if (right.x < mapSize.x && !obstacle.Contains(right))
{
neighbors.Add(target + Vector3Int.right);
}
//Left
if (left.x >= 0 && !obstacle.Contains(left))
{
neighbors.Add(target - Vector3Int.right);
}
//Down
if (down.y >= 0 && !obstacle.Contains(down))
{
neighbors.Add(target - Vector3Int.up);
}
return neighbors;
}
//獲取當前位置到終點的消耗
private int GetHeuristic(Vector3Int posA, Vector3Int posB)
{
return Mathf.Abs(posA.x - posB.x) + Mathf.Abs(posA.y - posB.y);
}
//獲取任務字典里面最少消耗的坐標
private Vector3Int GetShortestPos()
{
KeyValuePair<Vector3Int, int> shortest = new KeyValuePair<Vector3Int, int>(Vector3Int.zero, int.MaxValue);
foreach (var item in search)
{
if (item.Value < shortest.Value)
{
shortest = item;
}
}
search.Remove(shortest.Key);
return shortest.Key;
}
//顯示查找完成的路徑
private void ShowPath()
{
print(pathSave.Count);
Vector3Int current = endPos;
while (current != startPos)
{
Vector3Int next = pathSave[current];
tilemap.SetTile(current, pathTile);
current = next;
}
}
}
其實沒什么難點,主要是理解,每次取出來計算下一步的點,是字典里面最少消耗值的那個點,然后每個點的消耗值都是,起點到當前的步數與當前到終點的距離(專業名詞叫曼哈頓距離Manhattan distance2333)之和。
完結。
歡迎交流,轉載注明出處!
