跳至正文

(转载)BOX2D V2.3.0 用户手册中文版(第9章)-接触

Chapter 9 接触(contact)

9.1 关于

接触(contact)是由 Box2D 创建的用于管理fixture间碰撞的对象。如果 fixture 有诸如链接形状之类的子 fixture ,那么每个相应的子 fixture 都存在接触。接触有不同的种类,它们都派生自 b2Contact,用于管理不同类型 fixture 之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间碰撞的类。
这是与接触有关的术语。

接触点

接触点就是两个形状相互接触的点。在Box2D中,近似地认为在少数点处有接触。

接触法线

接触法线是一个单位向量,由一个fixture指向另一个fixture。按照惯例,向量由fixtureA指向fixtureB。

接触分隔

分隔正好与穿透(penetration)相反。当形状相重叠时,分隔为负。有可能将来的Box2D版本中会以正隔离来创建触点,所以当有触点的报告时你可能需要检查一下符号。

接触流形

两个凸多边形相互接触,有可能会产生两个接触点。这些点都有相同的法线,所以它们被分成一组,构成接触流形,这是连续区域接触的一个近似。

法向冲量

法向力作用于接触点,用于防止形状相互穿透。为方便起见,Box2D使用冲量(impulses)。法向力与时间步相乘,构成法向冲量。

切向冲量

切向力会在接触点生成,用于模拟摩擦。为方便起见,切向作用使用冲量的方式存储。

接触标识

Box2D试图复用上一个时间步计算出的接触力,做为下一个时间步的初始估计值。Box2D使用接触标识匹配跨越时间步的触点。标识包含了几何特征索引以便区分接触点。
当两个fixture的AABB重叠时,接触就被创建了。有时碰撞筛选会阻止接触的创建。当AABB 不再重叠,接触就会被摧毁。
也许你会皱起眉头,为了没有发生实际碰撞的形状(只是它们的 AABB)却创建了接触。好吧,的确是这样的,这是一个“鸡或蛋”的问题。我们并不知道是否需要一个接触,除非我们创建一个接触去分析碰撞。如果形状之间没有发生碰撞,我们需要正确地删除接触,或者,我们可以一直等到 AABB 不再重叠。为了提高性能,Box2D选择了后面这个方法,因为它可以使用系统的缓冲信息来提升性能。

9.2 接触类

之前已经提及过,接触对象是Box2D内部创建和摧毁的,并不是由用户来创建。然而,你还是能够访问接触类并和它交互的。
你可以访问原始的接触流形:

b2Manifold* GetManifold();
const b2Manifold* GetManifold() const;

你甚至可以修改流形,一般情况下不提倡你怎样做。修改流形是较高级的用法。
这个是获取b2WorldManifold的帮助函数:

void GetWorldManifold(b2WorldManifold* worldManifold) const;

这使用了物体的当前位置去计算出接触点在世界坐标下的位置。
传感器(Sensors)并不创建流形,所以要使用:

bool touching = sensorContact->IsTouching();

这函数对于非传感器(non-sensors)也有效。
从接触(contact)中你可以得到fixture,从而再得到body。

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

你可以使一个接触失效。这仅仅在b2ContactListener::PreSolve事件中有效,下面会再进行讨论。

9.3 访问接触

你有几种方法来访问接触。为了访问接触,你可以直接查询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 就可以很精确的得到全部结果。

9.4 接触监听器

通过实现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, const b2Manifold* oldManifold)
  { /* handle pre-solve event */ }

  void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse)
  { /* handle post-solve event */ }
};

注意
不要保存发送到b2ContactListener的指针。取而代之,用深拷贝的方式将触点数据保存到你自己的缓冲区。下面的例子演示了一种操作方法。

在运行期(run-time),你可以创建listener的实例对象,并使用b2World::SetContactListener来注册这个对象。 但要保证当listener在作用域中时,world对象是存在的。

开始接触事件

当两个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();
    }
  }
}

求解后事件

你可以在post-solve事件中,得到碰撞冲量(collision impulse)的结果。 如果你不关心冲量,你可能只需要实现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* 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;
        }

        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);
}

9.5 接触筛选

通常,你不希望游戏中的所有物体都发生碰撞。例如,你可能会创建一个只有特定角色才能通过的门。 这称之为接触筛选,因为一些交互被筛选出了。
通过实现b2ContactFilter类,Box2D允许定制接触筛选。这个类需要你实现一个ShouldCollide 函数,这个函数接收两个b2Shape的指针作为参数。如果形状会碰撞,那么就返回true。
默认的ShouldCollide实现使用了“第06章,夹具(Fixtures)”定义的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;
}

在运行期(run-time),你可以创建自己的接触筛选实例,并使用b2World::SetContactFilter函数来注册。 你要保证当world存在时,你的filter要保留在作用域中。

MyContactFilter filter;
world->SetContactFilter(&filter);
// filter留在作用域中
标签:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注