游戏AI研究(三):路径规划
目录
使用路径点(Way Point)作为节点
洪水填充算法创建路径点
使用导航网格(Navigation Mesh)作为节点
预计算
- 路径查询表
- 路径成本查询表
- 扩展障碍碰撞几何体
- 可视点寻径
寻路的改进
- 平均帧运算
- 路径平滑
- 双向搜索
- 路径拼接
- 节点评估
参考
在了解路径规划之前必须先了解基本的寻路算法。
可参考A*寻路算法:A*寻路算法-KillerAery-博客园
使用路径点(Way Point)作为节点
大部分讨论A*算法使用的节点是网格点(也就是简单的二维网格),但是这种内存开销往往比较大。
实际上A*寻路算法,对于图也是适用的,实现只要稍微改一下。
因此我们可以把地图看作一个图而不是一个网格,使用预先设好的路径点而不是网格来作为寻路节点,则可以减少大量节点数量。
使用路径点的好处:
- 减少大量节点数量,顺带也就减少了寻路的运算速度开销。
- 相比网格节点,路径点的路径更加平滑。
洪水填充算法创建路径点
倘若一个地图过大,开发人员手动预设好路径点+路径连接的工作就比较繁琐,而且很容易有错漏。
这时可以使用洪水填充算法来自动生成路径点,并为它们链接。
算法步骤:
1.以任意一点为起始点,往周围八个方向扩展点(不能通行的位置则不扩展)
2.已经扩展的点(在图中被标记成红色)不需要再次扩展,而扩展出来新的点继续扩展
3.直到所有的点都被扩展过,此时能得到一张导航图
- //洪水填充法:从一个点开始自动生成导航图
- void generateWayPoints(int beginx, int beginy, std::vector<WayPoint>& points) {
- //需要探索的点的列表
- std::queue<WayPoint*> pointsToExplore;
- //生成起点,若受阻,不能生成路径点,则退出
- if (!canGeneratePointIn(beginx, beginy))return;
- points.emplace_back(WayPoint(beginx, beginy));
- //扩展距离
- float distance = 2.3f;
- //预先写好8个方向的增值
- int direction[8][2] = { {1,0}, {0,1}, {0,-1}, {-1,0}, {1,1}, {-1,1}, {-1,-1},{1,-1} };
- //以起点开始探索
- WayPoint* begin = &points.back();
- pointsToExplore.emplace(begin);
- //重复探索直到探索点列表为空
- while (!pointsToExplore.empty()) {
- //先取出一个点开始进行探索
- WayPoint* point = pointsToExplore.front();
- pointsToExplore.pop();
- //往8个方向探索
- for (int i = 0; i < 8; ++i) {
- //若当前点的目标方向连着点,则无需往这方向扩展
- if (point->pointInDirection[i] == nullptr) {
- continue;
- }
- auto x = point->x + direction[i][0] * distance;
- auto y = point->y + direction[i][1] * distance;
- //如果目标位置受阻,则无需往这方向扩展
- if (!canGeneratePointIn(x, y)) {
- continue;
- }
- points.emplace_back(WayPoint(x, y));
- auto newPoint = &points.back();
- pointsToExplore.emplace(newPoint);
- //如果当前点能够无障碍通向目标点,则连接当前点和目标点
- if (canWalkTo(point, newPoint)) {
- point.connectToPoint(newPoint);
- }
- }
- }
- }
复制代码
自动生成的导航图可以调整扩展的距离,手游账号交易平台从而得到合适的节点和边的数量。
使用导航网格(Navigation Mesh)作为节点
导航网格将地图划分成若干个凸多边形,每个凸多边形就是一个节点。
使用导航网格更加可以大大减少节点数量,从而减少搜寻所需的计算量,同时也使路径更加自然。
然而该如何建立地图的导航网格,一般有两种方法:
- 手工划分导航网格往往工作量巨大。
- 程序化生成导航网格则实现稍微复杂。
导航网格是目前3D游戏的主流实现,例如《魔兽世界》就是典型使用导航网的游戏,Unity引擎也内置了基于导航网格的寻路系统。
如果你对如何将一个区域划分成多个凸多边形作为导航网格感兴趣,可以参考空间划分的数据结构(网格/四叉树/八叉树/BSP树/k-d树/BVH/自定义划分)-KillerAery-博客园里面的BSP树部分,也许会给你一些启发。
预计算
主要方式是通过预先计算好的数据,然后运行时使用这些数据减少运算量。
可以根据自己的项目权衡运行速度和内存空间来选择预计算。
路径查询表
借助预先计算好的路径查询表,可以以O(|v|)的时间复杂度极快完成寻路,但是占用空间为O(|v|²)。
(|v|为顶点数量)
实现:对每个顶点使用Dijkstra算法,求出该顶点到各顶点的路径,再通过对路径回溯得到前一个经过的点。
路径成本查询表
有时候,游戏AI需要考虑路径的成本来决定行为,
则可以预先计算好路径成本查询表,以O(1)的时间复杂度获取路径成本,但是占用空间为O(|v|²)。
实现:类似路径查询表,只不过记录的是路径成本开销,而不是路径点。
扩展障碍碰撞几何体
在寻路中,一个令游戏AI程序员头疼的问题是碰撞模型往往是一个几何形状而不是一个点。
这意味着在寻路时检测是否碰到障碍,得用几何形状与几何形状相交判断,而非几何形状包含点判断(毋庸置疑前者开销庞大)。
一个解决方案是根据碰撞模型的形状扩展障碍几何体,此时碰撞模型可以简化成一个点,这样可以将问题由几何形状与几何形状相交问题转换成几何形状包含点问题。
这里主要由两种扩展思路:
- 碰撞模型的各个顶点与障碍几何体顶点重合,然后扫过去锚点形成的边界即是扩展的边界(实际上就是让碰撞模型紧挨着障碍几何体走一圈)
- 碰撞模型的锚点与障碍几何体顶点重合,然后扫过去最外围顶点形成的边界即是扩展的边界(实际上就是让碰撞模型沿着原几何体边界走一圈)
这些扩展障碍几何形状的计算完全可以放到预计算(离线计算),不过要注意:
- 各个需要寻路的碰撞模型最好统一形状,这样我们只需要记录一张(或少量)扩展过的障碍图。
- 碰撞模型不可以是圆形,因为这样扩展出的障碍几何体将是圆曲的,很难计算。一个解决方案是用正方形近似替代圆形来生成扩展障碍几何体。
- 当遇到非凸多边形障碍时,在凹处可能会出现扩展出的顶点重复(交点),简单的处理是凹角处不插入新的点。
可视点寻径
寻路的改进
平均帧运算
有时候,大量物体使用A*寻路时,CPU消耗比较大。
我们可以不必一帧运算一次寻路,而是在N帧内运算一次寻路。
(虽然有所缓慢,但是就几帧的东西,一般实际玩家的体验不会有大影响)
所以我们可以通过每帧只搜索一定深度=深度限制/N(N取决于自己定义多少帧内完成一次寻路)。
路径平滑
基于网格的寻路算法结果得到的路径往往是不平滑的。
很容易看出来,寻路算法的路径太过死板,只能上下左右+斜45度方向走。
这里提供两种平滑方式:
- 快速而粗糙的平滑
它检查相邻的边是否可以无障碍通过,若可以则删除中间的点,不可以则继续往下迭代。
它的复杂度是O(n),得到的路径是粗略的平滑,还是稍微有些死板。
- void fastSmooth(std::list<OpenPoint*>& path) {
- //先获取p1,p2,p3,分别代表顺序的第一/二/三个迭代元素。
- auto p1 = path.begin();
- auto p2 = p1; ++p2;
- auto p3 = p2; ++p2;
- while (p3 != path.end()) {
- //若p1能直接走到p3,则移除p2,并将p2,p3往后一位
- // aa-bb-cc-dd-... => aa-cc-dd-...
- // p1 p2 p3 p1 p2 p3
- if (CanWalkBetween(p1, p3)) {
- ++p3;
- p2 = path.erase(p2);
- }
- //若不能走到,则将p1,p2,p3都往后一位。
- // aa-bb-cc-dd-... => aa-bb-cc-dd-...
- // p1 p2 p3 p1 p2 p3
- else {
- ++p1;
- ++p2;
- ++p3;
- }
- }
- }
复制代码
- 精准而慢的平滑
它每次推进一位都要遍历剩下所有的点,看是否能无障碍通过,推进完所有点后则得到精准平滑路径。
它的复杂度是O(n²),得到的路径是精确的平滑。
- void preciseSmooth(std::list<OpenPoint*>& path) {
- auto p1 = path.begin();
- while (p1 != path.end()) {
- auto p3 = p1; ++p3; ++p3;
- while (p3 != path.end()) {
- //若p1能直接走到p3,则移除p1和p3之间的所有点,并将p3往后一位
- if (CanWalkBetween(p1, p3)) {
- auto deleteItr = p1; ++deleteItr;
- p3 = path.erase(deleteItr,p3);
- }
- //否则,p3往后一位
- else {
- ++p3;
- }
- }
- //推进一位
- ++p1;
- }
- }
复制代码
双向搜索
与从开始点向目标点搜索不同的是,你也可以并行地进行两个搜索:
一个从开始点向目标点,另一个从目标点向开始点。当它们相遇时,你将得到一条路径。
双向搜索的思想是:单向搜索过程生成了一棵在地图上散开的大树,而双向搜索则生成了两颗散开的小树。
一棵大树比两棵小树所需搜索的节点更多,所以使用双向搜索性能更好。
不过实验表明,在A*算法往往得到的不会是一棵像BFS算法那样散开的树。
因此无论你的路径有多长,A*算法只尝试搜索地图上小范围的区域,而不进行散开的搜索。
若地图是复杂交错多死路的(例如迷宫,很多往前的路实际上并不通往终点),A*算法便会容易产生散开的树,这时双向搜索会更有用。
路径拼接
游戏世界往往很多动态的障碍,当这些障碍挡在计算好的路径上时,我们常常需要重新计算整个路径。但是这种简单粗暴的重新计算有些耗时,一个解决方法是用路径拼接替代重新计算路径。
首先我们需要设置拼接路径的频率K:
例如每K步检测K步范围内是否有障碍,若有障碍则该K步为阻塞路段。
接着,与重新计算整个路径不同,我们可以重新计算从阻塞路段首位置到阻塞路段尾的路径:
假设p[N]..P[N+K]为当前阻塞的路段。为p[N]到P[N+K]重新计算一条新的路径,并把这条新路径拼接(Splice)到旧路径:把p[N]..p[N+K]用新的路径值代替。
一个潜在的问题是新的路径也许不太理想,下图显示了这种情况(褐色为障碍物):
最初正常计算出的路径为红色路径(1 ->2- ->3 ->4)。
如果我们到达2并且发现从2到达3的路径被封锁了,路径拼接技术会把(2 ->3)用(2 ->5 ->3)取代,结果是寻路体沿着路径(1 ->2 ->5 ->3 ->4)运动。
我们可以看到这条路径不是这么好,因为蓝色路径(1 ->2 ->5 ->4)是另一条更理想的路径。
一个简单的解决方法是,设置一个阈值最大拼接路径长度M:
如果实际拼接的路径长度大于M,算法则使用重新计算路径来代替路径拼接技术。
M不影响CPU时间,而影响了响应时间和路径质量的折衷:
- 如果M太大,物体的移动将不能快速对地图的改变作出反应。
- 如果M太小,拼接的路径可能太短以致于不能正确地绕过障碍物,出现不理想的路径,如(1 ->2 ->5 ->3 ->4)。
路径拼接确实比重计算路径要快,但它可能算出不怎么理想的路径:
- 若经常发现这种情况出现,那么重新计算整条路径也不失为一个解决办法。
- 尝试使用不同的M值和不同的拼接频率K(如每34M34M步)以用于不同的情形。
- 此外应该使用栈来反向保存路径,因为删除和拼接都是在路径尾部进行的。
节点评估
在A*寻路算法里,一个节点的预测函数最直观的莫过于欧几里得距离。
然而对于复杂的游戏世界来说,特别是对于需要复杂决策的AI来说,节点的预测值可不仅仅就距离一个影响因素:
- 地形优势:例如平地节点走得更快而山地节点走得更慢。
- 视野优势:某些地方具有良好的视野(例如高地),AI需要准备战斗时应该倾向占领视野优势点。
- 战术优势:一些地方(例如刷出医疗包的地点,可操控机枪)提供了战术优势,AI应倾向占领这些战术优势地点。
- 其他...
因此,我们可以自定义寻路的预测函数,以调整成为适应复杂游戏世界的AI寻路。
游戏AI研究(三):路径规划相关推荐
- 深度强化学习制作森林冰火人游戏AI(三)向游戏输出键盘控制信息
概述 本文讲如何通过python发送键盘控制命令控制游戏 前篇:深度强化学习制作森林冰火人游戏AI(二)获取游戏屏幕 后篇:深度强化学习制作森林冰火人游戏AI(四)获取窗口部分界面 获取窗口句柄 窗口 ...
- 机器人导航(仿真)(三)——路径规划(更新中)
参考视频:[奥特学园]ROS机器人入门课程<ROS理论与实践>零基础教程_哔哩哔哩_bilibili 参考文档:http://www.autolabor.com.cn/book/ROSTu ...
- 如何写一个游戏AI(三)0-9手写数字图片识别AI训练
接上回书,那么如何写一个入门的简单AI训练(0-9)数字图片试别AI.本文的程序,配合我训练的模型试别准确率只有98.8%,不过也是算是给我开辟了新的知识面. 1.为什么使用卷积神经网络 原因有二:1 ...
- 基于强化学习的智能机器人路径规划算法研究(附代码)
目录 一.摘要 二.路径规划技术的研究进展 1.研究现状 2.算法分类 2.1 全局路径规划算法 2.2 局部路径规划算法 三.本文采用的路径规划算法--强化学习 1. 概念 2. 与其他机器学习方式 ...
- 清华大学团队与腾讯AI Lab专项合作夺冠FPS游戏AI竞赛VizDoom
在荷兰刚刚结束的 IEEE CIG 计算智能与游戏大会上,清华大学张钹院士领导的人工智能创新团队 TSAIL 在第一人称射击类游戏<毁灭战士>(Doom)AI竞赛 VizDoom(Visu ...
- 中国团队首次夺冠FPS游戏AI竞赛VizDoom,清华腾讯AI联手
允中 发自 凹非寺 量子位 报道 | 公众号 QbitAI 中国AI又有新突破. 在荷兰刚刚结束的 IEEE CIG 计算智能与游戏大会上,清华大学张钹院士领导的人工智能创新团队 TSAIL 在第一 ...
- 游戏AI的缘起与进化
作者:微软亚洲研究院 来源:微软研究院AI头条(ID: MSRAsia) 计算机科学家们一直对游戏 AI 乐此不疲,原因并非为了精进棋艺,而是希望在此过程中不断提升人工智能的算法和处理复杂问题的能力. ...
- C#,人工智能,机器人,路径规划,A*(AStar Algorithm)算法、源代码及计算数据可视化
Peter Hart Nils Nilsson Bertram Raphael 参考: C#,人工智能(AI)机器人路径规划(Path Planning)的ARA*(Anytime Replannin ...
- 游戏理论研究四:RPG游戏
游戏理论研究四(转)RPG游戏- - 作者:hitman 4.RPG游戏 RPG游戏 (角色扮演类游戏)无疑是最受欢迎的游戏类型.但很难对其进行确切定义 .本文采取用其性质或者说其构成要素来定 ...
最新文章
- linux下命令行安装anaconda3+pytorch+fastai
- 【计算机网络】数据链路层 : 局域网基本概念 ( 局域网分类 | 拓扑结构 | 局域网特点 | 局域网传输介质 | 介质访问控制方法 | IEEE 802 | 链路层 LLC、MAC 控制子层 )
- 华为防火墙的技术积累
- GIT项目管理工具(part7)--移动或者删除文件
- 【GOF23设计模式】原型模式
- C语言试题九十之实现输入一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数。
- 前端学习(3113):react-hello-类式组件
- C# ASP.NET MVC 之 SignalR 学习 实时数据推送显示 配合 Echarts 推送实时图表
- linux停止rpc服务,Linux系统安装启动rpc服务,解决Loadrunner监控不到资源问题
- Eclipse的一些常用的快捷键
- windows 环境下.Net使用Redis缓存
- 03-树2. List Leaves (25) 二叉树的层序遍历
- 推荐几款珍藏多年的插件,好用到爆,进来瞅瞅有没有
- 二、8【FPGA】Verilog中锁存器(Latch)原理、危害及避免
- Photoshop(PS)2021安装教程【64位】
- HTML5:爱奇艺首页图片轮播效果
- AI今年最大进展就是毫无进展?2019年AutoML、GAN将扛大旗
- swagger首页空白
- 「1.8W字」2020不可多得的 TS 学习指南
- 中国金融出版社出版的2013版《风险管理》