遊戲演算法整理(貼圖完整版)

pamxy發表於2013-10-24

轉自:http://wenku.baidu.com/view/799fe48671fe910ef12df8f9.html

演算法一:A*尋路初探

譯者序:很久以前就知道了A*演算法,但是從未認真讀過相關的文章,也沒有看過程式碼,只是腦子裡有個模糊的概念。這次決定從頭開始,研究一下這個被人推崇備至的簡單方法,作為學習人工智慧的開始。

這篇文章非常知名,國內應該有不少人翻譯過它,我沒有查詢,覺得翻譯本身也是對自身英文水平的鍛鍊。經過努力,終於完成了文件,也明白的A*演算法的原理。毫無疑問,作者用形象的描述,簡潔詼諧的語言由淺入深的講述了這一神奇的演算法,相信每個讀過的人都會對此有所認識。

原文連結:http://www.gamedev.net/reference/articles/article2003.asp

以下是翻譯的正文。(由於本人使用ultraedit編輯,所以沒有對原文中的各種連結加以處理(除了圖表),也是為了避免未經許可連結的嫌疑,有興趣的讀者可以參考原文。

會者不難,A*(唸作A星)演算法對初學者來說的確有些難度。

這篇文章並不試圖對這個話題作權威的陳述。取而代之的是,它只是描述演算法的原理,使你可以在進一步的閱讀中理解其他相關的資料。

最後,這篇文章沒有程式細節。你儘可以用任意的計算機程式語言實現它。如你所願,我在文章的末尾包含了一個指向例子程式的連結。 壓縮包包括C++和Blitz Basic兩個語言的版本,如果你只是想看看它的執行效果,裡面還包含了可執行檔案。我們正在提高自己。讓我們從頭開始。。。

 

序:搜尋區域

假設有人想從A點移動到一牆之隔的B點,如圖,綠色的是起點A,紅色是終點B,藍色方塊是中間的牆。

 [圖1]

    你首先注意到,搜尋區域被我們劃分成了方形網格。像這樣,簡化搜尋區域,是尋路的第一步。這一方法把搜尋區域簡化成了一個二維陣列。陣列的每一個元素是網格的一個方塊,方塊被標記為可通過的和不可通過的。路徑被描述為從A到B我們經過的方塊的集合。一旦路徑被找到,我們的人就從一個方格的中心走向另一個,直到到達目的地。

這些中點被稱為“節點”。當你閱讀其他的尋路資料時,你將經常會看到人們討論節點。為什麼不把他們描述為方格呢?因為有可能你的路徑被分割成其他不是方格的結構。他們完全可以是矩形,六角形,或者其他任意形狀。節點能夠被放置在形狀的任意位置-可以在中心,或者沿著邊界,或 其他什麼地方。我們使用這種系統,無論如何,因為它是最簡單的。

 

開始搜尋

正如我們處理上圖網格的方法,一旦搜尋區域被轉化為容易處理的節點,下一步就是去引導一次找到最短路徑的搜尋。在A*尋路演算法中,我們通過從點A開始,檢查相鄰方格的方式,向外擴充套件直到找到目標。

 

我們做如下操作開始搜尋:

   1,從點A開始,並且把它作為待處理點存入一個“開啟列表”。開啟列表就像一張購物清單。儘管現在列表裡只有一個元素,但以後就會多起來。你的路徑可能會通過它包含的方格,也可能不會。基本上,這是一個待檢查方格的列表。

   2,尋找起點周圍所有可到達或者可通過的方格,跳過有牆,水,或其他無法通過地形的方格。也把他們加入開啟列表。為所有這些方格儲存點A作為“父方格”。當我們想描述路徑的時候,父方格的資料是十分重要的。後面會解釋它的具體用途。

   3,從開啟列表中刪除點A,把它加入到一個“關閉列表”,列表中儲存所有不需要再次檢查的方格。

在這一點,你應該形成如圖的結構。在圖中,暗綠色方格是你起始方格的中心。它被用淺藍色描邊,以表示它被加入到關閉列表中了。所有的相鄰格現在都在開啟列表中,它們被用淺綠色描邊。每個方格都有一個灰色指標反指他們的父方格,也就是開始的方格。

 [圖2]

接著,我們選擇開啟列表中的臨近方格,大致重複前面的過程,如下。但是,哪個方格是我們要選擇的呢?是那個F值最低的。

 

路徑評分選擇路徑中經過哪個方格的關鍵是下面這個等式:

F = G + H

這裡:

    * G = 從起點A,沿著產生的路徑,移動到網格上指定方格的移動耗費。

    * H = 從網格上那個方格移動到終點B的預估移動耗費。這經常被稱為啟發式的,可能會讓你有點迷惑。這樣叫的原因是因為它只是個猜測。我們沒辦法事先知道路徑的長 度,因為路上可能存在各種障礙(牆,水,等等)。雖然本文只提供了一種計算H的方法,但是你可以在網上找到很多其他的方法。

我們的路徑是通過反覆遍歷開啟列表並且選擇具有最低F值的方格來生成的。文章將對這個過程做更詳細的描述。首先,我們更深入的看看如何計算這個方程。

 

正如上面所說,G表示沿路徑從起點到當前點的移動耗費。在這個例子裡,我們令水平或者垂直移動的耗費為10,對角線方向耗費為14。我們取這些值是因為沿對角線的距離是沿水平或垂直移動耗費的的根號2(別怕),或者約1.414倍。為了簡化,我們用10和14近似。比例基本正確,同時我們避免了求根運算和小數。這不是隻因為我們怕麻煩或者不喜歡數學。使用這樣的整數對計算機來說也更快捷。你不就就會發現,如果你不使用這些簡化方法,尋路會變得很慢。

 

既然我們在計算沿特定路徑通往某個方格的G值,求值的方法就是取它父節點的G值,然後依照它相對父節點是對角線方向或者直角方向(非對角線),分別增加14和10。例子中這個方法的需求會變得更多,因為我們從起點方格以外獲取了不止一個方格。

 

H 值可以用不同的方法估算。我們這裡使用的方法被稱為曼哈頓方法,它計算從當前格到目的格之間水平和垂直的方格的數量總和,忽略對角線方向,然後把結果乘以 10。這被成為曼哈頓方法是因為它看起來像計算城市中從一個地方到另外一個地方的街區數,在那裡你不能沿對角線方向穿過街區。很重要的一點,我們忽略了一切障礙物。這是對剩餘距離的一個估算,而非實際值,這也是這一方法被稱為啟發式的原因。想知道更多?你可以在這裡找到方程和額外的註解。

F的值是G和H的和。第一步搜尋的結果可以在下面的圖表中看到。F,G和H的評分被寫在每個方格里。正如在緊挨起始格右側的方格所表示的,F被列印在左上角,G在左下角,H則在右下角。

 [圖3]

現在我們來看看這些方格。寫字母的方格里,G = 10。這是因為它只在水平方向偏離起始格一個格距。緊鄰起始格的上方,下方和左邊的方格的G值都等於10。對角線方向的G值是14。

H 值通過求解到紅色目標格的曼哈頓距離得到,其中只在水平和垂直方向移動,並且忽略中間的牆。用這種方法,起點右側緊鄰的方格離紅色方格有3格距離,H值就 是30。這塊方格上方的方格有4格距離(記住,只能在水平和垂直方向移動),H值是40。你大致應該知道如何計算其他方格的H值了~。

每個格子的F值,還是簡單的由G和H相加得到

 

繼續搜尋

 

為了繼續搜尋,我們簡單的從開啟列表中選擇F值最低的方格。然後,對選中的方格做如下處理:

 

   4,把它從開啟列表中刪除,然後新增到關閉列表中。

   5,檢查所有相鄰格子。跳過那些已經在關閉列表中的或者不可通過的(有牆,水的地形,或者其他無法通過的地形),把他們新增進開啟列表,如果他們還不在裡面的話。把選中的方格作為新的方格的父節點。

   6,如果某個相鄰格已經在開啟列表裡了,檢查現在的這條路徑是否更好。換句話說,檢查如果我們用新的路徑到達它的話,G值是否會更低一些。如果不是,那就什麼都不做。

    另一方面,如果新的G值更低,那就把相鄰方格的父節點改為目前選中的方格(在上面的圖表中,把箭頭的方向改為指向這個方格)。最後,重新計算F和G的值。如果這看起來不夠清晰,你可以看下面的圖示。

好了,讓我們看看它是怎麼運作的。我們最初的9格方格中,在起點被切換到關閉列表中後,還剩8格留在開啟列表中。這裡面,F值最低的那個是起始格右側緊鄰的格子,它的F值是40。因此我們選擇這一格作為下一個要處理的方格。在緊隨的圖中,它被用藍色突出顯示。

 [圖4]

首先,我們把它從開啟列表中取出,放入關閉列表(這就是他被藍色突出顯示的原因)。然後我們檢查相鄰的格子。哦,右側的格子是牆,所以我們略過。左側的格子是起始格。它在關閉列表裡,所以我們也跳過它。

其 他4格已經在開啟列表裡了,於是我們檢查G值來判定,如果通過這一格到達那裡,路徑是否更好。我們來看選中格子下面的方格。它的G值是14。如果我們從當 前格移動到那裡,G值就會等於20(到達當前格的G值是10,移動到上面的格子將使得G值增加10)。因為G值20大於14,所以這不是更好的路徑。如果 你看圖,就能理解。與其通過先水平移動一格,再垂直移動一格,還不如直接沿對角線方向移動一格來得簡單。

當我們對已經存在於開啟列表中的4個臨近格重複這一過程的時候,我們發現沒有一條路徑可以通過使用當前格子得到改善,所以我們不做任何改變。既然我們已經檢查過了所有鄰近格,那麼就可以移動到下一格了。

於是我們檢索開啟列表,現在裡面只有7格了,我們仍然選擇其中F值最低的。有趣的是,這次,有兩個格子的數值都是54。我們如何選擇?這並不麻煩。從速度上 考慮,選擇最後新增進列表的格子會更快捷。這種導致了尋路過程中,在靠近目標的時候,優先使用新找到的格子的偏好。但這無關緊要。(對相同數值的不同對 待,導致不同版本的A*演算法找到等長的不同路徑。)

 

那我們就選擇起始格右下方的格子,如圖。

 

 [圖5]

 

這次,當我們檢查相鄰格的時候,發現右側是牆,於是略過。上面一格也被略過。我們也略過了牆下面的格子。為什麼呢?因為你不能在不穿越牆角的情況下直接到達 那個格子。你的確需要先往下走然後到達那一格,按部就班的走過那個拐角。(註解:穿越拐角的規則是可選的。它取決於你的節點是如何放置的。)

這樣一來,就剩下了其他5格。當前格下面的另外兩個格子目前不在開啟列表中,於是我們新增他們,並且把當前格指定為他們的父節點。其餘3格,兩個已經在開啟 列表中(起始格,和當前格上方的格子,在表格中藍色高亮顯示),於是我們略過它們。最後一格,在當前格的左側,將被檢查通過這條路徑,G值是否更低。不必 擔心,我們已經準備好檢查開啟列表中的下一格了。

 

我們重複這個過程,知道目標格被新增進開啟列表,就如在下面的圖中所看到的。

 

 [圖6]

 

注 意,起始格下方格子的父節點已經和前面不同的。之前它的G值是28,並且指向右上方的格子。現在它的G值是20,指向它上方的格子。這在尋路過程中的某處 發生,當應用新路徑時,G值經過檢查變得低了-於是父節點被重新指定,G和F值被重新計算。儘管這一變化在這個例子中並不重要,在很多場合,這種變化會導 致尋路結果的巨大變化。

 

那麼,我們怎麼確定這條路徑呢?很簡單,從紅色的目標格開始,按箭頭的方向朝父節點移動。這最終會引導你回到起始格,這就是你的路徑!看起來應該像圖中那樣。從起始格A移動到目標格B只是簡單的從每個格子(節點)的中點沿路徑移動到下一個,直到你到達目標點。就這麼簡單。

 

 [圖7]

 

 

 

 

A*方法總結

好,現在你已經看完了整個說明,讓我們把每一步的操作寫在一起:

 

   1,把起始格新增到開啟列表。

   2,重複如下的工作:

      a) 尋找開啟列表中F值最低的格子。我們稱它為當前格。

      b) 把它切換到關閉列表。

      c) 對相鄰的8格中的每一個?

          * 如果它不可通過或者已經在關閉列表中,略過它。反之如下。

          * 如果它不在開啟列表中,把它新增進去。把當前格作為這一格的父節點。記錄這一格的F,G,和H值。

          * 如果它已經在開啟列表中,用G值為參考檢查新的路徑是否更好。更低的G值意味著更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之後你可能需要重新對開啟列表排序。

      d) 停止,當你

          * 把目標格新增進了開啟列表,這時候路徑被找到,或者

          * 沒有找到目標格,開啟列表已經空了。這時候,路徑不存在。

   3.儲存路徑。從目標格開始,沿著每一格的父節點移動直到回到起始格。這就是你的路徑。

 

題外話

 

離題一下,見諒,值得一提的是,當你在網上或者相關論壇看到關於A*的不同的探討,你有時會看到一些被當作A*演算法的程式碼而實際上他們不是。要使用A*,你 必須包含上面討論的所有元素--特定的開啟和關閉列表,用F,G和H作路徑評價。有很多其他的尋路演算法,但他們並不是A*,A*被認為是他們當中最好的。 Bryan Stout在這篇文章後面的參考文件中論述了一部分,包括他們的一些優點和缺點。有時候特定的場合其他演算法會更好,但你必須很明確你在作什麼。好了,夠多 的了。回到文章。

 

實現的註解

 

現在你已經明白了基本原理,寫你的程式的時候還得考慮一些額外的東西。下面這些材料中的一些引用了我用C++和Blitz Basic寫的程式,但對其他語言寫的程式碼同樣有效。

 

1, 維護開啟列表:這是A*尋路演算法最重要的組成部分。每次你訪問開啟列表,你都需要尋找F值最低的方格。有幾種不同的方法實現這一點。你可以把路徑元素隨意 儲存,當需要尋找F值最低的元素的時候,遍歷開啟列表。這很簡單,但是太慢了,尤其是對長路徑來說。這可以通過維護一格排好序的列表來改善,每次尋找F值 最低的方格只需要選取列表的首元素。當我自己實現的時候,這種方法是我的首選。

 

在小地圖。這種方法工作的很好,但它並不是最快的解決方 案。更苛求速度的A*程式設計師使用叫做“binary heap”的方法,這也是我在程式碼中使用的方法。憑我的經驗,這種方法在大多數場合會快2~3倍,並且在長路經上速度呈幾何級數提升(10倍以上速度)。 如果你想了解更多關於binary heap的內容,查閱我的文章,Using Binary Heaps in A* Pathfinding。

 

2, 其他單位:如果你恰好看了我的例子程式碼,你會發現它完全忽略了其他單位。我的尋路者事實上可以相互穿越。取決於具體的遊戲,這也許可以,也許不行。如果你 打算考慮其他單位,希望他們能互相繞過,我建議在尋路演算法中忽略其他單位,寫一些新的程式碼作碰撞檢測。當碰撞發生,你可以生成一條新路徑或者使用一些標準 的移動規則(比如總是向右,等等)直到路上沒有了障礙,然後再生成新路徑。為什麼在最初的路徑計算中不考慮其他單位呢?那是因為其他單位會移動,當你到達 他們原來的位置的時候,他們可能已經離開了。這有可能會導致奇怪的結果,一個單位突然轉向,躲避一個已經不在那裡的單位,並且會撞到計算完路徑後,衝進它 的路徑中的單位。

 

然而,在尋路演算法中忽略其他物件,意味著你必須編寫單獨的碰撞檢測程式碼。這因遊戲而異,所以我把這個決定權留給你。參考文獻列表中,Bryan Stout的文章值得研究,裡面有一些可能的解決方案(像魯棒追蹤,等等)。

 

3, 一些速度方面的提示:當你開發你自己的A*程式,或者改寫我的,你會發現尋路佔據了大量的CPU時間,尤其是在大地圖上有大量物件在尋路的時候。如果你閱 讀過網上的其他材料,你會明白,即使是開發了星際爭霸或帝國時代的專家,這也無可奈何。如果你覺得尋路太過緩慢,這裡有一些建議也許有效:

 

    * 使用更小的地圖或者更少的尋路者。

    * 不要同時給多個物件尋路。取而代之的是把他們加入一個佇列,把尋路過程分散在幾個遊戲週期中。如果你的遊戲以40週期每秒的速度執行,沒人能察覺。但是他們會發覺遊戲速度突然變慢,當大量尋路者計算自己路徑的時候。

    * 儘量使用更大的地圖網格。這降低了尋路中搜尋的總網格數。如果你有志氣,你可以設計兩個或者更多尋路系統以便使用在不同場合,取決於路徑的長度。這也正是 專業人士的做法,用大的區域計算長的路徑,然後在接近目標的時候切換到使用小格子/區域的精細尋路。如果你對這個觀點感興趣,查閱我的文章Two- Tiered A* Pathfinding(雙層A*演算法)

    * 使用路徑點系統計算長路徑,或者預先計算好路徑並加入到遊戲中。

    * 預處理你的地圖,表明地圖中哪些區域是不可到達的。我把這些區域稱作“孤島”。事實上,他們可以是島嶼或其他被牆壁包圍等無法到達的任意區域。A*的下限是,當你告訴它要尋找通往那些區域的路徑時,它會搜尋整個地圖,直到所有可到達的方格/節點都被通過開啟列表和關閉列表的計算。這會浪費大量的CPU時 間。可以通過預先確定這些區域(比如通過flood-fill或類似的方法)來避免這種情況的發生,用某些種類的陣列記錄這些資訊,在開始尋路前檢查它。 在我Blitz版本的程式碼中,我建立了一個地圖前處理器來作這個工作。它也標明瞭尋路演算法可以忽略的死端,這進一步提高了尋路速度。

 

4, 不同的地形損耗:在這個教程和我附帶的程式中,地形只有兩種-可通過的和不可通過的。但是你可能會需要一些可通過的地形,但是移動耗費更高-沼澤,小山, 地牢的樓梯,等等。這些都是可通過但是比平坦的開闊地移動耗費更高的地形。類似的,道路應該比自然地形移動耗費更低。

 

這個問題很容易解 決,只要在計算任何地形的G值的時候增加地形損耗就可以了。簡單的給它增加一些額外的損耗就可以了。由於A*演算法已經按照尋找最低耗費的路徑來設計,所以 很容易處理這種情況。在我提供的這個簡單的例子裡,地形只有可通過和不可通過兩種,A*會找到最短,最直接的路徑。但是在地形耗費不同的場合,耗費最低的 路徑也許會包含很長的移動距離-就像沿著路繞過沼澤而不是直接穿過它。

 

一種需額外考慮的情況是被專家稱之為“influence mapping”的東西(暫譯為影響對映圖)。就像上面描述的不同地形耗費一樣,你可以建立一格額外的分數系統,並把它應用到尋路的AI中。假設你有一張 有大批尋路者的地圖,他們都要通過某個山區。每次電腦生成一條通過那個關口的路徑,它就會變得更擁擠。如果你願意,你可以建立一個影響對映圖對有大量屠殺 事件的格子施以不利影響。這會讓計算機更傾向安全些的路徑,並且幫助它避免總是僅僅因為路徑短(但可能更危險)而持續把隊伍和尋路者送到某一特定路徑。

 

5,處理未知區域:你是否玩過這樣的PC遊戲,電腦總是知道哪條路是正確的,即使它還沒有偵察過地圖?對於遊戲,尋路太好會顯得不真實。幸運的是,這是一格可以輕易解決的問題。

 

答 案就是為每個不同的玩家和電腦(每個玩家,而不是每個單位--那樣的話會耗費大量的記憶體)建立一個獨立的“knownWalkability”陣列,每個 陣列包含玩家已經探索過的區域,以及被當作可通過區域的其他區域,直到被證實。用這種方法,單位會在路的死端徘徊並且導致錯誤的選擇直到他們在周圍找到 路。一旦地圖被探索了,尋路就像往常那樣進行。

 

6,平滑路徑:儘管A*提供了最短,最低代價的路徑,它無法自動提供看起來平滑的路徑。看一下我們的例子最終形成的路徑(在圖7)。最初的一步是起始格的右下方,如果這一步是直接往下的話,路徑不是會更平滑一些嗎?

 

有 幾種方法來解決這個問題。當計算路徑的時候可以對改變方向的格子施加不利影響,對G值增加額外的數值。也可以換種方法,你可以在路徑計算完之後沿著它跑一遍,找那些用相鄰格替換會讓路徑看起來更平滑的地方。想知道完整的結果,檢視Toward More Realistic Pathfinding,一篇(免費,但是需要註冊)Marco Pinter發表在Gamasutra.com的文章

 

7,非方形搜尋區域:在我們的例子裡,我們使用簡單的2D方形圖。你可以不使用這種方式。你可以使用不規則形狀的區域。想想冒險棋的遊戲,和遊戲中那些國家。你可以設計一個像那樣的尋路關卡。為此,你可能需要建立一個國家相鄰關係的表格,和從一個國家移動到另一個的G值。你也需要估算H值的方法。其他的事情就和例子中完全 一樣了。當你需要向開啟列表中新增新元素的時候,不需使用相鄰的格子,取而代之的是從表格中尋找相鄰的國家。

 

類似的,你可以為一張確定的 地形圖建立路徑點系統,路徑點一般是路上,或者地牢通道的轉折點。作為遊戲設計者,你可以預設這些路徑點。兩個路徑點被認為是相鄰的如果他們之間的直線上 沒有障礙的話。在冒險棋的例子裡,你可以儲存這些相鄰資訊在某個表格裡,當需要在開啟列表中新增元素的時候使用它。然後你就可以記錄關聯的G值(可能使用 兩點間的直線距離),H值(可以使用到目標點的直線距離),其他都按原先的做就可以了。

 

另一個在非方形區域搜尋RPG地圖的例子,檢視我的文章Two-Tiered A* Pathfinding。

 

進一步的閱讀

 

好,現在你對一些進一步的觀點有了初步認識。這時,我建議你研究我的原始碼。包裡面包含兩個版本,一個是用C++寫的,另一個用Blitz Basic。順便說一句,兩個版本都註釋詳盡,容易閱讀,這裡是連結。

 

    * 例子程式碼:A* Pathfinder (2D) Version 1.71

 

如果你既不用C++也不用Blitz Basic,在C++版本里有兩個小的可執行檔案。Blitz Basic可以在從Blitz Basic網站免費下載的litz Basic 3D(不是Blitz Plus)演示版上執行。Ben O'Neill提供一個聯機演示可以在這裡找到。

 

你也該看看以下的網頁。讀了這篇教程後,他們應該變得容易理解多了。

 

    *  Amit的 A* 頁面:這是由Amit Patel製作,被廣泛引用的頁面,如果你沒有事先讀這篇文章,可能會有點難以理解。值得一看。尤其要看Amit關於這個問題的自己的看法。

    * Smart Moves:智慧尋路:Bryan Stout發表在Gamasutra.com的這篇文章需要註冊才能閱讀。註冊是免費的而且比起這篇文章和網站的其他資源,是非常物有所值的。Bryan 用Delphi寫的程式幫助我學習A*,也是我的A*程式碼的靈感之源。它還描述了A*的幾種變化。

    * 地形分析:這是一格高階,但是有趣的話題,Dave Pottinge撰寫,Ensemble Studios的專家。這傢伙參與了帝國時代和君王時代的開發。別指望看懂這裡所有的東西,但是這是篇有趣的文章也許會讓你產生自己的想法。它包含一些對 mip-mapping,influence mapping以及其他一些高階AI/尋路觀點。對"flood filling"的討論使我有了我自己的“死端”和“孤島”的程式碼的靈感,這些包含在我Blitz版本的程式碼中。

 

其他一些值得一看的網站:

 

    * aiGuru: Pathfinding

    * Game AI Resource: Pathfinding

    * GameDev.net: Pathfinding

 

演算法二:碰撞

1.   碰撞檢測和響應

碰撞在遊戲中運用的是非常廣泛的,運用理論實現的碰撞,再加上一些小技巧,可以讓碰撞檢測做得非常精確,效率也非常高。從而增加遊戲的功能和可玩性。

 

2D碰撞檢測

 

2D的碰撞檢測已經非常穩定,可以在許多著作和論文中查詢到。3D的碰撞還沒有找到最好的方法,現在使用的大多數方法都是建立在2D基礎上的。

 

碰撞檢測:

碰撞的檢測不僅僅是運用在遊戲中,事實上,一開始的時候是運用在模擬和機器人技術上的。這些工業上的碰撞檢測要求非常高,而碰撞以後的響應也是需要符合現實生活的,是需要符合人類常規認識的。遊戲中的碰撞有些許的不一樣,況且,更重要的,我們製作的東西充其量是商業級別,還不需要接觸到紛繁複雜的數學公式。

圖1

 

最理想的碰撞,我想莫過於上圖,完全按照多邊形的外形和執行路徑規劃一個範圍,在這個範圍當中尋找會產生阻擋的物體,不管是什麼物體,產生阻擋以後,我們運、動的物體都必須在那個位置產生一個碰撞的事件。最美好的想法總是在實現上有一些困難,事實上我們可以這麼做,但是效率卻是非常非常低下的,遊戲中,甚至於工業中無法忍受這種速度,所以我們改用其它的方法來實現。

圖2

最簡單的方法如上圖,我們尋找物體的中心點,然後用這個中心點來畫一個圓,如果是一個3D的物體,那麼我們要畫的就是一個球體。在檢測物體碰撞的時候,我們只要檢測兩個物體的半徑相加是否大於這兩個物體圓心的實際距離。

圖3

這個演算法是最簡單的一種,現在還在用,但是不是用來做精確的碰撞檢測,而是用來提高效率的模糊碰撞檢測查詢,到了這個範圍以後,再進行更加精密的碰撞檢測。 一種比較精密的碰撞檢測查詢就是繼續這種畫圓的思路,然後把物體細分,對於物體的每個部件繼續畫圓,然後再繼續進行碰撞檢測,直到系統規定的,可以容忍的 誤差範圍以後才觸發碰撞事件,進行碰撞的一些操作。

 

有沒有更加簡單的方法呢?2D遊戲中有許多圖片都是方方正正的,所以我們不必把碰撞的範圍畫成一個圓的,而是畫成一個方的。這個正方形,或者說是一個四邊形和座標軸是對齊的,所以運用數學上的一些方法,比如距離計算等還是比較方便的。這個檢測方法就叫AABBs(Axis-aligned Bounding Boxes)碰撞檢測,遊戲中已經運用的非常廣泛了,因為其速度快,效率高,計算起來非常方便,精確度也是可以忍受的。

 

做到這一步,許多遊戲的需求都已經滿足了。但是,總是有人希望近一步優化,而且方法也是非常陳舊的:繼續對物體的各個部分進行細分,對每個部件做AABB的矩形,那這個優化以後的系統就叫做OBB系統。雖然說這個優化以後的系統也不錯,但是,許多它可以運用到的地方,別人卻不愛使用它,這是後面會繼續介紹的 地方。

 

John Carmack不知道看的哪本書,他早在DOOM中已經使用了BSP系統(二分空間分割),再加上一些小技巧,他的碰撞做得就非常好了,再加上他發明的 castray演算法,DOOM已經不存在碰撞的問題,解決了這樣的關鍵技術,我想他不再需要在什麼地方分心了,只要繼續研究渲染引擎就可以了。 (Windows遊戲程式設計大師技巧P392~P393介紹)(凸多邊形,多邊形退化,左手定律)SAT系統非常複雜,是SHT(separating hyperplane theorem,分離超平面理論)的一種特殊情況。這個理論闡述的就是兩個不相關的曲面,是否能夠被一個超平面所分割開來,所謂分割開來的意思就是一個曲 面貼在平面的一邊,而另一個曲面貼在平面的另一邊。我理解的就是有點像相切的意思。SAT是SHT的特殊情況,所指的就是兩個曲面都是一些多邊形,而那個 超平面也是一個多邊形,這個超平面的多邊形可以在場景中的多邊形列表中找到,而超平面可能就是某個多邊形的表面,很巧的就是,這個表面的法線和兩個曲面的 切面是相對應的。接下來的證明,我想是非常複雜的事情,希望今後能夠找到原始碼直接運用上去。而我們現在講究的快速開發,我想AABB就足以滿足了。

 

3D碰撞檢測

 

3D 的檢測就沒有什麼很標準的理論了,都建立在2D的基礎上,我們可以沿用AABB或者OBB,或者先用球體做粗略的檢測,然後用AABB和OBB作精細的檢測。BSP技術不流行,但是效率不錯。微軟提供了D3DIntersect函式讓大家使用,方便了許多,但是和通常一樣,當物體多了以後就不好用了,明顯 的就是速度慢許多。

 

碰撞反應:

碰撞以後我們需要做一些反應,比如說產生反衝力讓我們反彈出去,或者停下來,或者讓阻擋我們的物體飛出去,或者穿牆,碰撞最討厭的就是穿越,本來就不合邏輯,查閱了那麼多資料以後,從來沒有看到過需要穿越的碰撞,有摩擦力是另外一回事。首先看看彈性碰撞。彈性碰撞就是我們初中物理中說的動量守恆。物體在碰撞前後的動量守恆,沒有任何能量損失。這樣的碰撞運用於打磚塊的遊戲中。引入質量的話,有的物體會是有一定的質量,這些物體通常來說是需要在碰撞以後進行另外一個方向的運動的,另外一些物體是設定為質量無限大的,這些物體通常是碰撞牆壁。

 

當物體碰到質量非常大的物體,預設為碰到了一個彈性物體,其速度會改變,但是能量不會受到損失。一般在程式碼上的做法就是在速度向量上加上一個負號。

 

絕對的彈性碰撞是很少有的,大多數情況下我們運用的還是非彈性碰撞。我們現在玩的大多數遊戲都用的是很接近現實的非彈性碰撞,例如Pain-Killer中的那把吸力槍,它彈出去的子彈吸附到NPC身上時的碰撞響應就是非彈性碰撞;那把殘忍的分屍刀把牆打碎的初始演算法就是一個非彈性碰撞,其後使用的剛體力學就是先建立在這個演算法上的。那麼,是的,如果需要非彈性碰撞,我們需要介入摩擦力這個因素,而我們也無法簡單使用動量守恆這個公式。

我們可以採取比較簡單的方法,假設摩擦係數μ非常大,那麼只要物體接觸,並且擁有一個加速度,就可以產生一個無窮大的摩擦力,造成物體停止的狀態。

基於別人的引擎寫出一個讓自己滿意的碰撞是不容易的,那麼如果自己建立一個碰撞系統的話,以下內容是無法缺少的:

——一個能夠容忍的碰撞系統;

——一個從概念上可以接受的物理系統;

——質量;

——速度;

——摩擦係數;

——地心引力。

演算法三:尋路演算法新思維

目前常用尋路演算法是A*方式,原理是通過不斷搜尋逼近目的地的路點來獲得。

 

如果通過影象模擬搜尋點,可以發現:非啟發式的尋路演算法實際上是一種窮舉法,通過固定順序依次搜尋人物周圍的路點,直到找到目的地,搜尋點在影象上的表現為一個不斷擴大的矩形。如下:

 

很快人們發現如此窮舉導致搜尋速度過慢,而且不是很符合邏輯,試想:如果要從(0,0)點到達(100,0)點,如果每次向東搜尋時能夠走通,那麼幹嗎還要 搜尋其他方向呢?所以,出現了啟發式的A*尋路演算法,一般通過已經走過的路程 + 到達目的地的直線距離 代價值作為搜尋時的啟發條件,每個點建立一個代價值,每次搜尋時就從代價低的最先搜尋,如下:

 

綜上所述,以上的搜尋是一種矩陣式的不斷逼近終點的搜尋做法。優點是比較直觀,缺點在於距離越遠、搜尋時間越長。

現在,我提出一種新的AI尋路方式——向量尋路演算法

通過觀察,我們可以發現,所有的最優路線,如果是一條折線,那麼、其每一個拐彎點一定發生在障礙物的突出邊角,而不會在還沒有碰到障礙物就拐彎的情況:如下圖所示:

 

我們可以發現,所有的紅色拐彎點都是在障礙物(可以認為是一個凸多邊形)的頂點處,所以,我們搜尋路徑時,其實只需要搜尋這些凸多邊形頂點不就可以了嗎?如果將各個頂點連線成一條通路就找到了最優路線,而不需要每個點都檢索一次,這樣就大大減少了搜尋次數,不會因為距離的增大而增大搜尋時間

 

這種思路我尚未將其演變為演算法,姑且提出一個偽程式給各位參考:

1.建立各個凸多邊形頂點的通路表TAB,表示頂點A到頂點B是否可達,將可達的頂點分組儲存下來。如: ( (0,0) (100,0) ),這一步驟在程式剛開始時完成,不要放在搜尋過程中空耗時間。

2.開始搜尋A點到B點的路線

3.檢測A點可以直達凸多邊形頂點中的哪一些,挑選出最合適的頂點X1。

4.檢測與X1相連(能夠接通)的有哪些頂點,挑出最合適的頂點X2。

5.X2是否是終點B?是的話結束,否則轉步驟4(X2代入X1)

 

如此下來,搜尋只發生在凸多邊形的頂點,節省了大量的搜尋時間,而且找到的路線無需再修剪鋸齒,保證了路線的最優性。

這種方法搜尋理論上可以減少大量搜尋點、缺點是需要實現用一段程式得出TAB表,從本質上來說是一種空間換時間的方法,而且搜尋時A*能夠用的啟發條件,在向量搜尋時依然可以使用。

 

 

演算法四:戰略遊戲中的戰爭模型演算法的初步探討

  《三國志》系列遊戲相信大家都有所瞭解,而其中的(巨集觀)戰鬥時關於雙方兵力,士氣,兵種剋制,攻擊力,增援以及隨戰爭進行兵力減少等數值的演算法是十分值得研究的。或許是由於簡單的緣故,我在網上幾乎沒有找到相關演算法的文章。 下面給出這個戰爭的數學模型演算法可以保證遊戲中戰爭的遊戲性與真實性兼顧,希望可以給有需要這方面開發的人一些啟迪。

假設用x(t)和y(t)表示甲乙交戰雙方在t時刻的兵力,如果是開始時可視為雙方士兵人數。

  假設每一方的戰鬥減員率取決於雙方兵力和戰鬥力,用f(x,y)和g(x,y)表示,每一方的增援率是給定函式用u(t)和v(t)表示。

   如果雙方用正規部隊作戰(可假設是相同兵種),先分析甲方的戰鬥減員率f(x,y)。可知甲方士兵公開活動,處於乙方沒一個士兵的監視和殺傷範圍之內, 一但甲方的某個士兵被殺傷,乙方的火力立即集中在其餘士兵身上,所以甲方的戰鬥減員率只與乙方的兵力有關可射為f與y成正比,即f=ay,a表示乙方平均 每個士兵對甲方士兵的殺傷率(單位時間的殺傷數),成為乙方的戰鬥有效係數。類似g= -bx

這個戰爭模型模型方程1為

x’(t)= -a*y(t)+u(t) x’(t)是x(t)對於t 的導數

y’(t)= -b*x(t)+v(t) y’(t)是y(t)對於t的導數

利用給定的初始兵力,戰爭持續時間,和增援兵力可以求出雙方兵力在戰爭中的變化函式。

(本文中解法略)

如果考慮由於士氣和疾病等引起的非戰鬥減員率(一般與本放兵力成正比,設甲乙雙方分別為h,w)

可得到改進戰爭模型方程2:

x’(t)= -a*y(t)-h*x(t)+u(t)

y’(t)= -b*x(t)-w*y(t)+v(t)

利用初始條件同樣可以得到雙方兵力在戰爭中的變化函式和戰爭結果。

此外還有不同兵種作戰(兵種剋制)的數學模型:

模 型1中的戰鬥有效係數a可以進一步分解為a=ry*py*(sry/sx),其中ry是乙方的攻擊率(每個士兵單位的攻擊次數),py是每次攻擊的命中 率。(sry/sx)是乙方攻擊的有效面積sry與甲方活動範圍sx之比。類似甲方的戰鬥有效係數b=rx*px*(srx/sy),rx和px是甲方的 攻擊率和命中率,(srx/sy)是甲方攻擊的有效面積與乙方活動範圍sy之比。由於增加了兵種剋制的攻擊範圍,所以戰鬥減員率不光與對方兵力有關,而且 隨著己放兵力增加而增加。因為在一定區域內,士兵越多被殺傷的就越多。

方程

x’(t)= -ry*py*(sry/sx)*x(t)*y(t)-h*x(t)+u(t)

y’(t)= -rx*px*(srx/sy)*x(t)*y(t)-w*y(t)+u(t) 

演算法五:飛行射擊遊戲中的碰撞檢測

  在遊戲中物體的碰撞是經常發生的,怎樣檢測物體的碰撞是一個很關鍵的技術問題。在RPG遊 戲中,一般都將場景分為許多矩形的單元,碰撞的問題被大大的簡化了,只要判斷精靈所在的單元是不是有其它的東西就可以了。而在飛行射擊遊戲(包括象荒野大 鏢客這樣的射擊遊戲)中,碰撞卻是最關鍵的技術,如果不能很好的解決,會影響玩遊戲者的興趣。因為飛行射擊遊戲說白了就是碰撞的遊戲——躲避敵人的子彈或 飛機,同時用自己的子彈去碰撞敵人。

  碰撞,這很簡單嘛,只要兩個物體的中心點距離小於它們的半徑之和就可以了。確實,而且我也看到很 多人是這樣做的,但是,這隻適合圓形的物體——圓形的半徑處處相等。如果我們要碰撞的物體是兩艘威力巨大的太空飛船,它是三角形或矩形或其他的什麼形狀,就會出現讓人尷尬的情景:兩艘飛船眼看就要擦肩而過,卻出人意料的發生了爆炸;或者敵人的子彈穿透了你的飛船的右弦,你卻安然無恙,這不是我們希望發生的。於是,我們需要一種精確的檢測方法。

  那麼,怎樣才能達到我們的要求呢?其實我們的前輩們已經總結了許多這方面的經驗,如上所述的半徑檢測法,三維中的標準平臺方程法,邊界框法等等。大多數遊戲程式設計師都喜歡用邊界框法,這也是我採用的方法。邊界框是在程式設計中加進去的不可見的邊界。邊界框法,顧名思義,就是用邊界框來檢測物體是否發生了碰撞,如果兩個物體的邊界框相互干擾,則發生了碰撞。用什麼樣的邊界框要視不同情況而定,用最近似的幾何形狀。當然,你可以用物體的準確幾何形狀作邊界框,但出於效率的考慮,我不贊成這樣做,因為遊戲中的物體一般都很複雜,用複雜的邊界框將增加大量的計算,尤其是浮點計算,而這正是我們想盡量避免的。但邊界框也不能與準確幾何形狀有太大的出入,否則就象用半徑法一樣出現奇怪的現象。

   在飛行射擊遊戲中,我們的飛機大多都是三角形的,我們可以用三角形作近似的邊界框。現在我們假設飛機是一個正三角形(或等要三角形,我想如果誰把飛機設計 成左右不對稱的怪物,那他的審美觀一定有問題),我的飛機是正著的、向上飛的三角形,敵人的飛機是倒著的、向下飛的三角形,且飛機不會旋轉(大部分遊戲中 都是這樣的)。我們可以這樣定義飛機:

中心點O(Xo,Yo),三個頂點P0(X0,Y0)、P1(X1,Y1)、P2(X2,Y2)。

中心點為正三角形的中心點,即中心點到三個頂點的距離相等。接下來的問題是怎樣確定兩個三角形互相干擾了呢?嗯,現在我們接觸到問題的實質了。如果你學過平面解析幾何,我相信你可以想出許多方法解決這個問題。判斷一個三角形的各個頂點是否在另一個三角形裡面,看起來是個不錯的方法,你可以這樣做,但我卻發現一個小問題:一個三角形的頂點沒有在另一個三角形的裡面,卻可能發生了碰撞,因為另一個三角形的頂點在這個三角形的裡面,所以要判斷兩次,這很麻煩。有沒有一次判斷就可以的方法?

我們把三角形放到極座標平面中,中心點為原點,水平線即X軸為零度角。我們發現三角形成了這個樣子:在每個角度我們都可以找到一個距離,用以描述三角形的邊。既然我們找到了邊到中心點的距離,那就可以用這個距離來檢測碰撞。如圖一,兩個三角形中心點座標分別為(Xo,Yo)和(Xo1, Yo1),由這兩個點的座標求出兩點的距離及兩點連線和X軸的夾角θ,再由θ求出中心點連線與三角形邊的交點到中心點的距離,用這個距離與兩中心點距離比 較,從而判斷兩三角形是否碰撞。因為三角形左右對稱,所以θ取-90~90度區間就可以了。哈,現在問題有趣多了,-90~90度區間正是正切函式的定義 域,求出θ之後再找對應的邊到中心點的距離就容易多了,利用幾何知識,如圖二,將三角形的邊分為三部分,即圖2中紅綠藍三部分,根據θ在那一部分而分別對 待。用正弦定理求出邊到中心點的距離,即圖2中淺綠色線段的長度。但是,如果飛機每次移動都這樣判斷一次,效率仍然很低。我們可以結合半徑法來解決,先用 半徑法判斷是否可能發生碰撞,如果可能發生碰撞,再用上面的方法精確判斷是不是真的發生了碰撞,這樣基本就可以了。如果飛機旋轉了怎麼辦呢,例如,如圖三 所示飛機旋轉了一個角度α,仔細觀察圖三會發現,用(θ-α)就可以求出邊到中心點的距離,這時你要注意邊界情況,即(θ-α)可能大於90度或小於- 90度。囉羅嗦嗦說了這麼多,不知道大家明白了沒有。我編寫了一個簡單的例程,用於說明我的意圖。在例子中假設所有飛機的大小都一樣,並且沒有旋轉。

/////////////////////////////////////////////////////////////////////

//example.cpp

//碰撞檢測演示

//作者 李韜

/////////////////////////////////////////////////////////////////////

//限於篇幅,這裡只給出了碰撞檢測的函式

//define/////////////////////////////////////////////////////////////

#define NUM_VERTICES 3

#define ang_30 -0.5236

#define ang60  1.0472

#define ang120 2.0944

//deftype////////////////////////////////////////////////////////////

 

struct object

{

    float xo, yo;

    float radio;

    float x_vel, y_vel;

    float vertices[NUM_VERTICES][2];

}

 

//faction/////////////////////////////////////////////////////////////

//根據角度求距離

float AngToDis(struct object obj, float angle)

{

    float dis, R;

    R = obj.radius;

    if (angle <= ang_30)

        dis = R / (2 * sin(-angle));

    else if (angle >= 0)

        dis = R / (2 * sin(angle + ang60));

    else dis = R / (2 * sin(ang120 - angle));

    return dis;

}

 

//碰撞檢測

int CheckHit(struct object obj1, struct object obj2)

{

    float deltaX, deltaY, angle, distance, bumpdis;

    deltaX = abs(obj1.xo - obj2.xo);

    deltaY = obj1.yo - obj2.yo;

    distance = sqrt(deltaX * deltaX + deltaY * deltaY);

    if (distance <= obj.radio)

    {

         angle = atan2(deltaY, deltaX);

         bumpdis1 = AngToDis(obj1, angle);

         return (distance <= 2 * bumpdis);

    }

    ruturn 0;

}

//End//////////////////////////////////////////////////////////////

 

  上面程式只是用於演示,並不適合放在遊戲中,但你應該明白它的意思,以便寫出適合你自己的碰撞檢測。遊戲中的情況是多種多樣的,沒有哪種方法能適應所有情況,你一定能根據自己的情況找到最適合自己的方法。

高階碰撞檢測技術 

 

高階碰撞檢測技術 第一部分

Advanced Collision Detection Techniques

 

這文章原載於Gamasutra,共有三部分。我想將它翻譯,請大家指教。

 

http://www.gamasutra.com/features/20000330/bobic_01.htm

http://www.gamasutra.com/features/20000330/bobic_02.htm

http://www.gamasutra.com/features/20000330/bobic_03.htm

 

 

/ 1 ………………………………………………………………………………………………….

自 從電腦遊戲降臨以來,程式設計師們不斷地設計各種方法去模擬現實的世界。例如Pong(著名的碰球遊戲),展示了一個動人的場面(一個球及兩根擺繩)。當玩家 將拽住擺繩移動到一定高度的,然後放開球,球就會離開玩家向對手衝去。以今天的標準,這樣的基礎操作也許就是原始碰撞檢測的起源。現在的電腦遊戲比以前的 Pong複雜多了,而且更多是基於3D的。這也使3D碰撞檢測的困難要遠遠高於一個簡單的2D Pong。一些較早的飛行模擬遊戲說明瞭糟糕的碰撞檢測技術是怎樣破壞一個遊戲。如:當你的飛機撞到一座山峰的時候,你居然還可以安全的倖存下來,這在現 實中是不可能發生的。甚至最近剛出的一些遊戲也存在此類問題。許多玩家對他們喜愛的英雄或是女英雄部分身體居然可以穿過牆而感到失望。甚至更壞的是玩家被 一顆沒有和他發生碰撞關係的火箭擊中。因為今天的玩家要求增加唯實論的要求越來越高,我們遊戲開發者們將盡可能在我們的遊戲世界做一些改進以便接近真實的 世界。

  Since the advent of computer games, programmers have continually devised ways to simulate the world more precisely. Pong, for instance, featured a moving square (a ball) and two paddles. Players had to move the paddles to an appropriate position at an appropriate time, thus rebounding the ball toward the opponent and away from the player. The root of this basic operation is primitive(by today’s standards) collision detection. Today’s games are much more advanced than Pong, and most are based in 3D. Collision detection in 3D is many magnitudes more difficult to implement than a simple 2D Pong game. The experience of playing some of the early flight simulators illustrated how bad collision detection can ruin a game. Flying through a mountain peak and surviving isn’t very realistic. Even some recent games have exhibited collision problems. Many game players have been disappointed by the sight of their favorite heroes or heroines with parts of their bodies inside rigid walls. Even worse, many players have had the experience of being hit by a rocket or bullet that was “not even close” to them. Because today’s players demand increasing levels of realism, we developers will have to do some hard thinking in order to approximate the real world in our game worlds as closely as possible.

 

/ 2 …………………………………………………………………………………………………

這 篇碰撞檢測的論文會使用一些基礎的幾何學及數學知識。在文章的結束,我也會提供一些參考文獻給你。我假定你已經讀過Jeff Lander寫的圖形教程中的碰撞檢測部分(“Crashing into the New Year,” ; “When Two Hearts Collide,”; and “Collision Response: Bouncy, Trouncy, Fun,” )。我將給你一些圖片讓你能快速的聯絡起核心例程。我們將要討論的碰撞檢測是基於portal-based 及BSP-based 兩種型別的引擎。因為每個引擎都有自己組織結構,這使得虛擬世界物體的碰撞檢測技術也不盡相同。物件導向的碰撞檢測是使用得比較多的,但這取決於你的現實 可實性,就想將引擎分成兩部分一樣。稍後,我們會概述多邊形碰撞檢測,也會研究如何擴充套件我們的彎曲物體。

This article will assume a basic understanding of the geometry and math involved in collision detection. At the end of the article, I’ll provide some references in case you feel a bit rusty in this area. I’ll also assume that you’ve read Jeff Lander’s Graphic Content columns on collision detection (“Crashing into the New Year,” ; “When Two Hearts Collide,”; and “Collision Response: Bouncy, Trouncy, Fun,” ). I’ll take a top-down approach to collision detection by first looking at the whole picture and then quickly inspecting the core routines. I’ll discuss collision detection for two types of graphics engines: portal-based and BSP-based engines. Because the geometry in each engine is organized very differently from the other, the techniques for world-object collision detection are very different. The object-object collision detection, for the most part, will be the same for both types of engines, depending upon your current implementation. After we cover polygonal collision detection, we’ll examine how to extend what we’ve learned to curved objects.

 

/ 3 …………………………………………………………………………………………………

重要的圖片

編寫一個最好的碰撞檢測例程。我們開始設計並且編寫它的基本程式框架,與此同時我們也正在開發著一款遊戲的圖形管線。要想在工程結束的時候才加入碰撞檢測是比較不好的。因為,快速的編寫一個碰撞檢測會使得遊戲開發週期延遲甚至會導致遊戲難產。在一個完美的遊戲引擎中,碰撞檢測應該是精確、有效、而且速度要快。這些意味著碰撞檢測必須通過場景幾何學的管理途徑。蠻力方法是不會工作的 — 因為今天,3D遊戲每幀執行時處理的資料量是令人難以置信的。你能想象一個多邊形物體的檢測時間。

在一個完美的比賽發動機,碰撞察覺應該是精確, 有效,並且很快的。這些要求意味著那碰撞察覺必須仔細到景色被繫住幾何學管理管道。禽獸力量方法嬴得’t 工作—今天’s 3D 比賽每框架處理的資料的數量能是介意猶豫。去是你能核對對在景色的每另外的多角形的一個物體的每多角形的時間。

The Big Picture

To create an optimal collision detection routine, we have to start planning and creating its basic framework at the same time that we’re developing a game’s graphics pipeline. Adding collision detection near the end of a project is very difficult. Building a quick collision detection hack near the end of a development cycle will probably ruin the whole game because it’ll be impossible to make it efficient. In a perfect game engine, collision detection should be precise, efficient, and very fast. These requirements mean that collision detection has to be tied closely to the scene geometry management pipeline. Brute force methods won’t work — the amount of data that today’s 3D games handle per frame can be mind-boggling. Gone are the times when you could check each polygon of an object against every other polygon in the scene.

 

/ 4 …………………………………………………………………………………………………

讓我們來看看一個遊戲的基本迴圈引擎。(Listing 1)

http://www.gamasutra.com/features/20000330/bobic_l1.htm

這 段程式碼簡要的闡明瞭我們碰撞檢測的想法。我們假設碰撞沒發生並且更新物體的位置,如果我們發現碰撞發生了,我們移動物體回來並且不允許它通過邊界(或刪除 它或採取一些另外預防措施)。然而,因為我們不知道物體的先前的位置是否仍然是可得到的,這個假設是太過分簡單化的。你必須為這種情況設計一個解決方案 (否則,你將可能經歷碰撞而你將被粘住)。如果你是一個細心的玩家,你可能在遊戲中會注意到,當你走近一面牆並且試圖通過它時,你會看見牆開始動搖。你正 在經歷的,是感動運動返回來的效果。動搖是一個粗糙的時間坡度的結果(時間片)。

Let’s begin by taking a look at a basic game engine loop (Listing 1). A quick scan of this code reveals our strategy for collision detection. We assume that collision has not occurred and update the object’s position. If we find that a collision has occurred, we move the object back and do not allow it to pass the boundary (or destroy it or take some other preventative measure). However, this assumption is too simplistic because we don’t know if the object’s previous position is still available. You’ll have to devise a scheme for what to do in this case (otherwise, you’ll probably experience a crash or you’ll be stuck). If you’re an avid game player, you’ve probably noticed that in some games, the view starts to shake when you approach a wall and try to go through it. What you’re experiencing is the effect of moving the player back. Shaking is the result of a coarse time gradient (time slice).

 

/ 5 …………………………………………………………………………………………………

但 是我們的方法是有缺陷的。我們忘記在我們的方程中加入時間。圖1顯示了時間的重要性,因而它不能省去。就算一個物體不在時間 t1 或 t2 牴觸,它可以在時間t1 < t < t2穿過t邊界哪兒。這是非常正確的,我們已經有大而連續的框架可操作。我們會發現必須還要一個好方法來處理差異。

But our method is flawed. We forgot to include the time in our equation. Figure 1 shows that time is just too important to leave out. Even if an object doesn’t collide at time t1 or t2, it may cross the boundary at time t where t1 < t < t2. This is especially true when we have large jumps between successive frames (such as when the user hit an afterburner or something like that). We'll have to find a good way to deal with discrepancy as well.

 

/ 6 …………………………………………………………………………………………………

我們應該將時間作為第4維也加入到所有的計算中去。這些使得計算變得很複雜,然而,我們只能捨棄它們。我們也可從原來的物體在時間 t1 和 t2 之間的佔據,然後靠著牆測試結果(圖 2 )。

We could treat time as a fourth dimension and do all of our calculations in 4D. These calculations can get very complex, however, so we’ll stay away from them. We could also create a solid out of the space that the original object occupies between time t1 and t2 and then test the resulting solid against the wall (Figure 2).

 

/ 7 …………………………………………………………………………………………………

一條簡單的途徑就是在 2 不同的時間在一個物體的地點附近創造凸殼。這條途徑的效率很低並且毫無疑問它會降低你遊戲的執行速度。如果不建立凸殼,我們可以在物體附近建立一個範圍框。在我們熟悉幾種技術後,我們要再次回到這個問題上。

An easy approach is to create a convex hull around an object’s location at two different times. This approach is very inefficient and will definitely slow down your game. Instead of constructing a convex hull, we could construct a bounding box around the solid. We’ll come back to this problem once we get accustomed to several other techniques.

 

/ 8 …………………………………………………………………………………………………

另外的途徑,它是更容易的實現但是少些精確,是在正中央為交叉的一半和測試細分給的時間間隔。

另外的途徑,其是更容易的實現但是少些精確,是細分在為在midpoint 的交叉的一半和測試的給的時間間隔。這計算能遞迴地為每個結果的一半返回。這途徑將比先前的方法更快,但是它不能保證精確檢測所有碰撞的。

Another approach, which is easier to implement but less accurate, is to subdivide the given time interval in half and test for intersection at the midpoint. This calculation can be done recursively for each resulting half, too. This approach will be faster than the previous methods, but it’s not guaranteed to catch all of the collisions.

 

/ 9 …………………………………………………………………………………………………

另外的隱藏的問題是 collide_with_other_objects ()例程,它檢查一個物件是否在場景內與任何另外的物件交叉。如果我們的場景有很多物體時,這例程會變得更重要。如果我們需要在場景對所有的別的物件檢查,我們將粗略地做

Another hidden problem is the collide_with_other_objects() routine, which checks whether an object intersects any other object in the scene. If we have a lot of objects in the scene, this routine can get very costly. If we have to check each object against all other objects in the scene, we’ll have to make roughly

 

圖三

(N choose 2 )的比較。因此,我們將要完成的工作就是比較數字的關係N2 (or O(N2))。但是我們能避免施行 O ( N2 )在若干方法之一的對明智的比較。例如,我們能把我們的世界劃分成是靜止的物體( collidees )並且移動的物體( colliders )的初速度 v=0 。例如,在一個房間裡的一面僵硬的牆是一碰撞面和向牆被扔的一個網球球是一碰撞物件。我們能建立一個二叉樹(為每個組的一個)給這些物件,並且然後檢查哪 個物件確實有碰撞的機會。我們能甚至進一步限制我們的環境以便一些碰撞物件不會與我們沒有在 2 顆子彈之間計算碰撞的對方發生牴觸,例程。當我們繼續前進,這個過程將變得更清楚,為現在,讓我們就說它是可能的。(為了減少場景方面數量的另外的方法就 是建立一個八叉樹,這已經超出這篇文章的範圍,但是你可以在文末參看我給你列出的參考文獻)現在讓看看基於portal-based引擎的碰撞檢測。

(N choose 2) comparisons. Thus, the number of comparisons that we’ll need to perform is of order N2 (or O(N2)). But we can avoid performing O(N2) pair-wise comparisons in one of several ways. For instance, we can divide our world into objects that are stationary (collidees) and objects that move (colliders) even with a v=0. For example, a rigid wall in a room is a collidee and a tennis ball thrown at the wall is a collider. We can build two spatial trees (one for each group) out of these objects, and then check which objects really have a chance of colliding. We can even restrict our environment further so that some colliders won’t collide with each other — we don’t have to compute collisions between two bullets, for example. This procedure will become more clear as we move on, for now, let’s just say that it’s possible. (Another method for reducing the number of pair-wise comparisons in a scene is to build an octree. This is beyond the scope of this article, but you can read more about octrees in Spatial Data Structures: Quadtree, Octrees and Other Hierarchical Methods, mentioned in the “For Further Info” section at the end of this article.) Now lets take a look at portal-based engines and see why they can be a pain in the neck when it comes to collision detection.

 

演算法六:關於SLG中人物可到達範圍計算的想法

下面的沒有經過實踐,因此很可能是錯誤的,覺得有用的初學朋友讀一讀吧:)

希望高人指點一二 :)

 

簡介:

在標準的SLG遊戲中,當在一個人物處按下滑鼠時,會以人物為中心,向四周生成一個菱形的可移動區範圍,如下:

 

  0

 000

00s00

 000

  0

 

這個圖形在剛開始學習PASCAL時就應該寫過一個畫圖的程式(是否有人懷念?)。那個圖形和SLG的擴充套件路徑一樣。

 

一、如何生成路徑:

從人物所在的位置開始,向四周的四個方向擴充套件,之後的點再進行擴充套件。即從人物所在的位置從近到遠進行擴充套件(類似廣寬優先)。

 

二、擴充套件時會遇到的問題:

1、當擴充套件到一個點時,人物的移動力沒有了。

2、當擴充套件的時候遇到了一個障礙點。

3、當擴充套件的時候這個結點出了地圖。

4、擴充套件的時候遇到了一個人物正好站在這個點(與2同?)。

5、擴充套件的點已經被擴充套件過了。當擴充套件節點的時候,每個節點都是向四周擴充套件,因此會產生重複的節點。

 

當 遇到這些問題的時候,我們就不對這些節點處理了。在程式中使用ALLPATH[]陣列記錄下每一個等擴充套件的節點,不處理這些問題節點的意思就是不把它們加 入到ALLPATH[]陣列中。我們如何去擴充套件一個結點周圍的四個結點,使用這個結點的座標加上一個偏移量就可以了,方向如下:

 

  3

  0 2

  1

 

偏移量定義如下:

int offx[4] = { -1, 0, 1, 0 };

int offy[4] = { 0, 1, 0, -1 };

 

擴充套件一個節點的相鄰的四個節點的座標為:

for(int i=0; i<4; i )

    temp.x = temp1.x offx[i];

    temp.y = temp1.y offy[i];

}

 

三、關於地圖的結構:

1、地圖的二維座標,用於確定每個圖塊在地圖中的位置。

2、SLG中還要引入一個變數decrease表示人物經過這個圖塊後他的移動力的減少值。例如,一個人物現在的移動力為CurMP=5,與之相領的圖塊的decrease=2;這時,如果人物移動到這裡,那它的移動力變成CurMP-decrease。

3、Flag域:寬度優先中好像都有這個變數,有了它,每一個點保證只被擴充套件一次。防止一個點被擴充套件多次。(一個點只被擴充套件一次真的能得到正確的結果嗎?)

4、一個地圖上的圖塊是否可以通過,我們使用了一個Block代表。1---不可以通過;0---可以通過。

 

這樣,我們可以定義一個簡單的地圖結構陣列了:

 

#define MAP_MAX_WIDTH 50

#define MAP_MAX_HEIGHT 50

typedef struct tagTILE{ 

    int x,y,decrease,flag,block;

}TILE,*LPTILE;

TILE pMap[MAP_MAX_WIDTH][MAP_MAX_HEIGHT];

 

以上是順序陣列,是否使用動態的分配更好些?畢竟不能事先知道一個地圖的寬、高。

 

四、關於路徑:

SLG遊戲中的擴充套件路徑是一片區域(以人物為中心向四周擴充套件,當然,當人物移動時路徑只有一個)。這些擴充套件的路徑必須要儲存起來,所有要有一個好的結構。我定義了一個結構,不是很好:

 

typedef struct tagNODE{ 

    int x,y;   //擴充套件路徑中的一個點在地圖中的座標。

    int curmp; //人物到了這個點以後的當前的移動力。

}NODE,*LPNODE;

 

上面的結構是定義擴充套件路徑中的一個點的結構。擴充套件路徑是點的集合,因此用如下的陣列進行定義:

 

NODE AllPath[PATH_MAX_LENGTH];

 

其中的PATH_MAX_LENGTH代表擴充套件路徑的點的個數,我們不知道這個擴充套件的路徑中包含多少個點,因此定義一個大一點的數字使這個陣列不會產生溢位:

 

#define PATH_MAX_LENGTH 200

 

上 面的這個陣列很有用處,以後的擴充套件就靠它來實現,它應該帶有兩個變數nodecount 代表當前的陣列中有多少個點。當然,陣列中的點分成兩大部分,一部分是已經擴充套件的結點,存放在陣列的前面;另一部分是等擴充套件的節點,放在陣列的後面為什麼 會出現已擴充套件節點和待擴充套件節點?如下例子:

 

當前的人物座標為x,y;移動力為mp。將它存放到AllPath陣列中,這時的起始節點為等 擴充套件的節點。這時我們擴充套件它的四個方向,對於合法的節點(如沒有出地圖,也沒有障礙......),我們將它們存放入AllPath陣列中,這時的新加入 的節點(起始節點的子節點)就是等擴充套件結點,而起始節點就成了已擴充套件節點了。下一次再擴充套件節點的時候,我們不能再擴充套件起始節點,因為它是已經擴充套件的節點 了。我們只擴充套件那幾個新加入的節點(待擴充套件節點),之後的情況以此類推。那麼我們如何知道哪些是已經擴充套件的結點,哪些是等擴充套件的節點?我們使用另一個變數 cutflag,在這個變數所代表的下標以前的結點是已擴充套件節點,在它及它之後是待擴充套件結點。

 

五、下面是基本框架(只擴充套件一個人物的可達範圍):

 

int nodecount=0; //AllPath陣列中的點的個數(包含待擴充套件節點和已經擴充套件的節點

int cutflag=0; //用於劃分已經擴充套件的節點和待擴充套件節點

NODE temp; //路徑中的一個點(臨時)

temp.x=pRole[cur]->x; //假設有一個關於人物的類,代表當前的人物

temp.y=pRole[cur]->y;

temp.curmp=pRole[cur]->MP; //人物的最大MP

AllPath[nodecount ]=temp; //起始點入AllPath,此時的起始點為等擴充套件的節點

 

while(curflag<nodecount) //陣列中還有待擴充套件的節點

    int n=nodecount; //記錄下當前的陣列節點的個數。

    for(int i=cutflag;i<nodecount;i ) //遍歷待擴充套件節點

    { 

        for(int j=0;j<4;j ) //向待擴充套件節點的四周各走一步

        { 

            //取得相鄰點的資料

            temp.x=AllPath[i].x offx[j];

            temp.y=AllPath[i].y offy[j];

            temp.curmp=AllPath[i].curmp-pMap[AllPath[i].x][AllPath[i].y].decrease;

//以下為檢測是否為問題點的過程,如果是問題點,不加入AllPath陣列,繼續處理其它的點

            if(pMap[temp.x][temp.y].block)

                continue; //有障礙,處理下一個節點

            if(temp.curmp<0)

                continue; //沒有移動力了

            if(temp.x<0||temp.x>=MAP_MAX_WIDTH|| temp.y<0||temp.y>=MAP_MAX_HEIGHT)

                continue; //出了地圖的範圍

            if(pMap[temp.x][temp.y].flag)

                continue; //已經擴充套件了的結點

            //經過了上面幾層的檢測,沒有問題的節點過濾出來,可以加入AllPath

            AllPath[nodecount]=temp;

        }

        pMap[AllPath[i].x][AllPath[i].y].flag=1; //將已經擴充套件的節點標記為已擴充套件節點

    }

    cutflag=n; //將已擴充套件節點和待擴充套件節點的分界線下標值移動到新的分界線

}

for(int i=0;i<nodecount;i )

    pMap[AllPath[i].x][AllPath[i].y].bFlag=0; //標記為已擴充套件節點的標記設回為待擴充套件節點。

演算法七:無限大地圖的實現

這已經不是什麼新鮮的東西了,不過現在實在想不到什麼好寫,而且版面上又異常冷清,我再不說幾句就想要倒閉了一樣。只好暫且拿這個東西來湊數吧。 

無限大的地圖,聽上去非常吸引人。本來人生活的空間就是十分廣闊的,人在這麼廣闊的空間裡活動才有一種自由的感覺。遊戲中的虛擬世界由於受到計算機儲存空間 的限制,要真實地反映這個無限的空間是不可能的。而對這個限制最大的,就是記憶體的容量了。所以在遊戲的空間裡,我們一般只能在一個狹小的範圍裡活動,在一 般的RPG中,從一個場景走到另一個場景,即使兩個地方是緊緊相連的,也要有一個場景的切換過程,一般的表現就是畫面的淡入淡出。

這樣的場景切換給人一種不連續的感覺(我不知道可不可以把這種稱作“蒙太奇”:o)),從城內走到城外還有情可緣,因為有道城牆嘛,但是兩個地方明明沒有界限, 卻偏偏在這一邊看不到另外一邊,就有點不現實了。當然這並不是毛病,一直以來的RPG都是遵循這個原則,我們(至少是我)已經習慣了這種走路的方式。我在 這裡說的僅僅是另外一種看起來更自然一點的走路方式,僅此而已。

當然要把整個城市的地圖一下子裝進記憶體,現在的確是不現實的,每一次只能放一部分,那麼應該怎麼放才是我們要討論的問題。

我們在以前提到Tile方法構造地圖時就談到過Tile的好處之一就是節省記憶體,這裡仍然可以借鑑Tile的思想。我們把整個大地圖分成幾塊,把每一塊稱作一個區域,在同一時間裡,記憶體中只儲存相鄰的四塊區域。這裡每個區域的劃分都有一定的要求:

每個區域大小應該相等這是一定的了,不然判斷當前螢幕在哪個區 域中就成了一個非常令人撓頭的事;另外每個區域的大小都要大於螢幕的大小,也只有這樣才能保證螢幕(就是圖中那塊半透明的藍色矩形)在地圖上盪來盪去的時 候,最多同時只能覆蓋四個區域(象左圖中所表示的),記憶體裡也只要儲存四個區域就足夠了;還有一點要注意的,就是地圖上的建築物(也包括樹啦,大石頭啦什 麼的)必須在一個區域內,這樣也是為了畫起來方便,當然牆壁——就是那種連續的圍牆可以除外,因為牆壁本來就是一段一段拼起來的。 

我們在程式中可以設定4個指標來分別指向這4個區域,當每次主角移動時,就判斷當前滾動的螢幕是否移出了這四個區域,如果移出了這四個區域,那麼就廢棄兩個 (或三個)已經在目前的四個相鄰區域中被滾出去的區域(說得很彆扭,各位見諒),讀入兩個(或三個)新滾進來的區域,並重新組織指標。這裡並不涉及記憶體區 域的拷貝。

 

這樣的區域劃分方法剛好適合我們以前提到的Tile排列方法,只要每個區域橫向Tile的個數是個偶數就行了,這樣相鄰的兩個 區域拼接起來剛好嚴絲合縫,而且每個區域塊的結構完全一致,沒有那些需要重複儲存的Tile(這個我想我不需要再畫圖說明了,大家自己隨便畫個草圖就看得 出來了)。在檔案中的儲存方法就是按一個個區域分別儲存,這樣在讀取區域資料時就可以直接作為一整塊讀入,也簡化了程式。另外還有個細節就是,我們的整個 地圖可能不是一個規則的矩形,可能有些地方是無法達到的,如右圖所示,背景是黑色的部分代表人物不能達到的地方。那麼在整個地圖中,這一部分割槽域(在圖中 藍色的3號區域)就可以省略,表現在檔案儲存上就是實際上不儲存這一部分割槽域,這樣可以節省下不少儲存空間。對於這種地圖可以用一個稀疏矩陣來儲存,大家 也可以發揮自己的才智用其他對於程式設計來說更方便的形式來儲存地圖。  

 

這就是對無限大地圖實現的一種方法,歡迎大家提出更好的方法。也希望整個版面能夠活躍一點。

Ogre中的碰撞檢測

Ogre採用樹樁管理場景中的各種"元素"(攝像機、燈光、物體等),所有的東西都掛在"樹"上,不在"樹"上的東西不會被渲染。

Ogre::SceneManager就是"樹"的管理者,Ogre::SceneNode是從SceneManager中建立的(當然BSP和8*樹的管理也和這兩個類有關,這暫時不討論)。

AABB(軸對齊包圍盒)

這個東西是碰撞檢測的基礎(怎麼總想起JJYY呢),和它類似的還有OBB(有向包圍盒),由於OBB建立複雜,所以Ogre採用了AABB。

最簡單的碰撞檢測:

通過Ogre::SceneNode::_getWorldAABB()可以取得這個葉子節點的AABB(Ogre::AxisAlignedBox), Ogre::AxisAlignedBox封裝了對AABB的支援,該類的成員函式Ogre::AxisAlignedBox::intersects ()可以判斷一個AABB和"球體、點、面以及其他面"的相交情況(碰撞情況)。

    m_SphereNode樹的葉子,掛了一個"球"

    m_CubeNode樹的葉子,掛了一個"正方體"

    AxisAlignedBox spbox=m_SphereNode->_getWorldAABB();

AxisAlignedBox cbbox=m_CubeNode->_getWorldAABB();

if(spbox.intersects(cbbox))

{

     //相交

}

區域查詢:

簡單的講就是,查詢某一區域中有什麼東西,分為AABB、球體、面查詢。

   //建立一個球體查詢,這裡的100是m_SphereNode掛著的那個球體的半徑

   SphereSceneQuery * pQuery=m_SceneMgr->createSphereQuery(Sphere(m_SphereNode->getPosition(),100));

   //執行這個查詢

   SceneQueryResult QResult=pQuery->execute();

   //遍歷查詢列表找出範圍內的物體

   for (std::list<MovableObject*>::iterator iter = QResult.movables.begin(); iter != QResult.movables.end();++iter)

   {

    MovableObject* pObject=static_cast<MovableObject*>(*iter);

    if(pObject)

    {

     if(pObject->getMovableType()=="Entity")

     {

      Entity* ent = static_cast<Entity*>(pObject);

      //這裡簡化了操作,由於只有一個"球體"和一個"正方體",

      //所以只判斷了球體和正方體的相交

      if(ent->getName()=="cube")

      {

       //改變位置防止物體重疊

       vtl=-vtl;

       m_SphereNode->translate(vtl);

       break;

      }

     }

    }

   }

相交查詢

遍歷所有的物件,找到一對一對的相交物體(廢話呀,相交當然至少兩個物體)。

    //建立相交檢測

    IntersectionSceneQuery* pISQuery=m_SceneMgr->createIntersectionQuery();

    //執行查詢

    IntersectionSceneQueryResult QResult=pISQuery->execute();

    //遍歷查詢列表找出兩個相交的物體

    for (SceneQueryMovableIntersectionList::iterator iter = QResult.movables2movables.begin();

     iter != QResult.movables2movables.end();++iter)

    {

     

     SceneQueryMovableObjectPair pObject=static_cast<SceneQueryMovableObjectPair>(*iter);

     //if(pObject)

     {

      String strFirst=pObject.first->getName();

      String strSecond=pObject.second->getName();

      //下面加入你自己的兩個物體相交判斷程式碼,或者簡單的用AABB的判斷方法,

     }

    }

 


相關文章