天天看點

BOX2D 第九章 接觸第九章 接觸

第九章 接觸

Dec 2nd, 2012| Comments

聲明:此文章翻譯自Box2D v2.2.0使用者手冊,僅供學習參考。

9.1 關于(About)

接觸是Box2D所建立,用來管理定制器之間碰撞的。如果定制器有孩子,就像鍊條一樣,接觸也會存在于每個孩子上。有很多派生自b2Contact類的不同的接觸,來管理不同定制器之間的碰撞。比如說有管理多邊形與多邊形之間碰撞的接觸,還有管理圓形與圓形之間的碰撞。

下面是一些與接觸相關的術語。

  • 接觸點(contact point)

一個接觸點是指兩個形狀相交的一個點。Box2D近似的認為有小量的點接觸。

  • 接觸法線(contact normal)

接觸法線是一個機關向量,從一個形狀指向另一個形狀。通常來說向量點從fixtureA指向fixtureB。

  • 接觸分離(contact separation)

分離與穿越(penetration)相反。當形狀重疊時,分離是負值。可能會在Box2D的未來版本中增加正值的分離接觸點,是以當接觸點有報告的時候,或許最好檢查一下正負号。

  • 接觸取樣(contact manifold)

兩個凸多邊形之間接觸或許會生成2個接觸點。這些接觸點都使用相同的法線,是以它們會被分組到一個近似持續接觸區域的接觸取樣裡。

  • 法向沖量(normal impulse)

法向力作用在一個接觸點上來防止形狀的穿透(penetrating)。為友善起見,Box2D使用沖量工作。法向沖量是法向力和時間步長的乘積。

  • 切向沖量(tangent impluse)

切向力在接觸點産生,用于模拟摩擦。為友善起見,切向作用力以沖量的方式存儲。

  • 接觸辨別(contact ids)

Box2D嘗試重用上一個時間步長中觸點壓力産生的結果作為下一個時間步長的推測的初始值。Box2D通過接觸辨別來比對橫跨多個時間步長的接觸點。辨別包括幾何特征索引來協助區分不同的接觸點。

當兩個定制器的AABB重疊的時候接觸點生成。有時候接觸過濾器将會阻止接觸點的生成。當重疊停止之後接觸點随之銷毀。

你擷取會發現看起來沒有接觸(僅僅因為它們的AABB的原因)的定制器之間産生了接觸點。那麼,這是正确的結果。這是一個“先有雞還是先有蛋”的問題。我們不知道我們是否需要一個接觸,除非我們建立一個接觸來分析碰撞。如果形狀之間沒有接觸我們可以馬上删除接觸,或者我們可以等到AABB停止重疊之後再删除接觸。Box2D會采用後一種方式,因為這可以通過系統緩存來增加性能。

9.2 接觸類(Contact Class)

正如之前所提到的,接觸類是由Box2D來建立和銷毀的。接觸對象不是由使用者來建立的。即便如此,你也可以通路接觸類并與它進行互動。

你可以通路原接觸取樣(contact manifold):

1
2
      
b2Manifold* GetMainfold();
const b2Manifold* GetManifold()const;

           

你甚至可以修改取樣(manifold),但是通常不提倡的這種做法,一般屬于進階用法。

有一個便捷的方法來獲得b2WorldManifold:

void GetWorldManifold(b2WorldManifold* worldManifold) const;

           

這是用目前物體的位置來計算接觸點的世界位置。

傳感器不會建立取樣,是以對它們而言使用:

bool touching = sensorContact->IsTouching();

           

這個方法對非傳感器(non-sensors)也有效。

你可以從一個接觸中獲得定制器,以此你也可以擷取物體。

1
2
3
      
b2Fixture* fixtureA = myContact->GetFixtureA();
b2Body* bodyA = fixtureA->GetBody();
MyActor* actorA = (MyActor*)bodyA->GetUserData();

           

你可以禁用(disable)一個接觸。這隻能在b2ContactListener::PreSolve事件中使用,接下來會進行讨論。

9.3 通路接觸(Accssing Contacts)

你可以有幾種方式來通路接觸。你可以在世界中直接通過物體通路接觸。你也可以實作一個接觸監聽器(contact listener)。

你可以周遊世界中所有接觸:

1
2
3
4
      
for(b2Contact* c = myWorld->GetContactList(); c; c = c->GetNext())
{
    //  process  c
}

           

你也可以周遊一個物體的所有接觸。這些通過一個接觸邊緣結構體(contact edge structure)存儲在一個圖(譯者注:指的是資料結構中的圖結構)中。

1
2
3
4
5
      
for(b2ContactEdge* ce = myBody->GetContactList(); ce; ce = ce->next)
{
    b2Contact* c = ce->contact;
    // process c
}

           

你也可以像下面描述的那樣通過接觸監聽器(contact listener)來通路接觸。

警告

通過b2World和b2Body通路接觸有肯能會丢失時間步長中間産生的短暫的接觸。使用b2ContactListener可以準确的獲得大部分結果。

9.4 接觸監聽器(Contact Listener)

你可以通過實作b2ContactListener來接收接觸資料。接觸監聽器支援幾種事件:開始,結束,求解前(pre-solve)以及求解後(post-solve)。

1
2
3
4
5
6
7
8
9
10
11
12
      
class  MyContactListener : public  b2ContactListener
{
    public:
        void  BeginContact(b2Contact*  contact)
        {/*handle begin event*/}
        void  EndContact(b2Contact*  contact)
        {/*handle end event */}
        void  PreSolve(b2Contact*  contact, const b2Manifold*  oldManifold)
        {/*handle pre-solve event*/}
        void  PostSolve(b2Contact*  contact, const b2ContactImpulse*  impulse)
        {/*handle post-solve event*/}
};

           

警告

不要儲存發送到b2ContactListener的引用。相反把接觸點資料做一個深度拷貝到你自己的緩存中。接下來的例子将會展示這種方法。

在運作期(run-time)你可以建立一個監聽器(listener)的對象并使用b2World::SetContactListener對其進行注冊。確定你的監聽器在世界對象存在的範圍内有效。

  • 開始接觸事件(Begin Contact Event)

當兩個定制器開始重疊的時候會發出此事件。傳感器(sensors)和非傳感器(non-sensors)都會出發此事件。此事件隻會發生在時間步長内。

  • 結束接觸事件(End Contact Event)

當兩個定制器結束重疊的時候會發出此事件。傳感器 (sensors) 和非傳感器(non-sensors)都會出發此事件。當物體被銷毀的時候此事件也會被觸發,是以這個時間會發生在時間步長以外。

  • (求)解前事件(Pre-Solve Event)

此事件在碰撞檢測之後被調用,但是早于碰撞沖突之前。這就給了你一個機會來基于目前配置禁用(disable)此接觸。比如說,通過回調并且調用b2Contact::SetEnabled(false)方法,來實作單面碰撞功能。接觸在每次通過碰撞處理後重新變為可用,是以需要在接觸的每一個步長中禁用此接觸。在每一次接觸過程中,機關時間步長内由于持續進行碰撞檢測而觸發多次解前事件(pre-solve event)。

1
2
3
4
5
6
7
8
9
      
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
{
    b2WorldManifold  worldManifold;
    contact->GetWorldManifold(&worldManifold);
    if(worldManifold.normal.y < -0.5f)
    {
        contact->SetEnabled(false);
    }
}

           

解前事件(pre-solve event)也是一個确定點的狀态和擷取碰撞速度的好地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
      
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
{
    b2WorldManifold  worldManifold;
    contact->GetWorldManifold(&worldManifold);
    b2PointState  state1[2], state2[2];
    b2GetPointStates(state1, state2, oldManifold, contact->GetManifold());
    if(state2[0] == b2_addState)
    {
        const  b2Body*  bodyA = contact->GetFixtureA()->GetBody();
        const  b2Body*  bodyB = contact->GetFixtureB()->GetBody();
        b2Vec2  point = worldManifold.points[0];
        b2Vec2  vA = bodyA->GetLinearVelocityFromWorldPoint(point);
        b2Vec2  vB = bodyB->GetLinearVelocityFromWorldPoint(point);
        float32  approachVelocity = b2Dot(vB - vA, worldManifold.normal);
        if(approachVelocity > 1.0f)
        {
            MyPlayCollisionSound();
        }
    }
}

           
  • (求)解後事件(Post-Solve Event)

可以在解後事件(post-solve event)發生的地方收集碰撞沖量結果。如果你不關心沖量,或許你隻要實作解前事件(pre-solve event)就可以了。

在接觸回調中改變實體世界以此來實作遊戲邏輯是一件吸引人的方式。舉例來說,你可能有一個碰撞來損壞并且試圖摧毀相關聯的角色及其剛體。然而,Box2D不允許你在回調事件中修改實體世界,因為你可能會銷毀Box2D目前正在處理的物體,以此導緻野指針。

建議你先把正在處理的以及你關心的所有接觸點先進行緩存,完成必要的時間步長之後再進行處理。應該總是在每個時間步長之後立即處理接觸點。否則其它客戶代碼可能會修改實體世界,使觸點緩存無效。當你處理觸點緩存中的接觸點以此來改變實體世界的時候,你仍然需要小心不要導緻野指針并将其存儲到觸點緩存中。在testbed中有處理接觸點時防止野指針的例子。

這是一段CollisionProcessing測試中的代碼,展示了在處理接觸緩存時如何處理孤立物體。下面是一個片段。注意讀列出的注釋部分。這段代碼假設所有的接觸點都已經緩存到了b2ContactPoint數組m_points中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
      
//We are going to destroy some bodies according to contact
//points.We must buffer the bodies that should be destroyed
//because they may belong to multiple contact points.
const int32 k_maxNuke = 6;
b2Body* nuke[k_maxNuke];
int32 nukeCount = 0;
//Traverse the contact buffer. Destroy bodies that 
//are touching heavier bodies.
for(int32 i = 0;i < m_pointCount; ++i)
{
    ContactPoint* point = m_points + i;
    b2Body* body1 = point->shape1->GetBody();
    b2Body* body2 = point->shape2->GetBody();
    float32 mass1 = body1->GetMass();
    float32 mass2 = body2->GetMass();
    if(mass1 > 0.0f && mass2 > 0.0f)
    {
        if(mass2 > mass1)
        {
            nuke[nukeCount++] = body1;
        }
        else
        {
            nuke[nukeCount++] = body2;
        }
        if(nukeCount == k_maxNuke)
        {
            break;
        }
    }
}

//Sort the nuke array to group duplicates.
std::sort(nuke, nuke + nukeCount);
//Destroy the bodies, skipping duplicates.
int32 i = 0;
while(i < nukeCount)
{
    b2Body*  b = nuke[i++];
    while(i < nukeCount && nuke[i] == b)
    {
        ++i;
    }
    m_world->DestroyBody(b);
}

           

9.5 接觸過濾(Contact Filtering)

遊戲中很多情況,你并不希望所有物體都發生碰撞。比如說,你想建立一個隻能有特定對象通過的門。這就稱為接觸過濾,因為一些互動被過濾掉了。

Box2D允許你通過實作b2ContactFilter類來完成用戶端的接觸過濾。此類需要你完成接收兩個b2Shape類型指針的ShouldCollide方法。如果物體可以碰撞那麼方法就傳回true。

預設的ShouldCollide方法實作是在第六章(定制器)使用b2FilterData定義的。

1
2
3
4
5
6
7
8
9
10
11
      
bool b2ContactFilter::ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB)
{
    const b2Filter& filterA = fixtureA->GetFilterData();
    const b2Filter& filterB = fixtureB->GetFilterData();
    if(filterA.groupIndex == filterB.groupIndex && filterA.groupIndex != 0)
    {
        return filterA.groupIndex > 0;
    }
    bool collide = (filterA.maskBits & filterB.categoryBits) != 0 && (filterA.categoryBits & filterB.maskBits) != 0;
    return collide;
}

           

在運作時你可以建立一個接觸過濾器并使用b2World::SetContactFilter進行注冊。當world存在時確定你的過濾器在有效作用域内。

1
2
3
      
MyContactFilter  filter;
world->SetContactFilter(&filter);
// filter remains in scope ...

           

轉自:http://ohcoder.com/blog/2012/12/02/contacts/