第九章 接触(contact)
9.1简介
接触是Box2D创建的用来管理装置间的碰撞的物体。如果装置具有子物体(child),例如链形状(chainshape),那么每一个相关的子物体都存在对应的接触。为了满足管理不同类型的装置之间的碰撞,Box2D中有定义了很多种接触类型,他们都继承自b2Contact。例如有些接触用来处理多边形和多边形的碰撞,而有些接触是用来处理圆形和圆形之间的碰撞的。
下面列出了一些跟接触有关的术语。
接触点(contact point)
接触点是两个形状彼此接触的位置,Box2D中用少量的接触点来近似地表示接触。
接触法线(contact normal)
接触法线是一个单位向量,它从一个形状指向另一个形状。按照通常的习惯,接触法线从fixture(装置A)指向fixture(装置B)。
接触分隔(contact separation)
分隔是穿透(penetration)的反义词,当形状之间重叠的时候,分隔(系数)是负值,Box2D在后面的版本中有可能创建具有正分隔(系数)的接触点,因此当接触点被报告出来的时候,你可能需要检查一下符号。
接触采样(contact manifold)
两个凸多边形之间的接触最多能产生2个接触点,这些接触点具有相同的法线,所以他们被组合到了一个接触采样结构中,作为一种近似来代表接触面上一系列连续的点。
法线冲量(normal impulse)
法线力(normalforce)是在接触点上施加的用来防止形状穿透彼此的力,为了方便起见,Box2D中使用冲量,法线冲量就是法线力和时间间隔(time step)的乘积。
切线冲量(tangent impulse)
切线力由一个接触点产生,用来模拟摩擦力。为了方便起见,也是用冲量来存储。
接触id(contact id)
Box2D尝试重用当前时间间隔(timestep)中接触面上的力的计算结果,作为下一个时间间隔中接触面上面的力的初始的预测值。Box2D使用接触id来跨越不同的时间间隔进行接触点的匹配。这些id包含几何特征索引用来帮助Box2D区分不同的接触点。
当两个装置的AABB(参考前面的章节定义)重叠时,接触就被创建出来。有时候冲突过滤条件会防止接触的创建。当图形的AABB不再重叠的时候,接触就被析构掉了。
你可能会皱眉说,尽管装置的AABB重叠了,装置可能也没有重叠,但是这时接触也被创建了。好吧,的确如此,这又是一个“鸡生蛋,蛋生鸡”的问题。我们并不知道我们是否需要一个接触物体直到一个接触物体被创建了,用来做碰撞分析(没有碰撞物体我们就不能分析是否碰撞了,不碰撞我们又不能创建接触物体…)。如果形状没有接触,我们可以立即删除接触物体,或者我们可以等到AABB不再重叠之后在删除它们。Box2D使用后面这种处理方法,因为后者能够让系统做一些缓存来提高效率。
9.2接触类(contactclass)
正如我们前面提到的,接触类由Box2D构造和析构,接触对象并不是开发者(用户)创建的,但是,你可以访问接触类并和它进行交互。
你可以通过下面的方法访问原始的接触采样结构(contact manifold):
b2Manifold* GetManifold();
const b2Manifold* GetManifold() const;
你甚至能够修改采样,但是通常情况下我们不建议你这样去做,这是一种比较高级的用法。
有一个帮助函数(helperfunction)用来获取b2WorldManifold对象:
void GetWorldManifold(b2WorldManifold* worldManifold) const;
这个方法使用的是物体当前的位置来计算接触点的世界坐标系中的位置。
Box2D不会为传感器创建接触采样,所以对于传感器,使用下面的方法来判断接触:
bool touching = sensorContact->IsTouching();
这个方法也适用于非传感器的装置。
你也可以通过接触来获取装置对象,进而获取它们附加到的物体:
b2Fixture* fixtureA = myContact->GetFixtureA();
b2Body* bodyA = fixture->GetBody();
MyActor* actorA = (MyActor*)bodyA->GetUserData();
你可以禁用一个接触,这仅仅在b2ContactListener::PreSolve事件中有效,后面我们会提到。
9.3访问接触(accessingcontact)
你可以通过实现b2ContactListener来接收接触数据,接触监听器支持这几种事件:开始(begin),结束(end),pre-solve和post-solve。
class MyContactListener: public b2ContactListener
{
public:
void BeginContact(b2Contact* contact)
{ /* handle begin event */ }
void EndContact(b2Contact* contact)
{ /* handle end event */ }
void PreSolve(b2Contact* contact, constb2Manifold* oldManifold)
{ /* handle pre-solve event*/ }
void PostSolve(b2Contact* contact, constb2ContactImpulse* impulse)
{ /* handle post-solve event*/ }
}
注意:不要保留传递给b2ContactListener的指针参数的引用,相反的,将接触指针指向的数据深拷贝到你自己的缓冲数据中,下面的例子说明了该如何实现这种方式。
在运行时你可以创建一个监听器的实例,并且利用b2World::SetContactListener来注册它。需要确保listener对象在作用域中,世界对象也同时存在。
开始接触事件(begin contact event)
当两个装置开始重叠的时候,这个事件被触发,对于传感器和非传感器都适用。这个事件仅在时间间隔内发生。
结束接触事件(end contact event)
当两个装置停止重叠的时候,这个事件被触发,对于传感器和非传感器都适用。当其中一个物体被析构的时候,这个事件也同样被触发,所以该事件也是仅在时间间隔内发生。
预解析事件(Pre-Solve event)
在检测到碰撞后,该事件被调用,但是先于碰撞处理逻辑(collisionresolution),这样你就有机会基于当前的配置来决定是否禁用接触。例如,你可以在回调中调用b2Contact::SetEnabled(false)来实现一个单侧的平台(one-sidedplatform)。每次碰撞处理过程中,接触都会被重新启用,所以你需要在每个时间间隔内都禁用接触。由于CCD(连续碰撞检测,参见我们前几节的注释)机制,预解析事件可能会在一个时间间隔内多次触发。
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
{
b2WorldManifold worldManifold;
contact->GetWorldManifold(&worldManifold);
if (worldManifold.normal.y < -0.5f)
{
contact->SetEnabled(false);
}
}
预解析事件是一个确认接触点状态和碰撞逼近速度的理想位置。
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);
float32approachVelocity = b2Dot(vB –vA, worldManifold.normal);
if (approachVelocity > 1.0f)
{
MyPlayCollisionSound();
}
}
}
补充解析事件(Post-Solve event)
(注:这里Post没有找到太恰当的词来对应,意思就是在End之后发生的事件,我们就理解为是对End事件的补充吧)
补充解析事件中,你可以统计碰撞冲量的结果,如果你不在乎冲量结果,那只要实现预解析方法就好了。
在回调事件处理方法中实现改变物理世界的逻辑是一件非常吸引人的事情,你可能有一个能够产生伤害的碰撞,甚至是能够去摧毁对应的角色和它的刚体对象。然而,Box2D中不允许你在回调方法中去修改物理世界,因为你毁掉的那个物体可能是Box2D正在处理的那个,引起野指针的问题。如果需要处理接触点,建议的做法是缓存所有你感兴趣的接触点,在事件间隔结束时处理他们。你应该在时间间隔结束的时候立即处理这些接触点,否则其他客户端代码可能修改物理世界,导致你缓存的接触数据失效。当你处理接触缓存的时候,你可以修改物理世界,但是你仍然要小心,不要致使接触指针缓存中出现野指针。testbed中有关于接触指针处理的例子,安全地避免了野指针的问题。
下面列出的是CollisionProcessing(碰撞处理)的测试代码,展示了当处理接触缓存(contactbuffer)时如何来避免物体的野指针。请仔细阅读下面摘录的代码中的注释,代码假定所有的接触点都已经在b2ContactPoint的数组m_points中缓存下来了。
// 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* bodyA = point->fixtureA->GetBody();
b2Body* bodyB = point->fixtureB->GetBody();
float32 massA = bodyA->GetMass();
float32 massB = bodyB->GetMass();
if (massA > 0.0f && massB > 0.0f)
{
if (massB > massA)
{
nuke[nukeCount++] = bodyA;
}
else
{
nuke[nukeCount++] = bodyB;
}
}
}
// 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接触过滤(contactfiltering)
通常在游戏中你不希望所有对象都互相碰撞,例如,你需要创建一个们,允许一部分角色可以穿过这扇门,这种情景就叫做接触过滤,因为一部分交互被过滤掉了。
Box2D中允许你定义自己的接触过滤,只要实现b2ContactFilter类就可以了。这个类需要你实现一个ShouldCollide方法,该方法接受两个b2Shape指针作为参数,当允许两个形状发生碰撞时,函数返回true。
ShouldCollide函数默认的实现方式使用第六章(装置)中定义的b2FilterData类:
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)对象同时存在。
MyContactFilter filter;
world->SetContactFilter(&filter);
// filter remains in scope …