Logo
Box2D-Contacts(七)

接触(Contacts)

接触是由 Box2D 创建的内部对象,用于管理形状之间的碰撞。它们是游戏中刚体模拟的基础。

术语

接触涉及到一些重要的术语,值得回顾:

接触的生命周期

当两个形状的 AABB 开始重叠时,接触会被创建。有时碰撞过滤会阻止接触的创建。当 AABB 不再重叠时,接触会被销毁。

所以你可能会注意到,可能会为未接触的形状创建接触对象(只是它们的 AABB 重叠)。确实如此,这就是“先有鸡还是先有蛋”的问题。我们不知道是否需要一个接触对象,直到创建它来分析碰撞。如果形状没有接触,我们可以立即删除接触对象,或者我们也可以等到 AABB 不再重叠。Box2D 采用后者的方法,因为这样可以让系统缓存信息,从而提高性能。

接触数据

如前所述,接触由 Box2D 自动创建和销毁。接触数据不是由用户创建的,但你可以访问接触数据。

你可以从形状或刚体获取接触数据。形状上的接触数据是刚体上接触数据的一个子集。接触数据仅在接触发生时返回。未接触的接触不会为应用程序提供有意义的信息。

接触数据以数组的形式返回。所以首先你可以询问形状或刚体数组所需的空间大小。这个数字是保守的,你实际收到的接触数可能少于这个数字,但绝不会超过。

int shapeContactCapacity = b2Shape_GetContactCapacity(myShapeId);
int bodyContactCapacity = b2Body_GetContactCapacity(myBodyId);

你可以分配数组空间以获取所有情况下的接触数据,或者你可以使用固定大小的数组并获取有限数量的结果。

b2ContactData contactData[10];
int shapeContactCount = b2Shape_GetContactData(myShapeId, contactData, 10);
int bodyContactCount = b2Body_GetContactData(myBodyId, contactData, 10);

b2ContactData 包含两个形状 ID 和流形信息。

for (int i = 0; i < bodyContactCount; ++i)
{
    b2ContactData* data = contactData + i;
    printf("point count = %d\n", data->manifold.pointCount);
}

从形状和刚体获取接触数据并不是处理接触数据的最高效方式。相反,你应该使用接触事件。

以下是“Sensor Events”部分及其后的内容的翻译:


传感器事件(Sensor Events)

传感器事件在每次调用 b2World_Step() 后可用。传感器事件是获取传感器重叠信息的最佳方式。当一个形状开始与传感器重叠时,会产生相应的事件。

b2SensorEvents sensorEvents = b2World_GetSensorEvents(myWorldId);
for (int i = 0; i < sensorEvents.beginCount; ++i)
{
    b2SensorBeginTouchEvent* beginTouch = sensorEvents.beginEvents + i;
    void* myUserData = b2Shape_GetUserData(beginTouch->visitorShapeId);
    // 处理开始重叠事件
}

当一个形状停止与传感器重叠时,也会产生相应的事件。

for (int i = 0; i < sensorEvents.endCount; ++i)
{
    b2SensorEndTouchEvent* endTouch = sensorEvents.endEvents + i;
    void* myUserData = b2Shape_GetUserData(endTouch->visitorShapeId);
    // 处理结束重叠事件
}

如果一个形状被销毁,则不会收到结束事件。传感器事件应在世界步进之后、其他游戏逻辑之前处理,这有助于避免处理陈旧数据。

传感器事件仅在非传感器形状的 b2ShapeDef::enableSensorEventstrue 时才启用。

接触事件(Contact Events)

接触事件在每次世界步进后可用。与传感器事件类似,这些事件应在执行其他游戏逻辑之前获取和处理,否则可能会访问孤立/无效的数据。

你可以在单个数据结构中访问所有接触事件。这比使用类似 b2Body_GetContactData() 的函数高效得多。

b2ContactEvents contactEvents = b2World_GetContactEvents(myWorldId);

这些数据不适用于传感器。所有事件至少涉及一个动态刚体。

接触事件有三种类型:

接触开始事件(Contact Touch Event)

当两个形状开始接触时,会记录 b2ContactBeginTouchEvent。这些事件仅包含两个形状的 ID。

for (int i = 0; i < contactEvents.beginCount; ++i)
{
    b2ContactBeginTouchEvent* beginEvent = contactEvents.beginEvents + i;
    ShapesStartTouching(beginEvent->shapeIdA, beginEvent->shapeIdB);
}

当两个形状停止接触时,会记录 b2ContactEndTouchEvent。这些事件也仅包含两个形状的 ID。

for (int i = 0; i < contactEvents.endCount; ++i)
{
    b2ContactEndTouchEvent* endEvent = contactEvents.endEvents + i;
    ShapesStopTouching(endEvent->shapeIdA, endEvent->shapeIdB);
}

当你销毁一个形状或其所属的刚体时,不会生成结束接触事件。

只有当 b2ShapeDef::enableContactEventstrue 时,形状才会生成开始和结束接触事件。

碰撞事件(Hit Events)

在游戏中,你通常关心的是当两个形状以显著速度碰撞时的接触事件,以便播放声音和/或粒子效果。碰撞事件就是为此而设计的。

for (int i = 0; i < contactEvents.hitCount; ++i)
{
    b2ContactHitEvent* hitEvent = contactEvents.hitEvents + i;
    if (hitEvent->approachSpeed > 10.0f)
    {
        // 播放声音
    }
}

只有当 b2ShapeDef::enableHitEventstrue 时,形状才会生成碰撞事件。我建议你只为需要碰撞事件的形状启用此功能,因为它会产生一些开销。Box2D 也只报告超过 b2WorldDef::hitEventThreshold 的接近速度的碰撞事件。

接触过滤(Contact Filtering)

在游戏中,你常常不希望所有对象都发生碰撞。例如,你可能希望创建一扇只有特定角色才能通过的门。这被称为接触过滤,因为某些交互被过滤掉。

接触过滤在形状上设置,具体内容在这里介绍。

高级接触处理(Advanced Contact Handling)

自定义过滤回调(Custom Filtering Callback)

为了获得最佳性能,请使用 b2Filter 提供的接触过滤。然而,在某些情况下,你可能需要自定义过滤。你可以通过注册一个实现 b2CustomFilterFcn() 的自定义过滤回调来实现这一点。

bool MyCustomFilter(b2ShapeId shapeIdA, b2ShapeId shapeIdB, void* context)
{
    MyGame* myGame = context;
    return myGame->WantsCollision(shapeIdA, shapeIdB);
}
 
// 在其他地方
b2World_SetCustomFilterCallback(myWorldId, MyCustomFilter, myGame);

此函数必须是线程安全的,且不得从 Box2D 世界中读取或写入数据,否则会出现竞态条件。

预求解回调(Pre-Solve Callback)

这在碰撞检测之后但在碰撞解决之前调用。它使你有机会根据接触几何体禁用接触。例如,你可以使用此回调实现单向平台。

在每次通过碰撞处理时,接触将重新启用,因此你需要在每个时间步中禁用接触。此函数必须是线程安全的,且不得从 Box2D 世界中读取或写入数据。

bool MyPreSolve(b2ShapeId shapeIdA, b2ShapeId shapeIdB, b2Manifold* manifold, void* context)
{
    MyGame* myGame = context;
 
    if (myGame->IsHittingBelowPlatform(shapeIdA, shapeIdB, manifold))
    {
        return false;
    }
 
    return true;
}
 
// 在其他地方
b2World_SetPreSolveCallback(myWorldId, MyPreSolve, myGame);

请注意,这目前不适用于高速碰撞,因此在这些情况下你可能会看到暂停。

有关更多详细信息,请参阅 Platformer 示例。