在Unity里调试A星(A*)寻路
2015-05-28 16:05
232 查看
原文地址:http://t-machine.org/index.php/2014/08/10/debugging-a-pathfinding-in-unity/
说明:在文章中有时出现A星有时出现A*,都是一个东西,要是不理解,可以先看看原作者推荐的那几篇文章……
正文:
我的项目需要一个快速、高效、强大和适应性强的寻路系统。我的角色要飞、走、爬、瞬移穿过一个程序生成的陆地。完成这个要求有些复杂,但是过程却很有趣。
我想要一个模块,它能智能的引导障碍周围的怪物,并选择比较容易走的路,而不是从悬崖边走过去(反之亦然,比如巨型蜘蛛)。就像这样:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.09.36.png)
我搜索排名靠前的几个寻路资源包,并且测试了有webPlayer或者免费版本的demo。
其中有些挺好,但是总的来说让人失望。性能不错,其中大部分都用了协程或者后台线程来控制CPU的使用,这很不错!(相对容易自己实现,话说这是A*的两大特性之一)
……但是用户接口很糟糕(我理解不了……)或者代码很难写,或者丢失了A星的核心元素的设置(比如动态边界dynamic edge:在游戏里使用A*的另一个原因)。
如果只是简单用用,它们很多都不错。24小时热卖资源里有一个看起来很好,由一个大团队维护——但是他们很精明(只能在你购买后才能看到文档!)而且看上去代码相对难写。
最后,我放弃了,于是我想:
也许……每个创新的游戏都需要你重新实现A*算法,来保证你的游戏独一无二的特点?
(A*不难实现,有很多选择方式,也许在这件事上重复造轮子是一件好事)。
2014: AmitP’s interactive A-star explanation— 很好理解
2009: AmitP’s original A-star explanation (non-interactive) —很多人自己实现A*时都参考它
这几篇文章留给我们一个基本算法:
其次:微软C#/.NET标准库有一个内置的SortedList,我直说吧它名字起错了。你在每个位置只能有一个元素(这是sorted Map或者Set-sorted List,不是sorted List)。在我们的例子里这是一个错误的数据结构,虽然你可以通过定制它让它工作(让每个物品一个List,把节点从List移到另一个List)——哎呦,复杂!
还是一样:很烂,但是实用。我个人写了我自己的SortedList。
我强烈建议你给MySortedList写单元测试。我没有这么做(我还没找到一个适用于Unity的好的单元测试框架),而且很后悔。特别关注以下几点:
测试Contains方法是否正确
测试Remove方法是否真的移除了对象
省略上面的任何一项,A星有可能陷入死循环,因为它持续添加节点到列表里。尤其是对Contains的测试,确保为你的自定义类实现了Equals和GetHashcode!
你第一步的思考可能是:
我可以给调试A星弄个界面吗?这样调试它就变得又快又容易?
我的思路:
做个Calculate A Path的组件
把开始节点和结束节点放进去
写一个编辑器类,增加一个按钮“Calculate Path”
A星输出一个Path对象
Calculate A Path组件持有这个对象
创建一个新的GameObject
把Path对象作为它的子对象
给它附加一个PathVisualizer组件
PathVisualizer组件实现了OnGizmos()
在每个节点都绘制一个立方体
在开始和结束节点的立方体和其他立方体颜色不同
在节点之间通过线段连接
需要注意的事:
因为每个路径都附加到一个GameObject上,你可以微调一下你的A*实现,在编辑器里重新跑一下,对比一下输出的2条路径,看看是否符合你修改的初衷。
我不需要我的Nodes和Paths类继承MonoBehaviours,因为我创建了一个可视化behaviour,而不是保存了一个需要可视化的路径的引用。(我也没懂)
本文的第一张图就是我的第一版可视化寻路。
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.00.49.png)
以及代价的可视化。我实现了“双重”的代价:下坡比上坡更容易,因此每个边界都有两种颜色,一种是上坡的代价,一种是下坡的代价:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-19.59.53.png)
编辑器优化
没有优化的情况下,你的A星算法在很短的时间内运行完,它可能要计算成千上万的节点,这是即时的。
但是,当你有bug时,A*会陷入死循环,Unity会卡住
a9a1
(Unity对于代码死循环基本没有什么措施)。
因此从实用性的角度讲,你会想把计算放到协程里,并且在出错时,在编辑器里显示一些信息。我计划添加一个进度条给Open和Close表,显示它们的容量。我知道节点的总数量,两个列表都不能超过节点的总数量,我可以这样显示:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.05.21.png)
但是Unity实现协程很简单,自己写也很简单。使用类似这样的类你就可以自己做协程。
我也这样做了(把你自己的回调添加到update里,手动运行IEnumerator),但是我添加了一些东西重绘GUI(经常但是不频繁)和其他一些小的特性。
这是一个重度数据的算法:在真正的游戏中,它会处理成千上万的节点,以及数以万计的边界。调试它简直就是下地狱,千万别那么做啊。
断言(Assertions)是我们的朋友。许多牛b的游戏开发者都喜欢它,尤其是在控制台上(一些机器运行不起来单元测试,断言能帮你在这种机器上完成单元测试)。
我修改了代码的实现,增加了下面的断言:
从一个节点到另一个节点,代价不能是负的
这应该是不可能的,即使你想在游戏里实现点特别的东西,你也不可能想要这个结果
因为A*会滥用这个,在你的“负”边界上来回跑一跑,会发现总的路径代价因为它们而减少
你可能想要0代价的边界——比如一个传送点——但是小心:它们也可能引起无限循环的问题
一般来讲当你读到一个边界代价<=0f时应该断言(Assert)
伪代码的核心循环的最后一行,把当前节点作为相邻节点的父节点。如果这个节点已经指向相邻节点了,就不要这么做!
再说一下:这个情况不会发生,如果算法正确,它就是绝对错误的
(0或者负的代价会导致这个情况)
更糟糕的是:一个链表里包含多个相同节点,这会在后面导致死循环
我增加了一个断言:当给一个链表的末尾添加了一个节点,确保这个节点之前不在链表里
不要允许open表里的节点数超过总节点数
应该不可能!不能包含相同的节点!
close表也一样(我把close表用集合实现了,但是如果你的Equals方法有bug,用标准库里的set也不管用)
说明:在文章中有时出现A星有时出现A*,都是一个东西,要是不理解,可以先看看原作者推荐的那几篇文章……
正文:
我的项目需要一个快速、高效、强大和适应性强的寻路系统。我的角色要飞、走、爬、瞬移穿过一个程序生成的陆地。完成这个要求有些复杂,但是过程却很有趣。
我想要一个模块,它能智能的引导障碍周围的怪物,并选择比较容易走的路,而不是从悬崖边走过去(反之亦然,比如巨型蜘蛛)。就像这样:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.09.36.png)
Unity Asset Store
Unity里要做任何事,访问Asset Store都应该是第一步。我搜索排名靠前的几个寻路资源包,并且测试了有webPlayer或者免费版本的demo。
其中有些挺好,但是总的来说让人失望。性能不错,其中大部分都用了协程或者后台线程来控制CPU的使用,这很不错!(相对容易自己实现,话说这是A*的两大特性之一)
……但是用户接口很糟糕(我理解不了……)或者代码很难写,或者丢失了A星的核心元素的设置(比如动态边界dynamic edge:在游戏里使用A*的另一个原因)。
如果只是简单用用,它们很多都不错。24小时热卖资源里有一个看起来很好,由一个大团队维护——但是他们很精明(只能在你购买后才能看到文档!)而且看上去代码相对难写。
最后,我放弃了,于是我想:
也许……每个创新的游戏都需要你重新实现A*算法,来保证你的游戏独一无二的特点?
(A*不难实现,有很多选择方式,也许在这件事上重复造轮子是一件好事)。
A星是什么,如何工作?如何实现?
我推荐以下几篇文章:2014: AmitP’s interactive A-star explanation— 很好理解
2009: AmitP’s original A-star explanation (non-interactive) —很多人自己实现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的好的单元测试框架),而且很后悔。特别关注以下几点:
测试Contains方法是否正确
测试Remove方法是否真的移除了对象
省略上面的任何一项,A星有可能陷入死循环,因为它持续添加节点到列表里。尤其是对Contains的测试,确保为你的自定义类实现了Equals和GetHashcode!
调试你的算法
现在到有趣的部分了,这是一个重度数据的算法:在真正的游戏中,它会处理成千上万的节点,以及数以万计的边界。调试它简直就是下地狱,千万别那么做啊。你第一步的思考可能是:
我可以给调试A星弄个界面吗?这样调试它就变得又快又容易?
我的思路:
做个Calculate A Path的组件
把开始节点和结束节点放进去
写一个编辑器类,增加一个按钮“Calculate Path”
A星输出一个Path对象
Calculate A Path组件持有这个对象
创建一个新的GameObject
把Path对象作为它的子对象
给它附加一个PathVisualizer组件
PathVisualizer组件实现了OnGizmos()
在每个节点都绘制一个立方体
在开始和结束节点的立方体和其他立方体颜色不同
在节点之间通过线段连接
需要注意的事:
因为每个路径都附加到一个GameObject上,你可以微调一下你的A*实现,在编辑器里重新跑一下,对比一下输出的2条路径,看看是否符合你修改的初衷。
我不需要我的Nodes和Paths类继承MonoBehaviours,因为我创建了一个可视化behaviour,而不是保存了一个需要可视化的路径的引用。(我也没懂)
本文的第一张图就是我的第一版可视化寻路。
可视化图和代价
我同时也做了一些可视化地图的工作:![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.00.49.png)
以及代价的可视化。我实现了“双重”的代价:下坡比上坡更容易,因此每个边界都有两种颜色,一种是上坡的代价,一种是下坡的代价:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-19.59.53.png)
编辑器优化
没有优化的情况下,你的A星算法在很短的时间内运行完,它可能要计算成千上万的节点,这是即时的。但是,当你有bug时,A*会陷入死循环,Unity会卡住
a9a1
(Unity对于代码死循环基本没有什么措施)。
因此从实用性的角度讲,你会想把计算放到协程里,并且在出错时,在编辑器里显示一些信息。我计划添加一个进度条给Open和Close表,显示它们的容量。我知道节点的总数量,两个列表都不能超过节点的总数量,我可以这样显示:
![](http://t-machine.org/wp-content/uploads/Screen-Shot-2014-08-10-at-20.05.21.png)
编辑器里的协程
可悲的是,虽然Unity允许在编辑器里使用协程(Unity的员工在4.5里这样用过),你这样用却不行。但是Unity实现协程很简单,自己写也很简单。使用类似这样的类你就可以自己做协程。
我也这样做了(把你自己的回调添加到update里,手动运行IEnumerator),但是我添加了一些东西重绘GUI(经常但是不频繁)和其他一些小的特性。
断言和异常
让我们回顾前文:这是一个重度数据的算法:在真正的游戏中,它会处理成千上万的节点,以及数以万计的边界。调试它简直就是下地狱,千万别那么做啊。
断言(Assertions)是我们的朋友。许多牛b的游戏开发者都喜欢它,尤其是在控制台上(一些机器运行不起来单元测试,断言能帮你在这种机器上完成单元测试)。
我修改了代码的实现,增加了下面的断言:
从一个节点到另一个节点,代价不能是负的
这应该是不可能的,即使你想在游戏里实现点特别的东西,你也不可能想要这个结果
因为A*会滥用这个,在你的“负”边界上来回跑一跑,会发现总的路径代价因为它们而减少
你可能想要0代价的边界——比如一个传送点——但是小心:它们也可能引起无限循环的问题
一般来讲当你读到一个边界代价<=0f时应该断言(Assert)
伪代码的核心循环的最后一行,把当前节点作为相邻节点的父节点。如果这个节点已经指向相邻节点了,就不要这么做!
再说一下:这个情况不会发生,如果算法正确,它就是绝对错误的
(0或者负的代价会导致这个情况)
更糟糕的是:一个链表里包含多个相同节点,这会在后面导致死循环
我增加了一个断言:当给一个链表的末尾添加了一个节点,确保这个节点之前不在链表里
不要允许open表里的节点数超过总节点数
应该不可能!不能包含相同的节点!
close表也一样(我把close表用集合实现了,但是如果你的Equals方法有bug,用标准库里的set也不管用)
相关文章推荐
- 使用UnityVS1.8.2搭配VS2013进行Unity3D代码调试(下载+安装+使用+问题解决)
- unity自带寻路功能实现
- Unity中使用和调试Dll
- 关于Unity中调试C#的方法
- Unity自动寻路Navmesh之入门
- Unity 自动寻路
- Unity发布IOS平台之创建真机调试证书以及发布IPA
- Unity5.2在VS2015中调试
- unity自带寻路Navmesh入门教程(一)
- Unity物体在多个物体之间循环自动寻路
- Unity使用MonoDevelop进行断点调试
- Unity5.X在VS2015中的调试
- Unity 自动寻路 Navmesh 之 跳跃,攀爬,斜坡
- unity 高级自动寻路(Advanced AI Pro 6.0插件)官网文档翻译(—)
- Unity物体跟随鼠标点的位置自动寻路
- Unity Navigation system自动寻路AI
- Unity Lua调试 ulua/tolua slua调试教程
- Unity Game Programming AI(5)A Star寻路算法
- Unity调试shader
- VS2015 调试 UGUI (Unity5.5)