赞
踩
人工智能是近几年的热门专业,国家也在大力发展人工智能这方面的人才
游戏领域从18年再次达到一个高度,有许许多多的独立游戏开发人励志做游戏,做好游戏
人工智能与游戏领域结合可以说是未来的主潮流。
今天要介绍的A星算法是在这两个领域都可以说是很有用的算法。
A星算法是导航寻路系统中的一个算法,在2D游戏涉及很多。比如说:自动寻路、怪物追踪你等。在人工智能寻路中也起了不少作用。
本篇介绍曼哈顿距离、A星算法
它是标明两个点在标准坐标系上的绝对轴距总和
想必大家知道两点之间的最短距离就是连接两个点的线段。如图中而言,就是那条绿色线段,这也是欧式距离。而我们所说的曼哈顿距离是什么呢?
曼哈顿距离就是图中的红线,用白话说就是南北方向上的距离加上在东西方向上的距离,即d(i,j)=|xi-xj|+|yi-yj|。
比如说,我们要计算蓝色线或者黄色线的长度(距离),我们可以用投影到坐标轴上来计算,你无论怎么投影,最后形成的线段就是那条红色的线段。而计算距离就是直接计算曼哈顿距离。
曼哈顿距离在计算机图形学中应用非常广,曼哈顿距离不是距离不变量,当坐标轴变动时,点间的距离就会不同。曼哈顿距离示意图在早期的计算机图形学中,屏幕是由像素构成,是整数,点的坐标也一般是整数,原因是浮点运算很昂贵,很慢而且有误差,如果直接使用AB的欧氏距离(欧几里德距离:在二维和三维空间中的欧氏距离的就是两点之间的距离),则必须要进行浮点运算,如果使用AC和CB,则只要计算加减法即可,这就大大提高了运算速度,而且不管累计运算多少次,都不会有误差。
这里只需要记住曼哈顿距离是:d(i,j)=|xi-xj|+|yi-yj|。
A星算法是用来计算行进路径的,通过它可以计算出避开阻挡的最短路径
A星算法的基本原理就是:不停的找自己周围的点,选出一个新的点作为起点再循环的找。
A星算法是通过f=g+v的公式进行不断计算,最终找到最短路径
下面我们看下f=g+v
A星算法的f=g+v中
f表示的是起点到终点的代价,就是衡量一个路径的长还是短,如果一个路径长那么消耗的代价就很大;如果一个路径短那么消耗的代价就很小。
g表示起点到当前点的代价
我们看一个格子
黄色的代表是起点,而由起点走向旁边的8个点,就是算法要进行的下一步。然而,大家知道直线的距离>斜线的距离的
我们假设直线的距离是10,那么图中的三角形的两条直角边就是10。根据勾股定理我们可以得到斜边的距离是14。
因此我们有,走直线要花费10的代价,走斜线要花费14的代价。这种代价标准就是g
h表示的是当前点到终点的预估代价。这种预估代价就是上面介绍的曼哈度距离。
我们看到这个地图。这里黄色是起点,红色是终点。图中灰色框是障碍。而h的求解是忽略障碍的从当前点到终点(红色)的曼哈顿距离
我们看到图中有一条从起点指向终点的有向线段,现在我们将这条有向线段投影到坐标轴,就是这样的
那么此时,我们把原先的的有向线段,投影到了先向下走一格,再向右走4格。这样加起来就是5格。那么我根据直线走一个是消耗代价是10。走了5格,所以预估代价就是50。也就是说,从起点走向终点的预估代价就是50。
当然我们不是算起点到终点,而是计算起点周围的8格方格分别于终点产生的代价。代价最小的那个方格,正是寻路导航要走的下一步。
接下来我们看下A星算法的详细原理
1、首先需要两个列表:开启列表、关闭列表
首先先将起点加到关闭列表里面,然后以起点为父对象,计算由起点到周围8个格子的f值(代价值)
开启列表存放的是,你计算的每个格子的代价值以及这个格子的父对象是谁(假设起点的父对象是空),注意障碍格不需要计算,也就是说开启列表不用存放障碍格
2、然后将开启列表里面的计算完的代价值,进行升序排序。将最小的代价的格子+标记好的父对象拿出来放到关闭列表里面。
3、再以刚刚拿到的那个点为父对象,继续计算周围8个格子的代价值。注意,在开启列表和关闭列表里面的点不需要重复计算。也就是说,如果这个格子存在于开启列表或者关闭列表或者它是一个障碍格,那么这个格子是不需要计算了。
4、最后我们将关闭列表里面的格子拿出来,从终点顺着父对象以此到起点(起点的父对象是空)。这一个路径就是算法选择的由起点到终点的最短路径。
下面我们举个例子:
首先先将起点加入关闭列表里
然后从起点出发,计算周围的8个格子的f
我们将其周围的格子起个名字
先从a1进行计算,由起点到a1走的是斜线,即g=14。预估代价是计算曼哈顿距离的。从a1到终点沿东西方向走了5格,南北方向走了2格。h=70,因此f(a1)=g+h=84
然后计算a2,一样的由起点到a2走的是直线,即g=10,预估代价是计算曼哈顿距离,数完格子后h(a2)=60,因此f(a2)=10+60=70
以此类推计算其他的点
然后每计算一个点,都要将代价和父结点一起存到开启列表里。
经过这一计算,开启列表变成了这样:
表中的a是起点,即父结点
然后我们将开启列表进行排序,选择代价最小的拿出来放到关闭列表里面,我们能看到a5是最小的,所以将a5拿出来,放到关闭列表里面。
故算法第一步选择了从起点走向a5格子。下面我们以a5格子为父结点计算它周围的8个格子的代价值
对令a5的结点名是b,对b周围的未命名的格子起别名
下面我们以b开始进行计算
先计算b1,由a5到b1走的是斜线,即g=14,经过数方格得到的h=30,因此f(b1)=44
同理,f(b2)=30
而a4,a6是已经在开启列表里面了,所以不需要再计算
因此得到了开启列表是
再次将开启列表进行排序,得到的最小的那个值是b2所以把b2的代价值以及父结点拿出来放到关闭列表里
紧接着看到了一种有意思的现象,如果我们以b2(结点命名c)开始进行计算的话,周围的8个格子,不是已经在开启列表了就是障碍格。根本不需要再计算
我们就直接将开启列表进行排序,麻烦来了。此时最小的44有两个,根据你写的程序逻辑,我们假设选择的是b1。我们把b1(结点名为death)拿出来。
哎,你会发现怎么越走越远了呢,这个算法有问题吧。
实则不是,这是因为障碍的原因,就弄出了这样的结果。程序不知道前面有没有障碍,所以只能一点点试探。这就是说为什么开启列表的结点不删去的原因,留着它进行试探。最后算法选着选着,选到了b3(结点名为d)
好,我们继续追踪,最终的结果一定是如图这样的
现在准备输出一条路径,怎么输出?
沿着关闭列表里的生成的格子吗?
你会看到会有好多和原问题的最短路径牛马不相及的格子啊。比如说,刚才我们提到的b1。所以就不能再沿着关闭列表里的生成的格子了。那怎么处理?
我们应该**沿着终点的父结点以此往前选择,直到起点的父结点是空。**那么算法选择的这一条路径就是最短路径。
这就是说为什么需要父结点的原因。
接下来再思考一个问题,如果终点是个死路怎么办?
按照游戏逻辑应该是停在里终点距离较近能移动的一个点
没错算法也是这样的,什么时候判定
就是将开启列表里面没有格子了,就表示走到了死路。
通过这样的逻辑来编写代码:
public class Point { public int x; //横坐标 public int y; //纵坐标 public Point() { // TODO Auto-generated constructor stub } public Point(int x, int y) { super(); this.x = x; this.y = y; } } public class AStarNode implements Comparable{ enum Road_Type{Walk,Stop} //枚举类型:Walk:表示能走,Stop:表示不能走 public int x; //格子横坐标 public int y; //格子纵坐标 public int f; //寻路消耗代价 public int g; //起点到该点的消耗的代价 public int h; //该点离终点的预估代价 public AStarNode father; //父对象 public Road_Type type; public AStarNode() { // TODO Auto-generated constructor stub } //传入点的坐标和类型 public AStarNode(int x, int y, Road_Type type) { super(); this.x = x; this.y = y; this.type=type; } //按f值进行升序排序 @Override public int compareTo(Object o) { // TODO Auto-generated method stub int f1=((AStarNode)o).f; if(f<f1) return -1; else if(f==f1) return 0; return 1; } } import java.util.Collections; import java.util.LinkedList; import java.util.Random; import other.AStarNode.Road_Type; public class AStarManager { //创建单例模式 public static AStarManager _instance; public AStarManager() { // TODO Auto-generated constructor stub } public static AStarManager getInstance() { if(_instance==null) _instance=new AStarManager(); return _instance; } Random random=new Random(); AStarNode aStarNode[][]; //开启列表 public LinkedList<AStarNode> startList=new LinkedList<AStarNode>(); //关闭列表 public LinkedList<AStarNode> endList=new LinkedList<AStarNode>(); //初始化地图:*表示格子 /表示障碍 O表示起点 X表示终点 .表示最短路径 public void Init(char a[][]) { aStarNode=new AStarNode[a.length][a[0].length]; for(int i=0;i<a.length;i++) for(int j=0;j<a.length;j++) { //随机生成障碍,随机产生0~99,在0~20中生成障碍 int k=random.nextInt(100); if(k>=0&&k<=20) { aStarNode[i][j]=new AStarNode(i, j, Road_Type.Stop); a[i][j]='/'; } else { aStarNode[i][j]=new AStarNode(i, j, Road_Type.Walk); a[i][j]='*'; } } } //寻路方法 public LinkedList<AStarNode> FindPath(char a[][],Point startPoint,Point endPoint){ //起点或者终点是空值 if(startPoint==null||endPoint==null) return null; //传入的点是否在地图范围内 if(startPoint.x<0||startPoint.x>=a.length|| startPoint.y<0||startPoint.y>=a.length|| endPoint.x<0||endPoint.x>=a[0].length|| endPoint.y<0||endPoint.y>=a[0].length ) { System.out.println("起点或者终点超过了范围"); return null; } //传入的点是否在障碍点上 AStarNode start=aStarNode[startPoint.x][startPoint.y]; AStarNode end=aStarNode[endPoint.x][endPoint.y]; if(start.type==Road_Type.Stop||end.type==Road_Type.Stop) { System.out.println("起点或者终点在障碍点上"); return null; } //将起点和终点标记在地图上 a[startPoint.x][startPoint.y]='O'; a[endPoint.x][endPoint.y]='X'; //把开始点放入关闭列表中 endList.add(start); //从起点开始,找周围点 while(start!=end) { //左上:x-1,y-1 PointToStartList(start.x-1, start.y-1, a, 14, start, end); //上:x-1,y PointToStartList(start.x-1, start.y, a, 10, start, end); //右上:x-1,y+1 PointToStartList(start.x-1, start.y+1, a, 14, start, end); //左:x,y-1 PointToStartList(start.x, start.y-1, a, 10, start, end); //右:x,y+1 PointToStartList(start.x, start.y+1, a, 10, start, end); //左下:x+1,y-1 PointToStartList(start.x+1, start.y-1, a, 14, start, end); //下:x+1,y PointToStartList(start.x+1, start.y, a, 10, start, end); //右下:x+1,y+1 PointToStartList(start.x+1, start.y+1, a, 14, start, end); //排序,寻找代价消耗最小的点 Collections.sort(startList); //将找到的点放入关闭列表中,然后让其成为父类,再从开启列表中移除 endList.add(startList.get(0)); start=startList.get(0); startList.remove(0); } //找到路径 LinkedList<AStarNode> tracingList=new LinkedList<AStarNode>(); tracingList.add(end); while(end.father!=null) { tracingList.add(end.father); end=end.father; } return tracingList; } //将点存入开启列表 public void PointToStartList(int x,int y,char a[][],int g,AStarNode father,AStarNode endPoint) { //边界判断 if(x<0||x>=a.length|| y<0||y>=a[0].length ) return; AStarNode node=aStarNode[x][y]; //判断点是否在障碍点上,是否是在开启列表或者关闭列表中 if(node.type==Road_Type.Stop||startList.contains(node)||endList.contains(node)) return; //记录父对象 node.father=father; //否则计算f=g+h node.g=g+father.g; node.h=Math.abs(endPoint.y-node.y)+Math.abs(endPoint.x-node.x); node.f=node.g+node.h; //将所有有效点添加在开启列表中 startList.add(node); } } public static void main(String[] args) { // TODO Auto-generated method stub char a[][]=new char[8][8]; AStarManager manager=AStarManager.getInstance(); System.out.println("初始化地图是:"); //初始化 manager.Init(a); //展示地图 CreateMap(a); Scanner scanner=new Scanner(System.in); System.out.println("请输入起点的横坐标"); int x=scanner.nextInt(); System.out.println("请输入起点的纵坐标"); int y=scanner.nextInt(); System.out.println("请输入终点的横坐标:"); int m=scanner.nextInt(); System.out.println("请输入终点的纵坐标:"); int n=scanner.nextInt(); //创建起点和终点 Point startPoint=new Point(x, y); Point endPoint=new Point(m, n); LinkedList<AStarNode> tracingList = manager.FindPath(a, startPoint, endPoint); System.out.println("经设置起点和终点后的地图是:"); CreateMap(a); if(tracingList!=null) tracingPath(tracingList, a); else System.out.println("因无法产生起点和终点,导致导航失败"); } //地图创建 public static void CreateMap(char a[][]) { for(int i=0;i<a.length;i++) { for(int j=0;j<a[0].length;j++) System.out.print(a[i][j]+" "); System.out.println(); } } //追踪解 public static void tracingPath(LinkedList<AStarNode> tracingList,char a[][]) { for(int i=tracingList.size()-1;i>=0;i--) a[tracingList.get(i).x][tracingList.get(i).y]='.'; System.out.println("导航路径是:"); for(int i=0;i<a.length;i++) { for(int j=0;j<a[0].length;j++) System.out.print(a[i][j]+" "); System.out.println(); } }
A星算法的代价消耗计算公式f=g+h
1、首先需要两个列表:开启列表、关闭列表
首先先将起点加到关闭列表里面,然后以起点为父对象,计算由起点到周围8个格子的f值(代价值)
开启列表存放的是,你计算的每个格子的代价值以及这个格子的父对象是谁(假设起点的父对象是空),注意障碍格不需要计算,也就是说开启列表不用存放障碍格
2、然后将开启列表里面的计算完的代价值,进行升序排序。将最小的代价的格子+标记好的父对象拿出来放到关闭列表里面。
3、再以刚刚拿到的那个点为父对象,继续计算周围8个格子的代价值。注意,在开启列表和关闭列表里面的点不需要重复计算。也就是说,如果这个格子存在于开启列表或者关闭列表或者它是一个障碍格,那么这个格子是不需要计算了。
4、最后我们将关闭列表里面的格子拿出来,从终点顺着父对象以此到起点(起点的父对象是空)。这一个路径就是算法选择的由起点到终点的最短路径。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。