Box2D C++ 教程-碰撞剖析

轉載文章:原貼地址:http://ohcoder.com/blog/2012/11/30/anatomy-of-a-collision/

在Box2D中,經常會遇到物體之間的碰撞問題,當一個碰撞發生時,就是利用定製器(fixtures)用來做碰撞檢測的。碰撞可以以很多方式進行,我們可以在碰撞過程中獲取大量信息用在遊戲邏輯中。

比如說,你可能想知道這幾條信息:

-碰撞的開始和結束 -定製器中的哪個點有碰撞接觸 -定製器之間法向量的關係 -碰撞過程中產生了多大的能量以及會產生有多大的響應

通常情況下碰撞發生的速度非常快,但是這篇文章我們會對一個碰撞進行討論,把它的速度降到很慢,以此讓我們來仔細看看碰撞過程中到底發生了什麼,以及我們可以從中得到哪些相關信息。

故事發生在兩個多邊形定製器(fixtures)之間,爲了能夠更好的控制這兩個多邊形,我們選擇在失重的世界(重力加速度爲零)創建它們。其中一個是靜止的四方盒子,另一個是面向四方盒子進行橫向移動的三角形。

pic

場景中,三角形底部的角會和盒子上方的角發生碰撞。話說爲什麼要設置成這樣的碰撞場景,原因不是本次討論的重點,本次的重點是碰撞進行過程的不同階段我們可以得到哪些信息,如果你想重現這裏提到的碰撞場景,可以直接到源代碼中查看。

  • 獲取碰撞信息

b2Contact對象包含了碰撞的信息。從對象中可以得知哪兩個定製器發生了碰撞,以及碰撞的位置和碰撞之後的反作用方向。在Box2D中有兩個方法可以獲取b2Contact對象,一個是遍歷接觸(contacts)鏈表每一個物體,另外一種方法是用接觸監聽器(contact listener)。下面先讓我們快速瀏覽一下接下來要討論的兩種方法。

-查看接觸鏈表

你可以在任何時候,通過查看世界接觸鏈表來獲取當前所有接觸:

1
2
for (b2Contact* contact = world->GetContactList(); contact; contact = contact->GetNext())
    contact->... //do something with the contact

或者通過某個物體查看它的接觸:

1
2
for (b2ContactEdge* edge = body->GetContactList(); edge; edge = edge->next)
    edge->contact->... //do something with the contact

如果你使用這種方法,有一點非常重要的地方就是,即便鏈表中存在接觸(contact)也並不代表着兩個定製器之間正在發生接觸-這僅僅代表了兩個物體的AABB框發生了接觸而已。如果你想知道定製器自身是否真正發生了碰撞,需要通過判斷IsTouching()方法來檢測。

-接觸監聽器

使用接觸鏈表進行碰撞檢測,對於需要進行大量快速的碰撞信息獲取來說,顯得就很低效。設置接觸監聽器可以讓Box2D告訴你你所感興趣的事情什麼時候會發生,比起你只是爲了得知碰撞的開始和結束而興師動衆的完成所有重量級工作(例如不停的遍歷接觸鏈表)來說,顯得非常高效。接觸監聽器是一個包括四個方法的類(b2ContactListener),這些方法可以根據你的需要進行重寫。

1
2
3
4
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);

具體的事件內容取決於當前正在發生什麼,一些事件只是給我們傳遞了b2Contact對象。世界中的Step方法執行過程中,當Box2D檢測到某個事件發生時,就會回調這些方法,你就會知道到底發生了什麼事情。實際當中的應用會在“碰撞回調”中討論,這裏我們只是關注碰撞的什麼時候發生。

通常我比較建議使用接觸監聽器這種方法來做碰撞檢測。乍看起來它好像有點煩雜笨重,但是從長遠來看,這種方法更加高效和有用。

上述兩種方法都可以獲取接觸,它們都包含了相同的信息。其中最根本的信息就是獲取哪兩個定製器發生了碰撞,可以通過如下方法獲取:

1
2
b2Fixture* a = contact->GetFixtureA();
b2Fixture* b = contact->GetFixtureB();

如果你正在使用遍歷接觸鏈表的方法來檢測碰撞,那麼或許你已經知道發生碰撞的定製器是哪連個。但是如果你使用的方法是接觸監聽器,你需要依靠兩個方法來判斷哪兩個定製器發生了碰撞。發生碰撞的定製器A和定製器B之間是沒有次序關係的,所以你需要通過對定製器(fixtures)或物體(bodies)設置用戶數據來說明當前的定製器屬於哪一個。通過定製器,你可以通過GetBody()方法來找到對應的物體。

  • 拆解碰撞

現在讓我們深入的來了解上面提到的碰撞場景中,所發生的一系列碰撞事件。幸運的是,我們可以通過表格的形勢進行呈現(譯者注:爲了編輯方便偷了個懶,這裏用了列表的形式,:P),表格的左邊可以展現每一步的場景。你還通過testbed教程源代碼頁面下載源碼,邊運行邊看教程。在testbed中,你可以對模擬器進行暫停和重新開始操作,然後按“Single Step”按鍵可以詳細觀察發生的一切。

我們就從定製器的AABB框尚未重疊的那一刻開始,這樣我們就可以瞭解全程。單擊’AABBs’選擇框,來查看圍繞在每個定製器外面的紫色四邊形(譯者注:這就是AABB框)。

pic

-定製器AABB框開始重疊(b2ContactManager::AddPair)

pic

雖然定製器自身並沒有發生重疊,但此時世界當中,相關物體的b2Contact對象被創建並添加到接觸鏈表中。如果你按照上面提到的方法遍歷接觸鏈表的話,你將會發現有相應的對象出現,這也就預示着告訴你將有可能發生碰撞,但是實際上來說,一般你並不會關心AABB框的重疊與否,因爲你在乎的是定製器自身是否真正有重疊。

結果:

-接觸對象存在於接觸鏈表中,但IsTouching()返回false

-定製器開始重疊(b2Contact::Update)

pic 
                            Step n

pic 
      Step n+1(non bullet bodies)

對盒子左上角的圖像進行縮放,可以通過左側(譯者注:這裏是上圖)就可以看到兩個圖形平移的情況。這一刻發生在某個時間步長,這也就是說真實的碰撞點(如上圖點線所示)已經被跳過。這是因爲Box2D是先移動所有物體,然後再進行重疊檢測,至少默認設置是這樣的。如果你想獲得真實的碰撞位置,需要設置物體的”bullet”屬性,這樣就可以得到下圖中所示的圖像。具體實現如下:

1
2
bodyDef.bullet = true; //before creating body, or      
body->SetBullet(true); //after creating body

pic 
   Step n+1(triangle as bullet body)

Bullte物體會花費更多CPU時間來計算碰撞點,而且對於很多應用來說這是沒有必要的。作爲默認設置,你需要知道有時候會發生碰撞完全丟失的情況-比如本例中,如果三角形移動過快,完全有肯能跳過四方盒子的右上角!如果一個物體需要運動的足夠快而且又不能丟失任何碰撞,例如呃~…子彈!:)那麼你需要把物體設置成bullet物體。接下來,我們還是延續之前的非bullet物體屬性進行討論。

結果:

-IsTouching現在變爲true 
-BeginContact回調方法會被執行

  • 碰撞點和法向量

此時我們有了一個已經重疊的接觸,這就意味着現在我們就可以回答文章一開始提出的幾個問題。首先,讓我們先獲取位置和法向量,下面這段代碼,我們假定你要麼放到接觸監聽器的BeginContact方法中,要麼是通過獲取接觸鏈表,放到手動檢測物體接觸鏈表方法中。

其中,接觸以物體自身座標系統爲標準,存儲了碰撞點的座標信息,然而這對我們來說用處不大。但是我們可以利用接觸獲取更有用的’world manifold’,它以世界座標系爲標準存儲了碰撞點的位置信息。’Manifold’只不過是憑空想出來,能更好的區分兩個定製器的那條線的名字而已。

1
2
3
4
5
6
7
8
9
10
//normal manifold contains all info...
int numPoints = contact->GetManifold()->pointCount;
//...world manifold is helpful for getting locations
b2WorldManifold worldManifold;
contact->GetWorldManifold( &worldManifold );
//draw collision points
glBegin(GL_POINTS);
    for (int i = 0; i < numPoints; i++)
        glVertex2f(worldManifold.points[i].x,worldManifold.points[i].y);
glEnd();

pic

當作用一個衝量,讓兩個定製器分開時,這些點就是碰撞反作用點。雖然這些點不是定製器第一次接觸時精準的座標點(除非你使用bullet類型物體)。實踐中,這些點足夠在遊戲邏輯中使用。

下面讓我們展示一下從定製器A指向定製器B的法向量:

1
2
3
4
5
6
7
8
9
10
float normalLength = 0.1f;
b2Vec2 normalStart = worldManifold.points[0] - normalLength * worldManifold.normal;
b2Vec2 normalEnd = worldManifold.points[0] + normalLength * worldManifold.normal;

glBegin(GL_LINES);
    glColor3f(1,0,0);//red
    glVertex2f( normalStart.x, normalStart.y );
    glColor3f(0,1,0);//green
    glVertex2f( normalEnd.x, normalEnd.y );
glEnd();

具體看起來像是這樣,以最快的方法解算出重疊產生的衝量,以此將三角形的一個角推向左上方,同時將四邊形的這個角推向右下方。請注意法向量只不過是一個方向而已,它並沒有座標也沒有連接其中任何一個點-我僅僅是圖方便,畫到了points[0]點而已。

pic

這裏有一個很重要的一點是碰撞法向量並不能爲你提供兩個碰撞定製器之間的角度-還記得這裏的三角形是水平移動的,對吧?它只能給出定製器不會重疊的短距離移動方向。比如,想象一下如果三角形移動的速度再快一點重疊的部分看起來像這樣:

pic

…那麼短距離內使兩個定製器分離,會把三角形向右上方推開。這就可以明顯看出,使用法向量來作爲定製器的碰撞角度並不是一個好的辦法。如果你想知道兩個定製器實際所受影響的角度,可以這樣:

1
2
3
b2Vec2 vel1 = triangleBody->GetLinearVelocityFromWorldPoint( worldManifold.points[0] );
b2Vec2 vel2 = squareBody->GetLinearVelocityFromWorldPoint( worldManifold.points[0] );
b2Vec2 impactVelocity = vel1 - vel2;

…以此來獲取碰撞過程中每個物體上點的實際相對速度向量。作爲一個簡單的例子,我們看看是否也可以獲取三角形的線速度,因爲我們知道四邊形盒子是靜態的並且三角形是不旋轉的,但是上面的代碼仍然考慮到了兩個物體同時旋轉或者平移的情況。

另外需要注意的一點是,不是所有碰撞都會有兩個碰撞點。本實例我是特地找了一個複雜點的例子,其中多邊形的兩個角發生重疊,但是現實中更常見的碰撞情景是隻有一個碰撞點。這裏展示了一些只有一個碰撞點的例子:

pic 
pic 
pic

ok,現在我們已經知道如何找到碰撞點以及法向量,並且我們也瞭解到這些點和法向量將會被Box2D用來正確響應碰撞重疊。下面讓我們繼續回到事件序列的正軌上來…

  • 碰撞響應(b2Contact::Update, b2Island::Report)

pic 
                           Impact

pic 
                      Impact + 1

pic 
                      Impact + 2

當定製器重疊的時候,Box2D默認的行爲是爲每一個定製器施加衝量,並讓它們分開,但是在單個步長中,並不是每次總能成功的。正如這裏所示,對於這個特殊的例子,這兩個定製器在被’彈力’完全彈開並再次分離之前會有三個時間步長。

在這個時間長度裏,如果我們願意我們可以對每一步進行自定義行爲。如果你使用接觸監聽器方法進行檢測,監聽器中的PreSolve和PostSolve方法會在定製器重疊過程中的每個時間步長裏被重複調用,這就給了你機會在處理碰撞響應(PreSolve方法)之前來改變接觸以及發現處理碰撞響應之後(PostSolve方法)所產生的碰撞向量。

爲了讓思路更清晰,這裏通過在例子的主Step方法以及每一個接觸監聽器方法中放入輸入模塊,來依次把事件順序進行了打印:

 
Step 
Step 
BeginContact 
PreSolve 
PostSovle 
Step 
PreSolve 
PostSolve 
Step 
PreSolve 
PostSolve 
Step 
PreSovle 
PostSovle 
Step 
EndContact
Step 
Step 

結果:

-PreSolve和PostSolve方法會被重複調用

  • PreSolve和PostSolve

PreSovle和PostSolve方法都爲你提供了一個b2Contact指針,那麼我們就可以訪問同一個指針以及在BeginContact方法中進行查看的常用信息。PreSolve方法爲我們提供了一個在碰撞響應計算之前改變接觸特性的機會,或者甚至是同時取消響應,而且通過PostSolve方法我們可以找到碰撞響應的具體信息。

下面是可以在PreSolve方法中對於接觸做出的一些改變:

1
2
3
4
void SetEnabled(bool flag);//non-persistent - need to set every time step
//these available from v2.2.1
void SetFriction(float32 friction);//persists for duration of contact
void SetRestitution(float32 restitution);//persists for duration of contact

調用SetEnabled(false)方法將會關閉接觸,這也就意味着正常的碰撞響應將會被跳過。你可以利用這一點暫時允許物體之間彼此穿透。一個典型的例子是單面牆或平臺,玩家可以從一面穿過,另一面卻是實牆,這只能在運行時根據不同的條件進行界定,類似於玩家此時的位置以及面部朝向,等等。

需要注意的重要的一點是接觸的狀態會在下一個時間步長恢復,所以如果你希望像上面那樣關閉接觸,那麼每個時間步長都應該調用SetEnabled(false)方法。

除了接觸指針以外,PreSolve還有第二個參數,此參數爲我們提供了先前時間步長中關於碰撞的相關信息。如果有人知道這個參數用來做什麼,也讓我瞭解一下 :D

PostSolve方法在碰撞響應計算以及應用之後進行調用。它也有第二個參數,我們可以獲取應用於碰撞的衝量信息。對於這個信息更常用的地方是用來檢查所產生的碰撞響應是否超過給定閥值,通過檢查這個閥值可以判斷某個物體是否會發生爆炸,等等。詳見’黏性拋體(“sticky projectiles”)’話題中的例子,使用PostSolve方法來檢測當一支箭進行射擊時是否應該黏到目標上。

Ok,回到時間線上…

pic

pic 
                          放大圖

-定製器完成重疊(b2Contact::Update)

AABB框仍然重疊,所以接觸仍然保留在物體/世界的接觸鏈表中。

結果:

-EndContact回調方法將會被調用 
-IsTouching()現在返回false

pic

定製器的AABB框結束重疊(b2ContactManager::Collide)

結果:

-Contact從物體/世界的接觸鏈表中移除

當調用EndContact方法的時候,接觸鏈表會傳入一個b2Contact指針,此時定製器將不再有接觸,所以也不會再獲得有效的相關信息。即便如此EndContact事件仍然是接觸鏈表當中不可或缺的一部分,因爲它允許你檢查定製器/物體/遊戲對象中哪個結束了接觸。詳見下一個話題中的例子。

  • 總結

希望此次話題,通過對Box2D碰撞事件毫秒級別一步步的講解,能夠讓你對此有一個清晰的大致瞭解。這個話題看起來也許不是那麼有意思(可以肯定的是,到目前爲止應該是令人興奮的話題!)但是我從一些論壇的問題中感受到,這裏所討論的一些細節經常被忽略掉,並且大部分時間都花在盲目的解決問題上,而不是真正瞭解到底是怎麼回事。我還注意到一種迴避使用接觸監聽器的傾向,從長遠來看以此會讓監聽器承擔更少的工作。瞭解這些細節可以讓你真正理解實際過程是什麼樣的,可以讓你有更好的設計,節約實現的時間。

發佈了36 篇原創文章 · 獲贊 5 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章