轉自:Complx_ok
第09章 接觸(Contacts)
接觸(contact)是由 Box2D 創建的用於管理fixture間碰撞的對象。接觸有不同的種類,它們都派生自 b2Contact,用於管理不同類型形狀之間的接觸。例如, 有管理多邊形之間碰撞的類,有管理圓形之間碰撞的類。
這是與接觸有關的術語
接觸點(contact point)
接觸點就兩個形狀相互接觸的點。在Box2D中,近似地認爲在少數點處有接觸。
接觸法線(contact normal)
接觸法線是一個單位向量,由一個fixture指向另一個fixture。按照慣例,向量由fixtureA指向fixtureB。
接觸分隔(contact separation)
分隔正好與穿透(penetration)相反。當形狀相重疊時, 分隔爲負。有可能將來的Box2D版本中會以正隔離來創建觸點, 所以當有觸點的報告時你可能需要檢查一下符號。
contact manifold
兩個凸多邊形相互接觸,有可能會產生兩個接觸點。這些點都有相同的法線,所以就相它們分成一組,構成contact manifold,這是連續區域接觸的一個慣例。
(譯註: manifold不知道怎麼翻譯。我猜測manifold是指有相同特性的東西歸成一類。接觸點有相同的法線,就歸成contact manifold。)
法向衝量(normal impulse)
法向力作用於接觸點,用於防止形狀相互穿透。爲方便起見,Box2D使用衝量(impulses)。法向力與時間步相乘,構成法向衝量
切向衝量(tangent impulse)
切向力會在接觸點生成,用於模擬摩擦。爲方便起見,切向作用使用衝量的方式存儲。
接觸標識(contact ids)
從最開始的猜測值出發,Box2d得出當前時間步的觸點壓力,再重新利用當前時間步的壓力結果去推測下一個時間步的壓力結果。接觸標識用於匹配跨越時間步的觸點。標識包含了幾何特徵索引以便區分觸點。
當兩個fixture的AABB重疊時,接觸就被創建了。有時碰撞篩選會阻止接觸的創建。當AABB 不再重疊後接觸會被摧毀。
也許你會皺起眉頭,爲了沒有發生實際碰撞的形狀(只是它們的 AABB)卻創建了接觸。好吧,的確是這樣的,這是一個“雞或蛋”的問題。我們並不知道是否需要一個接觸,除非我們創建一個接觸去分析碰撞。如果形狀之間沒有發生碰撞,我們需要正確地刪除接觸,或者,我們可以一直等到 AABB 不再重 疊。爲了提高性能, Box2D選擇了後面這個方法。
之前已經提及過,接觸對象是Box2D內部創建和摧毀的,並不是由用戶來創建。然而,你還是能夠訪問接觸類並和它交互的。
你可以訪問原始的contact manifold:
b2Manifold* GetManifold();
const b2Manifold* GetManifold()const;
你甚至可以修改manifold,一般情況下不提倡你怎樣做。修改manifold是較高級的用法。
這個是幫助函數,去獲取b2WorldManifold:
void GetWorldManifold(b2WorldManifold*worldManifold) const;
這使用了物體的當前位置去計算出接觸點在world座標下的位置。
傳感器(Sensors)並不創建manifolds,所以要使用:
bool touching =sensorContact->IsTouching();
這函數對於非傳感器(non-sensors)也有效。
從接觸(contact)中你可以得到fixture, 從而再得到body。
b2Fixture* fixtureA = myContact->GetFixtureA();
b2Body* bodyA =fixtureA->GetBody();
MyActor* actorA =(MyActor*)bodyA->GetUserData();
你可以使一個接觸失效。這僅僅在b2ContactListener::PreSolve事件中有效,下面會再進行討論。
你有幾種方法來訪問接觸。爲了訪問接觸,你可以直接查詢world或者body結構,還可以實現一個接觸監聽器(contact listener)。
在world中,你可以遍歷所有的接觸:
for (b2Contact* c =myWorld->GetContactList(); c; c = c->GetNext())
{
// process c
}
同樣在body中,你也可以遍歷所有接觸。接觸以圖的方式存儲,使用了接觸邊數據結構(contact edge structure),
for (b2ContactEdge* ce =myBody->GetContactList(); ce; ce = ce->next)
{
b2Contact* c = ce->contact;
// process c
}
通過下面描述的接觸監聽器,你也可以訪問接觸。
注意
通過b2World或者b2Body直接訪問,有可能會錯過一些時間步中產生的臨時接觸。而使用b2ContactListener 就可以很精確的得到全部結果。
通過實現 b2ContactListener 你就可以收到接觸數據。接觸監聽器支持幾種事件: 開始(begin),結束(end), 求解前(pre-solve), 求解後(post-solve)。
class MyContactListener : publicb2ContactListener
{
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來註冊這個對象。 但要保證當world對象存在時,listener要留在作用域中。
Begin事件
當兩個fixture開始有重疊時,事件會被觸發。傳感器和非傳感器都會觸發這事件。這事件只能在時間步內(譯註: 也就是b2World::step函數內部)發生。
End事件
當兩個fixture不再重疊時,事件會被觸發。傳感器和非傳感器都會觸發這事件。當一個body被摧毀時,事件也有可能被觸發。所以這事件也有可能發生在時間步之外。
Pre-Solve事件
在碰撞檢測之後,但在碰撞求解之前,事件會被觸發。這樣可以給你一個機會,根據當前情況來決定是否使這個接觸失效。 舉個例子,在回調中使用b2Contact::SetEnabled(false),你就可以實現單側碰撞的功能。每次碰撞處理時,接觸會重新生效,所以你在每一個時間步 中都應禁用那個接觸。由於連續碰撞檢測,pre-solve事件在單個時間步中有可能發生多次。
void PreSolve(b2Contact* contact,const b2Manifold* oldManifold)
{
b2WorldManifold worldManifold;
contact->GetWorldManifold(&worldManifold);
if (worldManifold.normal.y <-0.5f)
{
contact->SetEnabled(false);
}
}
如果要確認觸點狀態或得到碰撞速度,可以在pre-solve事件中處理。
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事件
當你可以得到碰撞衝量(collision impulse)的結果時,post-solve事件會發生。 如果你不關心衝量,你可能只需要實現pre-solve事件。
在一個接觸回調中去改變物理世界是誘人的。例如,你可能會以碰撞來施加傷害,並試圖摧毀關聯的角色和它的剛體。然而,Box2D並不允許你在回調中改變物理世界,因爲你可能會摧毀 Box2D 正在運算的對象, 造成野指針。
處理觸點的推薦方法是緩衝所有你關心的觸點,並在時間步之後處理它們。一般在時間步之後你應該立即處理它們,否則其它客戶端代碼可能會改變物理世界,使你的緩衝失效。當你處理觸點緩衝的時候,你可以去改變物理世界,但是你仍然應該小心不要造成無效的指針。在 testbed中有安全處理觸點以避免無效指針的例子。
這是一小段 CollisionProcessing 測試中的代碼,它演示了在操作觸點緩衝時如何處理孤立物體。請注意註釋。代碼假定所有觸點都緩衝於 b2ContactPoint 數組 m_points 中。
// 我們打算摧毀和contact有所關聯的物體指針.
// 我們必須先緩存那些需要摧毀的物體,因爲它們有可能被多個觸點所共有。
const int32 k_maxNuke = 6;
b2Body* nuke[k_maxNuke];
int32 nukeCount = 0;
// 遍歷contact緩存,摧毀相互接觸時,無那麼重的物體。
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;
}
}
}
// 將nuke數組排序,使得重複的指針歸在一起
std::sort(nuke, nuke + nukeCount);
// 刪除body, 跳過重複的
int32 i = 0;
while (i < nukeCount)
{
b2Body* b = nuke[i++];
while (i < nukeCount&& nuke[i] == b)
{
++i;
}
m_world->DestroyBody(b);
}
通常,你不希望遊戲中的所有物體都發生碰撞。例如,你可能會創建一個只有特定角色才能通過的門。 這稱之爲接觸篩選,因爲一些交互被篩選出了。
通過實現b2ContactFilter類, Box2D允許定製接觸篩選。這個類需要你實現一個ShouldCollide 函數, 這個函數接收兩個b2Shape的指針作爲參數。如果應該碰撞那麼就返回true。
默認的ShouldCollide實現使用了“第06章,夾具(Fixtures)”定義的b2FilterData。
bool b2ContactFilter::ShouldCollide(b2Shape*shape1, b2Shape* shape2)
{
const b2FilterData& filter1 =shape1->GetFilterData();
const b2FilterData& filter2 =shape2->GetFilterData();
if (filter1.groupIndex ==filter2.groupIndex &&
filter1.groupIndex != 0)
{
return filter1.groupIndex> 0;
}
bool collide = (filter1.maskBits &filter2.categoryBits) != 0 &&
(filter1.categoryBits& filter2.maskBits) != 0;
return collide;
}
在運行期(run-time), 你可以創建自己的接觸篩選實例,並使用b2World::SetContactFilter函數來註冊。 你要保證當world存在時,你的filter要保留在作用域中。
MyContactFilter filter;
world->SetContactFilter(&filter);
// filter留在作用域中