0x00 前言

请叫我标题党!请叫我标题党!请叫我标题党!因为下面的文字既不发生在美国曼哈顿,也不是一个讲述美国梦的故事。相反,这可能只是一篇没有那么枯燥的关于算法的文章。A星算法,这个在游戏寻路开发中难免会用到的算法便是我这篇文章的主角。

0x01 曼哈顿的街道



这是一张美国曼哈顿的俯视图,放眼望去除了能看到这里高楼林立之外,我们也能发现其另外一个特点,即横平竖直的街道将一整块地区整整齐齐的分成了好几个区块。人和车流只能行进在横穿其中的街道上,也只能在街道的交叉口改变自己的前进的方向。例如要找出地图中A点到B点的最佳路线,事实上就是从A点所在的交叉口沿着街道走到B点所在的交叉口,我们无法从区块内部穿越过去,除了沿街道走别无选择。

下面让我们把曼哈顿的这些街道交叉口当做结点,两个交叉口之间的街道当做边,做出一个如下图所示的二维网格。



那么A点到B点的实际距离是多少呢?考虑到我们只能沿着街道行走,而无法从街道围成的区块中穿越,因此在这种情况下A点到B点的实际距离并不是它们之间的直线距离,而是应该如下图所示的这样:



转换成数学语言就是这样:

dis = abs(A.x - B.x) + abs(A.y - B.y)

对了,这就是曼哈顿距离。也就是在A星算法中常常被用来作为启发函数的家伙。等等,启发函数是什么?让我继续。

0x02 醉汉寻“路”

从A点到B点的这条路径,显然包括了以A为起点B为终点的一系列结点,而每个结点也只能从和自己相邻的结点中选择下一个行走目标。但是正如现实生活一样,畅通无阻的街道总是奢求,在路上总会花费一些代价,例如路况不佳,交通拥堵等等原因造成从这条道路行走时会花费更多的时间。因此在寻路中,一条路径的代价等于在每个路口选择的道路的代价之和。

了解了这些之后,就让我们来实现一个最粗暴的寻路方式,仿佛一个醉汉,无视每条道路是否已经走过,也不关心每条道路所花费的时间代价,反正只需要在路口闭着眼睛做出一个选择就好了。

//伪代码
q = newqueue
q.enqueue(newpath(start))
while q is not empty
p = q.dequeue
if p.lastNode == destination
return p
foreach n in p.lastNode.neighbours
q.enqueue(p.continuepath(n))
//找不到合适路径
return null

这样做的后果是什么呢?不错,就像一个醉汉一样,从路口的四个方向中随机选择一个方向,甚至还有可能走回头路(因为没有记录他已经走过的路口),也许最后的确能够找到家,但是这个过程中却不知道消耗了多少时间,走了多少冤枉路。更有甚者,如果实际上并没有一条能够到达目的地的路径,甚至会出现“鬼打墙”的情况,即进入了一个无限的死循环之中无法自拔。

所以,让我们来帮他一下吧,既然醉汉不记得已经走过了哪些路口,那么就让我们来帮他记住他走过的路口。我们为上面的代码引入一个closed集合,用来保存已经走过路口。

//伪代码
//引入一个集合,用来保存已经走过的路口
closed = {}
q = newqueue
q.enqueue(newpath(start))
while q is not empty
p = q.dequeue
//如果下面closed集合中包含了路径p的最后一个路口
//p.last则忽略
if closed contains p.last
continue
//如果路径p的最后一个路口即是目的地,则直接返回p
if p.last == destination
return p
//否则将该点p.last加入到closed集合中
closed.add(p.last)
//把点p.last相邻的点加入到队列中
foreach n in p.last.neighbours
q.enqueue(p.continuepath(n))
//找不到合适的路
return null

这样,我们就帮醉汉解决了走回头路的问题,也消除了“鬼打墙”的隐患。但是,醉汉在选择道路时仍然没有一个明确的目标,这也就决定了他在寻找目的地的效率并不高效。因为他仍然会向四面八方寻路,虽然他在我们的帮助下已经不会走回头路了。显然,为了尽早让醉汉回到家,我们需要为他选择一条最佳的道路。但是,这条最佳的道路到底应该如何选择(预估)呢?

0x03 给我一个指南针

在考虑如何寻找最佳路径之前,我们第一步要做的显然就是为最佳路径定义一个可以量化的标准。到底以什么为标准来评价一条路径呢?最简单的,我们就选择两个路口之间的距离作为标准,这里我们将距离长度称之为路径的开销,且一个路口上下左右相邻的路口的消耗为1,而对角线上的路口消耗则为1.41。

而我们评价一条潜在路径的开销时,所依据的数据主要来自两个方面:

  1. 该路径到目前的路口为止,已经经过的路口的总消耗。这一点我们是已知的,我们将这个消耗的值记为G。
  2. 该路径到目前的路口为止,预估到目的地的消耗。这一点我们是猜测的,我们将这个消耗的值记为H。

而我们所要做的,便是在帮助醉汉不走回头路的基础上,再为醉汉指一个回家的方向。醉汉只要按照这个方向走,便能够很快的找到家。而这个方向又是如何确定的呢?其实十分简单,我们只需找到总消耗最小的路径便可以了。这里我们记总消耗为F,那么显然有如下这样的等式:

F = G + H

那么具体应该如何操作呢?我们需要一个优先队列,记录每条路径的总消耗以及这条路径,并且根据路径的总消耗来对该队列进行排序,这样消耗最小的路径便能轻易地获取了。所以,我们的代码拓展成了下面这个样子:

//伪代码
//引入一个集合,用来保存已经走过的路口
closed = {}
q = newqueue;
//q为优先队列,记录路径的消耗以及路径,起始点消耗为0
q.enqueue(0, newpath(start))
while q is not empty
//优先队列弹出消耗最小的路径
p = q.dequeueCheapest
if closed contains p.last
continue;
if p.last == destination
return p
closed.add(p.last)
foreach n in p.last.neighbours
//获得新的路径
newpath2 = p.continuepath(n)
//将新路径的总消耗(G+H),和新路径分别入队
q.enqueue(newpath.G + estimateCost(n, destination), newpath2) return null

其中,我们可以发现预估到目的地消耗的函数叫“estimateCost”,这便是在A星算法中我们常常提起的启发函数。它的作用便是估算当前位置到目的地的大概距离,而在本文一开始介绍的曼哈顿距离便是一种常用的启发函数。即计算当前路口(格子)到目标路口(格子)之间的垂直和水平的路口(格子)数量总和。

dis = abs(A.x - B.x) + abs(A.y - B.y)

而这个启发函数,便是我们送给醉汉回家的指南针。

当然,借这个醉汉回家的例子说明的仅仅是A星算法最基本的实现原理。而在实际的工程中,它也有更加复杂的使用环境,下面我就简单的介绍几种工程中实现A星寻路的工作方式。

0x04 工程中A星算法的实现方式

我们有了算法的实现思路,接下来便是如何在游戏中实现A星算法了。



要在游戏中进行寻路,首先要做的便是借助图来将游戏地形表示出来,而这个图便是导航图。

而最常见的导航图便是如下三种:

基于单元格的导航图



如上图所示,将游戏地图划分为许多单元格的形式便是我们所说的基于单元格的导航图。这种表示方式的结构十分规则,因此最容易理解和使用,且易于动态更新。因此在需要频繁动态更新场景的游戏中使用这种基于单元格的导航图便十分的恰当。

但是,为了追求寻路的结果更加精确,单元格的大小就成为了关键,过大的单元格显然和精确无缘,但是如果为了追求精确而使用很小的单元格,却又不得不面对另一个问题——需要存储和搜索的结点的数量会十分大。这样不仅需要大量的消耗内存,同时也会影响搜索效率。

基于路点的导航图



如果我们通过人工不规则的放置一些用来导航的点来代替刚刚的单元结点,那么是否会有更好的表现呢?因此,基于可视点,或者被称为路点(The waypoints)的导航图便出现了。如上图所示,红色的结点便是放置的路点,而路点之间的连线是游戏单位可以行走的路径。

这种基于路点的导航图的优势便是可以让场景设计师按照场景的特点来布置路点,由于可以按照设计师的想法来放置,因此基于路点的导航图的一大特点便是灵活性很高,且不像基于单元格的导航图那样,需要存储和搜索大量的结点,因此需要的内存和搜索的效率较前者都要优秀。

但是它的缺点也同样明显,那就是如果场景过大,放置少量的路点显然无法满足需要,但是放置很多路点时,会使得场景设计师的工作变得复杂且容易出错。而由于游戏单位只能在两个路点之间的连线上进行移动,因此如果游戏单位不在结点或结点间连线上的时候,会先到离它最近的路点上,之后再次移动,这样从视觉上看会出现不自然的情况。

导航网格



如图,导航网格将游戏地形划分成了大大小小的三角形,而这些三角形也就成为了A星算法中的节点。相邻的三角形可以直达,换言之,三角形相邻的其他三角形既其相邻的结点。

因此,与前两种导航图相比,由于其“节点”面积大,因此只需要少量的“节点”即可覆盖整个游戏区域,从而减少了“节点”的数量。其次,也正是由于节点全部覆盖了游戏场景,因此不必担心像基于路点的导航图那样由于缺少路点而造成的寻路不精确的问题。

但是,它同样并非十全十美的,相较前两者而言,生成导航网格的时间较长,因此推荐在静态场景中使用,而在地形经常发生变化的场景中减少使用。


趣说游戏AI开发:曼哈顿街角的A*算法的更多相关文章

  1. 趣说游戏AI开发:对状态机的褒扬和批判

    0x00 前言 因为临近年关工作繁忙,已经有一段时间没有更新博客了.到了元旦终于有时间来写点东西,既是积累也是分享.如题目所示,本文要来聊一聊在游戏开发中经常会涉及到的话题--游戏AI.设计游戏AI的 ...

  2. [Lua游戏AI开发指南] 笔记零 - 框架搭建

    一.图书详情 <Lua游戏AI开发指南>,原作名: Learning Game AI Programming with Lua. 豆瓣:https://book.douban.com/su ...

  3. Unity3D游戏UI开发经验谈

    原地址:http://news.9ria.com/2013/0629/27679.html 在Unity专场上,108km创始人梁伟国发表了<Unity3D游戏UI开发经验谈>主题演讲.他 ...

  4. 【Cocos2d-x游戏引擎开发笔记(25)】XML解析

    原创文章,转载请注明出处:http://blog.csdn.net/zhy_cheng/article/details/9128819 XML是一种非常重要的文件格式,由于C++对XML的支持非常完善 ...

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

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

  6. 游戏AI玩伴,是“神队友”还是“猪队友”?

    “一代英豪”暴雪迎来了自己的暴风雪. 2月13日,动视暴雪公布了2018年全年财报.财报显示,暴雪第四季度营业收入仅为28.4亿美元,低于华尔街分析师预期的30.4亿美元.在公布了财报业绩后,该公司又 ...

  7. 王亮:游戏AI探索之旅——从alphago到moba游戏

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由云加社区技术沙龙 发表于云+社区专栏 演讲嘉宾:王亮,腾讯AI高级研究员.2013年加入腾讯,从事大数据预测以及游戏AI研发工作.目前 ...

  8. 深入浅出node.js游戏服务器开发1——基础架构与框架介绍

    2013年04月19日 14:09:37 MJiao 阅读数:4614   深入浅出node.js游戏服务器开发1——基础架构与框架介绍   游戏服务器概述 没开发过游戏的人会觉得游戏服务器是很神秘的 ...

  9. 华为全栈AI技术干货深度解析,解锁企业AI开发“秘籍”

    摘要:针对企业AI开发应用中面临的痛点和难点,为大家带来从实践出发帮助企业构建成熟高效的AI开发流程解决方案. 在数字化转型浪潮席卷全球的今天,AI技术已经成为行业公认的升级重点,正在越来越多的领域为 ...

随机推荐

  1. opencv中Mat与IplImage,CVMat类型之间转换

    opencv中对图像的处理是最基本的操作,一般的图像类型为IplImage类型,但是当我们对图像进行处理的时候,多数都是对像素矩阵进行处理,所以这三个类型之间的转换会对我们的工作带来便利. Mat类型 ...

  2. nodejs进阶(3)—路由处理

    1. url.parse(url)解析 该方法将一个URL字符串转换成对象并返回. url.parse(urlStr, [parseQueryString], [slashesDenoteHost]) ...

  3. 恢复SQL Server被误删除的数据

    恢复SQL Server被误删除的数据 <恢复SQL Server被误删除的数据(再扩展)> 地址:http://www.cnblogs.com/lyhabc/p/4620764.html ...

  4. Linux安装LAMP开发环境及配置文件管理

    Linux主要分为两大系发行版,分别是RedHat和Debian,lamp环境的安装和配置也会有所不同,所以分别以CentOS 7.1和Ubuntu 14.04做为主机(L) Linux下安装软件,最 ...

  5. Apache 与 php的环境搭建

    Apache和PHP的版本分别为: httpd-2.4.9-win64-VC11.zip php-5.6.9-Win32-VC11-x64.zip 下载地址: php-5.6.9-Win32-VC11 ...

  6. Python列表去重

    标题有语病,其实是这样的: 假设有两个列表 : L1 = [1,2,3,4] ; L2 = [1,2,5,6] 然后去掉L1中包含的L2的元素 直接这样当然是不行的: def removeExists ...

  7. 初探Vue

    Vue.js(读音/vju:/,类似于view),是近来比较火的前端框架,但一直没有怎么具体了解.实现过,就知道个啥的MVVM啦,数据驱动啦,等这些关于Vue的虚概念. 由于最近,小生在公司中,负责开 ...

  8. 快递Api接口 & 微信公众号开发流程

    之前的文章,已经分析过快递Api接口可能被使用的需求及场景:今天呢,简单给大家介绍一下微信公众号中怎么来使用快递Api接口,来完成我们的需求和业务场景. 开发语言:Nodejs,其中用到了Neo4j图 ...

  9. Java compiler level does not match解决方法

    从别的地方导入一个项目的时候,经常会遇到eclipse/Myeclipse报Description  Resource Path Location Type Java compiler level d ...

  10. JDBC简介

    jdbc连接数据库的四个对象 DriverManager  驱动类   DriverManager.registerDriver(new com.mysql.jdbc.Driver());不建议使用 ...