Logo
Box2D-关节(八)

关节(Joints)

关节用于将物体约束到世界或彼此之间。在游戏中,常见的例子包括布娃娃(ragdolls)、跷跷板(teeters)和滑轮(pulleys)。关节可以以多种不同的方式组合在一起,以创造有趣的运动。

某些关节提供了限制功能,因此你可以控制运动范围。有些关节提供电机(motors),可以用来以规定的速度驱动关节,直到超过规定的力/扭矩为止。还有一些关节提供带阻尼的弹簧。

关节电机有多种用途。你可以通过指定与实际位置和期望位置之间的差异成比例的关节速度来控制位置。你还可以通过将关节速度设置为零并提供一个小但显著的最大电机力/扭矩来模拟关节摩擦。然后电机会试图阻止关节移动,直到负载变得过强。

关节定义(Joint Definition)

每种关节类型都有相关的关节定义。所有关节都连接在两个不同的物体之间。其中一个物体可以是静态的。静态和/或运动学物体之间的关节是允许的,但没有效果,并且会使用一些处理时间。

如果关节连接到已禁用的物体,该关节将被有效禁用。当关节上的两个物体都启用时,关节将自动启用。换句话说,你不需要显式启用或禁用关节。

你可以为任何关节类型指定用户数据,并且你可以提供一个标志,以防止连接的物体相互碰撞。这是默认行为,你必须设置 collideConnectedtrue 才能允许连接的物体之间发生碰撞。

许多关节定义要求你提供一些几何数据。通常,关节通过锚点(anchor points)定义。这些锚点是固定在连接物体上的点。Box2D 要求这些点在局部坐标中指定。这样,即使当前的物体变换违反了关节约束,也可以指定关节。此外,某些关节定义需要物体之间的参考角度。这可能是正确约束旋转所必需的。

关节定义的其余数据取决于关节类型。我们将在下面介绍这些内容。

关节的生命周期(Joint Lifetime)

关节是使用为每种关节类型提供的创建函数创建的,并且使用一个共享的函数销毁。所有关节类型共享一个 ID 类型 b2JointId

以下是一个旋转关节(revolute joint)的生命周期示例:

b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef();
jointDef.bodyIdA = myBodyA;
jointDef.bodyIdB = myBodyB;
jointDef.localAnchorA = (b2Vec2){0.0f, 0.0f};
jointDef.localAnchorB = (b2Vec2){1.0f, 2.0f};
 
b2JointId myJointId = b2CreateRevoluteJoint(myWorldId, &jointDef);
 
// ... 执行操作 ...
 
b2DestroyJoint(myJointId);
myJointId = b2_nullJointId;

销毁 ID 后将其设为 null 是个好习惯。

关节的生命周期与物体的生命周期相关。关节不能脱离物体而存在。因此,当一个物体被销毁时,所有连接到该物体的关节将自动销毁。这意味着你需要小心,避免在连接物体被销毁后使用关节 ID。如果你使用了一个悬空的关节 ID,Box2D 会触发断言。

注意:当连接的物体被销毁时,关节会被销毁。

幸运的是,你可以检查你的关节 ID 是否有效。

if (b2Joint_IsValid(myJointId) == false)
{
    myJointId = b2_nullJointId;
}

这肯定很有用,但不应过度使用,因为如果你创建和销毁许多关节,这可能最终会与不同的关节混淆。所有 ID 的代数限制为 64k 代。

使用关节(Using Joints)

许多模拟在创建关节后直到它们被销毁之前不会再次访问它们。然而,关节中包含许多有用的数据,你可以用来创建丰富的模拟。

首先,你可以从关节中获取类型、物体、锚点和用户数据。

b2JointType jointType = b2Joint_GetType(myJointId);
b2BodyId bodyIdA = b2Joint_GetBodyA(myJointId);
b2BodyId bodyIdB = b2Joint_GetBodyB(myJointId);
b2Vec2 localAnchorA = b2Joint_GetLocalAnchorA(myJointId);
b2Vec2 localAnchorB = b2Joint_GetLocalAnchorB(myJointId);
void* myUserData = b2Joint_GetUserData(myJointId);

所有关节都有反作用力和反作用力矩。反作用力与自由体图(free body diagram)相关。Box2D 的惯例是反作用力施加在 B 物体的锚点上。你可以使用反作用力来断开关节或触发其他游戏事件。这些函数可能会进行一些计算,因此如果你不需要结果,请不要调用它们。

b2Vec2 force = b2Joint_GetConstraintForce(myJointId);
float torque = b2Joint_GetConstraintTorque(myJointId);

有关更多详细信息,请参阅“BreakableJoint”示例。

距离关节(Distance Joint)

距离关节是最简单的关节之一,它规定两个物体上两点之间的距离必须保持恒定。当你指定一个距离关节时,这两个物体应已就位。然后,你在局部坐标中指定两个锚点。第一个锚点连接到 A 物体,第二个锚点连接到 B 物体。这些点隐含了距离约束的长度。

距离关节定义(Distance Joint Definition)

以下是一个距离关节定义的示例。在这种情况下,我决定允许物体碰撞。

b2DistanceJointDef jointDef = b2DefaultDistanceJointDef();
jointDef.bodyIdA = myBodyIdA;
jointDef.bodyIdB = myBodyIdB;
jointDef.localAnchorA = (b2Vec2){1.0f, -3.0f};
jointDef.localAnchorB = (b2Vec2){0.0f, 0.5f};
b2Vec2 anchorA = b2Body_GetWorldPoint(myBodyIdA, jointDef.localAnchorA);
b2Vec2 anchorB = b2Body_GetWorldPoint(myBodyIdB, jointDef.localAnchorB);
jointDef.length = b2Distance(anchorA, anchorB);
jointDef.collideConnected = true;
 
b2JointId myJointId = b2CreateDistanceJoint(myWorldId, &jointDef);

距离关节也可以变得柔软,如同一个弹簧阻尼器连接。柔软性通过启用弹簧并调节定义中的两个值来实现:赫兹(Hertz)和阻尼比(damping ratio)。

jointDef.enableSpring = true;
jointDef.hertz = 2.0f;
jointDef.dampingRatio = 0.5f;

赫兹是谐振器的频率(例如吉他弦)。通常,频率应小于时间步的一半频率。因此,如果你使用 60Hz 的时间步,距离关节的频率应小于 30Hz。原因与奈奎斯特频率有关。

阻尼比控制振荡消失的速度。阻尼比为 1 是临界阻尼,可防止振荡。

还可以为距离关节定义最小和最大长度。你甚至可以为距离关节添加电机以动态调整其长度。有关详细信息,请参阅 b2DistanceJointDef 和 DistanceJoint 示例。

下面是你提供的关于 Box2D 关节类型的文档的翻译:

旋转关节 (Revolute Joint)

旋转关节强制两个物体共享一个共同的锚点,通常称为铰链点或枢轴点。旋转关节具有一个自由度:即两个物体的相对旋转,这称为关节角度。

旋转关节的使用

与所有关节一样,锚点在局部坐标中指定。然而,你可以使用物体的实用函数来简化这一过程。

b2Vec2 worldPivot = {10.0f, -4.0f};
b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef();
jointDef.bodyIdA = myBodyIdA;
jointDef.bodyIdB = myBodyIdB;
jointDef.localAnchorA = b2Body_GetLocalPoint(myBodyIdA, worldPivot);
jointDef.localAnchorB = b2Body_GetLocalPoint(myBodyIdB, worldPivot);

b2JointId myJointId = b2CreateRevoluteJoint(myWorldId, &jointDef);

当 bodyB 围绕锚点逆时针旋转时,旋转关节角度为正值。与 Box2D 中的所有角度一样,旋转角度以弧度为单位。通常,当两个物体具有相同角度时,旋转关节角度为零。你可以使用 b2RevoluteJointDef::referenceAngle 来偏移这个值。

在某些情况下,你可能希望控制关节角度。为此,旋转关节可以模拟关节限制和/或电机。

关节限制

关节限制强制关节角度保持在下限角度和上限角度之间。限制将应用所需的扭矩以确保这一点。限制范围应包括零,否则当模拟开始时,关节会突然跳动。上下限角度是相对于参考角度的。

关节电机

关节电机允许你指定关节的速度。速度可以是负数或正数。电机可以具有无限的力,但这通常不理想。记住一个经典问题:“当不可抗力遇到不可移动的物体时,会发生什么?”答案是不太好的。所以你可以为关节电机提供一个最大扭矩。关节电机会保持指定的速度,除非所需的扭矩超过了指定的最大值。当超过最大扭矩时,关节会减速,甚至可能反转。

你可以使用关节电机来模拟关节摩擦。只需将关节速度设置为零,并将最大扭矩设置为一个较小但足够的值。电机会试图防止关节旋转,但在遇到显著负载时会让步。

以下是包含限制和电机功能的旋转关节定义修订版。在此版本中,电机被设置为模拟关节摩擦。

b2Vec2 worldPivot = {10.0f, -4.0f};
b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef();
jointDef.bodyIdA = myBodyIdA;
jointDef.bodyIdB = myBodyIdB;
jointDef.localAnchorA = b2Body_GetLocalPoint(myBodyIdA, worldPivot);
jointDef.localAnchorB = b2Body_GetLocalPoint(myBodyIdB, worldPivot);
jointDef.lowerAngle = -0.5f * b2_pi; // -90 度
jointDef.upperAngle = 0.25f * b2_pi; // 45 度
jointDef.enableLimit = true;
jointDef.maxMotorTorque = 10.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;

你可以访问旋转关节的角度、速度和电机扭矩。

float angleInRadians = b2RevoluteJoint_GetAngle(myJointId);
float speed = b2RevoluteJoint_GetMotorSpeed(myJointId);
float currentTorque = b2RevoluteJoint_GetMotorTorque(myJointId);

你还可以在每一步中更新电机参数。

b2RevoluteJoint_SetMotorSpeed(myJointId, 20.0f);
b2RevoluteJoint_SetMaxMotorTorque(myJointId, 100.0f);

关节电机有一些有趣的功能。你可以在每个时间步中更新关节速度,这样可以使关节像正弦波一样往返移动,或根据你想要的任何函数来移动。

// ... 游戏循环开始 ...

b2RevoluteJoint_SetMotorSpeed(myJointId, cosf(0.5f * time));

// ... 游戏循环结束 ...

你还可以使用关节电机来跟踪目标关节角度。例如:

// ... 游戏循环开始 ...

float angleError = b2RevoluteJoint_GetAngle(myJointId) - angleTarget;
float gain = 0.1f;
b2RevoluteJoint_SetMotorSpeed(myJointId, -gain * angleError);

// ... 游戏循环结束 ...

通常,增益参数不应太大,否则关节可能会变得不稳定。

平移关节 (Prismatic Joint)

平移关节允许两个物体沿局部轴线相对移动。平移关节防止相对旋转。因此,平移关节具有一个自由度。

平移关节的使用

平移关节定义与旋转关节的描述相似,只是将角度替换为平移,将力替换为扭矩。以下是一个带有关节限制和摩擦电机的平移关节定义示例:

b2Vec2 worldPivot = {10.0f, -4.0f};
b2Vec2 worldAxis = {1.0f, 0.0f};
b2PrismaticJointDef jointDef;
jointDef.bodyIdA = myBodyIdA;
jointDef.bodyIdB = myBodyIdB;
jointDef.localAnchorA = b2Body_GetLocalPoint(myBodyIdA, worldPivot);
jointDef.localAnchorB = b2Body_GetLocalPoint(myBodyIdB, worldPivot);
jointDef.localAxisA = b2Body_GetLocalVector(myBodyIdA, worldAxis);
jointDef.lowerTranslation = -5.0f;
jointDef.upperTranslation = 2.5f;
jointDef.enableLimit = true;
jointDef.maxMotorForce = 1.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;

旋转关节具有隐含的轴线,该轴线从屏幕中伸出。平移关节需要一个平行于屏幕的显式轴线。这个轴线固定在 body A 上。

当锚点重叠时,平移关节的平移值为零。我建议将平移关节的锚点放置在两个物体的质心附近。这将有助于提高关节的刚性。

使用平移关节与使用旋转关节类似。以下是相关的成员函数:

float PrismaticJoint::GetJointTranslation() const;
float PrismaticJoint::GetJointSpeed() const;
float PrismaticJoint::GetMotorForce() const;
void PrismaticJoint::SetMotorSpeed(float speed);
void PrismaticJoint::SetMotorForce(float force);

鼠标关节 (Mouse Joint)

鼠标关节用于在示例中通过鼠标操控物体。它试图将物体上的一个点驱动到当前的鼠标指针位置。鼠标关节对旋转没有任何限制。

鼠标关节定义包含一个目标点、最大力、赫兹频率和阻尼比。目标点最初与物体的锚点重合。最大力用于防止多个动态物体相互作用时发生剧烈反应。你可以根据需要将其设置得很大。频率和阻尼比用于创建类似于距离关节的弹簧/阻尼效果。

轮关节 (Wheel Joint)

轮关节将 bodyB 上的一个点限制在 bodyA 上的一条线上。轮关节还提供了悬挂弹簧和电机。详情请参见 Driving 示例。

焊接关节 (Weld Joint)

焊接关节试图约束两个物体之间的所有相对运动。有关焊接关节行为的示例,请参见 Cantilever 示例。

使用焊接关节定义可断裂结构可能很有诱惑力。然而,Box2D 的求解器是近似的,因此关节在某些情况下可能会变软,无论关节设置如何。因此,由焊接关节连接的物体链可能会发生弯曲。

有关如何在不使用焊接关节的情况下合并和拆分物体的示例,请参见 ContactEvent 示例。

电机关节 (Motor Joint)

电机关节允许你通过指定目标位置和旋转偏移来控制物体的运动。你可以设置达到目标位置和旋转所需的最大电机力和扭