动态寻路
目标
在一个所有物体都在动态移动的场景中,调用物理引擎现有的功能让一个实体从任意一点走到另一点的同时能够逼真地躲开所有可能与此物相撞的物体。
在这个话题上可说的很多。当你看下面的视频的时候你也会意识到这是一个挺难的任务,因为此实体是在几十个一直移动的物体中尝试寻路。
为什么要用物理引擎?
如果分析一个有AI(人工智能)元素的游戏场景,你会发现你需要:
- 一个分隔空间的方法,能够检查什么在哪儿,从而减少CPU使用
- 一个让实体在你的控制下移动、又看上去很合理的方法
- 一个在碰撞发生时通知并让相关物体采取行动的方法
Box2D物理引擎有上述所有特点。它的模型也相对容易使用,执行速度也挺快。写一写格子空间隔离(cell space partition)和基础物理系统(basic physics system)也许会有趣,但用那个时间可以做很多更好的事情。
什么是“逼真地躲避”?
在《星球大战之帝国反击战》中,当汉·索罗驾驶着千年隼号穿过小行星带时,他的经典台词是“永远别跟我说几率!”。当然我们也都知道,几率并不高。所以“逼真地躲避”算是有点夸张的说法,因为如果那艘船真的做得到的话,那就超出你的所知范围了。
我们在找的效果是让物体穿越障碍物时“没有明显地作弊”,然后让它遵循物理规律(质量、动量等)移动。
- 碰撞不应该被类似“岩石直接穿过船体”、“船瞬间移动”等的“魔法”避免
- 岩石的速度不能被减小到基本不动
- 岩石不能永远在预设轨道上前行(尽管这是一个有趣的选择)……如果被撞了,它们就该表现得和现实中的岩石一样
这个实体既要能够在附近的岩石中找出一条路径,又有一条能抵达目的地的大致路径,如果它最终能抵达的话。
如果这个实体正无所事事地坐着,而一块岩石猛冲过来,若实体有时间躲开的话,它就应能够躲开并逃到安全的地方。
有时碰撞会发生。这时候,AI不应该(也多半不可能)完全不操纵岩石躲开,或者说“作弊”。注意:“作弊”并不一定是一件坏事,我们的最终目标是让效果好看,只要玩家开心了,他们通常不在乎这个效果是怎么来的。
整体情况
主要场景
图中左侧的类和Cocos2d-x框架合起来就是这个MVC框架中的视图(View)部分。主要场景是控制器(Control)部分,把用户输入的东西发送到图片右侧模型(Model)部分。
这种视口方法在别的博客(英文版)中有讨论。在之前的版本中,视口(Viewport)在主窗口的内部操作(上下滑动、挤压、缩放)。在这个版本中,一个“摄像头”被创造出来隐藏这个过程的细节,也让它更容易应用于其他程序。
在此文中我们不会过细讨论视图和控制器的问题。
总体方法
Box2D引擎既可以让物体变成“固体”并有普通碰撞的反应,也可以让物体以其他方式路过彼此(此链接和此链接上有关于这个的好教程)。Box2D上有一个无形的特殊类,叫“传感器”。如果你将一个固定装置标记为传感器,那么物体会穿过它,而你同时会得到物体与它“相撞”的通知。
根据以上情况,这是我们的总体方法:
- 排好一些被标记为传感器的闭合凸多边形。我们此回用的是坐标方格,但圆、六边形和长方形也是可以的。它们也不一定要成坐标,可以是任何你需要的格式。但它们要能基本覆盖你想要用到的地区。
- 创造一个特殊的接触处理器,使得它在每个传感器与物体接触时开始计数、在接触结束时倒数。如果这些数字排成一行(而且看起来也像),那么,当数字是0时传感器的空间就是“空的”,其他情况下则“不是空的”。
如果你看了那个视频,你会发现在小行星带的下方有很多方格随着小行星的移动出现或消失。这就是“图像传感器接触层”,它会显示“不空”的传感器并隐藏空的那些。当然,实体上会有标志告诉处理器要“忽略我”。这样下来,实体,比如那艘船,就可以到处飞而不会让传感器工作了(这在图像搜索中很重要)。
所以,当小行星运动时我们就有了一个坐标格的传感器,那些可穿过的区域就被动态、即时地更新着。
传感器被创造出来时即被导入一个图像中,同被导入的还有关于哪些传感器相互毗邻的信息,和它们的欧氏距离。
这些东西加起来就给了你一幅你要走过的区域的图。这幅图通过物理引擎自动更新,它会告诉你图中的哪些部分可以通过。
最后的部分就是要开发一系列的能够利用这幅图并知道在节点“不是空的”之时跳过它们的搜索算法。这个只需在节点上(也可以是边缘上,尽管考虑到是哪个方向的边缘可能会让搜索变复杂)做一个简单的“限制”标志。
其他部分的功能
MVC的模型部分一半是Box2D引擎,在那之上是以下提到的其他主要成分,每一个都有其不同的功能。这些都是在执行模型中有着明确责任的单例模式(singleton)。其中一些只跟某些其他同类相互配合,另一些会跟所有同类相互配合。
通知器
通知器已被运用在很多其他设计中(参见其他博客文章,英文版)。这个单例模式是此系统中一个有效的整体性一对多的消息通讯装置。只要你把抽象的基础类发展出一个应用的界面,你所登记的事件就能被任意物体收到。
实体管理器
“实体”就是指游戏/系统中被实例化或进一步开发的部分。这个类是一个给实体的(有销毁责任的)组合容器以及实体ID索引的字典。在更复杂(更逼真)的系统中,实体的指针并不为其他实体所有。它们都有自己对应的ID或参考码,在你需要与它交流时只需给ID发送信息,这样就避免了指针引起的崩溃。
实体调度器
在这个代码库刚被开发时,小行星需要每帧都更新一次它们的AI。这就导致了很多浪费的CPU周期,因为每秒更新一次就足够了。“实体调度器”是一个安排小行星更新的时间的类。每帧,它都会对被安排那帧更新的实体执行Update(…)方法。通过把小行星的更新分散到多帧上,系统的总负载就减少了。
图像传感器管理器
图像传感器管理器会将传感器的信息加载到一副传感器图中,随后Box2D框架会(通过系统接触监听器)更新传感器附近物体的情况。此单例模式原是为了更大的目的而造,但最终在瞄准的范围上大大减小,现在多用于排除装置内传感器的故障。在未来的设计中,这个东西多半会被抹去(或改变成其他东西)。
在这里,我们讨论这个管理器的信息是为了强调一个非常重要的设计或开发考量:
在脑海中看来很棒的东西并不一定在设计上也很好。你需要永远记得反思你的设计并问自己“这真的能给我想要的东西吗?”、“是不是有另一种方法(也许在未来)能让它变得更好?”。
系统接触监听器
最后一个重要的部件就是这个“系统接触监听器”。这是Box2D使用的回调函数(callback),用来标记哪些物体被撞了。根据格子的大小,有很多(对于小型传感器来说甚至更多)撞击会在每次物理更新间发生。这就是你的CPU用得最多的地方,也是在未来设计中最需要监督和检查的地方。
代码
这篇文章中讨论到的源代码可以再GitHub的此处找到。
总结
这篇文章呈现了一项技术(一个视频和一个可以下载到代码的网址):使用一个物理引擎和寻路系统来动态地避开一个变化环境中的障碍物。初期的测试显示它未来的应用前景非常光明。唯一的美中不足是它的CPU负载(CPU load)比我预想的要高,不过也不是无法容忍的高。