前言:寻路是游戏比较重要的一个组成部分。因为不仅AI还有很多地方(例如RTS游戏里操控人物点到地图某个点,然后人物自动寻路走过去)都需要用到自动寻路的功能。

本文将介绍一个经常被使用且效率理想的寻路方法——A*寻路算法,并且提供额外的优化思路。

图片及信息参考自:https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/

A*算法介绍

寻路,即找到一条从某个起点到某个终点的可通过路径。而因为实际情况中,起点和终点之间的直线方向往往有障碍物,便需要一个搜索的算法来解决。

有一定算法基础的同学可能知道从某个起点到某个终点通常使用深度优先搜索(DFS),DFS搜索的搜索方向一般是8个方向(如果不允许搜索斜向,则有4个),但是并无优先之分。

为了让DFS搜索更加高效,结合贪心思想,我们给搜索方向赋予了优先级,直观上离终点最近的方向(直观上的意思是无视障碍物的情况下)为最优先搜索方向,这就是A*算法。

A*算法步骤解析

(如下图,绿色为起点,红色为终点,蓝色为不可通过的墙。)

从起点开始往四周各个方向搜索。

(这里的搜索方向有8个方向)

为了区分搜索方向的优先级,我们给每个要搜索的点赋予2个值。

G值(耗费值):指从起点走到该点要耗费的值。

H值(预测值):指从该点走到终点的预测的值(从该点到终点无视障碍物情况下预测要耗费的值,也可理解成该点到终点的直线距离的值)

在这里,值 = 要走的距离

(实际上,更复杂的游戏,因为地形不同(例如陷阱,难走的沙地之类的),还会有相应不同的权值:值 = 要走的距离 * 地形权值)

我们还定义直着走一格的距离等于10,斜着走一格的距离等于14(因为45°斜方向的长度= sqrt(10^2+10^2) ≈ 14)

F值(优先级值):F = G + H

这条公式意思:F是从起点经过该点再到达终点的预测总耗费值。通过计算F值,我们可以优先选择F值最小的方向来进行搜索。

(每个点的左上角为F值,左下角为G值,右下角为H值)

计算出每个方向对应点的F,G,H值后,

还需要给这些点赋予当前节点的指针值(用于回溯路径。因为一直搜下去搜到终点后,如果没有前一个点的指针,我们将无从得知要上次经过的是哪个点,只知道走到终点最终耗费的最小值是多少)

然后我们将这些点放入openList(开启列表:用于存放可以搜索的点)

然后再将当前点放入closeList(关闭列表:用于存放已经搜索过的点,避免重复搜索同一个点)

然后再从openList取出一个F值最小(最优先方向)的点,进行上述同样的搜索。

在搜索过程中,如果搜索方向上的点是障碍物或者关闭列表里的点,则跳过之。

通过递归式的搜索,多次搜索后,最终搜到了终点。

搜到终点后,然后通过前一个点的指针值,我们便能从终点一步步回溯通过的路径点。

(红色标记了便是回溯到的点)

A*算法优化思路

openList使用优先队列(二叉堆)

可以看到openlist(开启列表),需要实时添加点,还要每次取出最小值的点。

所以我们可以使用优先队列(二叉堆)来作为openList的容器。

优先队列(二叉堆):插入一个点的复杂度为O(logN),取出一个最值点复杂度为O(logN)

障碍物列表,closeList 使用二维表(二维数组)

由于障碍物列表和closeList仅用来检测是否能通过,所以我们可以使用bool二维表来存放。

//假设已经定义Width和Height分别为地图的长和宽
bool barrierList[Width][Height];
bool closetList[Width][Height];

有某个点(Xa,Yb),可以通过

if(barrierList[Xa][Yb]&&closeList[Xa][Yb])来判断。

因为二维表用下标访问,效率很高,但是耗空间比较多。(三维地图使用三维表则更耗内存。不过现在计算机一般都不缺内存空间,所以尽量提升运算时间为主)

这是一个典型的牺牲内存空间换取运算时间的例子。

深度限制

有时要搜的路径非常长,利用A*算法搜一次付出的代价很高,造成游戏的卡顿。

那么为了保证每次搜索不会超过一定代价,可以设置深度限制,每搜一次则深度+1,搜到一定深度限制还没搜到终点,则返还失败值。

A*算法实现(C++代码)

 #include <iostream>
#include <list>
#include <vector>
#include <queue> struct Point {
int x;
int y;
bool operator == (const Point&otherPoint) {
return x == otherPoint.x && y == otherPoint.y;
}
}; struct OpenPoint : public Point {
int cost; // 耗费值
int pred; // 预测值
OpenPoint* father; // 父节点
OpenPoint() = default;
OpenPoint(const Point & p, const Point& end, int c, OpenPoint* fatherp) :Point(p), cost(c), father(fatherp) {
//相对位移x,y取绝对值
int relativex = std::abs(end.x - p.x);
int relativey = std::abs(end.y - p.y);
//x,y偏移值n
int n = relativex - relativey;
//预测值pred = (max–n)*14+n*10+c
pred = std::max(relativex, relativey) * - std::abs(n) * + c;
}
}; //比较器,用以优先队列的指针类型比较
struct OpenPointPtrCompare {
bool operator()(OpenPoint* a, OpenPoint* b) {
return a->pred > b->pred;
}
}; const int width = ; //地图长度
const int height = ; //地图高度
char mapBuffer[width][height]; //地图数据
int deepth; //记录深度
bool closeAndBarrierList[width][height]; //记录障碍物+关闭点的二维表
//八方的位置
Point direction[] = { {,},{,},{-,},{,-},{,},{ -, },{ -,- },{ ,- } };
//使用最大优先队列
std::priority_queue<OpenPoint*, std::vector<OpenPoint*>, OpenPointPtrCompare> openlist;
//存储OpenPoint的内存空间
std::vector<OpenPoint> pointList = std::vector<OpenPoint>(width*height); //检查函数 返还成功与否值
inline bool inBarrierAndCloseList(const Point & pos) {
if (pos.x < || pos.y < || pos.x >= width || pos.y >= height)
return true;
return closeAndBarrierList[pos.x][pos.y];
} //创建一个开启点
inline OpenPoint* createOpenPoint(const Point & p, const Point& end, int c, OpenPoint* fatherp) {
pointList.emplace_back(p, end, c, fatherp);
return &pointList.back();
} // 开启检查,检查父节点
void open(OpenPoint& pointToOpen, const Point & end) {
//每检查一次,深度+1
deepth++;
//将父节点从openlist移除
openlist.pop();
Point toCreate;
//检查p点八方的点
for (int i = ; i < ; ++i)
{
toCreate = Point{ pointToOpen.x + direction[i].x, pointToOpen.y + direction[i].y };
if (!inBarrierAndCloseList(toCreate)) {
openlist.push(createOpenPoint(toCreate, end, pointToOpen.cost + , &pointToOpen));
closeAndBarrierList[toCreate.x][toCreate.y] = true;
}
}
for (int i = ; i < ; ++i)
{
toCreate = Point{ pointToOpen.x + direction[i].x, pointToOpen.y + direction[i].y };
if (!inBarrierAndCloseList(toCreate)) {
openlist.push(createOpenPoint(toCreate, end, pointToOpen.cost + , &pointToOpen));
closeAndBarrierList[toCreate.x][toCreate.y] = true;
}
}
} //开始搜索路径
std::list<OpenPoint*> findway(const Point& start, const Point& end) {
std::list<OpenPoint*> road;
deepth = ;
// 创建并开启一个父节点
openlist.push(createOpenPoint(start, end, , nullptr));
closeAndBarrierList[start.x][start.y] = false;
OpenPoint* toOpen = nullptr;
// 重复寻找预测和花费之和最小节点开启检查
while (!openlist.empty())
{
toOpen = openlist.top();
// 找到终点后,则停止搜索
if (*toOpen == end) {
break;
}
//若超出一定深度(1000深度),则搜索失败
else if (deepth >= ) {
toOpen = nullptr;
break;
}
open(*toOpen, end);
}
for (auto rs = toOpen; rs != nullptr; rs = rs->father) {
road.push_back(rs);
}
return road;
} //创建地图
void createMap() {
for (int i = ; i < width; ++i)
for (int j = ; j < height; ++j) {
//五分之一概率生成障碍物,不可走
if (rand() % == ) {
mapBuffer[i][j] = '*';
closeAndBarrierList[i][j] = true;
}
else {
mapBuffer[i][j] = ' ';
closeAndBarrierList[i][j] = false;
}
}
} //打印地图
void printMap() {
for (int i = ; i < width; ++i) {
for (int j = ; j < height; ++j)
std::cout << mapBuffer[i][j];
std::cout << std::endl;
}
std::cout << std::endl << std::endl << std::endl;
} int main() {
//起点
Point begin = { , };
//终点
Point end = { , };
//创建地图
createMap();
//打印初始化的地图
printMap();
//保证起点和终点都不是障碍物
mapBuffer[begin.x][begin.y] = mapBuffer[end.x][end.y] = ' ';
closeAndBarrierList[begin.x][begin.y] = closeAndBarrierList[end.x][end.y] = false;
//A*搜索得到一条路径
std::list<OpenPoint*> road = findway(Point{ begin.x,begin.y }, Point{ end.x,end.y });
//将A*搜索的路径经过的点标记为'O'
for (auto& p : road) {
mapBuffer[p->x][p->y] = 'O';
}
//打印走过路后的地图
printMap();
system("pause");
return ;
}

示例效果:

额外

若想了解更多关于寻路的东西,可以去了解下:

游戏AI之路径规划(4)https://www.cnblogs.com/KillerAery/p/10283768.html

游戏AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html

游戏AI之A*寻路算法(3)的更多相关文章

  1. 如何在Cocos2D游戏中实现A*寻路算法(一)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  2. 如何在Cocos2D游戏中实现A*寻路算法(八)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  3. 如何在Cocos2D游戏中实现A*寻路算法(六)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  4. 如何在Cocos2D游戏中实现A*寻路算法(四)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  5. 如何在Cocos2D游戏中实现A*寻路算法(二)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  6. 如何在Cocos2D游戏中实现A*寻路算法(七)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  7. 如何在Cocos2D游戏中实现A*寻路算法(五)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  8. 如何在Cocos2D游戏中实现A*寻路算法(三)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  9. 游戏AI之路径规划(3)

    目录 使用路径点(Way Point)作为节点 洪水填充算法创建路径点 使用导航网(Navigation Mesh)作为节点 区域分割 预计算 路径查询表 路径成本查询表 寻路的改进 平均帧运算 路径 ...

随机推荐

  1. Java面试官:兄弟,你确定double精度比float低吗?

    我有一个朋友,叫老刘,戴着度数比我还高的近视镜,显得格外的"程序员":穿着也非常"不拘一格",上半身是衬衣西服,下半身是牛仔裤运动鞋. 我和老刘的感情非常好,每 ...

  2. C++ STL容器

    不定长数组:vector vector是一个模板类,作用为创建一个不定长的数组 声明方式:vector<int>a或者vector<double>b这种类型的方式. 基本操作: ...

  3. Pull Request 工作流——更高效的管理代码

    目录 Pull Request 工作流--更高效的管理代码 1.问题 2.解决方案 3.Git分支流管理代码具体实施 3.1本地分支操作管理 3.1.1查看分支 3.1.2创建分支 3.1.3切换分支 ...

  4. Hibernate 框架入门

    接着上一篇的 Hibernate 框架的了解,我们就继续学习 Hibernate 框架.这次就进入 Hibernate 框架的入门学习. 首先在学习 Hibernate 框架之前,我们要准备好我们需要 ...

  5. 使用 Apache FOP 2.3 + docbook-xsl-ns-1.79.1 转换 Docbook 5.1 格式的 XML 文档成 PDF/RTF 文件

    使用 Docbook 编写折桂打印平台系统.折桂上传平台系统的产品文档,原因基于如下两点: 第一,文档的不同章节,可使用不同的 .xml 文件,由不同人员分别撰写,图片文件在XML文章中用相对目录方式 ...

  6. pytest系列(一):什么是单元测试界的高富帅?

    pytest是python语言中一款强大的单元测试框架,用来管理和组织测试用例,可应用在单元测试.自动化测试工作中. unittest也是python语言中一款单元测试框架,但是功能有限,没有pyte ...

  7. 给定数轴上的n个点,求距离最近的两个点的距离

    public class MinimumSpacing { //给定平面上的n个点,求距离最近的两个点的距离. //无从下手的话,先分解问题,分解成简单的,逐个分析,然后再合在一起考虑 //这是个2维 ...

  8. nginx(4)

    目录 一.安装配置 1.安装 2.配置文件 3.测试和启动 二.功能 1.虚拟主机 1.1 基于IP 1.2 基于域名 1.3 基于端口 2.访问控制 3.用户认证 4.文件共享 5.文件别名 6.状 ...

  9. day99_12_3numpy的索引以及pandas的两个数据结构。

    一.索引与切片. nump的索引和python中的索引差不多,都是左开右闭区间. 如一个普通的array的索引,是由0开始的: res = np.array([1,2,3,4,5]) #### npa ...

  10. 几行代码轻松实现PHP文件打包下载zip

    <?php //获取文件列表 function list_dir($dir){ $result = array(); if (is_dir($dir)){ $file_dir = scandir ...