五千年(敝帚自珍)

主题:初到贵宝地,灌一点游戏开发的东东 -- foundera

共:💬23 🌺4
全看分页树展 · 主题 跟帖
家园 即时战略游戏中如何协调对象移动

即时战略游戏中如何协调对象移动

   在图论中人们研究了通过怎样的计算才能找到一条从A点到B点的通路,以图论本身来说这已经解决了从A到B的问题,剩下的只是从A沿着找到的路线移动到B就可以了。这样的认识基于一个默认的假设--道路中的一切障碍物都是固定的,但是在现在已经广泛流行的即时战略类游戏中问题却远远不止这些。

在图论中人们研究了通过怎样的计算才能找到一条从A点到B点的通路,以图论本身来说这已经解决了从A到B的问题,剩下的只是从A沿着找到的路线移动到B就可以了。这样的认识基于一个默认的假设--道路中的一切障碍物都是固定的,但是在现在已经广泛流行的即时战略类游戏中问题却远远不止这些。举个例子说上下班高峰期的时候,路上的每一个人都清楚地知道自己的目的地和所要走的路径,但是由于某些个人不遵守规则或其他人为原因还是会造成堵车现象。而如果这样的事情发生在一个即时战略的游戏中,那么带给玩家的沮丧感和愤怒将远超过现实中的堵车现象。当游戏中的一个士兵接到玩家的命令要从基地的一侧移动到另一侧以帮助抵抗敌人的进攻时,它需要一个计划(更明确的说是一条寻找出来的路线)使它能够到达目的地,但很可能在它移动的过程中预定的路线上出现了变化(例如玩家让工人们在士兵的必经之路上修建一个建筑或另外一批士兵出现在路上,从而堵塞了道路),这时如果没有一个优秀的移动系统,那么之前的寻道工作等于是白费工夫。

  本文将介绍一种相当有效的个体移动系统,以从另一个角度探讨游戏中的自动移动问题。虽然本文主要是针对即时战略类型,但所介绍的方法可以很容易的扩展到其它的类型中使用。这里查看原文。

  一些需要解决的问题

  在进一步深入到我们的移动系统中之前,我将先简介一下人们在解决移动问题上遇到的一些问题,这些问题是消耗最少的CPU时间的同时达到最佳的智能效果和最高的移动精度的关键。

  首先让我们对比一下同时移动单个对象与同时移动数十、数百个对象的不同。一次移动一个对象是非常简单的,但是一个可以相当完美的移动单个对象的算法并不一定能很好地解决数百个对象的同时移动,这其中最大的问题就是CPU时间的消耗。请一定要切记如果你要制作一个需要同时移动大量个体的游戏程序,那么在CPU的使用上一定要非常的保守。

  某些移动算法是很依赖CPU速度的,这就是那些要同时移动大量个体的游戏中只有很少的一部分支持高级的移动方式(例如个体的加速和减速)的原因。玩家们总是认为游戏中的大船和被重装备武装起来的战士们应该具有能够加速和减速的能力,这样才能体现出真实性,但是这小小的真实性将会增加超乎想象的额外的CPU计算工作。这种情况下事实上用来处理个体移动的时间增加了,因为你不得不花费更多的时间来计算加速度,从而获得新的速度值。在后面我们扩展例子程序到处理移动的预操作部分时,你将清楚地看到这样的工作所增加的计算复杂度有多大。另一个将会大大增加CPU计算量的问题是个体的转动半径。大多数寻道算法都不考虑个体的转动半径(转动半径是指一个个体原地旋转一周所需的最小圆的半径,因为我们不可能让一个士兵倒退着走上半张地图去袭击敌人的基地,所以经常需要个体进行旋转)对道路选择的影响。于是就会出现一种情况,虽然我们的一头大象已经找到了一条通往目的地的通路,但是它却不能沿着这条路线移动到目的地,因为路线中的一个拐角面积要比大象的转动半径小一些。大多数移动系统通过减缓个体的速度,再作出一个缓慢而更节省空间的转身动作(相信很多人都看到过即时战略游戏中士兵在拐角处被堵住时所作的动作吧)来解决这一问题,但这种方法会极大的增加CPU的计算量。

  正如大家所想象的那样,即使是即时战略游戏也并不是即时的,而是不断的进行循环,在每次循环中处理游戏的全部数据和玩家的指令(我们可以称它为UL-Update Loop)。为了增加游戏的性能,一般采用的方法是记录上一次UL的所消耗的时间,以预测下一次UL大约将要花费的时间(为了能尽可能地逼近即时处理,这样做是很有必要的),但是这就给个体的移动带来了大问题--每次UL中个体的移动距离很可能是完全不同的,下面的图1就是这种情况的一个例子。负责个体移动的算法显然在面对每次移动相同距离时比每次都要为所有移动个体计算不同的时间下移动出的不同距离并将其显示出来要轻松得多。当然,如果游戏的UL系统制作的非常优秀,那将略微改善一点这样的窘境。

  不要忘记处理个体移动中的碰撞问题。一旦你的游戏中的士兵们碰撞到了一起,你要怎样将他们分开呢?一种方法是是个体之间根本不发生碰撞,但在实际的应用中这是不可能做到的。不仅仅是实现这要求的程序代码非常难写,而且无论你写再多的代码也是无用的,这些个体总是会找到一些途径来使彼此重叠在一起,而在更多的情况中,这些个体的重合是必须的。一些使用近距离兵器进行战斗的游戏,例如《帝国时代》,就是一个要求个体重合的实例。另外,如果你要限制你游戏中的个体不能碰撞在一起,那么他们很可能为了避开彼此而离开预设的移动路线,暴露在其它对手的攻击之下,受到意外的伤害,这会使玩家对你的游戏极端不满。因此你必须决定好你的那些个体相互靠的有多近,重合多少是可以容忍的,还要设法处理由这些决定所带来的问题。

  注意考虑地图的复杂性。在复杂的地图上实现良好的移动的算法比简单地图上做到同样效果的算法要复杂得多,由于现在地图越来越倾向于复杂和真实,对于移动算法的要求也进一步提高了。

  随机生成的地图可能造成意想不到的问题。由于不能通过给固定道路编写预定路线来减小寻道的难度,因此随机生成的地图在复杂性上要更高于预设地图,尤其对于寻道而言。当寻道变得使CPU负担过重时,唯一的解决方法是降低寻道的精度和质量,这时就要求提高移动算法的质量以弥补寻道上的不足造成的程序反应迟钝。

  一定要认真处理各类个体的体积和由此产生的空间问题,这个问题最能说明你所需要的移动算法的精度。如果你的程序中需要移动的物体很少,以至于几乎不会出现彼此互相碰撞的情况(例如大部分的第一人称射击游戏),那你可以放心地使用一些简单的算法。如果你的程序所要处理的移动物体非常多,并且还要处理彼此的碰撞和诸如检测两个个体之间的缝隙是否足够更小的个体从中间穿过等等操作,这时对你的移动算法在精确度上和质量上的要求将会使计算量大幅度的增加。

一个简单的移动算法

  让我们从一个简单的对个体状态进行处理的移动算法的伪码开始,这个算法所作的只是简单的使用一条给定路线前进,当遇到碰撞和冲突时重新寻道,因此它能在2D和3D游戏中都表现得很出色。使用该算法,我们将从一个给定的状态开始,持续循环直到找到一个可行的位置作为中继点作为个体移动的目标去接近之后才跳出循环。移动状态将会在整个UL中保存下来,这将使我们能够正确的设置未来的状态,例如"自动"添加中继点。这种保存机制可以保证减少一个个体在下一次UL中作出与当前UL移动相反的判断的可能性。

  我们假定被给定了一条通向目的地的路径,并且这条路径在提供给我们时是精确的,并且是可行的(也就是不会发生碰撞)。由于大多数即时战略游戏拥有巨大的地图,以至于一个个体可能要花费几分钟的时间来走完整个路程,而在这几分钟里地图上可能发生使当前路径不在可用的变化(例如在路径上新建了建筑)。为了解决这个问题,现在我们加入少许碰撞检测,一旦遇到碰撞,就进行重新寻道。在后面你将看到,我们可以采取几种方法来避免重新寻道,以减少CPU消耗。

碰撞检测

  所有碰撞检测系统的基本目标都是判断两个个体是否发生了碰撞。这次我们所介绍的碰撞检测都是假设两个物体的碰撞,将来我们会专门介绍大量物体互相碰撞的检测问题。但无论是两个物体还是多个物体发生碰撞有一点是共同的:每个物体都要收到碰撞信息以便作出适当的响应(分开彼此)。

  大部分即时战略游戏中使用的简单的碰撞判断实际上是将每一个个体看作一个球体(2D中是圆),再进行简单的球体碰撞检测,而不在意这样简单的判断是否能够满足游戏对于碰撞的要求。这样做确实有利于提升性能,即使一个游戏要进行非常复杂的碰撞判断--例如判断击出的拳头是否击中敌人,或者甚至是低精度的多边形(polygon to polygon)交叉判断,这时为可能出现的碰撞保有一个球体区域往往也能够提升程序的性能。

  当我们设计一个碰撞检测系统时要注意面对3种截然不同的实体:单个的个体、一群个体的集合以及经过队形编制的个体群(就像《帝国时代2》中的阵形一样,见图2)。事实上对所有这3种类型使用简单的球形判断都能工作得很好,单个个体可以简单的使用单个球体进行它的全部碰撞判断,而对于其它两种情况只需要再稍微增加一点工作量。

  对于一群个体的简单集合来说,可以接受的碰撞检测下限是对整个组中的每个个体进行检测。这种方法将允许那些不属于你所选定的组的个体轻松的混入你的组队中。相对来说,对编队所要进行的碰撞检测就要更加复杂一些了。我们还应该认识到这种简单的组群还有一种特殊的性质决定了我们应该尽可能的简化对它所采用的碰撞检测方法--这种组群应该能够随时随地的将排列方式变换成任何可能的适应当地空间大小的阵形。

  对于编队来说,不仅仅要进行如上面的组群那样简单的个体碰撞检测,还要进行大量更加复杂的检测操作。首先要保证编队中的个体之间不能互相碰撞,同时如果编队中的各个个体之间有一定的缝隙,还要保证任何一个不属于该编队的个体不能占用这一空间。另外,一个编队应该不能改变队形或重组,但是游戏的规则又可能规定当没有足够大的通路提供给整个编队保持队形穿越某一障碍物时,编队可以先散开,待各个体越过障碍物之后再重新组成编队,这样的设计更加体贴玩家。

  我们可以尝试使用基于时间的碰撞描述机制。立即碰撞用来描述当前正发生在两个个体之间的碰撞;未来碰撞用来记录预计在程序运行的后续时间中将会发生在预定地点的碰撞(当然,前提是将要碰撞的对象都不改变各自的移动路线)。在任何情况下立即碰撞的情况都应该比未来碰撞的情况更优先被处理。同时我们也应该定义碰撞的3种状态:未处理的、正在处理和已处理完毕的。

使用"离散"的算法达到"连续"的效果

  大多数移动算法从根本上都是"离散"的,不同于数学上的离散定义,这里所说的"离散"指移动算法在按照给定路径从A点移动到B点的过程中从不考虑中间路径上可能出现什么东西,相反,在"连续"的算法中就会考虑这些情况。这样做的一个问题就是当我们进行一个Internet游戏时(众所周知,由于网络速度的限制,这类游戏的UL时间一般较长)那些速度较高的个体很可能在一次UL时间中移动相当大的一段距离(由于UL时间变长),而当这样增长的UL连续出现时很可能出现个体跃过了其它本应发生碰撞的个体。如果这样的情况出现在一个工人的身上那并不会有人在意,但显然任何玩家也不会希望敌人能够从辛苦建设的城墙中穿越而过进而攻击玩家的基地(某些早期及时战略游戏中出现的"穿墙"的BUG就是有这种问题造成的)。大部分的移动系统现在采用限制个体移动距离的方法来对付这一问题,该方法可以有效的简化所需的处理。在离散型的移动算法中解决这类问题的方法如下图所示:

  一种有效地解决方法是将一次移动拆分成多次移动的集合。这种拆分需要满足一定的移动距离上的要求,这要求就是要保证每次移动的距离刚好短于任何个体的长度,这就可以保证不可能有任何个体移动到当前个体的路径上来,从而避免了从其它个体之上跨越过去的情况。当每次这种拆分后的移动结束时我们就要使用碰撞检测系统对个体的当前位置进行碰撞检测。你可能会想到如此频繁的计算大量点的碰撞信息将会极大的增大系统消耗,没关系,在后面的章节中我们将会介绍一种方法来降低这种计算对系统的消耗。

  另一种方法是创建一种称之为移动线路(Move Line)的对象。我们可以使用这条移动线路来描述个体的移动,个体的原始位置作为线段的起点,目的地作为线段的终点,就好像《红色警报2》里所表现出来的那样。这种方法并不用添加新的数据,但是会加大碰撞检测部分的复杂性--我们必须把简单的球形碰撞检测修改为对一个点到一条线段的距离的检测,而这样做将会增加计算的难度,也会消耗更多的时间。大多数3D游戏都已经实现了一种可以快速挑选出可被游戏者观察到的物体的分级系统(也就是能够迅速地判断出游戏中的哪些物体处于玩家角色的视野之内的系统),我们可以对该类系统进行修改,使它们可以用来快速挑选出那些在我们的游戏中可能发生碰撞的个体。这样做的好处是大幅度地减少了需要进行碰撞判断的个体的个数,于是所需要的计算量常常就能够降低到所能允许的范围之内了。

位置预测

  经过上周的工作,我们已经有了一个简单的移动算法和一个管理个体碰撞的列表,还有什么工作是强化个体之间协作所必需的呢?位置预测(Position prediction)。

  预测的位置只不过是一个位置列表(至少包含个体的运动方向和时间标记,有时也需要记录加速度等信息)以指出未来某时刻个体所在的位置,参考图4。一个移动系统可以将用来实现个体移动的算法拿来负责计算个体的位置预测,这些预测越准确其可用性也就越大。当然预测计算也会增大计算量,为了不降低游戏的效率,下面我们就来讨论一下如何减少多余的CPU消耗。

  显然,最方便的优化方法是避免在每一帧中重复计算每一个已经预测过的个体位置。一个简单的移动列表可以实现这样的目的并且能够工作得很好:你可以在每一帧中从表内删除当前的位置,并向表内添加新的预测位置以维持列表长度固定(见图5)。虽然这一方法并不会减少个体开始移动时创建整个列表的计算量,但可以保证在剩余的移动过程中维持固定数量的计算。

  下一种优化方法是设计一种能够处理点和线的位置预测系统,由于我们的碰撞系统支持处理点和线,因次添加这一功能将是很容易的事。如果一个个体按照一条直线进行移动,那么我们可以利用当前个体位置、预测位置和个体运动半径来指定一段移动的轨迹及范围。然而如果个体正在进行一次圆运动那么整个处理就会略微复杂一些。当然你可以将这种运动过程作为一个函数保存起来,但这显然会加大系统的负担。作为替代可以尝试通过对圆上的点进行取样来作出正确的位置判断(见图6)。最后,再次建议一定要使用能够实现对点和线的无缝交替处理的预测系统,以便在任何可能的情况下通过使用直线来减少对CPU的耗用。

  最后所要介绍的一种优化方法非常重要,但同时也可能有一些不够直观,不能简单的看出其优化作用。如果我们要使用这样的预测系统,为了尽可能少的消耗资源,显然不应该在计算了一次预测未知之后再进行一次计算来移动个体。因此解决的方法是精确地进行位置预测,并最终使用该位置移动个体。这样我们就能对每个个体的移动只计算一次并且除了前述的开始移动时的计算之外没有其他多余的计算开销。

  在实际的应用中,你可能每次只能进行一个UL时间的计算来进行位置预测,这时要注意未来每次UL的时间很可能并不等长。如果只是简单的按照预测位置移动个体而不考虑每次UL的长度,这将有可能造成一些问题,当然某些游戏(或者游戏中的某些类个体)可以很好的适应这样的操作。一般的游戏都通过每次对列表中的数据进行一些修正来改善预测的准确性,而这样做的同时也应注意何时应该完全抛掉原来计算的已与现在情况有较大误差的预测而重新计算整个列表。

  实际对位置预测的应用中主要的难题是由于我们在碰撞检测中将这些预测的位置做为个体的当前位置来使用所造成的。你将很容易的看到对某给定的区域内个体预测位置的比较所需要消耗的计算量,但是为了很好的实现个体间的协作我们必须知道未来一小段时间内每个个体的目的地以及它们可能会与哪些其它个体相碰撞,这都需要一个优秀而且快速的碰撞检测系统。此时最佳的优化措施就是如同前面所述使用3D引擎中的相关部分舍去那些不大可能碰撞的个体组合,这将允许你使用更多的CPU时间来处理最可能发生的那些碰撞。

个体之间的合作

  我们已经建立了一个复杂的系统来确定个体未来的可能位置,它支持3D移动,同时对计算量的提升也并不比一个简单的方法多多少,重要的是该方法提供给我们一个记录了一个个体在未来一小段时间内移动所需的一切信息的列表,这正是我们所需要的。现在我们可以进入较为有趣的部分了。

  如果我们的工作做得很好,那么我们所要处理的绝大部分碰撞将是未来的碰撞(因为我们已经使用位置预测尽量地避免了立即碰撞)。由于处理未来碰撞最后的方法将是停止移动并重新寻道,因此为了不使寻道过于频繁,尽量使用其他方法解决碰撞就变得很重要。

  下面就详细的介绍对于这种个体与个体碰撞的方法。

未处理的碰撞:

CASE 1:if 个体已经全部停止移动:

1. if 是低优先级的个体,什么也不做

2. if 是高优先级的个体,找出哪一个个体将要移动(如果存在),告知该个体进行尽可能最短的移动来解决碰撞,改变状态为正在处理的碰撞

CASE 2:if 个体没有移动,是另一个个体将要移动,什么也不做

CASE 3:if 当前个体正要移动,其它个体已经停止

1.if 是高优先级个体,其它停滞个体为低优先级并且能够从通路上移开,计算出下一步的位置并通知低优先级的个体从通路上移开(见图7),改变状态为正在处理的碰撞

2.Else,if 可以避开另一个个体,避开他以解决碰撞

3.Else,if 是高优先级个体并且能够沿移动路线推动低优先级个体,推动它,改变状态为正在处理的碰撞

4.Else,if 停下,重新寻道

CASE 4:if 当前个体正在移动,另一个个体也在移动:

1.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中低优先级个体并且高优先级个体已经抵达预计地点,恢复移动速度,碰撞解决

  解决碰撞的关键之一是排定个体优先级的顺序,如果没有一套强壮的完好定义的优先级体系,你将看到碰撞在一起的个体有如旋转木马一般运动,因为每个个体都要求对方让出道路,而同时又没有一个个体能拒绝这个要求。我们也应该为碰撞进行分级,在处理时应该优先处理那些有最高优先级的碰撞,当有足够富裕的时间时再去处理那些优先级低一些的碰撞。在游戏中碰撞处理也需要注意碰撞个体的密度。如果一场大型的战斗使得许多的战士在狭小的空间中碰撞在一起,那你就应该花费更多的CPU时间来处理这些碰撞而不是地图上远处两个矿工间的碰撞。对这类较容易发生碰撞的区域的关注的另一好处是你将能够在其它个体进行寻道时使它们避过这类区域。

计划编制基础

  计划编制是个体协作的关键,虽然我们尽可能地提升预测和计算的精确性,但是显然事情总是会出错的。例如我们在《帝国时代》中所犯的一个错误是我们总是在一帧的时间内使个体作出移动的决定,虽然这样的决定多数是正确的,但我们并没有在以后的UL中参考它。这样就造成了一个问题:个体对移动路线作出了决定,实行时发现出现问题必须重新决断,结果是使个体再次返回它的出发点。计划编制可以有效地避免这类问题。我们保存一定数量的个体以前移动中所遇到的障碍和碰撞的解决步骤(由其它的游戏细节定义),这就为我们未来遇到困境时提供了参考。举例来说,当我们要避免一次碰撞时我们将存储哪一个个体是我们所要闪避的。由于我们要设定一个可行的计划,没有任何理由对碰撞中的另一个体进行碰撞检测,除非其中的某一个个体得到了新的命令或发生其它类似的变化。一旦我们完成了闪避,就可以为其它的个体恢复正常的碰撞检测了。在下面的扩展中,你将看到我们将反复利用这一思想来达到我们的目的。

一些简单扩展

  游戏编程的乐趣之一就是要不停地创新来开发新技术以使设计人员能作出更优秀的游戏。在即时战略游戏中,越来越多的开发人员希望能够在他们下一批作品中加入对编队的处理能力。在这里我不会介绍现在那些低技术含量的移动方法,我所要讨论的是如何协调编队的移动,使每一个个体都能在智能的维持编队队形的同时在地图上随意的移动。

组队(Group)移动

  首先要弄清楚何谓组队(Group):由用户(玩家)为方便操作而选取的简单的个体集合(一般会对其成员发布相同的命令),除了在移动时要保持成员一同移动之外组队并没有其他对移动系统的限制。组队的使用使我们必须记录许多信息,例如组队成员的列表以及当整个组队还在一起时所能移动的最大速度。也许我们还应该保存整个组队的中心,以作为一个可以很容易得到的操作参考点。同时还应该选定一个组队的指挥者,大多数游戏中怎样选出这个个体并不重要,重要的是一定要有一个这样的个体。

  在我们开始工作之前有一个问题需要回答:当组队在地图上移动时我们有必要保持所有个体在一起吗?如果不,组队将只是为使用户方便操作而存在的,每一个个体都会独自寻道和移动就如同用户对每个个体分别下达指示一样。当我们关注如何加强组队的管理时,我们可以发现组队的凝聚力可以分为多个等级。

组队中的个体都以相同的速度移动。一般地这将使用组队中速度最低的个体的最大速度,不过有时让那些速度较慢的个体在组对中移动的稍快一些会更好(见图8)。然而一般游戏的设计人员给一类个体较低的速度总是有原因的,例如如果允许强力的个体能够非常高速的在地图上移动将会极大的破坏游戏的平衡性。

组队中的个体都以相同的速度移动并使用同一条路径。这种方法可以有效的避免当组队中一半的个体从森林一侧前往目的地时另一半却从另一侧移动(见图9),稍后你将看到实现这一方法的一条简单途径。

组队中的个体以相同的速度移动,使用同一条路径并同时抵达。这是最复杂的组队组织方式,它不但要求达到上述两点,并且还要求位于前面的个体能够等待落在后面的个体追上来,有时还要给后面的慢速个体短时间加速以使其能够追上前面的个体。

  怎样才能实现最后的要求?这要使用一种分级的移动系统,这样我们就能在处理每个个体的移动时兼顾那些同属于某个组队的个体了。如果我们对组队的个体创建一个组队对象,我们就能够记录所有必需的数据,为整个组队计算最大速度,以及判断何时需要前面的个体等待后面的个体。下面就是一个组队类的简单定义:

Listing 2. BUnitGroup.

//*****************************************************************************

// BUnitGroup

//*****************************************************************************

class BUnitGroup

{

public:

BUnitGroup( void );

~BUnitGroup( void );

//Returns the ID for this group instance.

int getID( void ) const { return(mID); }

//Various get and set functions. Type designates the type of the group

//(and is thus game specific). Centroid, maxSpeed, and commander are

//obvious. FormationID is the id lookup for any formation attached to

//the group (will be some sentinel value if not set).

int getType( void ) const

{

return(mType);

}

void setType( int v )

{

mType=v;

}

BVector& getCentroid( void ) const

{

return(mCentroid);

}

float getMaxSpeed( void ) const

{

return(mMaxSpeed);

}

int getCommanderID( void ) const

{

return(mCommanderID);

}

BOOL getFormationID( void ) const

{

return(mFormationID);

}

BOOL setFormationID( int fID );

//Standard update and render functions. Update generates all of the

//decision making within the group. Render is here for graphical

//debugging.

BOOL update( void );

BOOL render( BMatrix& viewMatrix );

//Basic unit addition and removal functions.

BOOL addUnit( int unitID );

BOOL removeUnit( int unitID );

int getNumberUnits( void ) const

{

return(mNumberUnits);

}

int getUnit( int index );

protected:

int mID;

int mType;

BVector mCentroid;

float mMaxSpeed;

int mCommanderID;

int mFormationID;

int mNumberUnits;

BVector* mUnitPositions;

BVector* mDesiredPositions;

};

  BGroup类在其内部管理整个组队中个体之间的交互操作。在任何时间点,它都应该有一个时间表以来处理组队内的个体之间的碰撞,它也应该有能力通过参数和优先级管理来控制或修正个体移动。如果你的游戏只支持一种移动优先级,那么你就应该为你在组队中的个体们添加第二种优先级。虽然一个组队对外的表现似乎只有一种优先级,但在其内部还是应该分为不同的移动优先级。基本上来说,BGroup类是另一个完善的封闭的移动系统。

  组队的指挥者将负责整个组队的寻道工作,它将决定整个组队的移动路线,在简单的组队移动系统中所需的工作只是由这个个体本身来寻道即可。然而在下面的部分中我们将看到指挥者所能够作的其它事情。

编队控制基础

  首先应该给出编队的定义:编队(Fomation)是一种更复杂的组队,编队有自己的方向(前方、后方、左翼和右翼)。编队中的每一个个体都试图保持自己在编队中的位置,而这个位置是唯一固定的也是相互关联的。更加复杂的模型使得编队中各个个体的朝向需要单独处理,而同时也要求在移动中提供整体旋转的方法。

  编队是建立在组队系统之上的,它是一种限制更加严格的组队,因为我们必须非常详尽的规定编队中每个个体的位置。所有的个体在移动中必须保持一起行动,并要求在速度、路径上一致以及相互之间的位置和距离保持不变--如果在移动中编队出现了大间距的缝隙,那么它也就与组队没有什么不同了。

  下面给出的这个BFomation类能够清晰的管理一个编队的预定位置(我们要求编队中的每个个体所处的位置以及它的方向)、编队方向和编队的状态。大多数游戏中所使用的编队都是预先定义的,显然,在开发过程中进行这项工作是很简单的(通过使用一些非专业人员也能熟练操作的文本编辑器就可以很好的完成这项工作)。我们当然希望能在游戏过程中实时的定义编队,但这样做就需要更多的内存以保证每一个由玩家定义的编队都能在内存中保留一份自身定义的副本。

Listing 3. The BFormation Class

//*********************************************************

// BFormation Class

//*********************************************************

class BFormation

{

public:

//The three formation states.

enum

{

cStateBroken=0,

cStateForming,

cStateFormed

};

BFormation( void );

~BFormation( void );

//Accessors for the formation’s orientation and state. The expectation

//is that BFormation is really a data storage class; BGroup drives the

//state by calling the set method as needed.

BVector& getOrientation( void )

{

return(mOrientation);

}

void setOrientation( BVector& v )

{

mOrientation=v;

}

int getState( void ) const

{

return(mState);

}

void setState( int v )

{

mState=v;

}

//The unit management functions. These all return information for the

//canonical definition of the formation. It would probably be a good

//idea to package the unit information into a class itself.

BOOL setUnits( int num, BVector* pos, BVector* ori, int* types );

int getNumberUnits( void ) const

{

return(mNumberUnits);

}

BVector& getUnitPosition( int index );

BVector& getUnitOrientation( int index );

int getUnitType( int index );

protected:

BVector mOrientation;

int mState;

int mNumberUnits;

BVector* mPositions;

BVector* mOrientations;

int* mTypes;

};

  使用这个模型,我们必须时刻关注编队的状态。cStateBroken表示编队并没有被创建也没有创建的企图;cStateForming表明我们的编队正在建立但还没有达到cStateFormed状态;一旦所有的个体都已位于它们的预定位置,我们就可以将状态改变为cStateFormed。为了使编队的移动简单化,我们可以使一个编队在完成组建之前(达到cStateFormed状态之前)不可移动。

  当我们准备使用一个编队时,第一件工作就是组建这个编队。当给定一个编队时,BFormation(译者注:原文这里是BGroup,但该类并没有编队管理功能,经过反复推敲认定为编写错误)控制每个个体移动到编队中的预定位置,该位置的计算是与当前编队方向相关的,如果这个方向发生了变化,那么预定位置将自动被重新计算并修正为正确的位置。

  为了组建一个编队,我们可以使用预定安置--每一个预定位置拥有一个预设值(由定义规定或由算法确定)来指明个体组建编队时应该按照那种顺序进驻那些预定位置,这样才能使整个组建过程从里到外进行得相当有条理(见图10)。下面的算法列表说明如何实现这样的组建方式。

Listing 4.

设置组队中的所有个体移动优先级到一个相同的低优先级

设置状态为cStateForming

While 状态为cStateForming

{

找出离编队中心最近的未有个体占据的位置

If 没有个体再可用

设置状态为cStateFormed,跳出组建循环

选定一个个体前往所找出的位置,要求满足如下条件:

使个体移动距离最短

与其它编队成员碰撞的几率最小

移动时间最短

设置个体的移动优先级到中等值

等待直到个体就位(可能要经过多个UL时间)

设置个体移动优先级为最大值,这样做可以保证以后进行组建工作的个体不会使该个体离开其位置

}

  现在我们所有的战士都已经就位了,接下来做什么呢?我们可以开始移动他们以穿过整个地图,我们可以假定寻道系统找出了一条以当前编队的形状和大小可以通过的路径来抵达目的地(见图11),如果没有这样一条路经那就必须对整个编队进行操作(不久我们就会探讨这个问题)。当编队在地图上移动时我们需要选出一个指挥者来控制整个移动,当指挥者沿路径前进并改变方向时其它所有编队中的个体都要改变方向以追随它,这种操作一般被称为flocking(聚集)。

  我们有两种方法处理编队的方向改变:忽略这种改变或者转动编队的方向。忽视方向的改变是简单的而且对于那些盒状的编队来说是非常合理的。

  对编队进行旋转并不会增加多少复杂性而同时对于某些编队方式(如直线形)来说是非常合理的。进行编队旋转时首先要做的是停止编队移动,完成方向的旋转之后我们要重新计算每个预定位置,然后回到cStateForming状态(见图13),使个体前往新的位置并在完成这一工作之后设置状态为cStateFormed,这样我们就可以继续原来的移动。

高级编队管理技术

  现在我们已经可以将编队在整个地图中移动了,但是由于游戏的地图是动态而且复杂的,所以很可能出现选出的移动路径不可用的情况。如果这样的事情发生,就需要我们对编队进行操作,一般的操作方式有3种,下面就逐一介绍。

缩放个体间距(Scaling unit positions).由于编队中的预定位置都是由矢量进行定义的,因此我们可以很方便的对整个编队的间距进行放缩以使它变得更小,这就使得编队能够通过城墙或树林中更小的缝隙(见图14)。这种方法对于那些排列得较为分散的编队很有效,但对于那些排列紧凑的编队就没有什么用处了。

简单的障碍回避(Simple ordered obstacle avoidance).如果我们在移动编队时遇到与其它游戏中实体相碰撞的情况时(无论是当前还是未来碰撞),我们可以假设即使有这样的碰撞发生,原来寻到的道路仍是可用的。简单的解决方法是沿着编队前进的路线找出第一个不再发生碰撞的位置,并在该位置完成编队的重组(见图15)。这样我们的步兵团就可以先分散,带穿越障碍物之后再在另一侧重新组建编队。在使用这一方法时一定要注意有时障碍物的范围非常大,以至于编队的重组工作必须在走出很远之后才能做,这时就得考虑是否应该重新寻道了。

二分和重组(Halving and rejoining).虽然简单的回避能够工作的很好,但是会降低玩家对整个编队穿越地图的感觉,相对来说二分法可以很好的保持住编队所带来的视觉冲击。当我们的编队在前方遇到一个障碍物时我们可以找出编队中的一个拆分点,从该点将编队一分为二,这两个编队分别通过障碍物之后再前进到重组位置恢复成一个编队(见图16)。这种方法只增加很少的计算量,但却能为编队移动带来良好的视觉效果。

路径栈

  路径栈就是一种简单的用来记录个体移动路由信息的栈操作(后进先出,见图17)。一个路径栈记录的信息一般包括个体当前所采用的路线,现在个体正在向哪个中继点移动以及个体是否正处于巡逻中。一个路径栈对我们的目的有两大作用。

  首先,它可以为一次分级寻道工作提供便利。一般来说游戏开发者会把寻道区分为两种明显不同的等级--高级(high-level)和低级(low-level)(见图18)。高级的寻道可以为个体找寻出穿越地图上不同地形和主要堵塞地点的道路,这就如同玩家大多数时候给个体制定的路径一样。低级的寻道则同时还会处理较小的障碍物并更注重处理细节。一个路径栈可以方便的存储高级和低级的寻道信息。我们可以先通过高级寻道找出一条路径并把它存储进路径栈中,而当我们必须注意避免与大片空地中的一颗树发生碰撞时就可以将低级寻道的一系列结果存入路径栈顶并执行它们。每当执行完一条路径,我们就可以将它从栈中弹出并继续执行现在位于栈顶的路径。

  第二点,路径栈可以允许高级寻道被重用(reuse)。如果你回顾前面的介绍将看到组队和编队在移动时的一大要素就是所有的成员都使用相同的路径来移动到目的地。如果我们设计的路径栈可以允许多个个体参考一条路径,那就将使同一条高级寻道路径很容易的被重用。一个编队的指挥者将使用高级寻道找出一条路径并把它拷贝给编队中的每个个体,而其它个体则什么也不用做。

  这样创建保存路径信息的结构还能提供给我们一些其它好处。通过将一条高级寻道路径拆分成多个低级寻道路径,我们可以在执行具体路径之前充分的对这些低级路径进行更精确的计算。而且如果我们确定高级寻道的结果是可用的话,也可以将低级寻道的工作略微推后再做。如果我们正在进行高协调度的个体移动,路径栈将允许我们向栈顶添加一条临时的用来避免碰撞的路径,并能够很好的使用这一路径修正个体移动(

解决混合碰撞

  我们定义混合碰撞为同时发生在两个以上个体之间的碰撞。大多数游戏都对于可以解决的混合碰撞中的个体个数有限制,超过这个数目就只能分几次解决了。下面我们将探讨如何使用已有的移动系统对这类情况进行简单的处理。

  如果我们遇到了一个由三个个体造成的混合碰撞(见图20),首要的工作是找出其中优先权最高的个体。一旦找到它,我们就要立即找到与之碰撞的另一个个体并确定优先权最高的个体所遇到的最主要的碰撞(该碰撞可能发生在其与次优先的个体之间,也可能不是)。当我们找到了这两个个体后,剩下的工作就交给原来的碰撞处理部分解决即可了。

  当最初的两个个体的碰撞被解决后,我们就要重新评估整个碰撞并更新个体之间的关系。一个更复杂的系统可以很好的解决这样的问题,但是如果简单的移走已经解决了碰撞问题的个体也能得到不错的效果。

  一旦我们更新了碰撞中的个体,下一步工作就又回到寻找优先权最高的个体的碰撞上来了。我们将一直重复这一步骤直到所有的碰撞都被解决。

  你可以在两个地方使用这一系统:碰撞解决部分或碰撞预测系统中。碰撞解决的规则必须被修改以适应对于个体优先级的要求,这样的修改并不难,但会增加一定的代码量。或者你可以修改你的碰撞预测系统使得只会发生两个个体碰撞的情况,然而这样做你仍然需要先找出一次碰撞中的全部个体并作出操作。

解决堆叠峡谷问题(The Stacked Canyon Problem)

  所有移动系统的最终目的都是要实现智能的移动效果,而所有处理中最能体现智能的就是处理堆叠峡谷问题了(什么是堆叠峡谷问题呢?事实上当一个个体要从一群排列紧凑的个体之间穿过时所需要解决的问题就是堆叠峡谷问题,图21就是一个例子)。虽然此类问题并不能简单的一次解决,但我们可以重用前面的一些简单方法来解决它。

  第一步是鉴定是否为一个堆叠峡谷问题。这是非常重要的,因为我们将要利用前进个体(driving unit)的优先级。当然我们可以利用每个个体自身的优先级来要求其它个体让出道路,但是更好的解决方法是使用前进个体的优先级。判断一个堆叠峡谷问题可以有两种方法:观察前进个体是否会把一个阻碍其移动的个体推到另一个身上或者观察移动个体的碰撞列表以寻找多重的碰撞。不论采用哪种方法,被推动的个体都应该拥有与前进个体相同的移动优先级。

  一旦我们判断出将要解决一个堆叠峡谷问题,就可以采用一种简单的递归调用个体协调运动系统的方法来解决它。把第一个被推动的个体作为前进个体处理其于第二个个体之间的关系,并如此循环。每一个个体被它的前进个体推动直到它可以移动到一边而让出道路。当最后一个个体也从陆上让开后,原来的前进个体就可以继续移动了。

  一个好的习惯是将已经移开的个体在移回原位。为了能够这样做,我们应该记录整个推动过程并在问题解决后倒序的执行该过程。另外如果负责移动的代码能够辨别出前进个体是否归属于一个组队,那就能保证组队中每个个体都能在原来阻碍道路的个体返回原位置之前通过。

注意

优化你的整体系统。如果你只是要做一个2D游戏,那就会有许多多余的计算是可以取消和简单化的。不论你是要做2D游戏还是3D的,你的碰撞检测系统都需要一个优秀的经过优化的个体分拣系统,这类系统已经不再仅仅用于绘图了。

对高级寻道和低级寻道使用不同的方法。过去大多数游戏对这两种寻道方式使用相同的算法。这样做的害处是如果对高级寻道使用低级寻道的算法将使高级寻道变得缓慢并且不能用于寻找长的通路;相反的,如果对低级寻道使用高级寻道的算法将会造成结果并没有将道路上的所有障碍物考虑在内或者造成一个个体能从其它个体之中穿过。一定要抓住要点制作两套寻道系统。

无论你做什么,个体总会交叠碰撞在一起。个体的交叠和碰撞是不可避免的,或者按最好情况说将是非常难以操作的。你最好尽早处理这些碰撞问题,这将使你的游戏更好一些。游戏的地图已经越来越复杂了,并且还会加入随机地图的处理。一个好的移动系统将能够很好的处理随机地图和相应的一切细节。

清楚地了解UL是怎样影响个体移动的。可变化的UL时间将是你的移动系统所必须解决的一大难题。可以使用一个简单的修正机制来解决此类大部分的问题。

只涉及单个UL的做法是过时的。没有计划的编制不可能解决好个体移动的协调问题,如果不记录上一次UL中的操作和将来要发生的问题又是不可能制作好的计划的。一个能够运作良好的移动协调系统必须在任何时候都能够参考以前的碰撞列表和预测碰撞的列表。切记解决碰撞的过程中出现的较小的变化是可以忽略的。

不要再出现傻乎乎的个体移动

  简单的个体移动是简单的。一套优秀的协调系统是我们所应该追求的,因为它能使你的游戏步上一个等级并能增加玩家的乐趣。在本次的文章中我们研究了一个移动协调系统的基础功能--使用多个UL时间制定行动计划以及一套可以解决任何两个个体碰撞的方法等等。现在你应该不会再满足于你的游戏中那些傻乎乎的个体移动了。

附:

本文的部分基本定义:

移动(Movement):

本文主要指对于一条已知路径的执行。简单的移动算法使个体沿给定的路线运动,复杂的移动算法在移动的同时进行碰撞判断,调整各个体的运动以避免碰撞并允许个体组成特殊的队形共同运动。

寻道(Pathfinding):

找出所需路径的工作。所使用的算法可以是简单的遍历也可以是经过高度优化的A*算法。

中继点(Waypoint):

当个体要前往目的地是所必须经过的路径上的点。每一条路径拥有至少2个中继点:起点和终点。

个体(Unit):

游戏中可以在整个游戏地图中移动的实体。

移动算法伪码:

移动循环

{

if "增加中继点(Waypoint)":

增加中继点。

if 正在巡逻:

沿指向目的地的方向获取下一个中继点

设置状态为"等待寻道"

else:

if 已经不能再得到下一个中继点:

设置状态为"已抵达目的地"

else:

设置设置状态为"等待寻道"

if "已抵达目的地":

作出适当的通知(如果存在通知方式的话)

移动完成,停止播放移动动画,退出函数

if "等待寻道":

找出一条通路并将之保存

if 无法找到通路

失败,函数退出

计算出向中继点移动所需的方向

通过旋转改变个体的方向指向中继点

使用新的方向,计算一次UL时间将会移动到的位置

if 新位置将会导致碰撞

设置状态为"等待寻道"

返回循环头

判断现在和移动后的位置:

if 移动之前距离中继点更近:

设置状态为"增加中继点"

返回循环头

if 移动过程中将会经过中继点:

设置状态为"增加中继点"

跳出移动循环

}

设置加速度

进行移动

设置并更新所需要使用的动画

刷新预期位置

全看分页树展 · 主题 跟帖


有趣有益,互惠互利;开阔视野,博采众长。
虚拟的网络,真实的人。天南地北客,相逢皆朋友

Copyright © cchere 西西河