淺談幾大遊戲算法



淺談幾大遊戲算法:A*尋路初探、碰撞、尋路算法新思維、

戰略遊戲中的戰爭模型算法的初步探討等等




算法一:A*尋路初探


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

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

原文鏈接:http://www.gamedev.net/reference/articles/article2003.asp

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

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

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

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

 

序:搜索區域

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

 [1]

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

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

 

開始搜索

正如我們處理上圖網格的方法,一旦搜索區域被轉化爲容易處理的節點,下一步就是去引導一次找到最短路徑的搜索。在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倍。爲了簡化,我們用1014近似。比例基本正確,同時我們避免了求根運算和小數。這不是只因爲我們怕麻煩或者不喜歡數學。使用這樣的整數對計算機來說也更快捷。你不就就會發現,如果你不使用這些簡化方法,尋路會變得很慢。

 

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

 

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

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

 [3]

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

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

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

 

繼續搜索

 

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

 

   4,把它從開啓列表中刪除,然後添加到關閉列表中。

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

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

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

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

 [4]

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

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

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

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

 

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

 

 [5]

 

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

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

 

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

 

 [6]

 

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

 

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

 

 [7]

 

 

 

 

A*方法總結

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

 

   1,把起始格添加到開啓列表。

   2,重複如下的工作:

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

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

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

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

          *如果它不在開啓列表中,把它添加進去。把當前格作爲這一格的父節點。記錄這一格的F,G,H值。

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

      d)停止,當你

          *把目標格添加進了開啓列表,這時候路徑被找到,或者

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

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

 

題外話

 

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

 

實現的註解

 

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

 

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

 

在小地圖。這種方法工作的很好,但它並不是最快的解決方 案。更苛求速度的A*程序員使用叫做“binary heap”的方法,這也是我在代碼中使用的方法。憑我的經驗,這種方法在大多數場合會快23倍,並且在長路經上速度呈幾何級數提升(10倍以上速度)。 如果你想了解更多關於binary heap的內容,查閱我的文章,Using Binary Heaps inA* 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 RealisticPathfinding,一篇(免費,但是需要註冊)MarcoPinter發表在Gamasutra.com的文章

 

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

 

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

 

另一個在非方形區域搜索RPG地圖的例子,查看我的文章Two-Tiered A* Pathfinding

 

進一步的閱讀

 

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

 

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

 

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

 

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

 

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

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

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

 

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

 

    *aiGuru: Pathfinding

    *Game AI Resource: Pathfinding

    *GameDev.net: Pathfinding

 

算法二:碰撞

1.  碰撞檢測和響應

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

 

2D碰撞檢測

 

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

 

碰撞檢測:

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

1

 

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

2

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

3

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

 

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

 

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

 

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

 

3D碰撞檢測

 

3D的檢測就沒有什麼很標準的理論了,都建立在2D的基礎上,我們可以沿用AABB或者OBB,或者先用球體做粗略的

檢測,然後用AABBOBB作精細的檢測BSP技術不流行,但是效率不錯。微軟提供了D3DIntersect函數讓大家使

用,方便了許多,但是和通常一樣,當物體多了以後就不好用了,明顯 的就是速度慢許多。

 

碰撞反應:

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

 

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

 

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

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

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

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

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

——質量;

——速度;

——摩擦係數;

——地心引力。

算法三:尋路算法新思維

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

 

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

 

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

 

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

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

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

 

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

 

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

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

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

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

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

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

 

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

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

 

 

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

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

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

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

  如果雙方用正規部隊作戰(可假設是相同兵種),先分析甲方的戰鬥減員率f(x,y)。可知甲方士兵公開活動,處於乙方沒一個士兵的監視和殺傷範圍之內, 一但甲方的某個士兵被殺傷,乙方的火力立即集中在其餘士兵身上,所以甲方的戰鬥減員率只與乙方的兵力有關可射爲fy成正比,即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)rxpx是甲方的攻擊率和命中率,(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軸的夾角θ,再由θ求出中心點連線與三角形邊的交點到中心點的距離,用這個距離與兩中心點距離比較,從而判斷兩三角形是否碰撞。因爲三角形左右對稱,所以θ-9090度區間就可以了。哈,現在問題有趣多了,-9090度區間正是正切函數的定義 域,求出θ之後再找對應的邊到中心點的距離就容易多了,利用幾何知識,如圖二,將三角形的邊分爲三部分,即圖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;

    floatvertices[NUM_VERTICES][2];

}

 

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

//根據角度求距離

float AngToDis(struct object obj, floatangle)

{

    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, structobject 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 CollisionDetection 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 theadvent of computer games, programmers have continually devised ways to simulatethe 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 atan appropriate time, thus rebounding the ball toward the opponent and away fromthe 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 mostare based in 3D. Collision detection in 3D is many magnitudes more difficult toimplement than a simple 2D Pong game. The experience of playing some of theearly flight simulators illustrated how bad collision detection can ruin agame. Flying through a mountain peak and surviving isn’t very realistic. Evensome recent games have exhibited collision problems. Many game players havebeen disappointed by the sight of their favorite heroes or heroines with partsof their bodies inside rigid walls. Even worse, many players have had theexperience of being hit by a rocket or bullet that was “not even close” tothem. Because today’s players demand increasing levels of realism, wedevelopers will have to do some hard thinking in order to approximate the realworld in our game worlds as closely as possible.

 

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

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

This article willassume a basic understanding of the geometry and math involved in collisiondetection. At the end of the article, I’ll provide some references in case youfeel a bit rusty in this area. I’ll also assume that you’ve read Jeff Lander’sGraphic 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 thewhole picture and then quickly inspecting the core routines. I’ll discusscollision detection for two types of graphics engines: portal-based andBSP-based engines. Because the geometry in each engine is organized verydifferently from the other, the techniques for world-object collision detectionare very different. The object-object collision detection, for the most part,will be the same for both types of engines, depending upon your currentimplementation. After we cover polygonal collision detection, we’ll examine howto extend what we’ve learned to curved objects.

 

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

重要的圖片

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

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

The Big Picture

To create an optimalcollision detection routine, we have to start planning and creating its basicframework 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 cyclewill probably ruin the whole game because it’ll be impossible to make itefficient. In a perfect game engine, collision detection should be precise,efficient, and very fast. These requirements mean that collision detection hasto be tied closely to the scene geometry management pipeline. Brute forcemethods won’t work — the amount of data that today’s 3D games handle per framecan be mind-boggling. Gone are the times when you could check each polygon ofan object against every other polygon in the scene.

 

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

讓我們來看看一個遊戲的基本循環引擎。(Listing 1

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

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

Let’s begin bytaking a look at a basic game engine loop (Listing 1). A quick scan of thiscode reveals our strategy for collision detection. We assume that collision hasnot occurred and update the object’s position. If we find that a collision hasoccurred, we move the object back and do not allow it to pass the boundary (ordestroy it or take some other preventative measure). However, this assumptionis too simplistic because we don’t know if the object’s previous position isstill 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’rean avid game player, you’ve probably noticed that in some games, the viewstarts to shake when you approach a wall and try to go through it. What you’reexperiencing is the effect of moving the player back. Shaking is the result ofa coarse time gradient (time slice).

 

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

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

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

 

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

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

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

 

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

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

An easy approach isto 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 aroundthe solid. We’ll come back to this problem once we get accustomed to several othertechniques.

 

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

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

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

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

 

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

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

Another hiddenproblem is the collide_with_other_objects() routine, which checks whether anobject intersects any other object in the scene. If we have a lot of objects inthe scene, this routine can get very costly. If we have to check each objectagainst 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 oforder N2 (or O(N2)). But we can avoid performing O(N2) pair-wise comparisons inone of several ways. For instance, we can divide our world into objects thatare 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 atthe wall is a collider. We can build two spatial trees (one for each group) outof these objects, and then check which objects really have a chance ofcolliding. We can even restrict our environment further so that some colliderswon’t collide with each other — we don’t have to compute collisions between twobullets, for example. This procedure will become more clear as we move on, fornow, let’s just say that it’s possible. (Another method for reducing the numberof pair-wise comparisons in a scene is to build an octree. This is beyond thescope of this article, but you can read more about octrees in Spatial DataStructures: Quadtree, Octrees and Other Hierarchical Methods, mentioned in the“For Further Info” section at the end of this article.) Now lets take a look atportal-based engines and see why they can be a pain in the neck when it comesto 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、地圖的二維座標,用於確定每個圖塊在地圖中的位置。

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

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

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

 

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

 

#defineMAP_MAX_WIDTH 50

#define MAP_MAX_HEIGHT50

typedef structtagTILE{

    intx,y,decrease,flag,block;

}TILE,*LPTILE;

TILEpMap[MAP_MAX_WIDTH][MAP_MAX_HEIGHT];

 

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

 

四、關於路徑:

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

 

typedef structtagNODE{

    intx,y;   //擴展路徑中的一個點在地圖中的座標。

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

}NODE,*LPNODE;

 

上面的結構是定義擴展路徑中的一個點的結構。擴展路徑是點的集合,因此用如下的數組進行定義:

 

NODEAllPath[PATH_MAX_LENGTH];

 

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

 

#definePATH_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)//數組中還有待擴展的節點

{

    intn=nodecount; //記錄下當前的數組節點的個數。

    for(inti=cutflag;i<nodecount;i ) //遍歷待擴展節點

    {

        for(intj=0;j<4;j ) //向待擴展節點的四周各走一步

        {

            //取得相鄰點的數據

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

            temp.y=AllPath[i].yoffy[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(inti=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中創建的(當然BSP8*樹的管理也和這兩個類有關,這暫時不討論)。

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();

AxisAlignedBoxcbbox=m_CubeNode->_getWorldAABB();

if(spbox.intersects(cbbox))

{

    //相交

}

區域查詢:

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

   //創建一個球體查詢,這裏的100m_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)

    {

    

    SceneQueryMovableObjectPairpObject=static_cast<SceneQueryMovableObjectPair>(*iter);

    //if(pObject)

    {

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

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

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

    }

    }

 

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