在Unity里调试A星(A*)寻路

本文讲述了在Unity中实现A*寻路算法的过程,包括在Unity Asset Store中寻找资源、自定义实现A*算法、创建Node类、构建graph/grid、调试算法和优化编辑器性能。作者分享了在C#中遇到的问题,如自定义SortedList,并提供了算法的可视化方法,以帮助调试和优化。
摘要由CSDN通过智能技术生成

原文地址:http://t-machine.org/index.php/2014/08/10/debugging-a-pathfinding-in-unity/

说明:在文章中有时出现A星有时出现A*,都是一个东西,要是不理解,可以先看看原作者推荐的那几篇文章……

正文:

我的项目需要一个快速、高效、强大和适应性强的寻路系统。我的角色要飞、走、爬、瞬移穿过一个程序生成的陆地。完成这个要求有些复杂,但是过程却很有趣。

我想要一个模块,它能智能的引导障碍周围的怪物,并选择比较容易走的路,而不是从悬崖边走过去(反之亦然,比如巨型蜘蛛)。就像这样:


Unity Asset Store

Unity里要做任何事,访问Asset Store都应该是第一步。

我搜索排名靠前的几个寻路资源包,并且测试了有webPlayer或者免费版本的demo。

其中有些挺好,但是总的来说让人失望。性能不错,其中大部分都用了协程或者后台线程来控制CPU的使用,这很不错!(相对容易自己实现,话说这是A*的两大特性之一)

……但是用户接口很糟糕(我理解不了……)或者代码很难写,或者丢失了A星的核心元素的设置(比如动态边界dynamic edge:在游戏里使用A*的另一个原因)。

如果只是简单用用,它们很多都不错。24小时热卖资源里有一个看起来很好,由一个大团队维护——但是他们很精明(只能在你购买后才能看到文档!)而且看上去代码相对难写。

最后,我放弃了,于是我想:


也许……每个创新的游戏都需要你重新实现A*算法,来保证你的游戏独一无二的特点?

(A*不难实现,有很多选择方式,也许在这件事上重复造轮子是一件好事)。

A星是什么,如何工作?如何实现?

我推荐以下几篇文章:

这几篇文章留给我们一个基本算法:

OPEN = priority queue containing START
CLOSED = empty set
while lowest rank in OPEN is not the GOAL:
  current = remove lowest rank item from OPEN
  add current to CLOSED
  for neighbors of current:
    cost = g(current) + movementcost(current, neighbor)
    if neighbor in OPEN and cost less than g(neighbor):
      remove neighbor from OPEN, because new path is better
    if neighbor in CLOSED and cost less than g(neighbor): **
      remove neighbor from CLOSED
    if neighbor not in OPEN and neighbor not in CLOSED:
      set g(neighbor) to cost
      add neighbor to OPEN
      set priority queue rank to g(neighbor) + h(neighbor)
      set neighbor's parent to current

reconstruct reverse path from goal to start
by following parent pointers

在Unity里用C#实现

首先:Unity在使用C#语法时有些问题。泛泛的说,一些C#代码在Unity里会引起问题——最常见的例子:多维数组。你可以给Unity打补丁修复它,但是这么做不太常规。替代方法是简单粗暴的实现一个更底层的结构(很烂,但是实用)。

其次:微软C#/.NET标准库有一个内置的SortedList,我直说吧它名字起错了。你在每个位置只能有一个元素(这是sorted Map或者Set-sorted List,不是sorted List)。在我们的例子里这是一个错误的数据结构,虽然你可以通过定制它让它工作(让每个物品一个List,把节点从List移到另一个List)——哎呦,复杂!

还是一样:很烂,但是实用。我个人写了我自己的SortedList。

Node类

可能你游戏里已经有类似的结构了:

public class Node
{
public int x, int y;
}

graph / grid类

举个例子:

public class Grid
{
public Node[] allNodes;
public int nodesAcross, nodesDown; // needed to workaround Unity bugs
public Node NodeAt( int x, int y ) // needed to workaround Unity bugs
{
return allNodes[ x + y*nodesAcross];
}
public void SetNodeAt( int x, int y, Node n ) // needed to workaround Unity bugs
{
allNodes[ x + y*nodesAcross] = n;
}

写一个自己的SortedList

超简单,但是第一次写的时候我绕进去了,因为我不熟悉C#内部的foreach循环里如何保持整洁。我认为你也可能有类似的问题。对于A*你只需要一个包含以下方法的类:

public class MySortedList<T> where T: class
{
	public MySortedList();
	public int Count();
	public T GetFirst();
	public T RemoveFirst();
	public void RemoveItem( T victimItem );
	public bool Contains( T searchItem );
	public int AddWithValue( T newItem, float newValue );
}

检查你的算法

这是一个数据敏感的算法,我们创建了一些数据结构(不太危险)和一个自定义的容器(恩,危险!容易出错)。

我强烈建议你给MySortedList写单元测试。我没有这么做(我还没找到一个适用于Unity的好的单元测试框架),而且很后悔。特别关注以下几点:

  1. 测试Contains方法是否正确
  2. 测试Remove方法是否真的移除了对象

省略上面的任何一项,A星有可能陷入死循环,因为它持续添加节点到列表里。尤其是对Contains的测试,确保为你的自定义类实现了Equals和GetHashcode!

调试你的算法

现在到有趣的部分了,这是一个重度数据的算法:在真正的游戏中,它会处理成千上万的节点,以及数以万计的边界。调试它简直就是下地狱,千万别那么做啊。

你第一步的思考可能是:

我可以给调试A星弄个界面吗?这样调试它就变得又快又容易?

我的思路:

  1. 做个Calculate A Path的组件
    1. 把开始节点和结束节点放进去
    2. 写一个编辑器类,增加一个按钮“Calculate Path”
  2. A星输出一个Path对象
  3. Calculate A Path组件持有这个对象
    1. 创建一个新的GameObject
    2. 把Path对象作为它的子对象
    3. 给它附加一个PathVisualizer组件
  4. PathVisualizer组件实现了OnGizmos()
    1. 在每个节点都绘制一个立方体
    2. 在开始和结束节点的立方体和其他立方体颜色不同
    3. 在节点之间通过线段连接

需要注意的事:

因为每个路径都附加到一个GameObject上,你可以微调一下你的A*实现,在编辑器里重新跑一下,对比一下输出的2条路径,看看是否符合你修改的初衷。

我不需要我的Nodes和Paths类继承MonoBehaviours,因为我创建了一个可视化behaviour,而不是保存了一个需要可视化的路径的引用。(我也没懂)

本文的第一张图就是我的第一版可视化寻路。

可视化图和代价

我同时也做了一些可视化地图的工作:

以及代价的可视化。我实现了“双重”的代价:下坡比上坡更容易,因此每个边界都有两种颜色,一种是上坡的代价,一种是下坡的代价:

编辑器优化

没有优化的情况下,你的A星算法在很短的时间内运行完,它可能要计算成千上万的节点,这是即时的。

但是,当你有bug时,A*会陷入死循环,Unity会卡住(Unity对于代码死循环基本没有什么措施)。

因此从实用性的角度讲,你会想把计算放到协程里,并且在出错时,在编辑器里显示一些信息。我计划添加一个进度条给Open和Close表,显示它们的容量。我知道节点的总数量,两个列表都不能超过节点的总数量,我可以这样显示:

编辑器里的协程

可悲的是,虽然Unity允许在编辑器里使用协程(Unity的员工在4.5里这样用过),你这样用却不行。

但是Unity实现协程很简单,自己写也很简单。使用类似这样的类你就可以自己做协程。

我也这样做了(把你自己的回调添加到update里,手动运行IEnumerator),但是我添加了一些东西重绘GUI(经常但是不频繁)和其他一些小的特性。

断言和异常

让我们回顾前文:

这是一个重度数据的算法:在真正的游戏中,它会处理成千上万的节点,以及数以万计的边界。调试它简直就是下地狱,千万别那么做啊。

断言(Assertions)是我们的朋友。许多牛b的游戏开发者都喜欢它,尤其是在控制台上(一些机器运行不起来单元测试,断言能帮你在这种机器上完成单元测试)。

我修改了代码的实现,增加了下面的断言:

  1. 从一个节点到另一个节点,代价不能是负的
    1. 这应该是不可能的,即使你想在游戏里实现点特别的东西,你也不可能想要这个结果
    2. 因为A*会滥用这个,在你的“负”边界上来回跑一跑,会发现总的路径代价因为它们而减少
    3. 你可能想要0代价的边界——比如一个传送点——但是小心:它们也可能引起无限循环的问题
    4. 一般来讲当你读到一个边界代价<=0f时应该断言(Assert)
  2. 伪代码的核心循环的最后一行,把当前节点作为相邻节点的父节点。如果这个节点已经指向相邻节点了,就不要这么做!
    1. 再说一下:这个情况不会发生,如果算法正确,它就是绝对错误的
    2. (0或者负的代价会导致这个情况)
    3. 更糟糕的是:一个链表里包含多个相同节点,这会在后面导致死循环
    4. 我增加了一个断言:当给一个链表的末尾添加了一个节点,确保这个节点之前不在链表里
  3. 不要允许open表里的节点数超过总节点数
    1. 应该不可能!不能包含相同的节点!
  4. close表也一样(我把close表用集合实现了,但是如果你的Equals方法有bug,用标准库里的set也不管用)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值