当目的地无法通行时,如何使A *更快完成?


31

我正在制作一个简单的基于图块的2D游戏,该游戏使用A *(“ A星”)寻路算法。我一切正常,但是搜索存在性能问题。简而言之,当我单击不可逾越的图块时,该算法显然会遍历整个地图以找到通往不可逾越的图块的路线-即使我站在它旁边。

我该如何规避?我想我可以减少对屏幕区域的寻路,但是也许我在这里遗漏了一些明显的东西?


2
您是否知道哪些图块是不可逾越的,或者只是由于AStar算法而知道?
user000user 2015年

您如何存储导航图?
Anko 2015年

如果将遍历的节点存储在列表中,则可能需要使用二进制堆来提高速度。
ChrisC

如果速度太慢,我可以提出一系列优化建议-还是您要完全避免搜索?
2015年

1
这个问题可能更适合计算机科学
拉斐尔

Answers:


45

有关避免完全导致路径失败的搜索的一些想法:

岛ID

有效完成A *搜索的最便宜的方法之一是完全不进行搜索。如果所有代理商都无法真正进入该区域,请在负载(或管道中)中为每个区域填充唯一的岛屿ID。当寻路检查,如果岛ID的路径来源的匹配岛ID的目的地。如果它们不匹配,则没有点搜索-这两个点位于不同的,未连接的岛上。这仅在所有代理都有真正无法通过的节点时才有用。

上限上限

我限制了可以搜索的最大节点数的上限。这可以帮助无法通行的搜索永远运行,但这确实意味着一些很长的可通过搜索可能会丢失。这个数字需要调整,并不能真正解决问题,但可以减轻长搜索的费用。

如果您发现所花费的时间太长,则可以使用以下技术:

使它异步和限制迭代

让搜索在单独的线程中运行,或者在每个帧中运行一点,这样游戏就不会停止等待搜索。在等待搜索结束时显示人物抓头或踩脚的动画,或适当的动画。为了有效地做到这一点,我将搜索的状态保留为单独的对象,并允许存在多个状态。当请求路径时,获取一个空闲状态对象并将其添加到活动状态对象的队列中。在您的寻路更新中,将活动项从队列的最前面拉出并运行A *,直到A.完成或B.运行了一些迭代限制。如果完成,则将状态对象放回空闲状态对象列表。如果尚未完成,请将其放在“有效搜索”的末尾,然后移至下一个。

选择正确的数据结构

确保使用正确的数据结构。这是我的StateObject的工作方式。出于性能原因,我所有的节点都预先分配了一个有限的数量,例如1024或2048。我使用一个节点池来加快节点的分配速度,它还允许我在数据结构中存储索引而不是指针,这些数据是u16(如果我有255个最大节点,则是u8)(在某些游戏中是这样)。对于我的寻路,我将优先级队列用于打开列表,存储指向Node对象的指针。它实现为二进制堆,并且我将浮点值排序为整数,因为它们始终为正,并且我的平台的浮点比较慢。我为封闭的地图使用哈希表来跟踪访问过的节点。它存储NodeID(而不是Node)以节省缓存大小。

缓存您所能

首次访问节点并计算到目标的距离时,请将其缓存在存储在状态对象中的节点中。如果您重新访问该节点,请使用缓存的结果,而不是再次进行计算。就我而言,这有助于不必在重新访问的节点上做平方根。您可能会发现还有其他值可以预先计算和缓存。

您可以研究的其他领域:使用双向寻路从任一端进行搜索。我没有这样做,但是正如其他人指出的那样,这可能有所帮助,但并非没有警告。我要尝试的另一件事是分层路径查找或群集路径查找。还有就是HavokAI文档中一个有趣的描述这里描述的群集概念,这是不是说明*实现了HPA不同位置

祝您好运,让我们知道您的发现。


如果存在具有不同规则但不太多的不同代理,则仍然可以通过使用ID的向量(每个代理类一个)来相当有效地将其推广。
MSalters

4
+1表示认识到问题很可能是障碍区域(不仅是不可逾越的瓷砖),而且可以通过早期加载时间计算更轻松地解决此类问题。
Slipp D. Thompson 2015年

对每个区域进行洪水填充或BFS。
wolfdawn

岛ID不必是静态的。如果需要能够连接两个单独的岛,但是有一个简单的算法将是合适的,但是之后它无法拆分一个岛。这些幻灯片中的第8页到第20页解释了该特定算法:cs.columbia.edu/~bert/courses/3137/Lecture22.pdf
kasperd,2015年

@kasperd当然没有什么可以阻止重新计算孤岛ID,并在运行时合并它们。关键是,孤岛ID允许您快速确认两个节点之间是否存在路径,而无需进行星形搜索。
史蒂文

26

AStar是一个完整的计划算法,这意味着如果存在到该节点的路径,则AStar可以保证找到它。因此,它必须先检查出起始节点之外的每个路径,然后才能确定目标节点不可达。当节点过多时,这是非常不希望的。

减轻这种情况的方法:

  • 如果您先验地知道某个节点不可访问(例如,它没有邻居或标记为UnPassable),则No Path无需调用AStar即可返回。

  • 限制终止前AStar将扩展的节点数。检查打开的设置。如果太大,终止并返回No Path。但是,这将限制AStar的完整性。因此它只能规划最大长度的路径。

  • 限制AStar查找路径的时间。如果时间用完,请退出并返回No Path。与以前的策略一样,这会限制完整性,但是会随着计算机的速度扩展。请注意,对于许多游戏来说,这是不希望的,因为计算机速度更快或速度较慢的玩家对游戏的体验会有所不同。


13
我想指出的是,根据CPU速度更改游戏的机制(是的,寻路是一种游戏机制)可能会是一个坏主意,因为这会使游戏变得难以预测,甚至在某些情况下甚至无法玩在距现在10年的计算机上。因此,我宁愿建议通过限制开放集而不是CPU时间来限制A *。
Philipp

@菲利普 修改了答案以反映这一点。
mklingen'1

1
请注意,对于给定的图,您可以确定(合理有效,O(节点)个)两个节点之间的最大距离。这是最长的路径问题,它为您提供了要检查的节点数的正确上限。
MSalters 2015年

2
@MSalters您如何在O(n)中执行此操作?什么是“合理有效”?如果这仅适用于成对的节点,那么您不只是在重复工作吗?
2015年

根据维基百科,不幸的是,最长的路径问题是NP困难的。
Desty

21
  1. 在同一循环中同时从相反方向同时从目标节点运行双重A *搜索,并发现一个不可解决的问题后立即中止这两个搜索

如果目标周围只有6个图块可访问,而原点有1002个图块可访问,则搜索将在6(双)次迭代时停止。

一旦一个搜索找到了另一个被访问的节点,您也可以将搜索范围限制到另一个被访问的节点,并更快地完成。


2
实施双向A星级搜索比您的陈述所暗示的要多得多,包括验证在这种情况下启发式方法仍然可以接受。(链接:homepages.dcc.ufmg.br/~chaimo/public/ENIA11.pdf
Pieter Geerkens 2015年

4
@StephaneHockenhull:在具有不对称成本的地形图上实现了双向A- *后,我向您保证,忽略学术性等等会导致错误的路径选择和错误的成本计算。
Pieter Geerkens

1
@MooingDuck:节点总数未更改,每个节点仍将仅被访问一次,因此,将地图一分为二的最坏情况与单向A- *相同。
Pieter Geerkens

1
@PieterGeerkens:在经典的A *中,只有一半节点是可访问的,因此可以访问。如果地图精确地分成两半,则在双向搜索时,您将触摸(几乎)每个节点。不过绝对是个
极端

1
@MooingDuck:我说错了;最坏的情况是不同的图,但是具有相同的行为-单向的最坏情况是完全隔离的目标节点,要求访问所有节点。
Pieter Geerkens

12

假设问题是目的地不可达。而且导航网格不是动态的。最简单的方法是导航图稀疏(足够稀疏,以至于相对较快的完整运行),并且仅在可能的情况下才使用详细图。


6
很好 通过将图块分组为“区域”,并首先检查图块所在的区域是否可以连接到另一个图块所在的区域,您可以更快地丢弃负片。
Konerak

2
正确-通常属于HPA *
史蒂文

@Steven谢谢,我确定我不是第一个想到这种方法的人,但不知道它叫什么。使利用现有研究变得更加容易。
ClassicThunder

3

使用具有不同特征的多种算法

A *具有一些优良的特性。特别是,它总是找到最短的路径(如果存在)。不幸的是,您还发现了一些不良特性。在这种情况下,它必须在不存在任何解决方案之前穷举搜索所有可能的路径。

您在A *中发现的“缺陷”是它不了解拓扑。您可能有一个2D世界,但并不知道。就其所知,在您的世界的最远角落是一个楼梯,将它带到世界的尽头。

考虑创建第二种了解拓扑的算法。首先,您可能需要每10或100个空间用“节点”填充世界,然后维护这些节点之间的连通性图。该算法将通过在起点和终点附近找到可访问的节点,然后尝试在图上的它们之间找到一条路径(如果存在)的方法来寻路。

一种简单的方法是将每个图块分配给一个节点。可以轻松地看出,您只需要为每个图块分配一个节点即可(您永远无法访问图中未连接的两个节点)。然后,将图边缘定义为两个具有不同节点的图块相邻的任何位置。

该图有一个缺点:找不到最佳路径。它只是找到一条道路。但是,现在表明A *可以找到最佳路径。

它还提供了一种启发式方法,可以帮助您提高对A *功能的低估,因为您现在对自己的情况有了更多的了解。在发现您需要退后一步以继续前进之前,您不太可能需要充分探索一个死胡同。


我有理由相信类似Google Maps的算法可以以类似(尽管更高级)的方式运行。
Cort Ammon-恢复莫妮卡2015年

错误。通过选择允许的启发式方法,A *非常了解拓扑。
MSalters 2015年

关于Google,在我上一份工作中,我们分析了Google Maps的性能,发现它不可能是A *。我们相信他们使用依赖地图预处理的ArcFlags或其他类似算法。
MSalters 2015年

@MSalters:这是一条有趣的细线。我认为A *不了解拓扑,因为它只与最近的邻居有关。我会说,更确切地说,生成可允许的启发式算法的算法知道拓扑,而不是A *本身。考虑有钻石的情况。在备份尝试钻石的另一面之前,A *会沿一条路径走一点。没有办法通知A *该分支的唯一“出口”是通过已经访问过的具有启发式的节点(节省计算)。
Cort Ammon-恢复莫妮卡2015年

1
不能代表Google Maps,但是Bing Map使用具有地标和三角形不等式(ALT)的平行双向A星,并预先计算了少量地标和每个节点之间的距离。
Pieter Geerkens 2015年

2

除上述答案外,还有其他一些想法:

  1. 缓存A *搜索的结果。将路径数据从单元格A保存到单元格B,并在可能的情况下重用。这在静态地图中更适用,您将不得不对动态地图做更多的工作。

  2. 缓存每个单元的邻居。一个*实现需要扩展每个节点,并将其邻居添加到要搜索的开放集中。如果此邻居是每次计算而不是缓存,则可能会大大降低搜索速度。如果您已经这样做,请为A *使用优先级队列。


1

如果您的地图是静态的,则可以让每个单独的部分都有自己的代码,并在运行A *之前先进行检查。这可以在创建地图时完成,甚至可以在地图中进行编码。

不可逾越的图块应带有一个标志,当移至类似图块时,您可以选择不运行A *或在其旁边选择一个图块。

如果您的动态地图经常更改,那么您将很不走运。您必须以自己的方式来决定算法是否在完成之前停止,或者对部分进行检查以经常关闭。


这正是我在答案中使用区域ID的建议。
2015年

如果地图是动态的,但不经常更改,则还可以减少使用的CPU /时间量。也就是说,无论何时解锁或上锁的门,您都可以重新计算区域ID。由于这通常是根据玩家的行为而发生的,因此您至少要排除地牢的锁定区域。
uliwitness,2015年

1

如何使A *更快地得出节点不可通过的结论?

分析您的Node.IsPassable()功能,找出最慢的部分,然后加快速度。

在确定节点是否可通过时,将最可能出现的情况放在顶部,以便在大多数情况下函数立即返回而无需费心检查更难理解的可能性。

但这是为了更快地检查单个节点。您可以剖析以查看在查询节点上花费了多少时间,但是听起来您的问题是正在检查太多节点。

当我单击不可逾越的图块时,该算法显然会遍历整个地图以找到通往不可逾越的图块的路线

如果目标图块本身无法通过,则该算法根本不应该检查任何图块。甚至在开始进行寻路之前,它应查询目标图块以检查是否可行,如果不能,则返回无路径结果。

如果您的意思是目的地本身是可通过的,但被不可逾越的图块环绕,因此没有路径,那么A *检查整个地图是正常的。怎么会知道没有路呢?

如果是后者,则可以通过进行双向搜索来加快速度-这样,从目的地开始的搜索可以迅速找到没有路径,并停止搜索。参见此示例,用墙壁围住目标,比较双向与单向。


0

向后寻找路径。

如果仅您的地图上没有连续的大面积不可达图块,那么这将起作用。寻路只会搜索封闭的不可达区域,而不是搜索整个可达地图。


如果无法到达的方块数量超过了无法到达的方块,则速度甚至会更慢
Mooing Duck 2015年

1
@MooingDuck 你的意思是连接的无法访问的图块。这是一个几乎可以与任何理智的地图设计一起使用的解决方案,并且非常易于实现。在没有更好地了解确切问题的情况下,我不会建议任何爱好者,例如A *实现的速度可能如此之慢,以至于访问所有图块实际上是一个问题。
aaaaaaaaaaaaaa

0

如果播放器已连接的区域(无远距传送等),而无法访问的区域通常连接得不太好,则可以简单地从要到达的节点开始执行A *。这样,您仍然可以找到到达目的地的任何可能的路线,并且A *将停止快速搜索不可达的区域。


关键是要比常规A *更快。
Heckel'1

0

当我单击不可逾越的图块时,该算法显然会遍历整个地图以找到通往不可逾越的图块的路线-即使我站在它旁边。

其他答案也不错,但我必须指出显而易见的一点-您根本不应将寻路工具用于无法逾越的图块。

这应该是算法的早期退出:

if not IsPassable(A) or not IsPasable(B) then
    return('NoWayExists');

0

要检查图中两个节点之间的最长距离:

(假设所有边缘的权重相同)

  1. 从任何顶点运行BFS v
  2. 使用结果选择距离最远的顶点v,我们称其为d
  3. 从运行BFS u
  4. 找到距离最远的顶点u,我们将其称为w
  5. u和之间的距离w是图中的最长距离。

证明:

                D1                            D2
(v)---------------------------r_1-----------------------------(u)
                               |
                            R  | (note it might be that r1=r2)
                D3             |              D4
(x)---------------------------r_2-----------------------------(y)
  • 比方说之间的距离yx更大!
  • 然后根据这个 D2 + R < D3
  • 然后 D2 < R + D3
  • 那么,v和之间的距离x大于vu
  • 那么u在第一阶段就不会被选中。

归功于教授。什洛米·鲁宾斯坦

如果使用加权边,则可以通过运行Dijkstra而不是BFS查找最远的顶点,从而在多项式时间内完成相同的操作。

请注意,我假设它是一个连通图。我还假设它是无向的。


A *对于一个简单的基于2D瓦片的游戏而言并没有真正的用处,因为如果我正确理解,假设这些生物朝4个方向移动,那么BFS将获得相同的结果。即使生物可以沿8个方向移动,偏爱靠近目标的节点的懒惰BFS仍将获得相同的结果。A *是Dijkstra的一种修改,与使用BFS相比,其计算量大得多。

BFS = O(| V |)应该是O(| V | + | E |),但实际上不是自上而下的地图。A * = O(| V | log | V |)

如果我们的地图只有32 x 32瓦片,那么BFS最多将花费1024,而真正的A *可能会花费您高达10,000。这是0.5秒和5秒之间的时差,如果考虑到缓存,则可能会更大。因此,请确保您的A *表现得像一个懒惰的BFS,它喜欢更接近所需目标的图块。

A *对于导航地图很有用,因为边缘的成本在决策过程中很重要。在简单的基于开销的平铺游戏中,边缘的成本可能不是重要的考虑因素。如果是这样(如果不同的图块成本不同),则可以运行BFS的修改版,该版本将推迟并惩罚通过图块的路径,这些路径会使字符变慢。

所以是的,在很多情况下,BFS> A *。


我不确定我是否理解这部分内容:“如果我们的地图只有32 x 32瓦片,那么BFS最多将花费1024,而真正的A *可能会花费您高达10,000”。您能解释一下您是如何达到10k的吗?请问号码?
Kromster说支持Monica 2015年

“懒惰的BFS倾向于更靠近目标的节点”到底是什么意思?您是指Dijkstra,纯BFS还是具有启发式的(您在这里重新创建了A *,或者如何从开放集中选择下一个最佳节点)?这log|V|在A *的复杂性确实来自于保持打开集,或条纹的大小,并为网格地图是非常小的-有关日志(的sqrt(| V |))使用的符号。log | V | 仅显示在超连接图中。这是最简单的最坏情况复杂性应用得出错误结论的示例。
congusbongus

@congusbongus这正是我的意思。不要使用A *的
原始

@KromStern假设您对基于图块的游戏使用A *的原始实现,则得到V * logV复杂度,V为图块数,对于32乘32的网格为1024。logV,大约是位数需要代表1024(即10)。因此,您不必要地长时间运行。当然,如果您专门实现该实现以利用您在图块网格上运行的事实,则可以克服这个限制,而这正是我所指的
wolfdawn,2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.