5、 创建一个动态物体
首先我们用 CreateBody 创建物体。
b2BodyDef bodyDef;
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);
接下来我们创建并添加一个多边形形状到物体上。注意我们把密度设置为 1,默认的密度是 0。并且,形状的摩擦设置到了 0.3。形状添加好以后,我们就使用 SetMassFromShapes 方法来命令物体通过形状去计算其自身的质量。这暗示了你可以给单个物体添加一个以上的形状。如果质量计算结果为 0,那么物体会变成真正的静态。
b2PolygonDef shapeDef;
shapeDef.SetAsBox(1.0f, 1.0f);
shapeDef.density = 1.0f;
shapeDef.friction = 0.3f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();
6、模拟(Box2D 的)世界
我们已经初始化好了地面盒和一个动态盒。现在我们只有少数几个问题需要考虑。Box2D 中有一些数学代码构成的积分器(integrator),积分器在离散的时间点上模拟物理方程,它将与游戏动画循环一同运行。所以我们需要为 Box2D 选取一个时间步,通常来说游戏物理引擎需要至少 60Hz 的速度,也就是 1/60 的时间步。你可以使用更大的时间步,但是你必须更加小心地为你的世界调整定义。我们也不喜欢时间步变化得太大,所以不要把时间步关联到帧频(除非你真的必须这样做)。直截了当地,这个就是时间步:float32 timeStep = 1.0f / 60.0f;
除了积分器之外,Box2D 中还有约束求解器(constraint solver)。约束求解器用于解决模拟中的所有约束,一次一个。单个的约束会被完美的求解,然而当我们求解一个约束的时候,我们就会稍微耽误另一个。要得到良好的解,我们需要迭代所有约束多次。建议的 Box2D 迭代次数是 10 次。你可以按自己的喜好去调整这个数,但要记得它是速度与质量之间的平衡。更少的迭代会增加性能并降低精度,同样地,更多的迭代会减少性能但提高模拟质量。这是我们选择的迭代次数:
int32 iterations = 10;//一个时间步遍历10次约束
现在我们可以开始模拟循环了,在游戏中模拟循环应该并入游戏循环。每次循环你都应该调用b2World::Step,通常调用一次就够了,这取决于帧频以及物理时间步。
这就是模拟 1 秒钟内 60 个时间步的循环
for (int32 i = 0; i
{
world.Step(timeStep, iterations);
}
7、API 设计
单位
Box2D 使用浮点数,所以必须使用一些公差来保证它正常工作。这些公差已经被调谐得适合米-千克-秒(MKS)单位。尤其是,Box2D 被调谐得能良好地处理 0.1 到 10 米之间的移动物体。这意味着从罐头盒到公共汽车大小的对象都能良好地工作。
• 注意:Box2D 已被调谐至 MKS 单位。移动物体的尺寸大约应该保持在 0.1 到 10 米之间。你可能需要一些缩放系统来渲染你的场景和物体。Box2D 中的例子是使用 OpenGL 的视口来变换的。
用户数据
b2Shape,b2Body 和 b2Joint 类都允许你通过一个 void 指针来附加用户数据。这在你测试 Box2D 数据结构,以及你想把它们联系到自己的引擎中的时候是较方便的。举个典型的例子,在角色上的刚体中附加到角色的指针,这就构成了一个循环引用。如果你有角色,你就能得到刚体。如果你有刚体,你就能得到角色。
GameActor* actor = GameCreateActor();
b2BodyDef bodyDef;
bodyDef.userData = actor;
actor->body = box2Dworld->CreateBody(&bodyDef);
这是一些需要用户数据的案例:
• 使用碰撞结果给角色施加伤害
• 当玩家进入一个包围盒时播放一段脚本事件
• 当 Box2D 通知你一个关节即将摧毁时访问一个游戏结构
记得用户数据是可选的,并且能放入任何东西。然而,你需要保持一致性。例如,如果你想在一个物体中保存一个角色的指针,那你就应该在所有物体中都保存一个角色指针。不要在一个物体中保存角色指针,却在另一个物体中保存一个其它指针。这可能会导致程序崩溃。
8、世界
b2World 类包含着物体和关节。它管理着模拟的方方面面,并允许异步查询(就像 AABB 查询)。你与 Box2D 的大部分交互都将通过 b2World 对象来完成。
要创建或摧毁一个世界你需要使用 new 和 delete:
b2World* myWorld = new b2World(aabb, gravity, doSleep);
//... do stuff ...delete myWorld;
世界类用于驱动模拟。你需要指定一个时间步和一个迭代次数。例如:
float32 timeStep = 1.0f / 60.f;
int32 iterationCount = 10;
myWorld->Step(timeStep, iterationCount);
在时间步完成之后,你可以调查物体和关节的信息。最可能的情况是你会获取物体的位置,这样你才能更新你的角色并渲染它们。你可以在游戏循环的任何地方执行时间步,但你应该意识到事情发生的顺序。例如,如果你想要在一帧中得到新物体的碰撞结果,你必须在时间步之前创建物体。推荐使用固定的时间步。使用大一些的时间步你可以在低帧率的情况下提升性能。1/60 的时间步通常会呈现一个高质量的模拟。
扫描世界:
世界就是一个物体和关节的容器。你可以获取世界中所有物体和关节并遍历它们。例如,这段代码会唤醒世界中的所有物体:
for (b2Body* b = myWorld->GetBodyList(); b; b = b->GetNext())
{
b->WakeUp();
}
AABB 查询:
有时你需要求出一个区域内的所有形状。b2World 类为此使用了 broad-phase 数据结构,提供了一个 log(N) 的快速方法。你提供一个世界坐标的 AABB,而 b2World 会返回一个所有大概相交于此 AABB 的形状之数组。这不是精确的,因为函数实际上返回那些 AABB 与规定之 AABB 相交的形状。例如,下面的代码找到所有大概与指定 AABB 相交的形状并唤醒所有关联的物体。
b2AABB aabb;
aabb.minVertex.Set(-1.0f, -1.0f);
aabb.maxVertex.Set(1.0f, 1.0f);
const int32 k_bufferSize = 10;
b2Shape *buffer[k_bufferSize];
int32 count = myWorld->Query(aabb, buffer, k_bufferSize);
for (int32 i = 0; i
{
buffer[i]->GetBody()->WakeUp();
}
9、 物体
物体具有位置和速度。你可以应用力,扭矩和冲量到物体。物体可以是静态的或动态的,静态物体永远不会移动,并且不会与其它静态物体发生碰撞。物体是形状的主干,物体携带形状在世界中运动。在 Box2D 中物体总是刚体,这意味着同一刚体上的两个形状永远不会相对移动。通常你会保存所有你所创建的物体的指针,这样你就能查询物体的位置,并在图形实体中更新它的位置。另外在不需要它们的时候你也需要通过它们的指针摧毁它们。
质量性质:
1)在物体定义中显式地设置
bodyDef.massData.mass = 2.0f;//物体的质量是2kg
2)显式地在物体上设置(在其创建之后)
3)基于物体上的形状来进行密度设置
b2PolygonDef shapeDef;
shapeDef.SetAsBox(1.0f, 1.0f);
shapeDef.density = 1.0f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();//这个函数成本较高,所以你应该只在需要时使用它。
你可以在运行时调整一个物体的质量,这通常是在添加或移除物体上之形状时完成的。可能你会根据物体上的当前形状来调整其质量。
可能你也会直接设置质量。例如,你可能会改变形状,但你只想使用自己的质量公式。
void SetMass(const b2MassData* massData);
通过以下这些函数可以获得物体的质量数据:
float32 GetMass() const;
float32 GetInertia() const;
const b2Vec2& GetLocalCenter() const;
位置和角度:
bodyDef.position.Set(0.0f, 2.0f); //the body's origin position.bodyDef.angle = 0.25f * b2_pi; //the body's angle in radians.
你可以访问一个物体的位置和角度,这在你渲染相关游戏角色时很常用。你也可以设置位置,尽管这不怎么常用。
bool SetXForm(const b2Vec2& position, float32 angle);
const b2XForm& GetXForm() const;
const b2Vec2& GetPosition() const;
float32 GetAngle() const;
你可以访问线速度与角速度,线速度是对于质心所言的。
void SetLinearVelocity(const b2Vec2& v);
b2Vec2 GetLinearVelocity() const;
void SetAngularVelocity(float32 omega);
float32 GetAngularVelocity() const;
阻尼:
阻尼用于减小物体在世界中的速率。阻尼与摩擦是不同的,因为摩擦仅在物体有接触的时候才会发生,而阻尼的模拟要比摩擦便宜多了。然而,阻尼并不能取代摩擦,往往这两个效果需要同时使用。阻尼参数的范围可以在 0 到无穷之间,0 的就是没有阻尼,无穷就是满阻尼。通常来说,阻尼的值应在 0 到 0.1 之间,我通常不使用线性阻尼,因为它会使物体看起来发飘。
bodyDef.linearDamping = 0.0f;
bodyDef.angularDamping = 0.01f;
阻尼相似于稳定性与性能,阻尼值较小的时候阻尼效应几乎不依赖于时间步,而阻尼值较大的时候阻尼效应将随着时间步而变化。如果你使用固定的时间步(推荐)这就不是问题了。
休眠参数:
模拟物体的成本是高昂的,所以如果物体更少,那模拟的效果就能更好。当一个物体停止了运动时,我们要停止去模拟它。 当 Box2D 确定一个物体(或一组物体)已经停止移动时,物体就会进入休眠状态,消耗很小的 CPU 开销。如果一个醒着的物体接触到了一个休眠中的物体,那么休眠中的物体就会醒来。当物体上的关节或
触点被摧毁的时候,它们同样会醒来。你也可以手动地唤醒物体。通过物体定义,你可以指定一个物体是否可以休眠,或者创建一个休眠的物体。
bodyDef.allowSleep = true;
bodyDef.isSleeping = false;
子弹:
高速移动的物体在 Box2D 被称为子弹(bullet),你需要按照游戏的设计来决定哪些物体是子弹。如果你决定一个物体应该按照子弹去处理,使用下面的设置。
bodyDef.isBullet = true;
子弹开关只影响动态物体。
有的时候,在一个时间步内可能会有大量的刚体同时运动。如果一个物理引擎没有处理好大幅度运动的问题,你就可能会看见一些物体错误地穿过了彼此。这种效果被称为隧道效应(tunneling)。默认情况下,Box2D 会通过连续碰撞检测(CCD)来防止动态物体穿越静态物体,这是通过从形状的旧位置到新位置的扫描来完成的。引擎会查找扫描中的新碰撞,并为这些碰撞计算碰撞时间(TOI)。物体会先被移动到它们的第一个 TOI,然后一直模拟到原时间步的结束。如果有必要这个步骤会重复执行。一般 CCD 不会应用于动态物体之间,这是为了保持性能。在一些游戏环境中你需要在动态物体上也使用 CCD,譬如,你可能想用一颗高速的子弹去射击薄壁。没有 CCD,子弹就可能会隧穿薄壁。 CCD 的成本是昂贵的,所以你可能不希望所有运动物体都成为子弹。所以 Box2D 默认只在动态物体和静态物体之间使用 CCD,这是防止物体逃脱游戏世界的一个有效方法。然而,可能你有一些高速移动的物体需要一直使用 CCD。
状态信息:
物体的状态含有多个方面,通过这些函数你可以访问这些状态数据:
bool IsBullet() const;
void SetBullet(bool flag);
bool IsStatic() const;
bool IsDynamic() const;
bool IsFrozen() const;
bool IsSleeping() const;
void AllowSleeping(bool flag);
void WakeUp();
力和冲量:
你可以对一个物体应用力,扭矩,以及冲量。当应用一个力或冲量时,你需要提供一个世界位置。这常常会导致对质心的一个扭矩。
void ApplyForce(const b2Vec2& force, const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyImpulse(const b2Vec2& impulse, const b2Vec2& point);
应用力,扭矩或冲量会唤醒物体,有时这是不合需求的。例如,你可能想要应用一个稳定的力,并允许物体休眠来提升性能。这时,你可以使用这样的代码:
if (myBody->IsSleeping() == false)
{
myBody->ApplyForce(myForce, myPoint);
}
坐标转换:
物体类包含一些工具函数,它们可以帮助你在局部和世界坐标系之间转换点和向量。如果你不了解这些概念,请看 Jim Van Verth 和 Lars Bishop 的“Essential Mathematics for Games and Interactive Applications”。这些函数都很高效,所以可放心使用。
b2Vec2 GetWorldPoint(const b2Vec2& localPoint);
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);
列表
你可以遍历一个物体的形状,其主要用途是帮助你访问形状的用户数据。
for (b2Shape* s = body->GetShapeList(); s; s = s->GetNext())
{
MyShapeData* data = (MyShapeData*)s->GetUserData();
... do something with data ...
}
你也可以用类似的方法遍历物体的关节列表。
10、 形状
形状就是物体上的碰撞几何结构。另外形状也用于定义物体的质量。也就是说,你来指定密度,Box2D 可以帮你计算出质量。形状具有摩擦和恢复的性质。形状还可以携带筛选信息,使你可以防止某些游戏对象之间的碰撞。形状永远属于某物体,单个物体可以拥有多个形状。形状是抽象类,所以在 Box2D 中可以实现许多
类型的形状。如果你有勇气,那便可以实现出自己的形状类型(和碰撞算法)。
形状定义 :
形状定义用于创建形状。通用的形状数据会保存在 b2ShapeDef 中,特殊的形状数据会保存在其派生类中。
1)摩擦和恢复
摩擦可以使对象逼真地沿其它对象滑动。Box2D 支持静摩擦和动摩擦,但使用相同的参数。摩擦参数经常会设置在 0 到 1 之间,0 意味着没有摩擦,1 会产生强摩擦。当计算两个形状之间的摩擦时,Box2D 必须联合两个形状的摩擦参数,这是通过以下公式完成的:
float32 friction;
friction = sqrtf(shape1->friction * shape2->friction);
恢复可以使对象弹起,想象一下,在桌面上方丢下一个小球。恢复的值通常设置在 0 到 1 之间,0 的意思是小球不会弹起,这称为非弹性碰撞;1 的意思是小球的速度会得到精确的反射,这称为完全弹性碰撞。恢复是通过这样的公式计算的:
float32 restitution;
restitution = b2Max(shape1->restitution, shape2->restitution);
当一个形状发生多碰撞时,恢复会被近似地模拟。这是因为 Box2D 使用了迭代求解器.
2) 密度
Box2D 可以根据附加形状的质量分配来计算物体的质量以及转动惯量。直接指定物体质量常常会导致不协调的模拟。因此,推荐的方法是使用b2Body::SetMassFromShape 来根据形状设置质量。
3) 筛选
碰撞筛选是一个防止某些形状发生碰撞的系统。
Box2D 支持 16 个种群,对于任何一个形状你都可以指定它属于哪个种群。你还可以指定这个形状可以和其它哪些种群发生碰撞。例如,你可以在一个多人游戏中指定玩家之间不会碰撞,怪物之间也不会碰撞,但是玩家和怪物会发生碰撞。这是通过掩码来完成的,例如:
playerShapeDef.filter.categoryBits = 0x0002;
monsterShapeDef.filter.categoryBits = 0x0004;
playerShape.filter.maskBits = 0x0004;
monsterShapeDef.filter.maskBits = 0x0002;
碰撞组可以让你指定一个整数的组索引。你可以让同一个组的所有形状总是相互碰撞(正索引)或永远不碰撞(负索引)。组索引通常用于一些以某种方式关联的事物,就像自行车的那些部件。在下面的例子中,shape1 和 shape2 总是碰撞,而 shape3 和 shape4 永远不会碰撞。
shape1Def.filter.groupIndex = 2;
shape2Def.filter.groupIndex = 2;
shape3Def.filter.groupIndex = -8;
shape4Def.filter.groupIndex = -8;
不同组索引之间形状的碰撞会按照种群和掩码来筛选。换句话说,组筛选比种群筛选有更高的优选权。
注意在 Box2D 中的其它碰撞筛选,这里是一个列表:
• 静态物体上的形状永远不会与另一个静态物体上的形状发生碰撞
• 同一个物体上的形状之间永远不会发生碰撞
• 你可以有选择地启用或禁止由关节连接的物体上的形状之间是否碰撞
有时你可能希望在形状创建之后去改变其碰撞筛选,你可以使用 b2Shape::GetFilterData 以及 b2Shape::SetFilterData 来存取已存在形状之 b2FilterData 结构。Box2D 会缓存筛选结果,所以你需要使用 b2World::Refilter 手动地进行重筛选。
4)传感器
有时候游戏逻辑需要判断两个形状是否相交,但却不应该有碰撞反应。这可以通过传感器(sensor)来完成。传感器会侦测碰撞而不产生碰撞反应。你可以将任一形状标记为传感器,传感器可以是静态或动态的。记得,每个物体上可以有多个形状,并且传感器和实体形状是可以混合的。
myShapeDef.isSensor = true;
5) 圆形定义
b2CircleDef 扩充了 b2ShapeDef 并增加一个半径和一个局部位置。
b2CircleDef def;
def.radius = 1.5f;
def.localPosition.Set(1.0f, 0.0f);
6)多边形定义
b2PolyDef 用于定义凸多边形。要正确地使用需要一点点技巧,所以请仔细阅读。最大顶点数由 b2_maxPolyVertices 定义,当前是 8。如果你需要更多顶点,你必须修改 b2Settings.h 中的 b2_maxPolyVertices。当创建多边形定义时,你需要给出所用的顶点数目。这些顶点必须按照相对于右手坐标系之 z 轴逆时
针(CCW)的顺序定义。在你的屏幕上可能是顺时针的,这取决于你的坐标系统规则。多边形必须是凸多边形,也就是,每个顶点都必须指向外面。最后,你也不应该重叠任何顶点。Box2D 会自动地封闭环路。
这里是一个三角形的多边形定义的例子:
b2PolygonDef triangleDef;
triangleDef.vertexCount = 3;
triangleDef.vertices[0].Set(-1.0f, 0.0f);
triangleDef.vertices[1].Set(1.0f, 0.0f);
triangleDef.vertices[2].Set(0.0f, 2.0f);
7)形状工厂
初始化一个形状定义,而后将其传递给父物体;形状就是这样创建的。
b2CircleDef circleDef;
circleDef.radius = 3.0f;
circleDef.density = 2.5f;
b2Shape* myShape = myBody->CreateShape(&circleDef);
11、关节
关节的作用是把物体约束到世界,或约束到其它物体上。在游戏中的典型例子是木偶,跷跷板和滑轮。关节可以用许多种不同的方法结合起来,创造出有趣的运动。
有些关节提供了限制(limit),以便你控制运动范围。有些关节还提供了马达(motor),它可以以指定的速度驱动关节,直到你指定了更大的力或扭矩。
1)关节定义
各种关节类型都派生自 b2JointDef。所有关节都连接两个不同的物体,可能其中一个是静态物体。如果你想浪费内存的话,那就创建一个连接两个静态物体的关节
你可以为任何一种关节指定用户数据。你还可以提供一个标记,用于预防相连的物体发生碰撞。实际上,这是默认行为,你可以设置 collideConnected 布尔值来允许相连的物体碰撞。很多关节定义需要你提供一些几何数据。一个关节常常需要一个锚点(anchor point)来定义,这是固定于相接物体中的点。在 Box2D 中这点需要在局部坐标系中指定,这样,即便当前物体的变化违反了关节约束,关节还是可以被指定 —— 在游戏存取进度时这经常会发生。另外,有些关节定义需要默认的
物体之间的相对角度。这样才能通过关节限制或固定的相对角来正确地约束旋转。初始化几何数据可能有些乏味。所以很多关节提供了初始化函数,消除了大部分工作。然而,这些初始化函数通常只应用于原型,在产品代码中应该直接地定义几何数据。这能使关节行为更加稳固。其余的关节定义数据依赖于关节的类型。下面我们来介绍它们。
2)距离关节
距离关节是最简单的关节之一,它描述了两个物体上的两个点之间的距离应该是常量。当你指定一个距离关节时,两个物体必须已在应有的位置上。随后,你指定两个世界坐标中的锚点。第一个锚点连接到物体 1,第二个锚点连接到物体 2。这些点隐含了距离约束的长度。
这是一个距离关节定义的例子。在此我们允许了碰撞。
b2DistanceJointDef jointDef;
jointDef.Initialize(myBody1, myBody2, worldAnchorOnBody1,
worldAnchorOnBody2);
jointDef.collideConnected = true;
3)旋转关节
一个旋转关节会强制两个物体共享一个锚点,即所谓铰接点。旋转关节只有一个自由度:两个物体的相对旋转。这称之为关节角。
要指定一个旋转关节,你需要提供两个物体以及一个世界坐标的锚点。初始化函数会假定物体已经在应有位置了。在此例中,两个物体被旋转关节连接于第一个物体的质心。
b2RevoluteJointDef jointDef;
jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter());
这里是对上面旋转关节定义的修订;这次,关节拥有一个限制以及一个马达,后者用于模拟摩擦。
b2RevoluteJointDef jointDef;
jointDef.Initialize(body1, body2, myBody1->GetWorldCenter());//使用 Initialize() 创建关节时,旋转关节角为 0,无论两个物体当前的角度怎样。
jointDef.lowerAngle = -0.5f * b2_pi; //-90 degrees最小角度jointDef.upperAngle = 0.25f * b2_pi; //45 degrees最大角度jointDef.enableLimit = true;
jointDef.maxMotorTorque = 10.0f;//马达
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;
你可以访问旋转关节的角度,速度,以及扭矩。
float32 GetJointAngle()const;
float32 GetJointSpeed() const;
float32 GetMotorTorque() const;
你也可以在每步中更新马达参数。
void SetMotorSpeed(float32 speed);
void SetMaxMotorTorque(float32 torque);
关节马达有一些有趣的能力。你可以在每个时间步中更新关节速度,这可以使关节像正弦波一样来回
移动,或者按其它什么函数运动。
//... Game Loop Begin ...myJoint->SetMotorSpeed(cosf(0.5f * time));
//... Game Loop End ...
你还可以使用关节马达来追踪某个关节角度。例如:
//... Game Loop Begin ...float32 angleError = myJoint->GetJointAngle() - angleTarget;
float32 gain = 0.1f;
myJoint->SetMotorSpeed(-gain * angleError);
//... Game Loop End ...
通常来讲你的增益参数不应过大,否则你的关节可能会变得不稳定。
4)移动关节
移动关节(prismatic joint)允许两个物体沿指定轴相对移动,它会阻止相对旋转。因此,移动关节只有一个自由度。
移动关节的定义有些类似于旋转关节;只是转动角度换成了平移,扭矩换成了力。以这样的类比,我们来看一个带有关节限制以及马达摩擦的移动关节定义:
b2PrismaticJointDef jointDef;
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter(),
worldAxis);
jointDef.lowerTranslation = -5.0f;
jointDef.upperTranslation = 2.5f;
jointDef.enableLimit = true;
jointDef.motorForce = 1.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;
旋转关节隐含着一个从屏幕射出的轴,而移动关节明确地需要一个平行于屏幕的轴。这个轴会固定于两个物体之上,沿着它们的运动方向。就像旋转关节一样,当使用 Initialize() 创建移动关节时,移动为 0。所以一定要确保移动限制范围内包含了 0。移动关节的用法类似于旋转关节,这是它的相关成员函数:
float32 GetJointTranslation() const;
float32 GetJointSpeed() const;
float32 GetMotorForce() const;
void SetMotorSpeed(float32 speed);
void SetMotorForce(float32 force);
5)滑轮关节
滑轮关节用于创建理想的滑轮,它将两个物体接地(ground)并连接到彼此。这样,当一个物体升起时,另一个物体就会下降。滑轮的绳子长度取决于初始时的状态。
length1 + length2 == constant
你还可以提供一个系数(ratio)来模拟滑轮组,这会使滑轮一侧的运动比另一侧要快。同时,一侧的约束力也比另一侧要小。你也可以用这个来模拟机械杠杆(mechanical leverage)。length1 + ratio * length2 == constant 举个例子,如果系数是 2,那么 length1 的变化会是 length2 的两倍。另外连接 body1 的绳子的约束力将会是连接 body2 绳子的一半。当滑轮的一侧完全展开时,另一侧的绳子长度为零,这可能会出问题。此时,约束方程将变得奇异。因此,滑轮关节约束了每一侧的最大长度。另外出于游戏原因你可能也希望控制这个最大长度。最大长度能提高稳定性,以及提供更多的控制。
这是一个滑轮定义的例子:
b2Vec2 anchor1 = myBody1->GetWorldCenter();
b2Vec2 anchor2 = myBody2->GetWorldCenter();
b2Vec2 groundAnchor1(p1.x, p1.y + 10.0f);
b2Vec2 groundAnchor2(p2.x, p2.y + 12.0f);
float32 ratio = 1.0f;
b2PulleyJointDef jointDef;
jointDef.Initialize(myBody1, myBody2, groundAnchor1, groundAnchor2,
anchor1, anchor2, ratio);
jointDef.maxLength1 = 18.0f;
jointDef.maxLength2 = 20.0f;
滑轮关节提供了当前长度:
float32 GetLength1() const;
float32 GetLength2() const;
6) 齿轮关节
如果你想要创建复杂的机械装置,你可能需要齿轮。原则上,在 Box2D 中你可以用复杂的形状来模拟轮齿,但这并不十分高效,而且这样的工作可能有些乏味。另外,你还得小心地排列齿轮,保证轮齿能平稳地啮合。Box2D 提供了一个创建齿轮的更简单的方法:齿轮关节。
齿轮关节需要两个被旋转关节或移动关节接地(ground)的物体,你可以任意组合这些关节类型。另外,创建旋转或移动关节时,Box2D 需要地(ground)作为 body1。类似于滑轮的系数,你可以指定一个齿轮系数(ratio),齿轮系数可以为负。另外值得注意的是,当一个是旋转关节(有角度的)而另一个是移动关节(平移)时,齿轮系数是长度或长度分之一。coordinate1 + ratio * coordinate2 == constant这是一个齿轮关节的例子:
b2GearJointDef jointDef;
jointDef.body1 = myBody1;
jointDef.body2 = myBody2;
jointDef.joint1 = myRevoluteJoint;
jointDef.joint2 = myPrismaticJoint;
jointDef.ratio = 2.0f * b2_pi / myLength;
• 注意:齿轮关节总应该先于旋转或移动关节被删除,否则你的代码将会由于齿轮关节中的无效关节
指针而导致崩溃。另外齿轮关节也应该在任何相关物体被删除之前删除。
7)关节工厂
关节是通过世界的工厂方法来创建和摧毁的,这引出了一个旧问题:
• 注意:不要试图在栈上创建物体或关节,也不要使用 new 或 malloc 在堆上创建。物体以及关节必须要通过 b2World 类的方法来创建或摧毁。
这是一个关于旋转关节生命期的例子:
b2RevoluteJointDef jointDef;
jointDef.body1 = myBody1;
jointDef.body2 = myBody2;
jointDef.anchorPoint = myBody1->GetCenterPosition();
b2RevoluteJoint* joint = myWorld->CreateJoint(&jointDef);
//... do stuff ...myWorld->DestroyJoint(joint);
joint = NULL;
8)使用关节
在许多模拟中,关节被创建之后便不再被访问了。然而,关节中包含着很多有用的数据,使你可以创建出丰富的模拟。首先,你可以在关节上得到物体,锚点,以及用户数据。
b2Body* GetBody1();
b2Body* GetBody2();
b2Vec2 GetAnchor1();
b2Vec2 GetAnchor2();
void* GetUserData();
11、接触
接触(contact)是由 Box2D 创建的用于管理形状间碰撞的对象。接触有不同的种类,它们都派生自 b2Contact,用于管理不同类型形状之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间碰撞的类。
这里是 Box2D 中的一些与碰撞有关的术语:
触点(contact point)
两个形状相互接触的点。实际上当物体的表面相接触时可能会有一定接触区域,在 Box2D 则近似地
以少数点来接触。
接触向量(contact normal)
从 shape1 指向 shape2 的单位向量。
接触分隔(contact separation)
分隔相反于穿透,当形状相重叠时,分隔为负。可能以后的 Box2D 版本中会以正隔离来创建触点,所以当有触点的报告时你可能会检查符号。
法向力(normal force)
Box2D 使用了一个迭代接触求解器,并会以触点保存结果。你可以安全地使用法向力来判断碰撞强度。例如,你可以使用这个力来引发破碎,或者播放碰撞的声音。
切向力(tangent force)
它是接触求解器关于摩擦力的估计量。
接触标识(contact ids)
Box2D 会试图利用一个时间步中的触点压力(contact force)结果来推测下一个时间步中的情况。接触标识用于匹配跨越时间步的触点,它包含了几何特征索引以便区分触点。
当两个形状的 AABB 重叠时,接触就被创建了。有时碰撞筛选会阻止接触的创建,有时尽管碰撞已筛选了 Box2D 还是须要创建一个接触,这种情况下它会使用 b2NullContact 来防止碰撞的发生。当 AABB 不再重叠之后接触会被摧毁。也许你会皱起眉头,为了没有发生实际碰撞的形状(只是它们的 AABB)却创建了接触。好吧,的确是这样的,这是一个“鸡或蛋”的问题。我们并不知道是否需要一个接触,除非我们创建一个接触去分析碰撞。如果形状之间没有发生碰撞,我们需要正确地删除接触,或者,我们可以一直等到 AABB 不再重叠。Box2D 选择了后面这个方法。
1)接触监听器
通过实现 b2ContactListener 你就可以接受接触数据。当一个触点被创建时,当它持续超过一个时间步时,以及当它被摧毁时,这个监听器(listener)就会发出报告。请留意两个形状之间可能会有多个触点。
class MyContactListener : public b2ContactListener
{
public:
void Add(const b2ContactPoint* point)
{
// handle add point
}
void Persist(const b2ContactPoint* point)
{
// handle persist point
}
void Remove(const b2ContactPoint* point)
{
// handle remove point
}
void Result(const b2ContactResult* point)
{
// handle results
}
};
2)接触筛选
通常,你不希望游戏中的所有物体都发生碰撞。例如,你可能会创建一个只有某些角色才能通过的门。这称之为接触筛选,因为一些交互被筛选出了。
通过实现 b2ContactFilter 类,Box2D 允许定制接触筛选。这个类需要一个 ShouldCollide 函数,用于接收两个 b2Shape 的指针,如果应该碰撞那么就返回 true。默认的 ShouldCollide 实现使用了 6 形状 中的 b2FilterData。
bool b2ContactFilter::ShouldCollide(b2Shape* shape1, b2Shape* shape2)
{
const b2FilterData& filter1 = shape1->GetFilterData();
const b2FilterData& filter2 = shape2->GetFilterData();
if (filter1.groupIndex == filter2.groupIndex && filter1.groupIndex != 0)
{
return filter1.groupIndex > 0;
}
bool collide = (filter1.maskBits & filter2.categoryBits) != 0 &&
(filter1.categoryBits & filter2.maskBits) != 0;
return collide;
}
12、杂项
1)
你可以实现一个 b2BoundaryListener,这样当有物体超出世界的 AABB 时 b2World 就能通知你。当你得到回调时,你不应该试图删除物体;取而代之的是,你可以为角色做个删除或错误处理标记,在物理时间步之后再进行这个事件的处理。
class MyBoundaryListener : public b2BoundaryListener
{
void Violation(b2Body* body)
{
MyActor* myActor = (MyActor*)body->GetUserData();
myActor->MarkForErrorHandling();
}
};
随后你可以在世界对象中注册你的边界监听器实例,这应该安排在世界初始化过程中。
myWorld->SetListener(myBoundaryListener);
2)隐式摧毁
如果你摧毁一个 Box2D 实体,你应该保证所有到它的引用都删除了。如果你只有实体的单个引用的话,那就简单了。但如果你有很多个引用,你可能要考虑实现一个处理类来封装原始指针。通常使用 Box2D 时你需要创建并摧毁许多物体,形状还有关节。管理这些实体有些自动化,如果你摧毁一个物体,所有它的形状,关节,以及接触都会摧毁,这称为隐式摧毁。任何连接于这些关节或接触之一的物体将被唤醒,通常这是便利的。然而,你应该意识到了一个关键问题:
Box2D 提供了一个名为 b2WorldListener 的监听器类,你可以实现它并提供给世界对象,随后当关节将被隐式摧毁时世界对象就会提醒你。
你可以实现一个 b2DestructionListener,这样当一个形状或关节隐式摧毁时 b2World 就能通知你,这可以帮助你预防访问无效指针。
class MyDestructionListener : public b2DestructionListener
{
void SayGoodbye(b2Joint* joint)
{
// remove all references to joint.
}
};
随后你可以注册它,这应该在世界初始化过程中。
myWorld->SetListener(myDestructionListener);