即時戰略遊戲中如何協調對象移動

作者:Dave C. Pottinger
翻譯改寫:lzc


  在圖論中人們研究了通過怎樣的計算才能找到一條從A點到B點的通路,以圖論本身來說這已經解決了從A到B的問題,剩下的只是從A沿着找到的路線移動到B就可以了。這樣的認識基於一個默認的假設--道路中的一切障礙物都是固定的,但是在現在已經廣泛流行的即時戰略類遊戲中問題卻遠遠不止這些。舉個例子說上下班高峯期的時候,路上的每一個人都清楚地知道自己的目的地和所要走的路徑,但是由於某些個人不遵守規則或其他人爲原因還是會造成堵車現象。而如果這樣的事情發生在一個即時戰略的遊戲中,那麼帶給玩家的沮喪感和憤怒將遠超過現實中的堵車現象。當遊戲中的一個士兵接到玩家的命令要從基地的一側移動到另一側以幫助抵抗敵人的進攻時,它需要一個計劃(更明確的說是一條尋找出來的路線)使它能夠到達目的地,但很可能在它移動的過程中預定的路線上出現了變化(例如玩家讓工人們在士兵的必經之路上修建一個建築或另外一批士兵出現在路上,從而堵塞了道路),這時如果沒有一個優秀的移動系統,那麼之前的尋道工作等於是白費工夫。

  
本文將介紹一種相當有效的個體移動系統,以從另一個角度探討遊戲中的自動移動問題。雖然本文主要是針對即時戰略類型,但所介紹的方法可以很容易的擴展到其它的類型中使用。部分基本定義:


 

移動(Movement):
    本文主要指對於一條已知路徑的執行。簡單的移動算法使個體沿給定的路線運動,複雜的移動算法在移動的同時進行碰撞判斷,調整各個體的運動以避免碰撞並允許個體組成特殊的隊形共同運動。

尋道(Pathfinding):
     找出所需路徑的工作。所使用的算法可以是簡單的遍歷也可以是經過高度優化的A*算法。

中繼點(Waypoint):
    當個體要前往目的地是所必須經過的路徑上的點。每一條路徑擁有至少2箇中繼點:起點和終點。

個體(Unit):
    遊戲中可以在整個遊戲地圖中移動的實體。

 

一些需要解決的問題

  在進一步深入到我們的移動系統中之前,我將先簡介一下人們在解決移動問題上遇到的一些問題,這些問題是消耗最少的CPU時間的同時達到最佳的智能效果和最高的移動精度的關鍵。

  首先讓我們對比一下同時移動單個對象與同時移動數十、數百個對象的不同。一次移動一個對象是非常簡單的,但是一個可以相當完美的移動單個對象的算法並不一定能很好地解決數百個對象的同時移動,這其中最大的問題就是CPU時間的消耗。請一定要切記如果你要製作一個需要同時移動大量個體的遊戲程序,那麼在CPU的使用上一定要非常的保守

  某些移動算法是很依賴CPU速度的,這就是那些要同時移動大量個體的遊戲中只有很少的一部分支持高級的移動方式(例如個體的加速和減速)的原因。玩家們總是認爲遊戲中的大船和被重裝備武裝起來的戰士們應該具有能夠加速和減速的能力,這樣才能體現出真實性,但是這小小的真實性將會增加超乎想象的額外的CPU計算工作。這種情況下事實上用來處理個體移動的時間增加了,因爲你不得不花費更多的時間來計算加速度,從而獲得新的速度值。在後面我們擴展例子程序到處理移動的預操作部分時,你將清楚地看到這樣的工作所增加的計算複雜度有多大。另一個將會大大增加CPU計算量的問題是個體的轉動半徑。大多數尋道算法都不考慮個體的轉動半徑(轉動半徑是指一個個體原地旋轉一週所需的最小圓的半徑,因爲我們不可能讓一個士兵倒退着走上半張地圖去襲擊敵人的基地,所以經常需要個體進行旋轉)對道路選擇的影響。於是就會出現一種情況,雖然我們的一頭大象已經找到了一條通往目的地的通路,但是它卻不能沿着這條路線移動到目的地,因爲路線中的一個拐角面積要比大象的轉動半徑小一些。大多數移動系統通過減緩個體的速度,再作出一個緩慢而更節省空間的轉身動作(相信很多人都看到過即時戰略遊戲中士兵在拐角處被堵住時所作的動作吧)來解決這一問題,但這種方法會極大的增加CPU的計算量。

  正如大家所想象的那樣,即使是即時戰略遊戲也並不是即時的,而是不斷的進行循環,在每次循環中處理遊戲的全部數據和玩家的指令(我們可以稱它爲UL-Update Loop)。爲了增加遊戲的性能,一般採用的方法是記錄上一次UL的所消耗的時間,以預測下一次UL大約將要花費的時間(爲了能儘可能地逼近即時處理,這樣做是很有必要的),但是這就給個體的移動帶來了大問題--每次UL中個體的移動距離很可能是完全不同的,下面的圖1就是這種情況的一個例子。負責個體移動的算法顯然在面對每次移動相同距離時比每次都要爲所有移動個體計算不同的時間下移動出的不同距離並將其顯示出來要輕鬆得多。當然,如果遊戲的UL系統製作的非常優秀,那將略微改善一點這樣的窘境。

圖1.不同的UL時間將導致運動個體
每次UL中移動不同的距離

  不要忘記處理個體移動中的碰撞問題。一旦你的遊戲中的士兵們碰撞到了一起,你要怎樣將他們分開呢?一種方法是是個體之間根本不發生碰撞,但在實際的應用中這是不可能做到的。不僅僅是實現這要求的程序代碼非常難寫,而且無論你寫再多的代碼也是無用的,這些個體總是會找到一些途徑來使彼此重疊在一起,而在更多的情況中,這些個體的重合是必須的。一些使用近距離兵器進行戰鬥的遊戲,例如《帝國時代》,就是一個要求個體重合的實例。另外,如果你要限制你遊戲中的個體不能碰撞在一起,那麼他們很可能爲了避開彼此而離開預設的移動路線,暴露在其它對手的攻擊之下,受到意外的傷害,這會使玩家對你的遊戲極端不滿。因此你必須決定好你的那些個體相互靠的有多近,重合多少是可以容忍的,還要設法處理由這些決定所帶來的問題。

  注意考慮地圖的複雜性。在複雜的地圖上實現良好的移動的算法比簡單地圖上做到同樣效果的算法要複雜得多,由於現在地圖越來越傾向於複雜和真實,對於移動算法的要求也進一步提高了。

  隨機生成的地圖可能造成意想不到的問題。由於不能通過給固定道路編寫預定路線來減小尋道的難度,因此隨機生成的地圖在複雜性上要更高於預設地圖,尤其對於尋道而言。當尋道變得使CPU負擔過重時,唯一的解決方法是降低尋道的精度和質量,這時就要求提高移動算法的質量以彌補尋道上的不足造成的程序反應遲鈍。

  一定要認真處理各類個體的體積和由此產生的空間問題,這個問題最能說明你所需要的移動算法的精度。如果你的程序中需要移動的物體很少,以至於幾乎不會出現彼此互相碰撞的情況(例如大部分的第一人稱射擊遊戲),那你可以放心地使用一些簡單的算法。如果你的程序所要處理的移動物體非常多,並且還要處理彼此的碰撞和諸如檢測兩個個體之間的縫隙是否足夠更小的個體從中間穿過等等操作,這時對你的移動算法在精確度上和質量上的要求將會使計算量大幅度的增加。

一個簡單的移動算法

  讓我們從一個簡單的對個體狀態進行處理的移動算法的僞碼(僞碼如下)開始,這個算法所作的只是簡單的使用一條給定路線前進,當遇到碰撞和衝突時重新尋道,因此它能在2D和3D遊戲中都表現得很出色。使用該算法,我們將從一個給定的狀態開始,持續循環直到找到一個可行的位置作爲中繼點作爲個體移動的目標去接近之後才跳出循環。移動狀態將會在整個UL中保存下來,這將使我們能夠正確的設置未來的狀態,例如"自動"添加中繼點。這種保存機制可以保證減少一個個體在下一次UL中作出與當前UL移動相反的判斷的可能性。



  我們假定被給定了一條通向目的地的路徑,並且這條路徑在提供給我們時是精確的,並且是可行的(也就是不會發生碰撞)。由於大多數即時戰略遊戲擁有巨大的地圖,以至於一個個體可能要花費幾分鐘的時間來走完整個路程,而在這幾分鐘裏地圖上可能發生使當前路徑不在可用的變化(例如在路徑上新建了建築)。爲了解決這個問題,現在我們加入少許碰撞檢測,一旦遇到碰撞,就進行重新尋道。在後面你將看到,我們可以採取幾種方法來避免重新尋道,以減少CPU消耗。

碰撞檢測

  所有碰撞檢測系統的基本目標都是判斷兩個個體是否發生了碰撞。這次我們所介紹的碰撞檢測都是假設兩個物體的碰撞,將來我們會專門介紹大量物體互相碰撞的檢測問題。但無論是兩個物體還是多個物體發生碰撞有一點是共同的:每個物體都要收到碰撞信息以便作出適當的響應(分開彼此)。

  大部分即時戰略遊戲中使用的簡單的碰撞判斷實際上是將每一個個體看作一個球體(2D中是圓),再進行簡單的球體碰撞檢測,而不在意這樣簡單的判斷是否能夠滿足遊戲對於碰撞的要求。這樣做確實有利於提升性能,即使一個遊戲要進行非常複雜的碰撞判斷--例如判斷擊出的拳頭是否擊中敵人,或者甚至是低精度的多邊形(polygon to polygon)交叉判斷,這時爲可能出現的碰撞保有一個球體區域往往也能夠提升程序的性能。

  當我們設計一個碰撞檢測系統時要注意面對3種截然不同的實體:單個的個體、一羣個體的集合以及經過隊形編制的個體羣(就像《帝國時代2》中的陣形一樣,見圖2)。事實上對所有這3種類型使用簡單的球形判斷都能工作得很好,單個個體可以簡單的使用單個球體進行它的全部碰撞判斷,而對於其它兩種情況只需要再稍微增加一點工作量。

圖2.碰撞檢測中所要面對的3種
不同的試題類型

  對於一羣個體的簡單集合來說,可以接受的碰撞檢測下限是對整個組中的每個個體進行檢測。這種方法將允許那些不屬於你所選定的組的個體輕鬆的混入你的組隊中。相對來說,對編隊所要進行的碰撞檢測就要更加複雜一些了。我們還應該認識到這種簡單的組羣還有一種特殊的性質決定了我們應該儘可能的簡化對它所採用的碰撞檢測方法--這種組羣應該能夠隨時隨地的將排列方式變換成任何可能的適應當地空間大小的陣形。

  對於編隊來說,不僅僅要進行如上面的組羣那樣簡單的個體碰撞檢測,還要進行大量更加複雜的檢測操作。首先要保證編隊中的個體之間不能互相碰撞,同時如果編隊中的各個個體之間有一定的縫隙,還要保證任何一個不屬於該編隊的個體不能佔用這一空間。另外,一個編隊應該不能改變隊形或重組,但是遊戲的規則又可能規定當沒有足夠大的通路提供給整個編隊保持隊形穿越某一障礙物時,編隊可以先散開,待各個體越過障礙物之後再重新組成編隊,這樣的設計更加體貼玩家。

  我們可以嘗試使用基於時間的碰撞描述機制。立即碰撞用來描述當前正發生在兩個個體之間的碰撞;未來碰撞用來記錄預計在程序運行的後續時間中將會發生在預定地點的碰撞(當然,前提是將要碰撞的對象都不改變各自的移動路線)。在任何情況下立即碰撞的情況都應該比未來碰撞的情況更優先被處理。同時我們也應該定義碰撞的3種狀態:未處理的、正在處理和已處理完畢的。

使用"離散"的算法達到"連續"的效果

  大多數移動算法從根本上都是"離散"的,不同於數學上的離散定義,這裏所說的"離散"指移動算法在按照給定路徑從A點移動到B點的過程中從不考慮中間路徑上可能出現什麼東西,相反,在"連續"的算法中就會考慮這些情況。這樣做的一個問題就是當我們進行一個Internet遊戲時(衆所周知,由於網絡速度的限制,這類遊戲的UL時間一般較長)那些速度較高的個體很可能在一次UL時間中移動相當大的一段距離(由於UL時間變長),而當這樣增長的UL連續出現時很可能出現個體躍過了其它本應發生碰撞的個體。如果這樣的情況出現在一個工人的身上那並不會有人在意,但顯然任何玩家也不會希望敵人能夠從辛苦建設的城牆中穿越而過進而攻擊玩家的基地(某些早期及時戰略遊戲中出現的"穿牆"的BUG就是有這種問題造成的)。大部分的移動系統現在採用限制個體移動距離的方法來對付這一問題,該方法可以有效的簡化所需的處理。在離散型的移動算法中解決這類問題的方法如下圖所示:

圖3."離散"移動系統中解決問題的方法

  一種有效地解決方法是將一次移動拆分成多次移動的集合。這種拆分需要滿足一定的移動距離上的要求,這要求就是要保證每次移動的距離剛好短於任何個體的長度,這就可以保證不可能有任何個體移動到當前個體的路徑上來,從而避免了從其它個體之上跨越過去的情況。當每次這種拆分後的移動結束時我們就要使用碰撞檢測系統對個體的當前位置進行碰撞檢測。你可能會想到如此頻繁的計算大量點的碰撞信息將會極大的增大系統消耗,沒關係,在後面的章節中我們將會介紹一種方法來降低這種計算對系統的消耗。

  另一種方法是創建一種稱之爲移動線路(Move Line)的對象。我們可以使用這條移動線路來描述個體的移動,個體的原始位置作爲線段的起點,目的地作爲線段的終點,就好像《紅色警報2》裏所表現出來的那樣。這種方法並不用添加新的數據,但是會加大碰撞檢測部分的複雜性--我們必須把簡單的球形碰撞檢測修改爲對一個點到一條線段的距離的檢測,而這樣做將會增加計算的難度,也會消耗更多的時間。大多數3D遊戲都已經實現了一種可以快速挑選出可被遊戲者觀察到的物體的分級系統(也就是能夠迅速地判斷出遊戲中的哪些物體處於玩家角色的視野之內的系統),我們可以對該類系統進行修改,使它們可以用來快速挑選出那些在我們的遊戲中可能發生碰撞的個體。這樣做的好處是大幅度地減少了需要進行碰撞判斷的個體的個數,於是所需要的計算量常常就能夠降低到所能允許的範圍之內了。

位置預測

  經過上週的工作,我們已經有了一個簡單的移動算法和一個管理個體碰撞的列表,還有什麼工作是強化個體之間協作所必需的呢?位置預測(Position prediction)。

  預測的位置只不過是一個位置列表(至少包含個體的運動方向和時間標記,有時也需要記錄加速度等信息)以指出未來某時刻個體所在的位置,參考圖4。一個移動系統可以將用來實現個體移動的算法拿來負責計算個體的位置預測,這些預測越準確其可用性也就越大。當然預測計算也會增大計算量,爲了不降低遊戲的效率,下面我們就來討論一下如何減少多餘的CPU消耗。

圖4.位置預測的圖示

  顯然,最方便的優化方法是避免在每一幀中重複計算每一個已經預測過的個體位置。一個簡單的移動列表可以實現這樣的目的並且能夠工作得很好:你可以在每一幀中從表內刪除當前的位置,並向表內添加新的預測位置以維持列表長度固定(見圖5)。雖然這一方法並不會減少個體開始移動時創建整個列表的計算量,但可以保證在剩餘的移動過程中維持固定數量的計算。

圖5.位置預測使用的移動列表及其操作示意

  下一種優化方法是設計一種能夠處理點和線的位置預測系統,由於我們的碰撞系統支持處理點和線,因次添加這一功能將是很容易的事。如果一個個體按照一條直線進行移動,那麼我們可以利用當前個體位置、預測位置和個體運動半徑來指定一段移動的軌跡及範圍。然而如果個體正在進行一次圓運動那麼整個處理就會略微複雜一些。當然你可以將這種運動過程作爲一個函數保存起來,但這顯然會加大系統的負擔。作爲替代可以嘗試通過對圓上的點進行取樣來作出正確的位置判斷(見圖6)。最後,再次建議一定要使用能夠實現對點和線的無縫交替處理的預測系統,以便在任何可能的情況下通過使用直線來減少對CPU的耗用。

圖6.對一次旋轉運動使用預測

  最後所要介紹的一種優化方法非常重要,但同時也可能有一些不夠直觀,不能簡單的看出其優化作用。如果我們要使用這樣的預測系統,爲了儘可能少的消耗資源,顯然不應該在計算了一次預測未知之後再進行一次計算來移動個體。因此解決的方法是精確地進行位置預測,並最終使用該位置移動個體。這樣我們就能對每個個體的移動只計算一次並且除了前述的開始移動時的計算之外沒有其他多餘的計算開銷。

  在實際的應用中,你可能每次只能進行一個UL時間的計算來進行位置預測,這時要注意未來每次UL的時間很可能並不等長。如果只是簡單的按照預測位置移動個體而不考慮每次UL的長度,這將有可能造成一些問題,當然某些遊戲(或者遊戲中的某些類個體)可以很好的適應這樣的操作。一般的遊戲都通過每次對列表中的數據進行一些修正來改善預測的準確性,而這樣做的同時也應注意何時應該完全拋掉原來計算的已與現在情況有較大誤差的預測而重新計算整個列表。

  實際對位置預測的應用中主要的難題是由於我們在碰撞檢測中將這些預測的位置做爲個體的當前位置來使用所造成的。你將很容易的看到對某給定的區域內個體預測位置的比較所需要消耗的計算量,但是爲了很好的實現個體間的協作我們必須知道未來一小段時間內每個個體的目的地以及它們可能會與哪些其它個體相碰撞,這都需要一個優秀而且快速的碰撞檢測系統。此時最佳的優化措施就是如同前面所述使用3D引擎中的相關部分捨去那些不大可能碰撞的個體組合,這將允許你使用更多的CPU時間來處理最可能發生的那些碰撞。

個體之間的合作

  我們已經建立了一個複雜的系統來確定個體未來的可能位置,它支持3D移動,同時對計算量的提升也並不比一個簡單的方法多多少,重要的是該方法提供給我們一個記錄了一個個體在未來一小段時間內移動所需的一切信息的列表,這正是我們所需要的。現在我們可以進入較爲有趣的部分了。

  如果我們的工作做得很好,那麼我們所要處理的絕大部分碰撞將是未來的碰撞(因爲我們已經使用位置預測儘量地避免了立即碰撞)。由於處理未來碰撞最後的方法將是停止移動並重新尋道,因此爲了不使尋道過於頻繁,儘量使用其他方法解決碰撞就變得很重要。

  下面就詳細的介紹對於這種個體與個體碰撞的方法。

未處理的碰撞

    CASE 1:if 個體已經全部停止移動:
       1. if 是低優先級的個體,什麼也不做
       2. if 是高優先級的個體,找出哪一個個體將要移動(如果存在),告知該個體進行儘可能最短的移動來解決碰撞,改變狀態爲正在處理的碰撞

    CASE 2:if 個體沒有移動,是另一個個體將要移動,什麼也不做

    CASE 3:if 當前個體正要移動,其它個體已經停止
      1.if 是高優先級個體,其它停滯個體爲低優先級並且能夠從通路上移開,計算出下一步的位置並通知低優先級的個體從通路上移開(見圖7),改變狀態爲正在處理的碰撞

圖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)。然而一般遊戲的設計人員給一類個體較低的速度總是有原因的,例如如果允許強力的個體能夠非常高速的在地圖上移動將會極大的破壞遊戲的平衡性。

圖8

組隊中的個體都以相同的速度移動並使用同一條路徑。這種方法可以有效的避免當組隊中一半的個體從森林一側前往目的地時另一半卻從另一側移動(見圖9),稍後你將看到實現這一方法的一條簡單途徑。

圖9

組隊中的個體以相同的速度移動,使用同一條路徑並同時抵達。這是最複雜的組隊組織方式,它不但要求達到上述兩點,並且還要求位於前面的個體能夠等待落在後面的個體追上來,有時還要給後面的慢速個體短時間加速以使其能夠追上前面的個體。

  怎樣才能實現最後的要求?這要使用一種分級的移動系統,這樣我們就能在處理每個個體的移動時兼顧那些同屬於某個組隊的個體了。如果我們對組隊的個體創建一個組隊對象,我們就能夠記錄所有必需的數據,爲整個組隊計算最大速度,以及判斷何時需要前面的個體等待後面的個體。下面就是一個組隊類的簡單定義:

Listing 2. BUnitGroup.

 

  BGroup類在其內部管理整個組隊中個體之間的交互操作。在任何時間點,它都應該有一個時間表以來處理組隊內的個體之間的碰撞,它也應該有能力通過參數和優先級管理來控制或修正個體移動。如果你的遊戲只支持一種移動優先級,那麼你就應該爲你在組隊中的個體們添加第二種優先級。雖然一個組隊對外的表現似乎只有一種優先級,但在其內部還是應該分爲不同的移動優先級。基本上來說,BGroup類是另一個完善的封閉的移動系統。

  組隊的指揮者將負責整個組隊的尋道工作,它將決定整個組隊的移動路線,在簡單的組隊移動系統中所需的工作只是由這個個體本身來尋道即可。然而在下面的部分中我們將看到指揮者所能夠作的其它事情。

編隊控制基礎

  首先應該給出編隊的定義:編隊(Fomation)是一種更復雜的組隊,編隊有自己的方向(前方、後方、左翼和右翼)。編隊中的每一個個體都試圖保持自己在編隊中的位置,而這個位置是唯一固定的也是相互關聯的。更加複雜的模型使得編隊中各個個體的朝向需要單獨處理,而同時也要求在移動中提供整體旋轉的方法。

  編隊是建立在組隊系統之上的,它是一種限制更加嚴格的組隊,因爲我們必須非常詳盡的規定編隊中每個個體的位置。所有的個體在移動中必須保持一起行動,並要求在速度、路徑上一致以及相互之間的位置和距離保持不變--如果在移動中編隊出現了大間距的縫隙,那麼它也就與組隊沒有什麼不同了。

  下面給出的這個BFomation類能夠清晰的管理一個編隊的預定位置(我們要求編隊中的每個個體所處的位置以及它的方向)、編隊方向和編隊的狀態。大多數遊戲中所使用的編隊都是預先定義的,顯然,在開發過程中進行這項工作是很簡單的(通過使用一些非專業人員也能熟練操作的文本編輯器就可以很好的完成這項工作)。我們當然希望能在遊戲過程中實時的定義編隊,但這樣做就需要更多的內存以保證每一個由玩家定義的編隊都能在內存中保留一份自身定義的副本。

Listing 3. The BFormation Class

 

  使用這個模型,我們必須時刻關注編隊的狀態。cStateBroken表示編隊並沒有被創建也沒有創建的企圖;cStateForming表明我們的編隊正在建立但還沒有達到cStateFormed狀態;一旦所有的個體都已位於它們的預定位置,我們就可以將狀態改變爲cStateFormed。爲了使編隊的移動簡單化,我們可以使一個編隊在完成組建之前(達到cStateFormed狀態之前)不可移動。

  當我們準備使用一個編隊時,第一件工作就是組建這個編隊。當給定一個編隊時,BFormation(譯者注:原文這裏是BGroup,但該類並沒有編隊管理功能,經過反覆推敲認定爲編寫錯誤)控制每個個體移動到編隊中的預定位置,該位置的計算是與當前編隊方向相關的,如果這個方向發生了變化,那麼預定位置將自動被重新計算並修正爲正確的位置。

  爲了組建一個編隊,我們可以使用預定安置--每一個預定位置擁有一個預設值(由定義規定或由算法確定)來指明個體組建編隊時應該按照那種順序進駐那些預定位置,這樣才能使整個組建過程從裏到外進行得相當有條理(見圖10)。下面的算法列表說明如何實現這樣的組建方式。

Listing 4.

設置組隊中的所有個體移動優先級到一個相同的低優先級
設置狀態爲cStateForming
While 狀態爲cStateForming
{
    找出離編隊中心最近的未有個體佔據的位置
    If 沒有個體再可用
        設置狀態爲cStateFormed,跳出組建循環

    選定一個個體前往所找出的位置,要求滿足如下條件:
        使個體移動距離最短
        與其它編隊成員碰撞的機率最小
        移動時間最短

    設置個體的移動優先級到中等值
    等待直到個體就位(可能要經過多個UL時間)
    設置個體移動優先級爲最大值,這樣做可以保證以後進行組建工作的個體不會使該個體離開其位置
}

圖10

  現在我們所有的戰士都已經就位了,接下來做什麼呢?我們可以開始移動他們以穿過整個地圖,我們可以假定尋道系統找出了一條以當前編隊的形狀和大小可以通過的路徑來抵達目的地(見圖11),如果沒有這樣一條路經那就必須對整個編隊進行操作(不久我們就會探討這個問題)。當編隊在地圖上移動時我們需要選出一個指揮者來控制整個移動,當指揮者沿路徑前進並改變方向時其它所有編隊中的個體都要改變方向以追隨它,這種操作一般被稱爲flocking(聚集)。

圖11

  我們有兩種方法處理編隊的方向改變:忽略這種改變或者轉動編隊的方向。忽視方向的改變是簡單的而且對於那些盒狀的編隊來說是非常合理的(見圖12)。

圖12

  對編隊進行旋轉並不會增加多少複雜性而同時對於某些編隊方式(如直線形)來說是非常合理的。進行編隊旋轉時首先要做的是停止編隊移動,完成方向的旋轉之後我們要重新計算每個預定位置,然後回到cStateForming狀態(見圖13),使個體前往新的位置並在完成這一工作之後設置狀態爲cStateFormed,這樣我們就可以繼續原來的移動。

圖13

高級編隊管理技術

  現在我們已經可以將編隊在整個地圖中移動了,但是由於遊戲的地圖是動態而且複雜的,所以很可能出現選出的移動路徑不可用的情況。如果這樣的事情發生,就需要我們對編隊進行操作,一般的操作方式有3種,下面就逐一介紹。

縮放個體間距(Scaling unit positions).由於編隊中的預定位置都是由矢量進行定義的,因此我們可以很方便的對整個編隊的間距進行放縮以使它變得更小,這就使得編隊能夠通過城牆或樹林中更小的縫隙(見圖14)。這種方法對於那些排列得較爲分散的編隊很有效,但對於那些排列緊湊的編隊就沒有什麼用處了。

圖14

簡單的障礙迴避(Simple ordered obstacle avoidance).如果我們在移動編隊時遇到與其它遊戲中實體相碰撞的情況時(無論是當前還是未來碰撞),我們可以假設即使有這樣的碰撞發生,原來尋到的道路仍是可用的。簡單的解決方法是沿着編隊前進的路線找出第一個不再發生碰撞的位置,並在該位置完成編隊的重組(見圖15)。這樣我們的步兵團就可以先分散,帶穿越障礙物之後再在另一側重新組建編隊。在使用這一方法時一定要注意有時障礙物的範圍非常大,以至於編隊的重組工作必須在走出很遠之後才能做,這時就得考慮是否應該重新尋道了。

圖15

二分和重組(Halving and rejoining).雖然簡單的迴避能夠工作的很好,但是會降低玩家對整個編隊穿越地圖的感覺,相對來說二分法可以很好的保持住編隊所帶來的視覺衝擊。當我們的編隊在前方遇到一個障礙物時我們可以找出編隊中的一個拆分點,從該點將編隊一分爲二,這兩個編隊分別通過障礙物之後再前進到重組位置恢復成一個編隊(見圖16)。這種方法只增加很少的計算量,但卻能爲編隊移動帶來良好的視覺效果。

圖16

路徑棧

  路徑棧就是一種簡單的用來記錄個體移動路由信息的棧操作(後進先出,見圖17)。一個路徑棧記錄的信息一般包括個體當前所採用的路線,現在個體正在向哪個中繼點移動以及個體是否正處於巡邏中。一個路徑棧對我們的目的有兩大作用。

圖17

  首先,它可以爲一次分級尋道工作提供便利。一般來說遊戲開發者會把尋道區分爲兩種明顯不同的等級--高級(high-level)和低級(low-level)(見圖18)。高級的尋道可以爲個體找尋出穿越地圖上不同地形和主要堵塞地點的道路,這就如同玩家大多數時候給個體制定的路徑一樣。低級的尋道則同時還會處理較小的障礙物並更注重處理細節。一個路徑棧可以方便的存儲高級和低級的尋道信息。我們可以先通過高級尋道找出一條路徑並把它存儲進路徑棧中,而當我們必須注意避免與大片空地中的一顆樹發生碰撞時就可以將低級尋道的一系列結果存入路徑棧頂並執行它們。每當執行完一條路徑,我們就可以將它從棧中彈出並繼續執行現在位於棧頂的路徑。

圖18

  第二點,路徑棧可以允許高級尋道被重用(reuse)。如果你回顧前面的介紹將看到組隊和編隊在移動時的一大要素就是所有的成員都使用相同的路徑來移動到目的地。如果我們設計的路徑棧可以允許多個個體參考一條路徑,那就將使同一條高級尋道路徑很容易的被重用。一個編隊的指揮者將使用高級尋道找出一條路徑並把它拷貝給編隊中的每個個體,而其它個體則什麼也不用做。

  這樣創建保存路徑信息的結構還能提供給我們一些其它好處。通過將一條高級尋道路徑拆分成多個低級尋道路徑,我們可以在執行具體路徑之前充分的對這些低級路徑進行更精確的計算。而且如果我們確定高級尋道的結果是可用的話,也可以將低級尋道的工作略微推後再做。如果我們正在進行高協調度的個體移動,路徑棧將允許我們向棧頂添加一條臨時的用來避免碰撞的路徑,並能夠很好的使用這一路徑修正個體移動(見圖19)。

圖19

解決混合碰撞

  我們定義混合碰撞爲同時發生在兩個以上個體之間的碰撞。大多數遊戲都對於可以解決的混合碰撞中的個體個數有限制,超過這個數目就只能分幾次解決了。下面我們將探討如何使用已有的移動系統對這類情況進行簡單的處理。

  如果我們遇到了一個由三個個體造成的混合碰撞(見圖20),首要的工作是找出其中優先權最高的個體。一旦找到它,我們就要立即找到與之碰撞的另一個個體並確定優先權最高的個體所遇到的最主要的碰撞(該碰撞可能發生在其與次優先的個體之間,也可能不是)。當我們找到了這兩個個體後,剩下的工作就交給原來的碰撞處理部分解決即可了。

圖20

 

  當最初的兩個個體的碰撞被解決後,我們就要重新評估整個碰撞並更新個體之間的關係。一個更復雜的系統可以很好的解決這樣的問題,但是如果簡單的移走已經解決了碰撞問題的個體也能得到不錯的效果。

  一旦我們更新了碰撞中的個體,下一步工作就又回到尋找優先權最高的個體的碰撞上來了。我們將一直重複這一步驟直到所有的碰撞都被解決。

  你可以在兩個地方使用這一系統:碰撞解決部分或碰撞預測系統中。碰撞解決的規則必須被修改以適應對於個體優先級的要求,這樣的修改並不難,但會增加一定的代碼量。或者你可以修改你的碰撞預測系統使得只會發生兩個個體碰撞的情況,然而這樣做你仍然需要先找出一次碰撞中的全部個體並作出操作。

解決堆疊峽谷問題(The Stacked Canyon Problem)

  所有移動系統的最終目的都是要實現智能的移動效果,而所有處理中最能體現智能的就是處理堆疊峽谷問題了(什麼是堆疊峽谷問題呢?事實上當一個個體要從一羣排列緊湊的個體之間穿過時所需要解決的問題就是堆疊峽谷問題,圖21就是一個例子)。雖然此類問題並不能簡單的一次解決,但我們可以重用前面的一些簡單方法來解決它。

圖21.堆疊峽谷問題

  第一步是鑑定是否爲一個堆疊峽谷問題。這是非常重要的,因爲我們將要利用前進個體(driving unit)的優先級。當然我們可以利用每個個體自身的優先級來要求其它個體讓出道路,但是更好的解決方法是使用前進個體的優先級。判斷一個堆疊峽谷問題可以有兩種方法:觀察前進個體是否會把一個阻礙其移動的個體推到另一個身上或者觀察移動個體的碰撞列表以尋找多重的碰撞。不論採用哪種方法,被推動的個體都應該擁有與前進個體相同的移動優先級。

  一旦我們判斷出將要解決一個堆疊峽谷問題,就可以採用一種簡單的遞歸調用個體協調運動系統的方法來解決它。把第一個被推動的個體作爲前進個體處理其於第二個個體之間的關係,並如此循環。每一個個體被它的前進個體推動直到它可以移動到一邊而讓出道路。當最後一個個體也從陸上讓開後,原來的前進個體就可以繼續移動了。

  一個好的習慣是將已經移開的個體在移回原位。爲了能夠這樣做,我們應該記錄整個推動過程並在問題解決後倒序的執行該過程。另外如果負責移動的代碼能夠辨別出前進個體是否歸屬於一個組隊,那就能保證組隊中每個個體都能在原來阻礙道路的個體返回原位置之前通過。

注意

優化你的整體系統。如果你只是要做一個2D遊戲,那就會有許多多餘的計算是可以取消和簡單化的。不論你是要做2D遊戲還是3D的,你的碰撞檢測系統都需要一個優秀的經過優化的個體分揀系統,這類系統已經不再僅僅用於繪圖了。

對高級尋道和低級尋道使用不同的方法。過去大多數遊戲對這兩種尋道方式使用相同的算法。這樣做的害處是如果對高級尋道使用低級尋道的算法將使高級尋道變得緩慢並且不能用於尋找長的通路;相反的,如果對低級尋道使用高級尋道的算法將會造成結果並沒有將道路上的所有障礙物考慮在內或者造成一個個體能從其它個體之中穿過。一定要抓住要點製作兩套尋道系統。

無論你做什麼,個體總會交疊碰撞在一起。個體的交疊和碰撞是不可避免的,或者按最好情況說將是非常難以操作的。你最好儘早處理這些碰撞問題,這將使你的遊戲更好一些。遊戲的地圖已經越來越複雜了,並且還會加入隨機地圖的處理。一個好的移動系統將能夠很好的處理隨機地圖和相應的一切細節。

清楚地瞭解UL是怎樣影響個體移動的。可變化的UL時間將是你的移動系統所必須解決的一大難題。可以使用一個簡單的修正機制來解決此類大部分的問題。

只涉及單個UL的做法是過時的。沒有計劃的編制不可能解決好個體移動的協調問題,如果不記錄上一次UL中的操作和將來要發生的問題又是不可能製作好的計劃的。一個能夠運作良好的移動協調系統必須在任何時候都能夠參考以前的碰撞列表和預測碰撞的列表。切記解決碰撞的過程中出現的較小的變化是可以忽略的。

不要再出現傻乎乎的個體移動

  簡單的個體移動是簡單的。一套優秀的協調系統是我們所應該追求的,因爲它能使你的遊戲步上一個等級並能增加玩家的樂趣。在本次的文章中我們研究了一個移動協調系統的基礎功能--使用多個UL時間制定行動計劃以及一套可以解決任何兩個個體碰撞的方法等等。現在你應該不會再滿足於你的遊戲中那些傻乎乎的個體移動了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章