文章目录

  • 一、A*算法是什么
  • 二、为什么要在Unity中用A*
  • 三、代码实现
    • 1.创建节点类
    • 2.创建网格类
    • 3.PathFinding核心代码
    • 4.物体运动脚本EnemyPathFinding
  • 四、运行、测试
  • 五、总结

一、A*算法是什么

A星算法是一种搜索策略,是一种启发式图搜索策略。不同于深度优先搜索或广度优先搜索等盲目搜索策略,它能够利用与问题有关的启发信息进行搜索。和迪杰斯特拉算法类似,它们之所以是启发式的,是因为融入了人们既嗤之以鼻又甘之如饴的思想:“贪心”。
   为什么说是“贪心”的呢?是因为每次扩展节点的时候都尽可能的选择路径最短的节点,而Dijkstra算法更看重的是已扩展的节点到起点的路径最短,而A星算法兼顾已扩展节点到起点的路径最短和到终点的路径最短,所以说A星算法应该可以说更高级一点。
   如何记录已扩展节点到起点的路径和已扩展节点到终点的路径呢?A星算法的每个节点通过G(n)和H(n)分别记录到起点的花销和到终点的最佳路径的估计代价,两者的和F(n)便是这个节点的估价函数——估价函数也就是该节点到终点的最小代价的估计值,是我们评判某一个节点优越与否的唯一参考标准。
   A星算法一定就是最好的吗?我们定义H*(n)是某个节点到终点的最优路径的代价,即真实的代价,而非估计值,且有H(n)≤H*(n)恒成立。我觉得某人写的A星算法的程序好与不好,主要看H(n)跟H*(n)是否足够接近,如果H(n)=H*(n)的话,我们就不会扩展任何无关的节点,那么这个算法就绝对是最好的。但我们接下来的算法使用节点到终点的对角距离当作H(n)的,所以是会扩展一些不必要的节点的。但瘦死的骆驼比马大,肯定比宽度优先搜索和深度优先搜索还是要快的。

二、为什么要在Unity中用A*

如果只是用Vector2.MoveTowards,或者只是用transform.position+=方向向量×速度×Time.deltaTime的话,那么这种怪的AI就有点太蠢了,一不会绕开障碍,二只会往主角脸上突。如果一种两种怪的AI是这样那么还可以,如果所有怪的AI都是这种单调乏味的移动方式,那么玩家就会感到疲劳。

这两只怪只能隔着障碍喷我

想象一下,如果有怪物绕开了障碍物跑到你旁边给你来个背刺,是不是游戏难度一下就加大了?各位高级玩家是不是立马就兴奋起来了♂?
   当然,A星算法在游戏中的应用远远不止这些,欢迎大家来补充。

三、代码实现

废话说了很多,还是直接上代码吧。我这个程序呢是参考了一个YouTube博主的视频的,大家如果感兴趣的话可以去看那个博主的视频学习。贴一下网址:
https://www.youtube.com/watch?v=alU04hvz6L4

1.创建节点类

public class Node
{private Grid<Node> grid;public int gridX;public int gridY;public int gCost;public int hCost;public bool isBarrier;public Node cameFromNode;public int FCost { get { return gCost + hCost; } }public Node(Grid<Node> _grid,int x,int y){this.grid = _grid;this.gridX = x;this.gridY = y;isBarrier = false;}public void SetIsBarrier(bool _isBarrier){this.isBarrier = _isBarrier;grid.TriggerGridObjectChanged(gridX, gridY);}
}

解释一下:每个节点有我们之前说的估价函数h(n)节点与起点的实际代价g(n),F(n)直接设一个getter返回它俩的和就好了。
   gridX和gridY是该节点在网格中的位置,我们节点的位置并不是用的World Position,而是一个非负的整型,就像下图这样,左下角的坐标是[0,0],往右x加一,往上y加一,以此类推。

   cameFrom节点很重要,也就是它的父节点,通过这个节点一直向上回溯才能找到我们最终要走的路。
   isBarrier这个布尔值用来记录该节点是不是有障碍物,有障碍物的话直接放到closed表里就不管它了。不是的话再考虑放到open表里扩展。

2.创建网格类

public class Grid<T>
{public event EventHandler<OnGridValueChangedEventArgs> OnGridValueChanged;public class OnGridValueChangedEventArgs:EventArgs{public int x;public int y;}private int width;private int height;private T[,] gridArray;//创建一个二维数组用来存储网格的每一个节点,大小为网格长度乘以网格宽度private float cellSize;private Vector3 originPosition;public Grid(int _width, int _height,float _cellSize,Vector3 _originPosition,Func<Grid<T>,int,int,T> _createGridObject){this.width = _width;this.height = _height;this.cellSize = _cellSize;this.originPosition = _originPosition;gridArray = new T[this.width, this.height];for (int x = 0; x < width; x++){for (int y = 0; y < height; y++){gridArray[x, y] = _createGridObject(this,x,y);Debug.DrawLine(GetWorldPosition(x, y), GetWorldPosition(x, y + 1));Debug.DrawLine(GetWorldPosition(x, y), GetWorldPosition(x + 1, y));}}Debug.DrawLine(GetWorldPosition(width, 0), GetWorldPosition(width, height));Debug.DrawLine(GetWorldPosition(0, height), GetWorldPosition(width, height));}public int GetWidth(){return this.width;}public int GetHeight(){return this.height;}public float GetCellSize(){return this.cellSize;}public T[,] GetGridArray(){return this.gridArray;}public Vector3 GetOriginPosition(){return this.originPosition;}private Vector3 GetWorldPosition(int x,int y){return new Vector3(x, y) * cellSize+originPosition;}public Vector2 GetXY(Vector3 _worldPosition){return new Vector2(_worldPosition.x - originPosition.x / cellSize,_worldPosition.y - originPosition.y /cellSize);}public void SetValue(int x, int y, T value){if(x>=0 && y>=0 && x<width && y<height){gridArray[x, y] = value;OnGridValueChanged?.Invoke(this, new OnGridValueChangedEventArgs { x = x, y = y });}}public void TriggerGridObjectChanged(int x,int y){OnGridValueChanged?.Invoke(this, new OnGridValueChangedEventArgs { x = x, y = y });}public void SetValue(Vector3 _worldPosition, T value){int x, y;x = Mathf.FloorToInt(GetXY(_worldPosition).x);y = Mathf.FloorToInt(GetXY(_worldPosition).y);SetValue(x, y, value);}public T GetValue(int x,int y){if (x >= 0 && y >= 0 && x < width && y < height){return gridArray[x, y];}else{return default;}}public T GetValue(Vector3 _worldPosition){int x, y;x= Mathf.FloorToInt(GetXY(_worldPosition).x);y = Mathf.FloorToInt(GetXY(_worldPosition).y);return GetValue(x, y);}
}

主要就是网格的初始化,World Position和网格坐标的转来转去,以及一些getter和setter。

3.PathFinding核心代码

public class PathFinding
{private const int MOVE_STRAIGHT_COST=10;private const int MOVE_DIAGONAL_COST = 14;//本A*算法使用对角距离private List<Node> openList;private List<Node> closedList;public static PathFinding Instance { get; private set; }public Grid<Node> Grid { get; set; }public Node GetNode(int x, int y){return Grid.GetValue(x, y);}public PathFinding(string sceneName){Instance = this;Vector3 barrierGridPosition = Vector3.zero;switch (sceneName){case "Hell_Mid":Grid = new Grid<Node>(13, 12, 1, new Vector3(-3, -8, 0), (Grid<Node> g, int x, int y) => new Node(g, x, y));break;}}public List<Vector3> FindPath(Vector3 _startWorldPosition,Vector3 _endWorldPosition){Vector2 startPosition=Grid.GetXY(_startWorldPosition);Vector2 endPosition=Grid.GetXY(_endWorldPosition);List<Node> path = FindPath(Mathf.FloorToInt(startPosition.x), Mathf.FloorToInt(startPosition.y), Mathf.FloorToInt(endPosition.x), Mathf.FloorToInt(endPosition.y));if(path==null){return null;}else{List<Vector3> worldPath=new List<Vector3>{ };foreach(Node node in path){worldPath.Add(Grid.GetOriginPosition()+new Vector3(node.gridX, node.gridY) * Grid.GetCellSize() + new Vector3(1, 1, 0) * Grid.GetCellSize() * .5f);}return worldPath;}}public List<Node> FindPath(int _startX,int _startY,int _endX,int _endY){Node startNode = Grid.GetValue(_startX, _startY);//定义起始节点,起始节点将作为Open表中的第一个元素Node endNode = Grid.GetValue(_endX, _endY);openList = new List<Node> { startNode};closedList = new List<Node>();#region//初始化所有节点,让每个节点的gCost设为无穷大,前一节点设为空值for(int x=0;x<Grid.GetWidth();x++){for(int y=0;y<Grid.GetHeight();y++){Node node = Grid.GetValue(x,y);node.gCost = int.MaxValue;node.cameFromNode = null;}}#endregionstartNode.gCost = 0;startNode.hCost = CaculateDistanceCost(startNode, endNode);while(openList.Count>0){SortList();Node currentNode = openList[0];if (currentNode==endNode){openList.Remove(currentNode);closedList.Add(currentNode);return CaculatePath(currentNode);}else{openList.Remove(currentNode);closedList.Add(currentNode);foreach (Node neighbourNode in GetNeighbourList(currentNode)){if (closedList.Contains(neighbourNode)) continue;if(neighbourNode.isBarrier){closedList.Add(neighbourNode);continue;}int tentativeGCost = currentNode.gCost + CaculateDistanceCost(currentNode, neighbourNode);if(tentativeGCost<neighbourNode.gCost){neighbourNode.cameFromNode = currentNode;neighbourNode.gCost = tentativeGCost;neighbourNode.hCost = CaculateDistanceCost(neighbourNode, endNode);if(!openList.Contains(neighbourNode)){openList.Add(neighbourNode);}}}}}return null;}private List<Node> GetNeighbourList(Node _currentNode){List<Node> neighbourList = new List<Node> { };if((_currentNode.gridX-1)>=0){neighbourList.Add(GetNode(_currentNode.gridX - 1, _currentNode.gridY));//左邻居if((_currentNode.gridY-1)>=0){neighbourList.Add(GetNode(_currentNode.gridX - 1, _currentNode.gridY - 1));//左下邻居}if((_currentNode.gridY+1)<Grid.GetHeight()){neighbourList.Add(GetNode(_currentNode.gridX - 1, _currentNode.gridY + 1));//左上邻居}}if((_currentNode.gridX+1)<Grid.GetWidth()){neighbourList.Add(GetNode(_currentNode.gridX + 1, _currentNode.gridY));//右邻居if((_currentNode.gridY-1)>=0){neighbourList.Add(GetNode(_currentNode.gridX + 1, _currentNode.gridY - 1));//右下邻居}if((_currentNode.gridY+1)<Grid.GetHeight()){neighbourList.Add(GetNode(_currentNode.gridX + 1, _currentNode.gridY + 1));//右上邻居}}if((_currentNode.gridY-1)>=0){neighbourList.Add(GetNode(_currentNode.gridX, _currentNode.gridY - 1));//下邻居}if((_currentNode.gridY+1)<Grid.GetHeight()){neighbourList.Add(GetNode(_currentNode.gridX, _currentNode.gridY + 1));//上邻居}return neighbourList;}private List<Node> CaculatePath(Node node){List<Node> path = new List<Node>();path.Add(node);Node currentNode = node;while(currentNode.cameFromNode!=null){path.Add(currentNode.cameFromNode);currentNode = currentNode.cameFromNode;}path.Reverse();return path;}private int CaculateDistanceCost(Node a,Node b){int distanceX = Mathf.Abs(a.gridX - b.gridX);int distanceY = Mathf.Abs(a.gridY - b.gridY);int remaining = Mathf.Abs(distanceX - distanceY);return MOVE_DIAGONAL_COST * Mathf.Min(distanceX, distanceY) + MOVE_STRAIGHT_COST * remaining;}private void SortList(){for(int i=0;i<this.openList.Count-1;i++){int lowestIndex = i;for(int j=this.openList.Count-1;j<i;j--){if(this.openList[j].FCost<this.openList[lowestIndex].FCost){lowestIndex = j;}}Node tempNode = this.openList[lowestIndex];this.openList[lowestIndex] = this.openList[i];this.openList[i] = tempNode;}}
}

真正的PathFinding核心实现代码。首先从头开始看,上面说过我们使用的是对角距离,那么我们就定义两个常数,两节点的对角距离是14(√2的十倍),直线距离是10,先定义,后续会用到。open表和closed表,我觉得这个不用多说。
   构造函数,这个自己随意,我的程序是通过传一个场景名称的参数,来实现不同的场景建立不同的网格。
   先从第二个FindPath方法看起,根据A星算法,我们首先要知道我们的起始节点和目的节点,这四个参数分别是初始节点在网格里的坐标和目的节点在网格里的坐标。先把初始节点不由分说地扔进open表里,下面来初始化网格里的每个节点,注意初始化的是节点而不是网格,网格已经初始化了。
   while循环里的判断条件是open表是否为空,若为空则搜索失败。我们通过选择排序把FCost最小的节点放在open表的首位,即重排open表。首先判断一下open表的第一个节点N是否是目标节点,是的话则搜索成功。否则的话将N移出open表里,扔到closed表里,然后试图去扩展N,即找它的邻居,它的邻居都有谁呢?无非是八个:上面的、下面的、左面的、右面的、左上的、左下的、右上的、右下的。
   遍历邻居,两个之前提到过的判断,判断该节点是否在closed表里,是否是障碍。
   注意注意!下面这个if语句就比较重要了。

if(tentativeGCost<neighbourNode.gCost){neighbourNode.cameFromNode = currentNode;neighbourNode.gCost = tentativeGCost;neighbourNode.hCost = CaculateDistanceCost(neighbourNode, endNode);if(!openList.Contains(neighbourNode)){openList.Add(neighbourNode);}}

如果新扩展的邻居节点有N的先辈节点,那么我们需要判断一下走当前N走的这条路代价小还是之前的代价小,比较一下新先辈节点节点的g(n)和老先辈节点节点的g(n)。也就是说我们的open表里面有可能已经存在这些新扩展的邻居节点了,我们需要更新它们的值,如何更新呢?就是通过选F(n)小的路径走。用个不恰当的比喻,N生的子节点可能是它的祖宗,到底用哪个当N的祖宗呢?F(n)小的当它的祖宗,现在不都是说小祖宗小祖宗嘛,估计就是这么来的:)。

public List<Vector3> FindPath(Vector3 _startWorldPosition,Vector3 _endWorldPosition)

第一个FindPath方法是传入World Position的起点和终点,转换成网格里的起点和终点,调用第二个FindPath方法,返回类型为Vector3的World Position路径,方便我们直接使用。

private List<Node> GetNeighbourList(Node _currentNode)

找邻居的代码我就不再解释了,非常的简单,无非是if套if。

private List<Node> CaculatePath(Node node)

CaculatePath方法,一路找爹,找目标节点的爹,找目标节点的爹的爹……直到找到没有爹的起始节点。

private int CaculateDistanceCost(Node a,Node b)

感觉这个算距离的方法比较有意思,总共有几个对角距离14和几个直线距离10呢?先算出两节点的x坐标的距离和y坐标的距离,具体如下。

int distanceX = Mathf.Abs(a.gridX - b.gridX);
int distanceY = Mathf.Abs(a.gridY - b.gridY);

然后呢?我们看x的距离能和y的距离能抵消几个。假如说x距离是5,y距离是3,那就说是抵消3个;假如说x距离是3,y距离是1,那就说是抵消1个。被抵消的走对角线,剩下的走直线,不难发现,被抵消的总是x和y里的最小值,剩下的就是它们的差了!记得加上绝对值。

4.物体运动脚本EnemyPathFinding

不能跑的代码说得再天花乱坠也没用,让它跑起来吧!

public class EnemyPathFinding : MonoBehaviour
{private int currentPathIndex;private List<Vector3> pathVectorList;private PathFinding pathFinding;private GameObject target;private Grid<Node> grid;[SerializeField]private float moveSpeed;void Awake(){pathVectorList = new List<Vector3> { };pathFinding = new PathFinding(SceneManager.GetActiveScene().name);target = GameObject.FindGameObjectWithTag("Player");}// Start is called before the first frame updatevoid Start(){SetTargetPosition(target.transform.position);}// Update is called once per framevoid FixedUpdate(){HandleMovement();}public void SetTargetPosition(Vector3 targetPosition){currentPathIndex = 0;pathVectorList = PathFinding.Instance.FindPath(transform.position, targetPosition);if(pathVectorList!=null && pathVectorList.Count>1){pathVectorList.RemoveAt(0);}}private void HandleMovement(){if(pathVectorList!=null){Vector3 targetPosition = pathVectorList[currentPathIndex];if (Vector3.Distance(transform.position,targetPosition)>=0.1f){Debug.DrawLine(transform.position, targetPosition);Vector3 moveDir = (targetPosition - transform.position).normalized;float distanceBefore = Vector3.Distance(transform.position, targetPosition);transform.position = transform.position + moveDir * moveSpeed * Time.deltaTime;}else{currentPathIndex++;if(currentPathIndex>=pathVectorList.Count){StopMoving();}}}else{StartCoroutine(FindTarget());}}private void StopMoving(){pathVectorList = null;}IEnumerator FindTarget(){yield return new WaitForSeconds(1.5f);SetTargetPosition(target.transform.position);}
}

这个我不再解释了,还是要根据个人的工程去写代码吧。就是有个小提醒,不要把寻路的代码写到Update里面了,每一帧都寻路,CPU和内存多少有点蚌埠住。我用的是协程,大家可以按自己的喜好来。

四、运行、测试

终于到了我们最爱的测试阶段了,我没有合适的东西能跑,就让一块儿肉去追我们的主角吧!

   看起来这块儿肉绕开了墙壁和障碍!很不错!
   接下来大家就可以尽情地发挥,让这块儿肉灵活走位,然后背刺我们的主角啦!

   还是让它收了神通吧。


五、总结

这里对文章进行总结:
A星算法在游戏中的应用非常广泛,很多游戏都有自己独特的寻路算法,比如说玩MOBA游戏的时候,玩家只需要单击右键,角色就会自动往目标去走,而且路径在地图上也画得清清楚楚;玩吃鸡的时候,总有一些电脑人,它们能穿越千山万水找到你,然后点爆你的一级头。以上只是一个比较基础的算法,通过C#写脚本,在Unity中运行。我关于障碍判断的算法还是比较蠢的,欢迎大家给我指正。
人工智能在游戏中的应用能够大大地增加游戏的趣味性,当一个NPC的行为非常接近真人,或者非常诡谲的时候,我们便会不由自主地沉浸其中想要探索它的奥秘,因此玩家对手的代理的控制逻辑要尽量复杂些才更好。但这些复杂的逻辑不是一蹴而就的,要像搭积木一样一步一步地尝试,才能建造一整幢大楼。

A*算法在Unity中的实现相关推荐

  1. 【项目记录】过程内容生成(PCG)与快速泊松碟采样算法实现Unity中的物件摆放

    背景   在当今游戏开发过程中,创建一个内容丰富的虚拟世界一直是一个十分费时的工作.游戏制作者们希望在更短的时间内在游戏中增加更加多样的内容,过程内容生成技术为有限时间内制作复杂的虚拟世界提供了一种解 ...

  2. 如何在Unity中实现AStar寻路算法及地图编辑器

    文章目录 AStar算法 简介 实现 Node节点 节点间的估价 算法核心 邻节点的搜索方式 地图编辑器 简介 实现 绘制地图网格 障碍/可行走区域 地图数据存储 AStar算法 简介 Unity中提 ...

  3. 在Unity中使用四叉树算法绘制地形

    四叉树算法在游戏中获得了广泛的应用,前几年3D引擎实现的地形绘制大部分都是用四叉树生成的,因为移动端在硬件方面的限制,我们的地形使用的是美术自己制作的地形,对于程序来说省去了不少工作量,但是作为程序开 ...

  4. Unity中的AI算法和实现1-Waypoint

    本文分享Unity中的AI算法和实现1-Waypoint 在Unity中, 我们有一些有趣且常见的AI算法可以研究和使用, 其中最常见的就是怪物的简单AI, 比如在RPG游戏里, 怪物在某些点定点巡逻 ...

  5. 本文分享Unity中的AI算法和实现3-有限状态机FSM(下)

    本文分享Unity中的AI算法和实现3-有限状态机FSM(下) 回家生孩子, 暂停了一个多月的更新, 今天重新续上, ^_^. 在上一篇文章中, 我们分享了状态机的基本理论, 然后结合Unity的An ...

  6. Unity中的AI算法和实现2-有限状态机FSM(上)

    本文分享Unity中的AI算法和实现2-有限状态机FSM(上) 在上一篇文章中, 我们基于Waypoint构建了一个最简单的AI怪物, 这个怪物可以在几个点之间巡逻. 本篇文章会在此基础之上, 添加怪 ...

  7. 使用示例_在Unity中使用ComputeShader示例

    写这篇的缘由是最近老师给了一个UE4的工程,是一个海水模拟的Demo,实现了二十年前一篇paper的算法,paper的地址是: http://citeseerx.ist.psu.edu/viewdoc ...

  8. 浅析Unity中的Enlighten与混合光照

    0x00 前言 在Unity的5.6版本之前的5.x中,主要使用了Geomerics公司的Enlighten[1]来提供实时全局照明以及烘焙全局照明,在5.6之后Unity引入了新的Lightmapp ...

  9. 各Rendering Path技术以及其在Unity中的实现

    Rendering Path其实指的就是渲染场景中光照的方式.由于场景中的光源可能很多,甚至是动态的光源.所以怎么在速度和效果上达到一个最好的结果确实很困难.以当今的显卡发展为契机,人们才衍生出了这么 ...

  10. 【工具】Unity中如何自动化对模型减面?

    本文首发公众号洪流学堂.洪流学堂,让你快人几步.你好,我是你的技术探路者大智. 在Unity中,模型的面数对性能的影响非常大.特别是工业领域,工业模型面数通常非常多,在Unity中直接使用如果能跑起来 ...

最新文章

  1. swift懒加载(lazy load)VS OC懒加载
  2. SAP UI5 应用 filter 功能不能正常工作时的单步调试方法
  3. 视不可当:信息图与可视化传播
  4. 事务对性能影响_DRDS 柔性事务漫谈
  5. ZBlog菜鸟精致灰黑简约风格MiNi主题
  6. MATLAB-S函数
  7. 《推荐系统笔记(一)》线性回归方法的数学推导
  8. 戏说前端 JavaScript 之『防抖节流』基础知识
  9. 单维度量表验证性因子分析_验证性因子分析.ppt
  10. 图像处理中的白化处理
  11. 大话数据结构学习笔记(8)二叉树
  12. ar面部识别_【华为P20Pro评测】系统的进化:面部识别、AR该有的都有_华为 P20 Pro_手机评测-中关村在线...
  13. 自动化测试和测试自动化你分的清楚吗?
  14. A7 ~ A11处理器(iphone5s~iphoneX) 14.0 ~ 14.8.1免越狱安装Trollstore教程
  15. label标签的两次click触发
  16. CVE-2012-0003:Microsoft Windows Media Player winmm.dll MIDI 文件堆溢出漏洞调试分析
  17. 使用SSH做SOCKS代理
  18. .net core依赖注入
  19. shell脚本小工具之万能解压和压缩器---实现思路
  20. Add-appxpackage : Deployment failed with HRESULT: 0x80070005, 无法注册包。

热门文章

  1. nested exception is java.lang.NumberFormatException: For input string: “swagger-ui.html“]
  2. 华为eNSP静态路由下一跳实验
  3. php怎么生成word模板,PHP 使用word模板生成word文档示例
  4. 功能测试几种方法简单介绍
  5. 听刘万祥老师讲“竖向折线图”、“点图”画法
  6. C++ 按值传递的切割问题(Slicing Problem)
  7. Github每日精选(第17期):Django下的内容管理系统wagtail
  8. 计算机逻辑与 或 非的表达式,逻辑表达式
  9. bmvc哪个出版社出版_在出版饮食上
  10. HTTP基础知识你知道吗?