作者:Dave C. Pottinger
移动(Movement): 寻道(Pathfinding): 中继点(Waypoint): 个体(Unit):
在进一步深入到我们的移动系统中之前,我将先简介一下人们在解决移动问题上遇到的一些问题,这些问题是消耗最少的CPU时间的同时达到最佳的智能效果和最高的移动精度的关键。
不要忘记处理个体移动中的碰撞问题。一旦你的游戏中的士兵们碰撞到了一起,你要怎样将他们分开呢?一种方法是是个体之间根本不发生碰撞,但在实际的应用中这是不可能做到的。不仅仅是实现这要求的程序代码非常难写,而且无论你写再多的代码也是无用的,这些个体总是会找到一些途径来使彼此重叠在一起,而在更多的情况中,这些个体的重合是必须的。一些使用近距离兵器进行战斗的游戏,例如《帝国时代》,就是一个要求个体重合的实例。另外,如果你要限制你游戏中的个体不能碰撞在一起,那么他们很可能为了避开彼此而离开预设的移动路线,暴露在其它对手的攻击之下,受到意外的伤害,这会使玩家对你的游戏极端不满。因此你必须决定好你的那些个体相互靠的有多近,重合多少是可以容忍的,还要设法处理由这些决定所带来的问题。
让我们从一个简单的对个体状态进行处理的移动算法的伪码(伪码如下)开始,这个算法所作的只是简单的使用一条给定路线前进,当遇到碰撞和冲突时重新寻道,因此它能在2D和3D游戏中都表现得很出色。使用该算法,我们将从一个给定的状态开始,持续循环直到找到一个可行的位置作为中继点作为个体移动的目标去接近之后才跳出循环。移动状态将会在整个UL中保存下来,这将使我们能够正确的设置未来的状态,例如"自动"添加中继点。这种保存机制可以保证减少一个个体在下一次UL中作出与当前UL移动相反的判断的可能性。
所有碰撞检测系统的基本目标都是判断两个个体是否发生了碰撞。这次我们所介绍的碰撞检测都是假设两个物体的碰撞,将来我们会专门介绍大量物体互相碰撞的检测问题。但无论是两个物体还是多个物体发生碰撞有一点是共同的:每个物体都要收到碰撞信息以便作出适当的响应(分开彼此)。
对于一群个体的简单集合来说,可以接受的碰撞检测下限是对整个组中的每个个体进行检测。这种方法将允许那些不属于你所选定的组的个体轻松的混入你的组队中。相对来说,对编队所要进行的碰撞检测就要更加复杂一些了。我们还应该认识到这种简单的组群还有一种特殊的性质决定了我们应该尽可能的简化对它所采用的碰撞检测方法--这种组群应该能够随时随地的将排列方式变换成任何可能的适应当地空间大小的阵形。
大多数移动算法从根本上都是"离散"的,不同于数学上的离散定义,这里所说的"离散"指移动算法在按照给定路径从A点移动到B点的过程中从不考虑中间路径上可能出现什么东西,相反,在"连续"的算法中就会考虑这些情况。这样做的一个问题就是当我们进行一个Internet游戏时(众所周知,由于网络速度的限制,这类游戏的UL时间一般较长)那些速度较高的个体很可能在一次UL时间中移动相当大的一段距离(由于UL时间变长),而当这样增长的UL连续出现时很可能出现个体跃过了其它本应发生碰撞的个体。如果这样的情况出现在一个工人的身上那并不会有人在意,但显然任何玩家也不会希望敌人能够从辛苦建设的城墙中穿越而过进而攻击玩家的基地(某些早期及时战略游戏中出现的"穿墙"的BUG就是有这种问题造成的)。大部分的移动系统现在采用限制个体移动距离的方法来对付这一问题,该方法可以有效的简化所需的处理。在离散型的移动算法中解决这类问题的方法如下图所示:
一种有效地解决方法是将一次移动拆分成多次移动的集合。这种拆分需要满足一定的移动距离上的要求,这要求就是要保证每次移动的距离刚好短于任何个体的长度,这就可以保证不可能有任何个体移动到当前个体的路径上来,从而避免了从其它个体之上跨越过去的情况。当每次这种拆分后的移动结束时我们就要使用碰撞检测系统对个体的当前位置进行碰撞检测。你可能会想到如此频繁的计算大量点的碰撞信息将会极大的增大系统消耗,没关系,在后面的章节中我们将会介绍一种方法来降低这种计算对系统的消耗。
经过上周的工作,我们已经有了一个简单的移动算法和一个管理个体碰撞的列表,还有什么工作是强化个体之间协作所必需的呢?位置预测(Position prediction)。
显然,最方便的优化方法是避免在每一帧中重复计算每一个已经预测过的个体位置。一个简单的移动列表可以实现这样的目的并且能够工作得很好:你可以在每一帧中从表内删除当前的位置,并向表内添加新的预测位置以维持列表长度固定(见图5)。虽然这一方法并不会减少个体开始移动时创建整个列表的计算量,但可以保证在剩余的移动过程中维持固定数量的计算。
下一种优化方法是设计一种能够处理点和线的位置预测系统,由于我们的碰撞系统支持处理点和线,因次添加这一功能将是很容易的事。如果一个个体按照一条直线进行移动,那么我们可以利用当前个体位置、预测位置和个体运动半径来指定一段移动的轨迹及范围。然而如果个体正在进行一次圆运动那么整个处理就会略微复杂一些。当然你可以将这种运动过程作为一个函数保存起来,但这显然会加大系统的负担。作为替代可以尝试通过对圆上的点进行取样来作出正确的位置判断(见图6)。最后,再次建议一定要使用能够实现对点和线的无缝交替处理的预测系统,以便在任何可能的情况下通过使用直线来减少对CPU的耗用。
最后所要介绍的一种优化方法非常重要,但同时也可能有一些不够直观,不能简单的看出其优化作用。如果我们要使用这样的预测系统,为了尽可能少的消耗资源,显然不应该在计算了一次预测未知之后再进行一次计算来移动个体。因此解决的方法是精确地进行位置预测,并最终使用该位置移动个体。这样我们就能对每个个体的移动只计算一次并且除了前述的开始移动时的计算之外没有其他多余的计算开销。
我们已经建立了一个复杂的系统来确定个体未来的可能位置,它支持3D移动,同时对计算量的提升也并不比一个简单的方法多多少,重要的是该方法提供给我们一个记录了一个个体在未来一小段时间内移动所需的一切信息的列表,这正是我们所需要的。现在我们可以进入较为有趣的部分了。 CASE 1:if 个体已经全部停止移动: CASE 2:if 个体没有移动,是另一个个体将要移动,什么也不做 CASE 3:if 当前个体正要移动,其它个体已经停止
2.Else,if 可以避开另一个个体,避开他以解决碰撞 3.Else,if 是高优先级个体并且能够沿移动路线推动低优先级个体,推动它,改变状态为正在处理的碰撞 4.Else,if 停下,重新寻道 CASE 4:if 当前个体正在移动,另一个个体也在移动: 2.if 碰撞不可避免,并且当前个体是高优先级,通知另一个体停止移动,转状态为CASE 3.1 3.Else,if 当前个体是高优先级的,计算出下步移动位置,通知另一个体减速到足以避免碰撞。 正在处理的碰撞: 1.if 是一个移动的个体要处理CASE 1的碰撞,并已经移动到了目的地,碰撞解决 2.if 是CASE 3.1中低优先级个体,并且高优先级个体已经抵达预定位置,开始返回原位置,碰撞解决 3.if 是CASE 3.1中高优先级个体,等待(减速或停止)直到低优先级个体从通路上离开,之后继续移动 4.if 是CASE 3.3中高优先级个体并且现在低优先级个体已可以从通路中离开,转状态为CASE 3.1 5.if 是CASE 4.3中低优先级个体并且高优先级个体已经抵达预计地点,恢复移动速度,碰撞解决
计划编制是个体协作的关键,虽然我们尽可能地提升预测和计算的精确性,但是显然事情总是会出错的。例如我们在《帝国时代》中所犯的一个错误是我们总是在一帧的时间内使个体作出移动的决定,虽然这样的决定多数是正确的,但我们并没有在以后的UL中参考它。这样就造成了一个问题:个体对移动路线作出了决定,实行时发现出现问题必须重新决断,结果是使个体再次返回它的出发点。计划编制可以有效地避免这类问题。我们保存一定数量的个体以前移动中所遇到的障碍和碰撞的解决步骤(由其它的游戏细节定义),这就为我们未来遇到困境时提供了参考。举例来说,当我们要避免一次碰撞时我们将存储哪一个个体是我们所要闪避的。由于我们要设定一个可行的计划,没有任何理由对碰撞中的另一个体进行碰撞检测,除非其中的某一个个体得到了新的命令或发生其它类似的变化。一旦我们完成了闪避,就可以为其它的个体恢复正常的碰撞检测了。在下面的扩展中,你将看到我们将反复利用这一思想来达到我们的目的。
游戏编程的乐趣之一就是要不停地创新来开发新技术以使设计人员能作出更优秀的游戏。在即时战略游戏中,越来越多的开发人员希望能够在他们下一批作品中加入对编队的处理能力。在这里我不会介绍现在那些低技术含量的移动方法,我所要讨论的是如何协调编队的移动,使每一个个体都能在智能的维持编队队形的同时在地图上随意的移动。 组队(Group)移动 首先要弄清楚何谓组队(Group):由用户(玩家)为方便操作而选取的简单的个体集合(一般会对其成员发布相同的命令),除了在移动时要保持成员一同移动之外组队并没有其他对移动系统的限制。组队的使用使我们必须记录许多信息,例如组队成员的列表以及当整个组队还在一起时所能移动的最大速度。也许我们还应该保存整个组队的中心,以作为一个可以很容易得到的操作参考点。同时还应该选定一个组队的指挥者,大多数游戏中怎样选出这个个体并不重要,重要的是一定要有一个这样的个体。 在我们开始工作之前有一个问题需要回答:当组队在地图上移动时我们有必要保持所有个体在一起吗?如果不,组队将只是为使用户方便操作而存在的,每一个个体都会独自寻道和移动就如同用户对每个个体分别下达指示一样。当我们关注如何加强组队的管理时,我们可以发现组队的凝聚力可以分为多个等级。 组队中的个体都以相同的速度移动。一般地这将使用组队中速度最低的个体的最大速度,不过有时让那些速度较慢的个体在组对中移动的稍快一些会更好(见图8)。然而一般游戏的设计人员给一类个体较低的速度总是有原因的,例如如果允许强力的个体能够非常高速的在地图上移动将会极大的破坏游戏的平衡性。
组队中的个体都以相同的速度移动并使用同一条路径。这种方法可以有效的避免当组队中一半的个体从森林一侧前往目的地时另一半却从另一侧移动(见图9),稍后你将看到实现这一方法的一条简单途径。
组队中的个体以相同的速度移动,使用同一条路径并同时抵达。这是最复杂的组队组织方式,它不但要求达到上述两点,并且还要求位于前面的个体能够等待落在后面的个体追上来,有时还要给后面的慢速个体短时间加速以使其能够追上前面的个体。 怎样才能实现最后的要求?这要使用一种分级的移动系统,这样我们就能在处理每个个体的移动时兼顾那些同属于某个组队的个体了。如果我们对组队的个体创建一个组队对象,我们就能够记录所有必需的数据,为整个组队计算最大速度,以及判断何时需要前面的个体等待后面的个体。下面就是一个组队类的简单定义:
BGroup类在其内部管理整个组队中个体之间的交互操作。在任何时间点,它都应该有一个时间表以来处理组队内的个体之间的碰撞,它也应该有能力通过参数和优先级管理来控制或修正个体移动。如果你的游戏只支持一种移动优先级,那么你就应该为你在组队中的个体们添加第二种优先级。虽然一个组队对外的表现似乎只有一种优先级,但在其内部还是应该分为不同的移动优先级。基本上来说,BGroup类是另一个完善的封闭的移动系统。 组队的指挥者将负责整个组队的寻道工作,它将决定整个组队的移动路线,在简单的组队移动系统中所需的工作只是由这个个体本身来寻道即可。然而在下面的部分中我们将看到指挥者所能够作的其它事情。 编队控制基础 首先应该给出编队的定义:编队(Fomation)是一种更复杂的组队,编队有自己的方向(前方、后方、左翼和右翼)。编队中的每一个个体都试图保持自己在编队中的位置,而这个位置是唯一固定的也是相互关联的。更加复杂的模型使得编队中各个个体的朝向需要单独处理,而同时也要求在移动中提供整体旋转的方法。 编队是建立在组队系统之上的,它是一种限制更加严格的组队,因为我们必须非常详尽的规定编队中每个个体的位置。所有的个体在移动中必须保持一起行动,并要求在速度、路径上一致以及相互之间的位置和距离保持不变--如果在移动中编队出现了大间距的缝隙,那么它也就与组队没有什么不同了。 下面给出的这个BFomation类能够清晰的管理一个编队的预定位置(我们要求编队中的每个个体所处的位置以及它的方向)、编队方向和编队的状态。大多数游戏中所使用的编队都是预先定义的,显然,在开发过程中进行这项工作是很简单的(通过使用一些非专业人员也能熟练操作的文本编辑器就可以很好的完成这项工作)。我们当然希望能在游戏过程中实时的定义编队,但这样做就需要更多的内存以保证每一个由玩家定义的编队都能在内存中保留一份自身定义的副本。 Listing 3. The BFormation Class
使用这个模型,我们必须时刻关注编队的状态。cStateBroken表示编队并没有被创建也没有创建的企图;cStateForming表明我们的编队正在建立但还没有达到cStateFormed状态;一旦所有的个体都已位于它们的预定位置,我们就可以将状态改变为cStateFormed。为了使编队的移动简单化,我们可以使一个编队在完成组建之前(达到cStateFormed状态之前)不可移动。 当我们准备使用一个编队时,第一件工作就是组建这个编队。当给定一个编队时,BFormation(译者注:原文这里是BGroup,但该类并没有编队管理功能,经过反复推敲认定为编写错误)控制每个个体移动到编队中的预定位置,该位置的计算是与当前编队方向相关的,如果这个方向发生了变化,那么预定位置将自动被重新计算并修正为正确的位置。 为了组建一个编队,我们可以使用预定安置--每一个预定位置拥有一个预设值(由定义规定或由算法确定)来指明个体组建编队时应该按照那种顺序进驻那些预定位置,这样才能使整个组建过程从里到外进行得相当有条理(见图10)。下面的算法列表说明如何实现这样的组建方式。 Listing 4. 设置组队中的所有个体移动优先级到一个相同的低优先级 选定一个个体前往所找出的位置,要求满足如下条件: 设置个体的移动优先级到中等值
现在我们所有的战士都已经就位了,接下来做什么呢?我们可以开始移动他们以穿过整个地图,我们可以假定寻道系统找出了一条以当前编队的形状和大小可以通过的路径来抵达目的地(见图11),如果没有这样一条路经那就必须对整个编队进行操作(不久我们就会探讨这个问题)。当编队在地图上移动时我们需要选出一个指挥者来控制整个移动,当指挥者沿路径前进并改变方向时其它所有编队中的个体都要改变方向以追随它,这种操作一般被称为flocking(聚集)。
我们有两种方法处理编队的方向改变:忽略这种改变或者转动编队的方向。忽视方向的改变是简单的而且对于那些盒状的编队来说是非常合理的(见图12)。
对编队进行旋转并不会增加多少复杂性而同时对于某些编队方式(如直线形)来说是非常合理的。进行编队旋转时首先要做的是停止编队移动,完成方向的旋转之后我们要重新计算每个预定位置,然后回到cStateForming状态(见图13),使个体前往新的位置并在完成这一工作之后设置状态为cStateFormed,这样我们就可以继续原来的移动。
高级编队管理技术 缩放个体间距(Scaling unit positions).由于编队中的预定位置都是由矢量进行定义的,因此我们可以很方便的对整个编队的间距进行放缩以使它变得更小,这就使得编队能够通过城墙或树林中更小的缝隙(见图14)。这种方法对于那些排列得较为分散的编队很有效,但对于那些排列紧凑的编队就没有什么用处了。
简单的障碍回避(Simple ordered obstacle avoidance).如果我们在移动编队时遇到与其它游戏中实体相碰撞的情况时(无论是当前还是未来碰撞),我们可以假设即使有这样的碰撞发生,原来寻到的道路仍是可用的。简单的解决方法是沿着编队前进的路线找出第一个不再发生碰撞的位置,并在该位置完成编队的重组(见图15)。这样我们的步兵团就可以先分散,带穿越障碍物之后再在另一侧重新组建编队。在使用这一方法时一定要注意有时障碍物的范围非常大,以至于编队的重组工作必须在走出很远之后才能做,这时就得考虑是否应该重新寻道了。
二分和重组(Halving and rejoining).虽然简单的回避能够工作的很好,但是会降低玩家对整个编队穿越地图的感觉,相对来说二分法可以很好的保持住编队所带来的视觉冲击。当我们的编队在前方遇到一个障碍物时我们可以找出编队中的一个拆分点,从该点将编队一分为二,这两个编队分别通过障碍物之后再前进到重组位置恢复成一个编队(见图16)。这种方法只增加很少的计算量,但却能为编队移动带来良好的视觉效果。
路径栈 路径栈就是一种简单的用来记录个体移动路由信息的栈操作(后进先出,见图17)。一个路径栈记录的信息一般包括个体当前所采用的路线,现在个体正在向哪个中继点移动以及个体是否正处于巡逻中。一个路径栈对我们的目的有两大作用。
首先,它可以为一次分级寻道工作提供便利。一般来说游戏开发者会把寻道区分为两种明显不同的等级--高级(high-level)和低级(low-level)(见图18)。高级的寻道可以为个体找寻出穿越地图上不同地形和主要堵塞地点的道路,这就如同玩家大多数时候给个体制定的路径一样。低级的寻道则同时还会处理较小的障碍物并更注重处理细节。一个路径栈可以方便的存储高级和低级的寻道信息。我们可以先通过高级寻道找出一条路径并把它存储进路径栈中,而当我们必须注意避免与大片空地中的一颗树发生碰撞时就可以将低级寻道的一系列结果存入路径栈顶并执行它们。每当执行完一条路径,我们就可以将它从栈中弹出并继续执行现在位于栈顶的路径。
第二点,路径栈可以允许高级寻道被重用(reuse)。如果你回顾前面的介绍将看到组队和编队在移动时的一大要素就是所有的成员都使用相同的路径来移动到目的地。如果我们设计的路径栈可以允许多个个体参考一条路径,那就将使同一条高级寻道路径很容易的被重用。一个编队的指挥者将使用高级寻道找出一条路径并把它拷贝给编队中的每个个体,而其它个体则什么也不用做。 这样创建保存路径信息的结构还能提供给我们一些其它好处。通过将一条高级寻道路径拆分成多个低级寻道路径,我们可以在执行具体路径之前充分的对这些低级路径进行更精确的计算。而且如果我们确定高级寻道的结果是可用的话,也可以将低级寻道的工作略微推后再做。如果我们正在进行高协调度的个体移动,路径栈将允许我们向栈顶添加一条临时的用来避免碰撞的路径,并能够很好的使用这一路径修正个体移动(见图19)。
解决混合碰撞 我们定义混合碰撞为同时发生在两个以上个体之间的碰撞。大多数游戏都对于可以解决的混合碰撞中的个体个数有限制,超过这个数目就只能分几次解决了。下面我们将探讨如何使用已有的移动系统对这类情况进行简单的处理。 如果我们遇到了一个由三个个体造成的混合碰撞(见图20),首要的工作是找出其中优先权最高的个体。一旦找到它,我们就要立即找到与之碰撞的另一个个体并确定优先权最高的个体所遇到的最主要的碰撞(该碰撞可能发生在其与次优先的个体之间,也可能不是)。当我们找到了这两个个体后,剩下的工作就交给原来的碰撞处理部分解决即可了。
当最初的两个个体的碰撞被解决后,我们就要重新评估整个碰撞并更新个体之间的关系。一个更复杂的系统可以很好的解决这样的问题,但是如果简单的移走已经解决了碰撞问题的个体也能得到不错的效果。 一旦我们更新了碰撞中的个体,下一步工作就又回到寻找优先权最高的个体的碰撞上来了。我们将一直重复这一步骤直到所有的碰撞都被解决。 你可以在两个地方使用这一系统:碰撞解决部分或碰撞预测系统中。碰撞解决的规则必须被修改以适应对于个体优先级的要求,这样的修改并不难,但会增加一定的代码量。或者你可以修改你的碰撞预测系统使得只会发生两个个体碰撞的情况,然而这样做你仍然需要先找出一次碰撞中的全部个体并作出操作。 解决堆叠峡谷问题(The Stacked Canyon Problem) 所有移动系统的最终目的都是要实现智能的移动效果,而所有处理中最能体现智能的就是处理堆叠峡谷问题了(什么是堆叠峡谷问题呢?事实上当一个个体要从一群排列紧凑的个体之间穿过时所需要解决的问题就是堆叠峡谷问题,图21就是一个例子)。虽然此类问题并不能简单的一次解决,但我们可以重用前面的一些简单方法来解决它。
第一步是鉴定是否为一个堆叠峡谷问题。这是非常重要的,因为我们将要利用前进个体(driving unit)的优先级。当然我们可以利用每个个体自身的优先级来要求其它个体让出道路,但是更好的解决方法是使用前进个体的优先级。判断一个堆叠峡谷问题可以有两种方法:观察前进个体是否会把一个阻碍其移动的个体推到另一个身上或者观察移动个体的碰撞列表以寻找多重的碰撞。不论采用哪种方法,被推动的个体都应该拥有与前进个体相同的移动优先级。 一旦我们判断出将要解决一个堆叠峡谷问题,就可以采用一种简单的递归调用个体协调运动系统的方法来解决它。把第一个被推动的个体作为前进个体处理其于第二个个体之间的关系,并如此循环。每一个个体被它的前进个体推动直到它可以移动到一边而让出道路。当最后一个个体也从陆上让开后,原来的前进个体就可以继续移动了。 一个好的习惯是将已经移开的个体在移回原位。为了能够这样做,我们应该记录整个推动过程并在问题解决后倒序的执行该过程。另外如果负责移动的代码能够辨别出前进个体是否归属于一个组队,那就能保证组队中每个个体都能在原来阻碍道路的个体返回原位置之前通过。 注意 优化你的整体系统。如果你只是要做一个2D游戏,那就会有许多多余的计算是可以取消和简单化的。不论你是要做2D游戏还是3D的,你的碰撞检测系统都需要一个优秀的经过优化的个体分拣系统,这类系统已经不再仅仅用于绘图了。 对高级寻道和低级寻道使用不同的方法。过去大多数游戏对这两种寻道方式使用相同的算法。这样做的害处是如果对高级寻道使用低级寻道的算法将使高级寻道变得缓慢并且不能用于寻找长的通路;相反的,如果对低级寻道使用高级寻道的算法将会造成结果并没有将道路上的所有障碍物考虑在内或者造成一个个体能从其它个体之中穿过。一定要抓住要点制作两套寻道系统。 无论你做什么,个体总会交叠碰撞在一起。个体的交叠和碰撞是不可避免的,或者按最好情况说将是非常难以操作的。你最好尽早处理这些碰撞问题,这将使你的游戏更好一些。游戏的地图已经越来越复杂了,并且还会加入随机地图的处理。一个好的移动系统将能够很好的处理随机地图和相应的一切细节。 清楚地了解UL是怎样影响个体移动的。可变化的UL时间将是你的移动系统所必须解决的一大难题。可以使用一个简单的修正机制来解决此类大部分的问题。 只涉及单个UL的做法是过时的。没有计划的编制不可能解决好个体移动的协调问题,如果不记录上一次UL中的操作和将来要发生的问题又是不可能制作好的计划的。一个能够运作良好的移动协调系统必须在任何时候都能够参考以前的碰撞列表和预测碰撞的列表。切记解决碰撞的过程中出现的较小的变化是可以忽略的。
简单的个体移动是简单的。一套优秀的协调系统是我们所应该追求的,因为它能使你的游戏步上一个等级并能增加玩家的乐趣。在本次的文章中我们研究了一个移动协调系统的基础功能--使用多个UL时间制定行动计划以及一套可以解决任何两个个体碰撞的方法等等。现在你应该不会再满足于你的游戏中那些傻乎乎的个体移动了。 |