A*寻路算法

广度优先算法

广度优先算法搜索以广度做未优先级进行搜索。

从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点。

这种算法就像洪水(Flood fill)一样向外扩张。直至洪水将整张地图都漫延。在访问节点时候,每个点需要记录到达该点的前一个点的位置(父节点),访问到终点时候,便可以从终点沿着父节点一路走回到起点,从而找出路径。(注意:这也是A*算法的一部分)

这种洪水蔓延式寻找路径的方式较为野蛮粗暴,仅仅依据广度来找路径难以找到最短的路径

Dijkstra算法

在实际寻路场景中,要考虑“移动成本”。不同的路径有不同的成本,例如,穿过平原或沙漠可能需要 1 个移动点,但穿过森林或丘陵可能需要 5 个移动点。玩家在水中行走的成本是在草地上行走的 10 倍。为此我们需要Dijkstra算法。

在Dijkstra算法中,需要计算每一个节点距离起点移动的总移动代价,同时,还需要一个优先队列结构,对于所有待遍历的节点,放入优先队列中,优先队列会按照代价进行排序。(这也是在下面要介绍的A*算法实现案例中待优化的点!)

在算法运行过程中,每次都从优先队列中选出移动成本最小的作为下一个要遍历检查的节点,直到访问到达终点为止。

两种算法的对比:

考虑这样一种场景,在一些情况下,图形中相邻节点之间的移动代价并不相等。例如,游戏中的一幅图,既有平地也有山脉,那么游戏中的角色在平地和山脉中移动的速度通常是不相等的。

在Dijkstra算法中,需要计算每一个节点距离起点的总移动代价。同时,还需要一个优先队列结构。对于所有待遍历的节点,放入优先队列中会按照代价进行排序。

在算法运行的过程中,每次都从优先队列中选出代价最小的作为下一个遍历的节点。直到到达终点为止。

下面对比了不考虑节点移动代价差异的广度优先搜索与考虑移动代价的Dijkstra算法的运算结果:

广度优先算法: Dijkstra算法:

当然,如果不考虑移动代价的因素,在同一个网格图中Dijkstra算法将和广度优先算法一样。

启发式搜索?

通过广度优先搜索和 Dijkstra 算法,边界向各个方向扩展。如果试图找到通往所有位置或许多位置的路径,这是一个合理的选择。

如果仅仅是找到到达一个位置的路径,我们应当控制洪水的流向,时期朝向目标位置流动。而控制方向的条件判断便可以通过启发式搜索来完成。

而所谓启发式搜索函数便是用来计算当前点到达目标点的距离(步数),预测到达目标点的距离。

启发函数来计算到目标点距离方式有多种选择:

  • 曼哈顿距离

    如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:

    曼哈顿距离=abs(node.x-target.x)+abs(node.y-target.y)

  • 对角移动

    如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。

  • 欧几里得距离

    如果图形中允许朝任意方向移动,则可以使用欧几里得距离。

    欧几里得距离2=(node.x-target.x)2+(node.y-target.y)2

采用启发函数控制广度优先搜索的方式为(贪婪)最佳优先搜索。

最佳优先搜索(Best First)

最佳优先搜索的原理也简单,与Dijkstra算法类似(广度优先+移动成本),也使用一个优先队列存储启发函数值(该点与目标点的距离),每次选取与目标点最近的点(邻居)来访问,直至访问到终点。

可以理解算法是广度优先算法+选择最小目标点距离参数的结果。

同样,由于没有考虑移动成本,在移动成本不同的地图中也不能考虑到森林、沼泽等移动速度不同的场景造成的移动成本的增加。不能保证找到的路径是最短的路径。

那就结合在一起呗!

A*算法=广度优先算法+考虑“移动成本”+考虑“与目标点的距离”;

既可以保证聪明地选择好走的路线避开难走的路线或者障碍物,也可以集中管控搜索方向向着目标点中的位置搜寻。

下面是对三个算法的详细对比:

A*算法集合了前两个算法的优势,可以保证在两点之间找到最佳的路线。

A*寻路算法

基本原理:

A*算法使用如下所示的函数来计算每个节点的优先级:

\[f(n)=g(n)+h(n)
\]

f(n)是节点n的综合优先级。

g(n)表示节点n距离起点的代价(距离或步数),即:从起点出发到达当前点所走的步数;

h(n)则表示节点n距离终点(目标点)的预估代价,即:从当前点达到终点需要走的步数,这是A*算法的启发函数,相当于提前预测要走到目标点还需要的步数。由于是预测,按照最短的路径(步数)来表示,所以实际上到达终点的路程要等于或大于预测的数值。

寻路算法的启发函数控制:(理解两个参数对搜寻的影响)

  • 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
  • 如果h(n)始终小于等于节点n到终点的代价,则A*算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。
  • 如果h(n)完全等于节点n到终点的代价,则A*算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。
  • 如果h(n)的值比节点n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。
  • 在另外一个极端情况下,如果h()n相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。

在寻路过程中,每次依据周边节点的f(n)数值来选择,每次选择最小的f(n);

有障碍物的情况下的寻路(黑色方块为障碍物)

代码实现:

//节点类
public class NodeBase{
public NodeBase Connection{get;private set;}//存储自己的相连接路径中的节点
public float G{get;private set;}
public float H{get;private set;}
public float F=>G+H; public void SetConnetion(NodeBase nodeBase)=>Connection=nodeBase;
public void SetG(float g)=>G=g;
public void SetH(float h)=>H=h;
}
//核心算法
public static class Pathfinding {
/// <summary>
/// A*算法核心
/// </summary>
/// <param name="startNode">开始点</param>
/// <param name="targetNode">目标点</param>
/// <returns>最短路径</returns>
/// <exception cref="Exception"></exception>
public static List<NodeBase> FindPath(NodeBase startNode, NodeBase targetNode) {
var toSearch = new List<NodeBase>() { startNode };//要处理搜寻的节点(未访问过)
var processed = new List<NodeBase>();//存储访问过的节点 while (toSearch.Any()) {//判断是否有要搜寻的元素节点
//fixit 待优化的点 可以使用一个堆排序思想
var current = toSearch[0];
foreach (var t in toSearch)
if (t.F < current.F || t.F == current.F && t.H < current.H) current = t; processed.Add(current);
toSearch.Remove(current);//搜寻过后就移除 current.SetColor(ClosedColor); //终点检查
if (current == targetNode) {
var currentPathTile = targetNode;
var path = new List<NodeBase>();//保存路径
var count = 100;
//从终点到出发点的节点路径寻找
while (currentPathTile != startNode) {
path.Add(currentPathTile);
currentPathTile = currentPathTile.Connection;
count--;//这里根据地图的大小设置一个路径长度极限(可以忽略)
if (count < 0) throw new Exception();
Debug.Log("sdfsdf");
}
foreach (var tile in path) tile.SetColor(PathColor);
startNode.SetColor(PathColor);
Debug.Log(path.Count);
return path;//返回路径
}
//关心一下自己的邻居--可以到达的邻居,并且还未访问过的(非障碍物)
foreach (var neighbor in current.Neighbors.Where(t => t.Walkable && !processed.Contains(t))) {
var inSearch = toSearch.Contains(neighbor);//获取对邻居的访问状况
var costToNeighbor = current.G + current.GetDistance(neighbor);
//更新一下邻居的G值
//对于已经访问过的邻居 如果从另一个方向过来想再次访问,那么G值应当比之前访问的要小(只能是最近路程访问)
if (!inSearch || costToNeighbor < neighbor.G) {
neighbor.SetG(costToNeighbor);
neighbor.SetConnection(current);//记录与之连接的节点(算作路径中)
//如果是第一次访问 还需要进行H值的计算
if (!inSearch) {
neighbor.SetH(neighbor.GetDistance(targetNode));//计算启发函数值
toSearch.Add(neighbor);//将邻居添加到要访问的列表中
}
}
}
}
return null;
}
}

参考文章:https://www.redblobgames.com/pathfinding/a-star/introduction.html

参考视频:https://www.youtube.com/watch?v=i0x5fj4PqP4

参考项目:https://github.com/zhm-real/PathPlanning

算法修养--A*寻路算法的更多相关文章

  1. 算法:Astar寻路算法改进,双向A*寻路算法

    早前写了一篇关于A*算法的文章:<算法:Astar寻路算法改进> 最近在写个js的UI框架,顺便实现了一个js版本的A*算法,与之前不同的是,该A*算法是个双向A*. 双向A*有什么好处呢 ...

  2. 算法:Astar寻路算法改进

    早前写了一篇<RCP:gef智能寻路算法(A star)> 出现了一点问题. 在AStar算法中,默认寻路起点和终点都是N x N的方格,但如果用在路由上,就会出现问题. 如果,需要连线的 ...

  3. 一种高效的寻路算法 - B*寻路算法

    在此把这个算法称作B* 寻路算法(Branch Star 分支寻路算法,且与A*对应),本算法适用于游戏中怪物的自动寻路,其效率远远超过A*算法,经过测试,效率是普通A*算法的几十上百倍. 通过引入该 ...

  4. 数据结构和算法总结(三):A* 寻路算法

    前言 复习下寻路相关的东西,而且A star寻路在游戏开发中应用挺多的,故记录下. 正文 迪杰斯特拉算法 说起A*得先谈谈Dijkstra算法,它是在BFS基础上的一种带权值的两点最短寻路贪心算法. ...

  5. 基于Unity的A星寻路算法(绝对简单完整版本)

    前言 在上一篇文章,介绍了网格地图的实现方式,基于该文章,我们来实现一个A星寻路的算法,最终实现的效果为: 项目源码已上传Github:AStarNavigate 在阅读本篇文章,如果你对于里面提到的 ...

  6. A星寻路算法介绍

    你是否在做一款游戏的时候想创造一些怪兽或者游戏主角,让它们移动到特定的位置,避开墙壁和障碍物呢? 如果是的话,请看这篇教程,我们会展示如何使用A星寻路算法来实现它! 在网上已经有很多篇关于A星寻路算法 ...

  7. A*寻路算法探究

    A*寻路算法探究 A*算法常用在游戏的寻路,是一种静态网路中求解最短路径的搜索方法,也是解决很多搜索问题的算法.相对于Dijkstra,BFS这些算法在复杂的搜索更有效率.本文在U3D中进行代码的测试 ...

  8. A*寻路算法

    对于初学者而言,A*寻路已经是个比较复杂的算法了,为了便于理解,本文降低了A*算法的难度,规定只能横竖(四方向)寻路,而无法直接走对角线,使得整个算法更好理解. 简而言之,A*寻路就是计算从起点经过该 ...

  9. js实现A*寻路算法

    这两天在做百度前端技术学院的题目,其中有涉及到寻路相关的,于是就找来相关博客进行阅读. 看了Create Chen写的理解A*寻路算法具体过程之后,我很快就理解A*算法的原理.不得不说作者写的很好,通 ...

  10. 用简单直白的方式讲解A星寻路算法原理

    很多游戏特别是rts,rpg类游戏,都需要用到寻路.寻路算法有深度优先搜索(DFS),广度优先搜索(BFS),A星算法等,而A星算法是一种具备启发性策略的算法,效率是几种算法中最高的,因此也成为游戏中 ...

随机推荐

  1. 使用 Transformers 为多语种语音识别任务微调 Whisper 模型

    本文提供了一个使用 Hugging Face Transformers 在任意多语种语音识别 (ASR) 数据集上微调 Whisper 的分步指南.同时,我们还深入解释了 Whisper 模型.Com ...

  2. 最近很火的开源培训系统,支持免费商用,3个月1000star!

    项目简介 PlayEdu 开源培训系统自发布以来,3个月内帮助上千位开发者部署了私有化培训平台,并在 Github 上获得了1000star. 项目地址 Github 地址:https://githu ...

  3. 二、GCC编译器工作过程

    从更直观的角度来说,编译器是一种工具,将高级语言转化为机器语言.举个例子,我们可以使用编译器将用C++语言编写的程序转换为机器可执行的指令和数据.之前提到过,用机器指令或汇编语言编写程序非常繁琐和乏味 ...

  4. Debian12配置NTP时间同步

    环境 查看系统版本:lsb_release -a 配置NTP时间同步 下面的配置需要用到管理员权限,可以使用su切换到管理员权限. 查看/修正 时区 查看系统时区:timedatectl 如果时区不是 ...

  5. Centos7安装Python3.x

    一.修改yum源 查看Centos发行版本 cat /etc/redhat-release 换阿里云yum源 备份原始yum源 mv /etc/yum.repos.d/CentOS-Base.repo ...

  6. Cilium系列-5-Cilium替换KubeProxy

    系列文章 Cilium 系列文章 前言 将 Kubernetes 的 CNI 从其他组件切换为 Cilium, 已经可以有效地提升网络的性能. 但是通过对 Cilium 不同模式的切换/功能的启用, ...

  7. React: 路由重定向

    解决方案 参考链接 https://v5.reactrouter.com/web/example/route-config

  8. [db2]数据库管理

    前言 db2版本:10.5 实例所有者:db2inst1 待新建数据库:ticm,授权用户:ticm/123456.(用户是系统用户) 创建数据库 建库 # create database ticm: ...

  9. 基于redis6搭建集群

    前言 系统版本:CentOS 7 redis版本:redis6.2.4,官方tar.gz包 两台服务器: 172.50.11.11 端口7002.7004.7006 172.50.12.11 端口70 ...

  10. 分布式存储系统举例剖析(elasticsearch,kafka,redis-cluster)

    1. 概述 对于分布式系统,人们首先对现实中的分布式系统进行高层抽象,然后做出各种假设,发展了诸如CAP, FLP 等理论,提出了很多一致性模型,Paxos 是其中最璀璨的明珠.我们对分布式系统的时序 ...