原文地址:http://blog.csdn.net/complex_ok/article/details/6725365
关节用于把物体约束到世界,或约束到其它物体上。在游戏中, 典型例子有木偶, 跷跷板和滑轮。用不同的方式将关节结合起来使用, 可以创造出有趣的运动。
有些关节提供了限制(limit), 使你可以控制运动的范围。有些关节还提供了马达(motor), 它可以以指定的速度驱动关节一直运动, 直到你指定了更大的力或扭矩来抵消这种运动。
关节马达有许多不同的用途。你可以使用关节来控制位置,只要提供一个与目标之距离成正比例的关节速度即可。你还可以模拟关节摩擦:将关节速度置零,并且提供一个小的、但有效的最大力或扭矩; 那么马达就会努力保持关节不动, 直到负载变得过大。
每种关节类型都有各自的定义(definition), 但都派生自b2JointDef。所有关节都连接两个不同的物体, 其中一个物体有可能是静态的。关节也可以连接两个static或者kinematic类型的物体,但这没有任何实际用途,只会浪费处理器时间。
你可以为任何一种关节类型指定用户数据。你还可以提供一个标记, 用于防止用关节相连的物体发生碰撞,实际上, 这是默认行为。你也可以设置 collideConnected 布尔值来允许相连的物体发生碰撞。
很多关节定义需要你提供一些几何数据。一个关节常常需要一个锚点(anchor point)来定义,这是固定于相接物体中的点。Box2D要求这些点要在局部坐标系中指定, 这样, 即便当前物体的变化违反了关节约束(joint constraint),关节还是可以被指定 —— 在游戏保存或载入进度时这经常会发生。另外,有些关节定义需要默认的物体之间的相对角度。这样才能正确地约束旋转。
初始化几何数据可能有些乏味。所以很多关节提供了初始化函数,消除了大部分工作。然而,这些初始化函数通常只应用于原型, 在产品代码中应该直接地定义几何数据。这能使关节行为更具健壮性。
其余的关节定义数据依赖于关节的类型。下面我们来介绍它们。
关节使用world的工厂方法来创建和摧毁。这引入一个老问题:
注意:
不要使用new或malloc在栈(stack)或堆(heap)中创建关节。你想创建或摧毁物体和关节,必须使用b2World类中对应的创建或摧毁函数。
这里有个例子,展示了旋转关节(revolute joint)从创建到摧毁的过程:
b2RevoluteJointDef jointDef;
jointDef.body1 = myBody1;
jointDef.body2 = myBody2;
jointDef.anchorPoint =myBody1->GetCenterPosition();
b2RevoluteJoint* joint =myWorld->CreateJoint(&jointDef);
... do stuff ...
myWorld->DestroyJoint(joint);
joint = NULL;
一个很好的习惯: 当对象摧毁后,就将对应的指针清零。不清零的话,当你试图再次使用这个指针,程序在特定时候会崩溃。
关节的生命周期并不简单,要特别留心下面的警告,敲响警钟。
注意
物体被摧毁时,依附其上的关节也会被摧毁。
上面的注意并非时时必要。你可以组织好自己的游戏引擎,保证物体被摧毁前,依附其上的关节已经先被摧毁。这种情况下,你没有必要实现监听类(listener class), 去监听物体被摧毁的事件。更多细节请看隐式摧毁(Implicit Destruction)那小节。
在许多模拟中,关节被创建之后,直到摧毁也不会再被访问。然而,关节中包含着很多有用的数据, 使你可以创建出丰富的模拟。
首先,你可以在关节上得到物体,锚点,以及用户数据。
b2Body* GetBody1();
b2Body* GetBody2();
b2Vec2 GetAnchor1();
b2Vec2 GetAnchor2();
void* GetUserData();
所有的关节都有反作用力和反扭矩,这个反作用力应用于 body2 的锚点之上。你可以用反作用力来折断关节(break joints),或引发其它游戏事件。这些函数可能需要做一些计算,所以没有必要就不要去调用它们。
b2Vec2 GetReactionForce();
float32 GetReactionTorque();
距离关节是最简单的关节之一, 它是说, 两个物体上面各自有一点,两点之间的距离必须固定不变。当你指定一个距离关节时, 两个物体必须已在应有的位置上。之后,你指定世界坐标中的两个锚点。第一个锚点连接 到物体1,第二个锚点连接到物体2。这两点隐含了距离约束的长度。
这是一个距离关节定义的例子。这种情况下, 我们允许物体碰撞。
b2DistanceJointDef jointDef;
jointDef.Initialize(myBody1,myBody2, worldAnchorOnBody1, worldAnchorOnBody2);
jointDef.collideConnected = true;
距离关节也可以是软的,就像用橡皮筋来连接。看看testbed中的Web例子,可以知道出它有什么样的行为。
要使关节有弹性,可以调节一下两个参数:频率(frequency)和阻尼率(damping ratio)。将频率想象成谐振子(harmonic oscillator, 比如吉他弦)振动的快慢。频率使用单位赫兹(Hertz)来指定。典型情况下,关节频率要小于一半的时间步(time step)频率。比如每秒执行60次时间步, 距离关节的频率就要小于30赫兹。这样做的理由可以参考Nyquist频率理论。
阻尼率无单位,典型是在0到1之间, 也可以更大。1是阻尼率的临界值, 当阻尼率为1时,没有振动。
jointDef.frequencyHz = 4.0f;
jointDef.dampingRatio = 0.5f;
旋转关节会强制两个物体共享一个锚点,即所谓铰接点。旋转关节只有一个自由度:两个物体的相对旋转。这称之为关节角。
要指定一个旋转关节,你需要提供两个物体以及世界坐标的一个锚点。初始化函数会假定物体已经在 应有位置了。
在此例中,两个物体被旋转关节连接起来, 铰接点为第一个物体的质心。
b2RevoluteJointDef jointDef;
jointDef.Initialize(myBody1,myBody2, myBody1->GetWorldCenter());
在 body2 逆时针旋转时,关节角为正。像所有 Box2D 中的角度一样,旋转角也是弧度制的。按规 定,使用Initialize() 创建关节时, 无论两个物体当前的角度怎样,旋转关节角都为0。
有时候,你可能需要控制关节角。为此,旋转关节可以随意地模拟关节限制和马达。
关节限制(joint limit)会强制关节角度保持在一定范围内。为此它会应用足够的扭矩。0应该在范围内,否则在开始模拟时关节会有点倾斜。
关节马达允许你指定关节的角速度(角度的时间导数),速度可正可负。马达可以有产生无限大的力,但这通常是没有必要。想想那个经典问题:
"当一个不可抵抗的力作用在一个不可移动的物体上, 会发生什么?"
我可以告诉你这并不有趣。所以你应该为关节马达提供一个最大扭矩。关节马达会维持在指定的速 度,除非其所需的扭矩超出了最大扭矩。当超出最大扭矩时,关节会慢下来,甚至会反向运动。
你还可以使用关节马达来模拟关节摩擦。只要把关节速度设为0,并将最大扭矩设得很小且有效。这样马达会试图阻止关节旋转, 除非有过大的负载。
这里是对上面旋转关节定义的修订; 这次,关节拥有一个限制以及一个马达,后者用于模拟摩擦。
b2RevoluteJointDef jointDef;
jointDef.Initialize(body1, body2,myBody1->GetWorldCenter());
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;
执行step后,你也可以更新马达的参数。
void SetMotorSpeed(float32 speed);
void SetMaxMotorTorque(float32torque);
关节马达有些有趣的功能。你可以在每个时间步中更新关节的速度,使得它像正弦波那样前后摆动,或者指定一个你想要的函数。
... 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 ...
通常你的增益参数不能太大,不然关节会变得不稳定。
移动关节(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);
滑轮关节用于创建理想的滑轮,它将两个物体接地(ground)并彼此连接。这样,当一个物体上升,另一个物体就会下降。滑轮的绳子长度取决于初始配置。
length1 + length2 == constant
你还可以提供一个系数(ratio)来模拟block and tackle,这会使滑轮一侧的运动比另一侧要快。同时,一侧的约 束力也比另一侧要小。你也可以用这个来模拟机械杠杆(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;
如果你想创建复杂的机械装置,可能需要齿轮。原则上,在 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;
注意,齿轮关节依赖于两个其它关节,这是脆弱的:当其它关节被删除了会发生什么?
注意
齿轮关节总应该先于旋转或移动关节被删除。否则由于齿轮关节中的关节指针无效,你的代码将会因访问这些无效指针而导致崩溃。另外齿轮关节也应该在任何相关物体被删除之前删除。
在testbed例子中, 鼠标关节用于通过鼠标来操控物体。它试图将物体拖向当前鼠标光标的位置。而在旋转方面就没有限制。
鼠标关节的定义需要一个目标点(target point),最大力(maximum force),频率(frequency),阻尼率(damping ratio)。目标点最开始与物体的锚点重合。最大力用于防止在多个动态物体相互作用时,会有激烈反应。你想将最大力设为多大就多大。频率和阻尼率用于创造一种弹性效果,就跟距离关节类似。
许多用户为了游戏的可玩性,会试图修改鼠标关节。用户常常希望鼠标关节有即时反应,精确的去到某个点。这情况下,鼠标关节表现并不好。你可以考虑一下用kinematic物体来替代。
线性关节跟移动关节(prismatic joint)类似,但没有旋转方面的约束。线性关节可以用来模拟悬挂着的车轮。更多细节请看b2LineJoint.h。
焊接关节的用途是使两个物体不能相对运动。看看testbed中的Cantilever例子,可以知道焊接关节有怎么样的表现。
用焊接关节来定义一个可分裂物体,这想法很诱人。但是,由于Box2D的迭代求解,关节焊得有点不稳。 导致用焊接关节连接起来的物体会有所摆动。
创建可裂物体的更好方法是使用单个的物体,上面有很多fixture。 当物体分裂时,你可以删掉原物体其中一个fixture,并重新创建一个新的物体,带有那个fixture。参考一下testbed中的Breakable例子。