刚体(Rigid Bodies),或简称为“物体”,具有位置和速度。你可以对物体施加力、力矩和冲量。物体可以是静态的、运动的或动态的。以下是物体类型的定义:
b2_staticBody(静态物体):静态物体在模拟过程中不移动,表现得好像它有无限大的质量。Box2D内部将其质量和质量的倒数存储为零。静态物体没有速度。静态物体不会与其他静态或运动物体发生碰撞。
b2_kinematicBody(运动物体):运动物体根据其速度在模拟中移动。运动物体不响应力。你可以通过设置其速度来移动运动物体。运动物体表现得好像它有无限大的质量,不过Box2D内部将其质量和质量的倒数存储为零。运动物体不会与其他运动或静态物体碰撞。通常,如果你希望某个形状被动画化且不受力或碰撞影响,应该使用运动物体。
b2_dynamicBody(动态物体):动态物体是完全模拟的,并根据力和力矩移动。动态物体可以与所有物体类型发生碰撞。动态物体总是有有限且非零的质量。
注意:通常在创建物体后不应再设置其变换。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)在物理模拟中常用于那些需要控制运动但不受物理力影响的对象。它们通常用于场景中的元素,需要在特定路径上移动,或者在某种程度上影响其他物体的行为。以下是一些典型的运动物体例子:
移动平台:在许多平台游戏中,玩家需要跳到和从一个平台跳到另一个平台。为了实现这一点,可以使用运动物体来创建这些移动平台,它们会沿预定路径移动,而不会被玩家或其他物体的重量或力影响。
自动门:在一些游戏或模拟中,门可能会在玩家接近时自动打开和关闭。可以使用运动物体来实现这种行为,因为门需要沿着设定的轨迹移动,并且不应受任何物理碰撞或力的影响。
动画化的物体:如果游戏中有一些对象需要以特定的方式移动,比如旋转的风车叶片、摆动的钟摆或正在前后移动的障碍物,这些通常使用运动物体来实现,因为它们需要精准的控制,而不是受物理力的随机影响。
滑动的障碍物:在一些解谜游戏或平台游戏中,可能存在一些会滑动或移动的障碍物,这些障碍物通常用于改变游戏环境,迫使玩家采取不同的行动策略。
跟踪摄像机或光源:在一些场景中,摄像机或光源可能需要跟随特定路径进行平滑移动,以突出显示场景的某些部分或增强氛围。可以将这些摄像机或光源设置为运动物体,使其移动顺滑且不受外界干扰。
这些例子中的运动物体都是根据预设的路径或速度进行移动,并不会因为物理力或碰撞而改变其运动状态。这使它们非常适合需要精确控制运动行为的场景。
以下是Box2D中涉及刚体(Rigid Bodies)的几个概念:
阻尼用于减少物体在世界中的速度。它不同于摩擦,因为摩擦只在接触时发生,而阻尼与摩擦是可以一起使用的,不能相互替代。
阻尼参数通常在0到1之间,阻尼值越大,速度减得越快。一般推荐使用较小的角阻尼值,比如:
bodyDef.linearDamping = 0.0f;
bodyDef.angularDamping = 0.1f;
在小阻尼值下,阻尼效果基本独立于时间步长,而在大阻尼值下,阻尼效果会随着时间步长的变化而变化。通常建议使用固定的时间步长,以确保模拟的一致性。
重力缩放用于调整单个物体的重力效果。可以通过修改重力缩放来实现某个物体在重力场中的特殊行为,比如让一个物体漂浮:
bodyDef.gravityScale = 0.0f;
为了提高性能,Box2D允许静止的物体进入休眠状态。当一个物体(或一组物体)停止运动时,Box2D会让其进入休眠状态,以减少CPU的开销。如果一个物体与休眠状态的物体发生碰撞,后者会被唤醒。你可以通过以下设置控制物体的休眠行为:
bodyDef.enableSleep = true;
bodyDef.isAwake = true;
有些刚体,例如角色,可以设置为不旋转,即使它受到外力作用。使用固定旋转可以实现这一点:
bodyDef.fixedRotation = true;
在游戏中,高速运动的物体可能会由于离散模拟而穿过其他物体,这种现象称为“隧穿”。为了防止动态物体穿过静态物体,Box2D使用连续碰撞检测(CCD)。如果你有高速移动的物体,比如子弹,可以将其设置为子弹模式,以便进行更精确的碰撞检测:
bodyDef.isBullet = true;
你可以创建一个不参与碰撞或模拟的物体,这种物体类似于休眠状态,但它不会被其他物体唤醒,也不会参与碰撞、射线检测等。你可以稍后启用这个物体:
bodyDef.isEnabled = false;
// 之后启用
b2Body_Enable(myBodyId);
用户数据是一个void指针,允许你将应用程序对象与刚体关联起来,这在处理查询结果(如射线检测或事件)时非常有用。
bodyDef.userData = &myGameObject;
刚体通过世界id创建和销毁,这样可以高效地管理内存,并将刚体添加到世界数据结构中。
b2BodyId myBodyId = b2CreateBody(myWorldId, &bodyDef);
// ... 执行操作 ...
b2DestroyBody(myBodyId);
// 出于安全考虑,将id置空
myBodyId = b2_nullBodyId;
销毁一个刚体时,附加的形状和关节也会自动销毁,因此需要小心管理这些id。
Box2D 允许你在创建刚体后对其进行各种操作,包括修改其质量属性、访问和调整其位置和速度,以及施加力。以下是与 Box2D 中刚体使用相关的关键概念和函数的详细说明:
Box2D 中的刚体有几个关键的质量相关属性:
对于静态刚体,其质量和转动惯量被设置为零。如果一个刚体的旋转是固定的,那么它的转动惯量也会为零。
通常,刚体的质量属性是由添加到刚体的形状自动确定的。不过,你也可以在运行时调整刚体的质量属性,这通常在一些特殊的游戏场景中会用到。
你可以通过以下函数来设置或获取刚体的质量数据:
b2Body_SetMassData(myBodyId, &myMassData)
:设置刚体的质量数据。b2Body_ApplyMassFromShapes(myBodyId)
:将刚体的质量恢复为由形状决定的质量。float mass = b2Body_GetMass(myBodyId)
:获取刚体的质量。float inertia = b2Body_GetInertiaTensor(myBodyId)
:获取刚体的转动惯量。b2Vec2 localCenter = b2Body_GetLocalCenterOfMass(myBodyId)
:获取刚体的局部质心。b2MassData massData = b2Body_GetMassData(myBodyId)
:获取刚体的完整质量数据。刚体的状态包含多个方面的信息,你可以通过以下函数访问或修改这些状态数据:
b2Body_SetType(myBodyId, b2_kinematicBody)
:设置刚体的类型(如静态、运动或动态)。b2BodyType bodyType = b2Body_GetType(myBodyId)
:获取刚体的类型。b2Body_SetBullet(myBodyId, true)
:将刚体设置为“子弹”类型(用于高速物体的碰撞检测)。bool isBullet = b2Body_IsBullet(myBodyId)
:检查刚体是否为“子弹”类型。b2Body_EnableSleep(myBodyId, false)
:启用或禁用刚体的休眠功能。bool isSleepEnabled = b2Body_IsSleepingEnabled(myBodyId)
:检查刚体是否启用了休眠功能。b2Body_SetAwake(myBodyId, true)
:设置刚体为唤醒状态。bool isAwake = b2Body_IsAwake(myBodyId)
:检查刚体是否处于唤醒状态。b2Body_Disable(myBodyId)
:禁用刚体。b2Body_Enable(myBodyId)
:启用刚体。bool isEnabled = b2Body_IsEnabled(myBodyId)
:检查刚体是否启用。b2Body_SetFixedRotation(myBodyId, true)
:设置刚体的旋转是否固定。bool isFixedRotation = b2Body_IsFixedRotation(myBodyId)
:检查刚体的旋转是否固定。这些函数都有详细的注释,可以参考 Box2D 的文档获取更多细节。
你可以访问刚体的位置和旋转角度,这在渲染相关游戏对象时非常常见。你也可以设置刚体的位置和角度,但这相对少见,因为通常会使用 Box2D 来模拟运动。
请记住,Box2D 的接口使用弧度表示角度。
b2Body_SetTransform(myBodyId, position, rotation)
:设置刚体的位置和旋转角度。b2Transform transform = b2Body_GetTransform(myBodyId)
:获取刚体的变换信息(位置和旋转)。b2Vec2 position = b2Body_GetPosition(myBodyId)
:获取刚体的位置。b2Rot rotation = b2Body_GetRotation(myBodyId)
:获取刚体的旋转信息。float angleInRadians = b2Rot_GetAngle(rotation)
:获取刚体的旋转角度(以弧度表示)。你还可以访问刚体的质心位置(局部和全局坐标)。虽然 Box2D 的内部模拟主要使用质心,但通常你不需要访问质心,而是直接使用刚体的变换。例如,你可能有一个正方形的刚体,刚体的原点可能在正方形的一个角,而质心在正方形的中心。
b2Vec2 worldCenter = b2Body_GetWorldCenterOfMass(myBodyId)
:获取刚体质心的世界坐标。b2Vec2 localCenter = b2Body_GetLocalCenterOfMass(myBodyId)
:获取刚体质心的局部坐标。你还可以访问刚体的线速度和角速度。线速度是针对质心的,因此如果质心位置改变,线速度也可能会改变。由于 Box2D 使用弧度,因此角速度的单位是弧度每秒。
b2Vec2 linearVelocity = b2Body_GetLinearVelocity(myBodyId)
:获取刚体的线速度。float angularVelocity = b2Body_GetAngularVelocity(myBodyId)
:获取刚体的角速度。你可以对一个刚体施加力、力矩和冲量。当你施加力或冲量时,你可以指定施加作用力的世界坐标点,这通常会导致围绕质心的力矩产生。
b2Body_ApplyForce(myBodyId, force, worldPoint, wake)
:对刚体施加力。b2Body_ApplyTorque(myBodyId, torque, wake)
:对刚体施加力矩。b2Body_ApplyLinearImpulse(myBodyId, linearImpulse, worldPoint, wake)
:对刚体施加线性冲量。b2Body_ApplyAngularImpulse(myBodyId, angularImpulse, wake)
:对刚体施加角冲量。在施加力、力矩或冲量时,你可以选择是否唤醒刚体。如果你不唤醒刚体并且它处于休眠状态,那么该力或冲量将被忽略。
你还可以将力或线性冲量直接施加到质心上,以避免旋转的发生。
b2Body_ApplyForceToCenter(myBodyId, force, wake)
:将力施加到质心上。b2Body_ApplyLinearImpulseToCenter(myBodyId, linearImpulse, wake)
:将线性冲量施加到质心上。注意:由于 Box2D 使用子步长(sub-stepping)算法,不建议你在多个帧上连续施加稳定的冲量。相反,你应该施加一个力,这样 Box2D 会在子步长中均匀分配该力,从而实现更平滑的运动。
刚体提供了一些实用函数,帮助你在局部空间和世界空间之间转换点和向量。如果你不理解这些概念,建议阅读 Jim Van Verth 和 Lars Bishop 所著的《Essential Mathematics for Games and Interactive Applications》。
b2Vec2 worldPoint = b2Body_GetWorldPoint(myBodyId, localPoint)
:将局部坐标点转换为世界坐标点。b2Vec2 worldVector = b2Body_GetWorldVector(myBodyId, localVector)
:将局部坐标向量转换为世界坐标向量。b2Vec2 localPoint = b2Body_GetLocalPoint(myBodyId, worldPoint)
:将世界坐标点转换为局部坐标点。b2Vec2 localVector = b2Body_GetLocalVector(myBodyId, worldVector)
:将世界坐标向量转换为局部坐标向量。你可以访问刚体上的形状。首先,你可以获取形状的数量。
int shapeCount = b2Body_GetShapeCount(myBodyId)
:获取刚体上形状的数量。如果刚体上有多个形状,你可以分配一个数组,或者如果已知数量有限,你可以使用固定大小的数组。
b2ShapeId shapeIds[10]
:定义一个形状 ID 数组。int returnCount = b2Body_GetShapes(myBodyId, shapeIds, 10)
:获取刚体上的形状 ID 数组。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);
}
}
刚体事件还指示了该刚体是否在这个时间步中进入了休眠状态。这可能对优化你的应用程序有用。
力和冲量的区别可以用一个简单的例子来说明。想象一下,你在操场上玩橡皮球。
力就像你用手轻轻推橡皮球。你一直在推它,球会慢慢开始滚动。力是一个持续的作用,你的手一直在施加这个力,球也会继续加速。
冲量更像是你快速地打了一下橡皮球,然后立即松开手。这个动作虽然很快,但会让球瞬间加速并滚得很远。冲量是短时间内的一个强力动作。
用简单的话来说,力是长时间的推动,而冲量是短时间的强力推动。