Oh!Coder

Coding Life

第九章 接触

| 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:

1
void GetWorldManifold(b2WorldManifold* worldManifold) const;

这是用当前物体的位置来计算接触点的世界位置。

传感器不会创建取样,所以对它们而言使用:

1
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 ...

Comments