接触是由 Box2D 创建的内部对象,用于管理形状之间的碰撞。它们是游戏中刚体模拟的基础。
接触涉及到一些重要的术语,值得回顾:
接触点(contact point):接触点是两个形状接触的点。Box2D 使用少量的点来近似接触。具体来说,两个形状之间的接触点数为 0、1 或 2。这是可能的,因为 Box2D 使用凸形状。
接触法线(contact normal):接触法线是从一个形状指向另一个形状的单位向量。按照惯例,法线从 shapeA
指向 shapeB
。
接触间距(contact separation):间距是穿透的反面。当形状重叠时,间距为负。
接触流形(contact manifold):两个凸多边形之间的接触可能生成多达两个接触点。所有这些点使用相同的法线,因此它们被分组为一个接触流形,它是接触区域的一个近似。
法线冲量(normal impulse):法向力是为了防止形状穿透而在接触点施加的力。为了方便,Box2D 使用冲量。法线冲量只是法向力乘以时间步长。由于 Box2D 使用子步长(sub-stepping),因此这是子步长时间步长。
切线冲量(tangent impulse):在接触点生成的切向力用于模拟摩擦。同样地,这也以冲量的形式存储。
接触点 ID(contact point id):Box2D 尝试重用一个时间步长的接触冲量结果,作为下一个时间步长的初始猜测。Box2D 使用接触点 ID 来匹配不同时间步长中的接触点。ID 包含几何特征索引,这有助于区分不同的接触点。
推测接触(speculative contact):当两个形状非常接近时,Box2D 即使在形状未接触的情况下也会创建最多两个接触点。这让 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”部分及其后的内容的翻译:
传感器事件在每次调用 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::enableSensorEvents
为 true
时才启用。
接触事件在每次世界步进后可用。与传感器事件类似,这些事件应在执行其他游戏逻辑之前获取和处理,否则可能会访问孤立/无效的数据。
你可以在单个数据结构中访问所有接触事件。这比使用类似 b2Body_GetContactData()
的函数高效得多。
b2ContactEvents contactEvents = b2World_GetContactEvents(myWorldId);
这些数据不适用于传感器。所有事件至少涉及一个动态刚体。
接触事件有三种类型:
当两个形状开始接触时,会记录 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::enableContactEvents
为 true
时,形状才会生成开始和结束接触事件。
在游戏中,你通常关心的是当两个形状以显著速度碰撞时的接触事件,以便播放声音和/或粒子效果。碰撞事件就是为此而设计的。
for (int i = 0; i < contactEvents.hitCount; ++i)
{
b2ContactHitEvent* hitEvent = contactEvents.hitEvents + i;
if (hitEvent->approachSpeed > 10.0f)
{
// 播放声音
}
}
只有当 b2ShapeDef::enableHitEvents
为 true
时,形状才会生成碰撞事件。我建议你只为需要碰撞事件的形状启用此功能,因为它会产生一些开销。Box2D 也只报告超过 b2WorldDef::hitEventThreshold
的接近速度的碰撞事件。
在游戏中,你常常不希望所有对象都发生碰撞。例如,你可能希望创建一扇只有特定角色才能通过的门。这被称为接触过滤,因为某些交互被过滤掉。
接触过滤在形状上设置,具体内容在这里介绍。
为了获得最佳性能,请使用 b2Filter
提供的接触过滤。然而,在某些情况下,你可能需要自定义过滤。你可以通过注册一个实现 b2CustomFilterFcn()
的自定义过滤回调来实现这一点。
bool MyCustomFilter(b2ShapeId shapeIdA, b2ShapeId shapeIdB, void* context)
{
MyGame* myGame = context;
return myGame->WantsCollision(shapeIdA, shapeIdB);
}
// 在其他地方
b2World_SetCustomFilterCallback(myWorldId, MyCustomFilter, myGame);
此函数必须是线程安全的,且不得从 Box2D 世界中读取或写入数据,否则会出现竞态条件。
这在碰撞检测之后但在碰撞解决之前调用。它使你有机会根据接触几何体禁用接触。例如,你可以使用此回调实现单向平台。
在每次通过碰撞处理时,接触将重新启用,因此你需要在每个时间步中禁用接触。此函数必须是线程安全的,且不得从 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 示例。