Logo
Box2D-刚体 (十一)

刚体(Rigid Bodies),或简称为“物体”,具有位置和速度。你可以对物体施加力、力矩和冲量。物体可以是静态的、运动的或动态的。以下是物体类型的定义:

物体类型

注意:通常在创建物体后不应再设置其变换。Box2D将此视为传送行为,可能会导致不良后果。

物体承载形状并在世界中移动它们。在Box2D中,物体始终是刚体。这意味着附加到同一个刚体的两个形状永远不会相对移动,且附加到同一个物体的形状不会相互碰撞。

形状具有碰撞几何体和密度。通常,物体从形状中获取它们的质量属性。不过,你可以在构建物体后覆盖质量属性。

你通常会保留所有创建的物体的id。这样你可以查询物体的位置,以更新图形实体的位置。你还应该保留物体id,以便在不再需要它们时销毁它们。

物体定义

在创建物体之前,你必须创建一个物体定义(b2BodyDef)。物体定义保存了正确创建和初始化物体所需的数据。

由于Box2D使用C API,因此提供了一个函数来创建默认的物体定义。

b2BodyDef myBodyDef = b2DefaultBodyDef();

这确保了物体定义是有效的,并且此初始化是强制性的。

Box2D会从物体定义中复制数据,它不会保留对物体定义的指针。这意味着你可以重复使用一个物体定义来创建多个物体。

让我们来看看物体定义的一些关键成员。

物体类型

如前所述,有三种不同的物体类型:静态、运动和动态。默认值为b2_staticBody。你应该在创建时确定物体类型,因为稍后更改物体类型的代价很高。

b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;

位置和角度

你可以在物体定义中初始化物体的位置和角度。与在世界原点创建物体然后移动物体相比,这种方法的性能更好。

注意:不要在原点创建物体然后再移动它。如果你在原点创建多个物体,性能将会受到影响。

物体有两个主要的关注点。第一个关注点是物体的原点。形状和关节是相对于物体的原点附加的。第二个关注点是质心。质心由附加形状的质量分布决定,或通过b2MassData显式设置。Box2D的许多内部计算使用质心位置。例如,物体存储质心的线速度,而不是物体的原点。

物体原点和质心

在构建物体定义时,你可能不知道质心的位置。因此,你需要指定物体原点的位置。你还可以用弧度指定物体的角度。如果稍后更改物体的质量属性,则质心可能会在物体上移动,但原点位置和物体角度不会变化,附加的形状和关节也不会移动。

b2BodyDef bodyDef = b2DefaultBodyDef();
bodyDef.position = (b2Vec2){0.0f, 2.0f};
bodyDef.angle = 0.25f * b2_pi;

刚体是一个参考系。你可以在这个参考系中定义形状和关节。这些形状和关节锚点在物体的局部参考系中永远不会移动。

运动物体(Kinematic Body)在物理模拟中常用于那些需要控制运动但不受物理力影响的对象。它们通常用于场景中的元素,需要在特定路径上移动,或者在某种程度上影响其他物体的行为。以下是一些典型的运动物体例子:

  1. 移动平台:在许多平台游戏中,玩家需要跳到和从一个平台跳到另一个平台。为了实现这一点,可以使用运动物体来创建这些移动平台,它们会沿预定路径移动,而不会被玩家或其他物体的重量或力影响。

  2. 自动门:在一些游戏或模拟中,门可能会在玩家接近时自动打开和关闭。可以使用运动物体来实现这种行为,因为门需要沿着设定的轨迹移动,并且不应受任何物理碰撞或力的影响。

  3. 动画化的物体:如果游戏中有一些对象需要以特定的方式移动,比如旋转的风车叶片、摆动的钟摆或正在前后移动的障碍物,这些通常使用运动物体来实现,因为它们需要精准的控制,而不是受物理力的随机影响。

  4. 滑动的障碍物:在一些解谜游戏或平台游戏中,可能存在一些会滑动或移动的障碍物,这些障碍物通常用于改变游戏环境,迫使玩家采取不同的行动策略。

  5. 跟踪摄像机或光源:在一些场景中,摄像机或光源可能需要跟随特定路径进行平滑移动,以突出显示场景的某些部分或增强氛围。可以将这些摄像机或光源设置为运动物体,使其移动顺滑且不受外界干扰。

这些例子中的运动物体都是根据预设的路径或速度进行移动,并不会因为物理力或碰撞而改变其运动状态。这使它们非常适合需要精确控制运动行为的场景。

以下是Box2D中涉及刚体(Rigid Bodies)的几个概念:

阻尼(Damping)

阻尼用于减少物体在世界中的速度。它不同于摩擦,因为摩擦只在接触时发生,而阻尼与摩擦是可以一起使用的,不能相互替代。

阻尼参数通常在0到1之间,阻尼值越大,速度减得越快。一般推荐使用较小的角阻尼值,比如:

bodyDef.linearDamping = 0.0f;
bodyDef.angularDamping = 0.1f;

在小阻尼值下,阻尼效果基本独立于时间步长,而在大阻尼值下,阻尼效果会随着时间步长的变化而变化。通常建议使用固定的时间步长,以确保模拟的一致性。

重力缩放(Gravity Scale)

重力缩放用于调整单个物体的重力效果。可以通过修改重力缩放来实现某个物体在重力场中的特殊行为,比如让一个物体漂浮:

bodyDef.gravityScale = 0.0f;

休眠参数(Sleep Parameters)

为了提高性能,Box2D允许静止的物体进入休眠状态。当一个物体(或一组物体)停止运动时,Box2D会让其进入休眠状态,以减少CPU的开销。如果一个物体与休眠状态的物体发生碰撞,后者会被唤醒。你可以通过以下设置控制物体的休眠行为:

bodyDef.enableSleep = true;
bodyDef.isAwake = true;

固定旋转(Fixed Rotation)

有些刚体,例如角色,可以设置为不旋转,即使它受到外力作用。使用固定旋转可以实现这一点:

bodyDef.fixedRotation = true;

子弹(Bullets)

在游戏中,高速运动的物体可能会由于离散模拟而穿过其他物体,这种现象称为“隧穿”。为了防止动态物体穿过静态物体,Box2D使用连续碰撞检测(CCD)。如果你有高速移动的物体,比如子弹,可以将其设置为子弹模式,以便进行更精确的碰撞检测:

bodyDef.isBullet = true;

禁用(Disabling)

你可以创建一个不参与碰撞或模拟的物体,这种物体类似于休眠状态,但它不会被其他物体唤醒,也不会参与碰撞、射线检测等。你可以稍后启用这个物体:

bodyDef.isEnabled = false;

// 之后启用
b2Body_Enable(myBodyId);

用户数据(User Data)

用户数据是一个void指针,允许你将应用程序对象与刚体关联起来,这在处理查询结果(如射线检测或事件)时非常有用。

bodyDef.userData = &myGameObject;

刚体生命周期(Body Lifetime)

刚体通过世界id创建和销毁,这样可以高效地管理内存,并将刚体添加到世界数据结构中。

b2BodyId myBodyId = b2CreateBody(myWorldId, &bodyDef);

// ... 执行操作 ...

b2DestroyBody(myBodyId);

// 出于安全考虑,将id置空
myBodyId = b2_nullBodyId;

销毁一个刚体时,附加的形状和关节也会自动销毁,因此需要小心管理这些id。

Box2D 允许你在创建刚体后对其进行各种操作,包括修改其质量属性、访问和调整其位置和速度,以及施加力。以下是与 Box2D 中刚体使用相关的关键概念和函数的详细说明:

质量数据

Box2D 中的刚体有几个关键的质量相关属性:

对于静态刚体,其质量和转动惯量被设置为零。如果一个刚体的旋转是固定的,那么它的转动惯量也会为零。

通常,刚体的质量属性是由添加到刚体的形状自动确定的。不过,你也可以在运行时调整刚体的质量属性,这通常在一些特殊的游戏场景中会用到。

质量属性的相关函数

你可以通过以下函数来设置或获取刚体的质量数据:

状态信息

刚体的状态包含多个方面的信息,你可以通过以下函数访问或修改这些状态数据:

这些函数都有详细的注释,可以参考 Box2D 的文档获取更多细节。

位置和速度

你可以访问刚体的位置和旋转角度,这在渲染相关游戏对象时非常常见。你也可以设置刚体的位置和角度,但这相对少见,因为通常会使用 Box2D 来模拟运动。

请记住,Box2D 的接口使用弧度表示角度。

你还可以访问刚体的质心位置(局部和全局坐标)。虽然 Box2D 的内部模拟主要使用质心,但通常你不需要访问质心,而是直接使用刚体的变换。例如,你可能有一个正方形的刚体,刚体的原点可能在正方形的一个角,而质心在正方形的中心。

你还可以访问刚体的线速度和角速度。线速度是针对质心的,因此如果质心位置改变,线速度也可能会改变。由于 Box2D 使用弧度,因此角速度的单位是弧度每秒。

力和冲量

你可以对一个刚体施加力、力矩和冲量。当你施加力或冲量时,你可以指定施加作用力的世界坐标点,这通常会导致围绕质心的力矩产生。

在施加力、力矩或冲量时,你可以选择是否唤醒刚体。如果你不唤醒刚体并且它处于休眠状态,那么该力或冲量将被忽略。

你还可以将力或线性冲量直接施加到质心上,以避免旋转的发生。

注意:由于 Box2D 使用子步长(sub-stepping)算法,不建议你在多个帧上连续施加稳定的冲量。相反,你应该施加一个力,这样 Box2D 会在子步长中均匀分配该力,从而实现更平滑的运动。

坐标转换

刚体提供了一些实用函数,帮助你在局部空间和世界空间之间转换点和向量。如果你不理解这些概念,建议阅读 Jim Van Verth 和 Lars Bishop 所著的《Essential Mathematics for Games and Interactive Applications》。

访问形状和关节

你可以访问刚体上的形状。首先,你可以获取形状的数量。

如果刚体上有多个形状,你可以分配一个数组,或者如果已知数量有限,你可以使用固定大小的数组。

for (int i = 0; i < returnCount; ++i)
{
    b2ShapeId shapeId = shapeIds[i];
 
    // 对 shapeId 进行操作
}

你还可以以类似的方式获取刚体上的关节数组。

刚体事件

虽然你可以在每个时间步后收集所有刚体的变换信息,但这样做效率不高。许多刚体可能没有移动,因为它们处于休眠状态。此外,遍历大量刚体时会有很多缓存未命中的情况。

Box2D 提供了 b2BodyEvents,你可以在每次调用 b2World_Step() 后访问它来获取一个刚体移动事件的数组。由于这些数据是连续的,因此对缓存更友好。

b2BodyEvents events = b2World_GetBodyEvents(m_worldId);
for (int i = 0; i < events.moveCount; ++i)
{
    const b2BodyMoveEvent* event = events.moveEvents + i;
    MyGameObject* gameObject = event->userData;
    MoveGameObject(gameObject, event->transform);
    if (event->fellAsleep)
    {
        SleepGameObject(gameObject);
    }
}

刚体事件还指示了该刚体是否在这个时间步中进入了休眠状态。这可能对优化你的应用程序有用。

力和冲量的区别可以用一个简单的例子来说明。想象一下,你在操场上玩橡皮球。

力:

力就像你用手轻轻推橡皮球。你一直在推它,球会慢慢开始滚动。力是一个持续的作用,你的手一直在施加这个力,球也会继续加速。

冲量:

冲量更像是你快速地打了一下橡皮球,然后立即松开手。这个动作虽然很快,但会让球瞬间加速并滚得很远。冲量是短时间内的一个强力动作。

总结:

用简单的话来说,力是长时间的推动,而冲量是短时间的强力推动。