Unity A*算法
Posted 海 月
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity A*算法相关的知识,希望对你有一定的参考价值。
概念
A*算法的思路,简单来说就是:
从每一个已搜索的位置中,找到一个代价最小的位置,再从这个位置继续搜索,直到找到终点。
开放列表:可以理解为待搜索列表
关闭列表:可以理解为已搜索列表
启发函数:用于计算一个点的总代价,以便找到最小代价的点,以启发后续的优先搜索方向,F=G+H
F:一个点的总代价
G:从起点走到该点的代价。如果是四邻域内,就在父节点的G值基础上加上10。如果是八邻域且非四邻域,就在父节点G值基础上加上14(10*1.414)。
H:从该点走到终点的估计距离。本文使用曼哈顿距离,就是该点与终点在水平方向的距离与垂直方向距离的和。
算法
将起点加入待搜索的列表
然后遍历待搜索列表(open)中的全部元素,直到列表空为止
每次遍历从中取一个总代价(F值)最小的点,把它从待搜索列表中移除,再看看这个点是不是终点。如果是,返回这个点。如果不是,则把它加入已搜索列表(close),再遍历它的八邻域。
遍历八邻域时,如果是障碍物或者在边界之外,不再遍历,如果在已搜索列表中,也不再遍历。
如果不在待搜索列表中,则记录其父节点,并计算其G、H、和F值,再把它加入待搜索列表。
如果在待搜索列表中,则比较其G值与新的G值谁更小,如果有更小的G值,则更新父节点,重新计算G值和F值。
效果
可以一步一步执行
也可以一次就执行完毕
如果起点或终点设置在了障碍物上,可以用鼠标左键重新选择。
显示层代码
AstarGUI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using AStar;
using System.Data;
using System.Text;
using System.IO;
using UnityEditor;
using System;
public class AstarGUI : MonoBehaviour
[Header("配置")]
public RunMode runMode = RunMode.ByStep;
public enum RunMode
Directly,
ByStep,
public Vector2 Size;
public Vector2 startPoint;
public Vector2 endPoint;
public GameObject canvas;
public Texture2D tilePic;
public int[,] map; //0 路 1 墙
public GameObject[,] tiles;
public string mapReadPath = "map";
public string mapOutputName = "newmap";
Vector3 originPos = new Vector3(100, 1100, 0);
Vector3 cellScale = new Vector3(0.8f, 0.8f, 0.8f);
public AStar.AStar astar;
[Header("Read Only")]
public int step = 0;
void Start()
Init();
public void Init()
instance = this;
font = Resources.GetBuiltinResource<Font>("Arial.ttf");
public void DisplayDirectly()
astar.FindPath(new AStar.Cell((int)startPoint.x, (int)startPoint.y), new AStar.Cell((int)endPoint.x, (int)endPoint.y));
MarkValue();
MarkKeyCells();
SaveMapToCsv();
public void DisplayByStep(int step)
astar.FindPathByStep(startPoint, endPoint, step);
MarkValue();
MarkKeyCells();
void Update()
//RefreshPos();
if (Input.GetKeyDown(KeyCode.N))
Debug.Log(step);
DisplayByStep(step);
step++;
if (Input.GetKeyDown(KeyCode.O))
SetRandomObstacle();
MarkBasicMap();
if (Input.GetKeyDown(KeyCode.C))
CreateMapByGivenSize();
DrawTheMap();
SetRandomObstacle();
MarkBasicMap();
if (Input.GetMouseButtonDown(0) && setWhich != -1)
SetStartAndEndPoint();
if (Input.GetMouseButtonDown(0) && setWhich == -1)
SetObstacle();
public void DrawTheMap()
//创建画布
if (canvas == null)
canvas = new GameObject("Canvas");
canvas.AddComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
canvas.AddComponent<CanvasScaler>();
canvas.GetComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvas.GetComponent<CanvasScaler>().referenceResolution = new Vector2(2400, 1200);
canvas.GetComponent<CanvasScaler>().screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
canvas.GetComponent<CanvasScaler>().matchWidthOrHeight = 0;
canvas.AddComponent<GraphicRaycaster>();
//清空地图
if (canvas.transform.childCount > 0) ClearChilds(canvas.transform);
//绘制网格地图
for (int x = 0; x < map.GetLength(0); x++)
for (int y = 0; y < map.GetLength(1); y++)
//地图坐标系转换为Unity坐标系
float posX = y;
float posY = -x;
Vector3 pos = new Vector3(posX, posY, 0);
tiles[x, y] = new GameObject(x + " " + y);
tiles[x, y].transform.SetParent(canvas.transform);
tiles[x, y].transform.position = originPos + pos * 100;
tiles[x, y].AddComponent<Image>();
tiles[x, y].GetComponent<Image>().sprite = Sprite.Create(tilePic, new Rect(0, 0, tilePic.width, tilePic.height), new Vector2(0.5f, 0.5f));
tiles[x, y].transform.localScale = cellScale;
if (map[x, y] == 1) MarkPoint(x, y, Color.black);
//使用Selectable接收点击事件
tiles[x, y].AddComponent<Selectable>();
//使用物理碰撞体接收点击事件并不明智
//tiles[x, y].AddComponent<BoxCollider2D>().size = new Vector2(100,100);
//tiles[x, y].AddComponent<GraphicRaycaster>();
public void ErrorCheck()
if (!astar.isReachable(startPoint))
Debug.LogError("起点或终点设置有误,请重新设置");
MarkPoint(startPoint, Color.red);
setWhich = 0;
if (!astar.isReachable(endPoint))
Debug.LogError("起点或终点设置有误,请重新设置");
MarkPoint(endPoint, Color.red);
setWhich = 0;
public void ClearObstacle()
Array.Clear(map, 0, map.Length);
//清除子物体
public void ClearChilds(Transform parent)
if (parent.childCount > 0)
int totalCount = parent.childCount;
for (int i = 0; i < totalCount; i++)
DestroyImmediate(parent.GetChild(0).gameObject);
Font font;
void MarkValue()
for (int x = 0; x < map.GetLength(0); x++)
for (int y = 0; y < map.GetLength(1); y++)
Cell c = astar.Get(x, y);
if (c == null) continue;
if (c.F != null)//c.F != 0
if (tiles[x, y].GetComponentInChildren<Text>() == null)
GameObject obj = new GameObject("Text");
obj.AddComponent<Text>();
obj.GetComponent<Text>().color = Color.black;
obj.GetComponent<Text>().font = font;
obj.transform.SetParent(tiles[x, y].transform);
obj.transform.localPosition = new Vector3(40, -30, 0);
Transform text = tiles[x, y].transform.GetChild(0);
text.GetComponent<Text>().text = c.F + "\\n" + c.G + "\\n" + c.H;
public static AstarGUI instance;
public void MarkPoint(int x, int y, Color color)
tiles[x, y].GetComponent<Image>().color = color;
public void MarkPoint(Cell c, Color color)
tiles[c.x, c.y].GetComponent<Image>().color = color;
public void MarkPoint(Vector2 p, Color color)
tiles[(int)p.x, (int)p.y].GetComponent<Image>().color = color;
public void MarkKeyCells()
//绘制开放列表
foreach (var i in astar.openList)
MarkPoint(i, Color.cyan);
//绘制关闭列表
foreach (var i in astar.closeList)
MarkPoint(i, Color.grey);
//绘制最小F点
MarkPoint(astar.openList.GetCellwithMinF(), Color.red);
//绘制路径
MarkPath();
//绘制起点、终点
MarkPoint(startPoint, Color.yellow);
MarkPoint(endPoint, Color.yellow);
//绘制障碍
public void MarkBasicMap()
//绘制路与障碍
for (int x = 0; x < map.GetLength(0); x++)
for (int y = 0; y < map.GetLength(1); y++)
if (map[x, y] == 1) MarkPoint(x, y, Color.black);
else MarkPoint(x, y, Color.white);
//绘制起点终点
MarkPoint(startPoint, Color.yellow);
MarkPoint(endPoint, Color.yellow);
ErrorCheck();
public void MarkPath()
var parent = astar.openList.GetCellwithMinF();
while (parent != null)
MarkPoint(parent, Color.green);
parent = parent.parent;
/// <summary>
/// 用于调整地图的位置
/// </summary>
void RefreshPos()
for (int x = 0; x < map.GetLength(0); x++)//数组第一维是各行(x)的头指针
for (int y = 0; y < map.GetLength(1); y++)//数组第二维是各行的各个元素(列(y))
float posX = x;
float posY = -y;
Vector3 pos = new Vector3(posX, posY, 0);
tiles[x, y].transform.position = originPos + pos * 100;
public void CreateMapByPraseCSV()
string text = System.IO.File.ReadAllText(Application.streamingAssetsPath + "/"+mapReadPath+".csv");
//TextAsset ta = Resources.Load<TextAsset>(mapReadPath);//不能有后缀名.csv
//按行读取
if (text == null) Debug.LogError("读取失败");
string[] lines = text.Split('\\n');
map = null;
tiles = null;
for (int x = 0; x < lines.Length; x++)
if (string.IsNullOrEmpty(lines[x])) continue;
//移除回车
lines[x] = lines[x].Replace("\\r", "");
//按逗号解析
string[] linePrased = lines[x].Split(',');
//注:地图文档最后一行不能是空行
if(map == null) map = new int[lines.Length, linePrased.Length]; //行,列
if(tiles == null) tiles = new GameObject[lines.Length, linePrased.Length];
for (int y = 0; y < linePrased.Length; y++)
//转换为整形
map[x, y] = int.Parse(linePrased[y]);
//注:表中每行最后一位无逗号
public void CreateMapByGivenSize()
map = new int[(int)Size.x, (int)Size.y];
tiles = new GameObject[(int)Size.x, (int)Size.y];
public void SetRandomObstacle()
for (int x = 0; x < map.GetLength(0); x++)
for (int y = 0; y < map.GetLength(1); y++)
var p = new Vector2(x, y);
if (p == startPoint || p == endPoint) continue;
map[x, y] = UnityEngine.Random.Range(0, 100) % 10 == 0 ? 1 : 0; //障碍块出现率 0.1
public void SaveMapToCsv()
//读取地图数据到字符串
StringBuilder stringBuilder = new StringBuilder();
for (int x = 0; x < map.GetLength(0); x++)
for (int y = 0; y < map.GetLength(1); y++)
if(y != map.GetLength(1)-1)
stringBuilder.Append(map[x, y] + ",");
else
stringBuilder.Append(map[x, y]);
if(x != map.GetLength(0) - 1) stringBuilder.Append("\\r\\n");
//创建目录
if (Directory.Exists(Application.streamingAssetsPath) == false)
Directory.CreateDirectory(Application.streamingAssetsPath);
//写入文件
using (FileStream fileStream = new FileStream(Application.streamingAssetsPath + "\\\\" + mapOutputName + ".csv", FileMode.Create, FileAccess.Write))
using (TextWriter textWriter = new StreamWriter(fileStream, Encoding.UTF8))
textWriter.Write(stringBuilder.ToString());
[HideInInspector]
public int setWhich = 2;
public void SetStartAndEndPoint()
var obj = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject;
Debug.Log(obj.transform.name);
string[] pos = obj.transform.name.Split(' ');
if (pos.Length != 2) return;
if (setWhich >= 2) return;
if(setWhich %2 == 0)
startPoint.x = int.Parse(pos[0]);
startPoint.y = int.Parse(pos[1]);
MarkBasicMap();
else if (setWhich % 2 == 1)
endPoint.x = int.Parse(pos[0]);
endPoint.y = int.Parse(pos[1]);
MarkBasicMap();
setWhich++;
public void SetObstacle()
var obj = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject;
Debug.Log(obj.transform.name);
string[] pos = obj.transform.name.Split(' ');
if (pos.Length != 2) return;
int x = int.Parse(pos[0]);
int y = int.Parse(pos[1]);
if (map[x, y] == 0) map[x, y] = 1;
else if(map[x, y] == 1) map[x, y] = 0;
MarkBasicMap();
//通过特性指定按钮在哪个组件下
[CustomEditor(typeof(AstarGUI))]
[CanEditMultipleObjects]
public class InspectorExtension : Editor
public override void OnInspectorGUI()
//这个是绘制原生的GUI
base.OnInspectorGUI();
GUILayout.Space(10f);
if (GUILayout.Button("创建地图"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.CreateMapByGivenSize();
drawMap.astar = new AStar.AStar(drawMap.map);
drawMap.DrawTheMap();
drawMap.MarkBasicMap();
if (GUILayout.Button("加载地图"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.CreateMapByPraseCSV();
drawMap.astar = new AStar.AStar(drawMap.map);
drawMap.DrawTheMap();
drawMap.MarkBasicMap();
if (GUILayout.Button("保存地图"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.SaveMapToCsv();
if (GUILayout.Button("清空地图"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.ClearChilds(drawMap.canvas.transform);
GUILayout.Space(10f);
if (GUILayout.Button("设置起点、终点"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.setWhich = 0;
if (GUILayout.Button("设置障碍"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.setWhich = -1;
if (GUILayout.Button("随机设置障碍"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.SetRandomObstacle();
drawMap.MarkBasicMap();
if (GUILayout.Button("清除障碍块"))
AstarGUI drawMap = this.target as AstarGUI;
drawMap.ClearObstacle();
drawMap.MarkBasicMap();
GUILayout.Space(10f);
if (GUILayout.Button("执行"))
AstarGUI drawMap = this.target as AstarGUI;
if(drawMap.runMode == AstarGUI.RunMode.ByStep)
Debug.Log(drawMap.step);
drawMap.DisplayByStep(drawMap.step);
drawMap.step++;
else
drawMap.DisplayDirectly();
//数组下标需要统一标准
//地图数据是从0开始存的
//x为横坐标,表示行; y为纵坐标,表示列
计算层代码
AStar.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AStar
public class AStar
public const int obliqueUnit = 14; //斜向代价
public const int unit = 10; //横向代价
public int[,] map get; private set; //迷宫数组
public List<Cell> closeList;
public List<Cell> openList;
public AStar(int[,] map)
this.map = map;
openList = new List<Cell>(this.map.Length);
closeList = new List<Cell>(this.map.Length);
public Cell FindPath(Cell start, Cell end, int step = -1)//可选参数
if (!isReachable(start) || !isReachable(end))
Debug.LogError("起点和终点不能设在障碍块上");
return null;
//Debug.Log(map.GetLength(0)+"## "+map.GetLength(1));
openList.Add(start);
AstarGUI.instance.MarkPoint(start, Color.yellow);
AstarGUI.instance.MarkPoint(end, Color.yellow);
int n = 0;
while (openList.Count != 0)
if (n == step)
//返回F值最小的点,通过parent可以获取到路径
return openList.GetCellwithMinF();
n++;
var pointWithMinF = openList.GetCellwithMinF();
openList.Remove(pointWithMinF);//??
//如果成功找到解:F值最小的点是终点
if (pointWithMinF.Equal(end))
//返回终点
//逆溯路径
//倒序输出
return openList.Get(end);
//不是终点
else
//加入关闭列表,遍历时忽略该点
closeList.Add(pointWithMinF);
AstarGUI.instance.MarkPoint(pointWithMinF, Color.gray);
//将下一步可到达的位置加入openList,并检查记录的最短路径G是否需要更新,记录最短路径经过的上一个点
//维护当前已知的最短G,如果未遍历或需更新
TraverseNeighbour(pointWithMinF,end);
if (openList.Get(end) != null)
return openList.Get(end);
return openList.Get(end);
public Cell FindPathByStep(Vector2 s, Vector2 e, int step)//可选参数
Cell start = new Cell(s);
Cell end = new Cell(e);
if (!isReachable(start) || !isReachable(end))
Debug.LogError("起点和终点不能设在障碍块上");
return null;
//Debug.Log(map.GetLength(0)+"## "+map.GetLength(1));
if(step==0)
openList.Add(start);
AstarGUI.instance.MarkPoint(start, Color.yellow);
AstarGUI.instance.MarkPoint(end, Color.yellow);
if (openList.GetCellwithMinF().Equal(end))
return end;
if (openList.Count == 0)
openList.Get(end);
else
var pointWithMinF = openList.GetCellwithMinF();
openList.Remove(pointWithMinF);//??
//如果成功找到解:F值最小的点是终点
if (pointWithMinF.Equal(end))
return openList.Get(end);
//不是终点
else
//加入关闭列表,遍历时忽略该点
closeList.Add(pointWithMinF);
//将下一步可到达的位置加入openList,并检查记录的最短路径G是否需要更新,记录最短路径经过的上一个点
//维护当前已知的最短G,如果未遍历或需更新
TraverseNeighbour(pointWithMinF, end);
if (openList.Get(end) != null)
return openList.Get(end);
return pointWithMinF;
return null;
public void TraverseNeighbour(Cell cellWithMinF,Cell end)
for (int x = cellWithMinF.x - 1; x <= cellWithMinF.x + 1; x++)
for (int y = cellWithMinF.y - 1; y <= cellWithMinF.y + 1; y++)
//如果是障碍物,不再遍历
if (!isReachable(x,y))
continue;
//如果已在关闭列表中,不再遍历
if (closeList.Contains(x, y))
continue;
var p = openList.Get(x, y) == null ? new Cell(x, y) : openList.Get(x, y);
//如果未遍历
if (!openList.Contains(x,y))
p.parent = cellWithMinF;
p.G = CalG(p);
CalH(end, p);
CalF(p);
openList.Add(p);
//DrawMap.instance.MarkPoint(p.x, p.y, Color.cyan);
//需更新
else
//用G值检查这条路径是否更好
if (p.G > CalNewG(p, cellWithMinF))//这里不能写CalG(p),因为要拿minF计算,而不是父节点
p.parent = cellWithMinF;
p.G = CalG(p);
p.F = CalF(p);
/// <summary>
/// 判断某个格子是否为路
/// </summary>
/// <param name="x">行</param>
/// <param name="y">列</param>
/// <returns></returns>
public bool isReachable(int x,int y)
//超越地图边界
if (x < 0 || y < 0 || x > map.GetLength(0)-1 || y > map.GetLength(1)-1)
return false;
//判断是否是障碍物
return map[x, y] == 0;
public bool isReachable(Cell n)
//超越地图边界
if (n.x < 0 || n.y < 0 || n.x > map.GetLength(0) - 1 || n.y > map.GetLength(1) - 1)
return false;
//判断是否是障碍物
return map[n.x, n.y] == 0;
public bool isReachable(Vector2 n)
//超越地图边界
if (n.x < 0 || n.y < 0 || n.x > map.GetLength(0) - 1 || n.y > map.GetLength(1) - 1)
return false;
//判断是否是障碍物
return map[(int)n.x, (int)n.y] == 0;
/// <summary>
/// 计算s-n的移动代价
/// </summary>
/// <param name="start"></param>
/// <param name="n"></param>
/// <returns></returns>
private int CalG(Cell n)
//起点G值为0
if (n.parent == null)
return 0;
//判断节点(可能是父节点的节点)到当前节点的方向
if (Math.Abs(n.parent.x - n.x) + Math.Abs(n.parent.y - n.y) == 1)//四邻域和为1,八邻域除四邻域外和为2
//直线单位距离+10
return n.parent.G + unit;
else
//斜线单位距离+14
return n.parent.G + obliqueUnit;
private int CalNewG(Cell n, Cell cellWithMinF)
//起点G值为0
if (n.parent == null)
return 0;
//判断节点(可能是父节点的节点)到当前节点的方向
if (Math.Abs(cellWithMinF.x - n.x) + Math.Abs(cellWithMinF.y - n.y) == 1)//四邻域和为1,八邻域除四邻域外和为2
//直线单位距离+10
return cellWithMinF.G + unit;
else
//斜线单位距离+14
return cellWithMinF.G + obliqueUnit;
/// <summary>
/// 计算n-e的预估成本——曼哈顿距离(对于每个点,H是一开始就固定的)
/// </summary>
/// <param name="end"></param>
/// <param name="n"></param>
/// <returns></returns>
private int CalH(Cell end, Cell n)
//忽略障碍物,使用曼哈顿距离
n.H = (Math.Abs(end.x - n.x) + Math.Abs(end.y - n.y)) * unit;
return n.H;
int CalF(Cell n)
n.F = n.G + n.H;
return n.F;
/// <summary>
/// 判断某点是路
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private bool isRoad(int x, int y)
return map[x, y] == 0;
public Cell Get(int x,int y)
return closeList.Get(x, y) == null ? openList.Get(x, y) : closeList.Get(x, y);
//return closeList.Get(x, y);
public class Cell
/// <summary>
/// 行(对应Unity的y轴负方向)
/// </summary>
public int x get; set;
/// <summary>
/// 列(对应Unity的x轴)
/// </summary>
public int y get; set;
public Cell parent get; set;
public int F get; set; //F=G+H
public int G get; set;
public int H get; set;
public Cell(int x, int y)
this.x = x;
this.y = y;
public Cell(Vector2 p)
this.x = (int)p.x;
this.y = (int)p.y;
public void CalF()
this.F = this.G + this.H;
/// <summary>
/// 比较两个点是否相等
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public bool Equal(Cell p)
if (p.x == this.x && p.y == this.y)
return true;
else return false;
//对 List<Point> 的一些扩展方法
public static class ListHelper
/// <summary>
/// 判断某点是否存在
/// </summary>
/// <param name="cells"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public static bool Contains(this List<Cell> cells, Cell cell)
foreach (Cell c in cells)
if ((c.x == cell.x) && (c.y == cell.y))
return true;
return false;
/// <summary>
/// 判断某点是否存在
/// </summary>
/// <param name="cells"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public static bool Contains(this List<Cell> cells, int x, int y)
foreach (Cell c in cells)
if ((c.x == x) && (c.y == y))
return true;
return false;
/// <summary>
/// 返回列表中F值最小的点(排在列表首位)
/// </summary>
/// <param name="cells"></param>
/// <returns></returns>
public static Cell GetCellwithMinF(this List<Cell> cells)
//对points升序排列
cells = cells.OrderBy(c => c.F).ToList();
//返回最小的那个
return cells[0];
public static void Add(this List<Cell> cells, int x, int y)
Cell cell = new Cell(x, y);
cells.Add(cell);
public static Cell Get(this List<Cell> cells, int x, int y)
foreach (Cell c in cells)
if ((c.x == x) && (c.y == y))
return c;
return null;
public static Cell Get(this List<Cell> cells, Cell cell)
foreach (Cell c in cells)
if ((c.x == cell.x) && (c.y == cell.y))
return c;
return null;
/// <summary>
/// 移除指定的点
/// </summary>
/// <param name="cells"></param>
/// <param name="x"></param>
/// <param name="y"></param>
public static void Remove(this List<Cell> cells, int x, int y)
foreach (Cell c in cells)
if (c.x == x && c.y == y)
cells.Remove(c);
以上是关于Unity A*算法的主要内容,如果未能解决你的问题,请参考以下文章