内容很多摘自
Aman JIANG(江超宇)翻译的Box2D v2.0.1 用户手册
接触(contact)是由 Box2D 创建的用于管理fixture间碰撞的对象。接触有不同的种类,它们都派生自 b2Contact,用于管理不同类型形状之间的接触。例如, 有管理多边形之间碰撞的类,有管理圆形之间碰撞的类。
这是与接触有关的术语
接触点就两个形状相互接触的点。在Box2D中,近似地认为在少数点处有接触。
接触法线是一个单位向量,由一个fixture指向另一个fixture。按照惯例,向量由fixtureA指向fixtureB。
分隔正好与穿透(penetration)相反。当形状相重叠时, 分隔为负。有可能将来的Box2D版本中会以正隔离来创建触点, 所以当有触点的报告时你可能需要检查一下符号。
两个凸多边形相互接触,有可能会产生两个接触点。这些点都有相同的法线,所以就相它们分成一组,构成contact manifold,这是连续区域接触的一个惯例。
(译注: manifold不知道怎么翻译。我猜测manifold是指有相同特性的东西归成一类。接触点有相同的法线,就归成contact manifold。)
法向力作用于接触点,用于防止形状相互穿透。为方便起见,Box2D使用冲量(impulses)。法向力与时间步相乘,构成法向冲量
切向力会在接触点生成,用于模拟摩擦。为方便起见,切向作用使用冲量的方式存储。
从最开始的猜测值出发,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要留在作用域中。
当两个fixture开始有重叠时,事件会被触发。传感器和非传感器都会触发这事件。这事件只能在时间步内(译注: 也就是b2World::step函数内部)发生。
当两个fixture不再重叠时,事件会被触发。传感器和非传感器都会触发这事件。当一个body被摧毁时,事件也有可能被触发。所以这事件也有可能发生在时间步之外。
在碰撞检测之后,但在碰撞求解之前,事件会被触发。这样可以给你一个机会,根据当前情况来决定是否使这个接触失效。 举个例子,在回调中使用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();
}
}
}
当你可以得到碰撞冲量(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留在作用域中