第八章 关节(joint)
8.1简介
关节用来将物体约束到世界(world),或者约束到其他物体上。在游戏中比较典型的例子有木偶,跷跷板和滑轮等等。可以由很多种不同的方式构成关节来生成有趣的移动特效。
有些关节具有限制(limit),你可以用它来控制关节的移动范围。有些关节具有马达,能够驱动关节以一定的速度移动,直到你通过施加一定的力或扭矩将其抵消掉。
关节马达可以有很多种使用方式,你可以使用马达来控制位置,只需要指定一个与原位置到目标位置的距离成比例的关节速度就可以了。你也可以使用马达来模拟关节摩擦力:设置关节速度为0,然后提供一个小的但是有效果的最大力/扭矩,接着马达就会阻碍关节扭动,直到负载变得足够大。
8.2关节定义(thejoint definition)
每一种关节类型都有一个继承自b2JointDef类的定义类。所有的关节都是用来连接两个不同的物体的,其中一个物体可能会是静态的(static)。用关节来连接两个静态物体和/或运动的(kinematic)物体也是可以的,但是这没有任何效果并且需要消耗一定的CPU时间。(这么说应该是不建议我们使用这种方式喽)
你可以为任何关节类型指定用户数据(userdata)并且你可以提供碰撞过滤标记来防止关节连接的两个物体彼此发生碰撞,事实上默认设置就是这样的,如果你想要允许关节连接的物体发生碰撞的话,需要额外设置布尔属性collideConnected。
很多关节定义需要你提供一些几何数据。通常关节由锚点来定义,这些点是用来固定所连接的物体的点,Box2D中这些点需要在本地坐标系中指定,这样,即便是物体变换违反了关节约束(在游戏保存或者重新载入的时候经常发生),关节仍然能够被指定。此外,有些关节定义需要知道初始时两个物体之间的角度,这对于正确地约束旋转是有必要的。
初始化几何数据的过程可能很枯燥,因此很多关节定义了初始化函数,使用当前物体的变换来减轻工作量。然而,这些初始化函数通常只被应用于原型设计(prototyping),产品代码中应当直接定义几何数据,这样关节的行为能够更加稳定。
剩下的关节定义数据以来具体的关节类型,我们接下来会继续介绍。
8.3关节工厂
我们通过世界类的工厂方法来构造和析构关节,这里我们又要强调一句那个需要注意的地方:不要尝试去使用new或者malloc方法在堆栈上创建关节,你一定要使用世界类中提供的方法来构造和析构关节。
下面这个例子展示了一个旋转关节(revolute joint)的生命周期:
b2RevoluteJointDef jointDef;
jointDef.bodyA = myBodyA;
jointDef.bodyB = myBodyB;
jointDef.anchorPoint = myBodyA->GetCenterPosition();
b2RevoluteJoint* joint =(b2RevoluteJoint*)myWorld->CreateJoint(&jointDef);
…do stuff…
myWorld->DestroyJoint(joint);
joint = NULL;
当对象析构之后,记得将对象指针指向NULL是个好习惯,否则,如果你再次企图使用这个指针,程序就会崩溃掉。
关节的生命周期并不简单,注意下面的警告:
当附加到关节上的物体析构之后,关节也随之析构。
预防措施并不总是必要的,你可以让游戏引擎总是在析构附加到关节上的物体之前先析构关节对象,这样的话你就不需要实现listener类中的方法了。详细内容请参考Implicit Destruction(隐式析构)一节。
8.4使用关节
在很多模拟情形中,关节在创建出来之后,知道析构前也不会被再次访问。然而,关节中包含很多有用的数据,你可以使用它们创建非常丰富的模拟效果。
首先,你能够通过关节对象获取物体、锚点和用户数据。
b2Body* GetBodyA();
b2Body* GetBodyB();
b2Vec2 GetAnchorA();
b2Vec2 GetAnchorB();
void* GetUserData();
所有的关节都有反作用力和反扭矩,这个反作用力作用于第二个物体的锚点处,你可以使用这个反作用力来折断关节或者触发其他游戏事件。这些方法会消耗一定的计算时间,所以如果不必要情况下,不要调用他们。
b2Vec2 GetReactionForce();
float32 GetReactionTorque();
8.5距离关节(distancejoint)
距离关节是最简单的关节类型之一。距离关节规定了两个物体上面的两个点的距离为一个定值。当你指定一个距离关节时,绑定的两个物体应该已经在所在位置上了。接着你需要指定两个世界坐标系下的锚点,第一个锚点连接第一个物体(body1),第二个锚点连接第二个物体(body2)。两个锚点的距离隐含了距离约束的长度。
下面是一个距离关节定义的例子,在这个例子中,我们定义了允许两个物体发生碰撞:
b2DistanceJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, worldAnchorOnBodyA,worldAnchorOnBodyB);
jointDef.collideConnected = true;
距离定义也可以被设计成软连接(soft),类似于用弹簧来连接一样,你可以查看testbed中的Web例子来看看具体的效果。
这种软连接的效果可以通过调节定义中的两个常量来实现:频率(frequency)和阻尼比率(dampingratio)。你可以把频率当成谐振子(harmonicoscillator)的频率(类似于吉他琴弦),频率的单位值赫兹(Hertz,Hz),通常频率应该小于时间间隔(timestep)频率的一半。因此,如果你使用60Hz的时间间隔的话,距离关节的频率应该最好小于30Hz,具体的原因请参考奈奎斯特频率的相关介绍(Nyquistfrequency,奈奎斯特频率通常是离散信号系统采样频率的一半)。
阻尼比率没有单位,通常在0到1之间,当然也可以更大。阻尼比率为1时,阻尼效果非常显著,震动效果消失。
jointDef.frequencyHz = 4.0f;
jointDef.dampingRatio = 0.5f;
8.6旋转关节(revolutejoint)
旋转关节定义了两个物体使用相同的锚点,通常称为一个铰链点。旋转关节只有一个自由度:两个物体的相对旋转,我们称之为旋转角。
创建一个旋转关节你需要提供两个物体和一个世界坐标系下的锚点。初始化函数假定两个物体都在各自的位置上了。
在这个例子中,两个物体通过旋转关节连接到一起,连接点为第一个物体的质心:
b2RevoluteJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, myBodyA->GetWorldCenter());
当物体B逆时针方向旋转的时候,旋转关节的旋转角为正。和Box2D中所有的角度一样,旋转角的单位是弧度。我们约定,当通过Initialize()函数创建好关节的时候,旋转角为0,无论当前两个物体的旋转情况是怎样的。
有些情况下你可能需要控制旋转角度,针对这种情况,旋转关节可以有选择的模拟关节限制(jointlimit)和/或关节马达(motor)。
关节限制强制关节的旋转角保持在一个最小值和最大值之间,为了实现这种效果,关节限制会对关节施加足够大的扭矩。关节限制的范围应该包含0(也就是0应该在最小值到最大值这个区间内),否则当模拟开始的时候(初始化的时候旋转角就是0),关节会倾斜。
关节马达允许你指定关节速度(角度的时间导数),速度可正可负,马达可以产生无限大的力,但是通常我们不需要这样,还是那个老问题:
“当一个不可抗力施加在一个无法移动的物体上时,会发生什么?”
讨论这个并没有什么意义,所以你可以为关节马达提供一个最大扭矩,关节马达会维持指定的速度,除非需要的扭矩超过了其最大扭矩,当出现这种情况的时候,关节转动速度会减慢甚至反向转动。
你可以使用关节马达来模拟关节摩擦力,只需要设置关节速度为0,然后提供一个小的但是有效果的最大力/扭矩,接着马达就会阻碍关节扭动,直到负载变得足够大。
下面的代码是上面旋转关节定义的修订版,这次我们启用了关节限制和关节马达,其中关节马达用来模拟关节摩擦力。
b2RevoluteJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, myBodyA->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(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 …
通常上你的gain参数不能太大,不然关节会变得不稳定。
棱柱关节(prismatic joint)
棱柱关节允许两个物体沿指定轴相对移动,棱柱关节会防止两个物体相对旋转。因此,棱柱关节只有一个自由度。
棱柱关节的定义类似于旋转关节的定义,只不过将旋转角(angle)替换成位移(translation),将扭矩(torque)替换为力(force)。利用这种类比关系,我们可以提供一个具有关节限制和摩擦马达(motorfriction)的棱柱关节定义:
b2PrismaticJointDef jointDef;
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.Initialize(myBodyA, myBodyB, myBody->GetWorldCenter(),worldAxis);
jointDef.lowerTranslation = -0.5f;
jointDef.upperTranslation = 2.5f;
jointDef.enableLimit = true;
jointDef.maxMotorForce = 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);
8.8滑轮关节(pulleyjoint)
滑轮关节用来创建理想滑轮,滑轮将两个物体接地,并将两个物体连在一起。一个物体上升的时候,另一个物体就下降,滑轮的绳子长度保持固定,由初始设置决定。
length1 + length2 == constant
你可以提供一个比例系数来模拟滑轮组,这样滑轮一端的滑动速度就会比另一端快,同时一端的约束力也比另一端要小。你可以通过这种方式来创建机械杠杆。
length1 + ratio * length2 == constant
例如,如果比例系数是2的话,那么length1的变化量将是length2的两倍,同时,绳子附加在body1上的约束力将是附加在body2上的一半。
当滑轮一端完全展开时,问题变得很麻烦,绳子的另一端的长度为0,这时约束方程就出问题了(糟糕),所以你应该通过设置碰撞形状(collisionshapes)来避免这种问题。
下面是一个滑轮定义的例子:
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);
滑轮关节提供下面的方法来获取当前滑轮两侧的绳长:
float32 GetLengthA() const;
float32 GetLengthB() const;
8.9齿轮关节(gearjoint)
如果你想创建复杂的机械装置,你可能会用到齿轮。原则上讲,你可以在Box2D中通过组合物体(compoundshapes)创建齿轮来模拟齿轮的轮齿,但是这并不是高效的方法,过程可能很繁琐枯燥,而且你还要小心翼翼地把齿轮排列起来让轮齿无缝地啮合。而在Box2D中,提供了很方便的方法帮助我们创建齿轮:齿轮关节。
齿轮关节只能用来连接旋转关节和/或棱柱关节。
和滑轮比例系数类似,你可以指定齿轮比例系数。然而,这里齿轮比例系数可以为负值。另外一点需要注意的是,当一个关节是旋转关节(有角度的)而另一个关节是棱柱关节(平移)时,齿轮系数的单位是长度单位或者长度单位的倒数。
coordinate1 + ratio * coordinate2 == constant;
下面是一个齿轮关节的例子,物体myBodyA和myBodyB是任意的来自两个不同的关节的物体,只要他们不是同一个物体就可以了:
b2GearJointDef jointDef;
jointDef.bodyA = myBodyA;
jointDef.bodyB = myBodyB;
jointDef.joint1 = myRevoluteJoint;
jointDef.joint2 = myPrismaticJoint;
jointDef.ratio = 2.0f * b2_pi / myLength;
注意齿轮关节依赖于另外两个关节,这种情况非常脆弱(不稳定),如果另外两个关节被删除了会发生什么?
注意:一定要在删除齿轮关节关联的旋转关节或者棱柱关节之前,先删除齿轮关节,否则由于齿轮关节中的两个指向其他关节的指针现在指向无效的地址了,程序会以错误的方式崩溃。另外,在你要删除任何相关的物体之前,也要先删除齿轮关节。
8.10鼠标关节(mousejoint)
鼠标关节被用在testbed中,方便使用鼠标来操作物体。它尝试将物体拖拽到当前光标所在的位置,在旋转上没有任何限制。
鼠标关节定义包含一个目标点(target point),力的最大值(maximumforce),频率(frequency)和阻尼系数(dampingratio)。目标点的初始位置和物体的锚点重合,力的最大值用来防止多个动态物体相互作用的时候出现强烈地反应,这个值你想设置多大都可以。频率和阻尼系数用来创建弹簧/阻尼效果(类似于距离关节)。
很多用户尝试修改鼠标关节来增加游戏性,他们希望能够做到即时反馈和更精确的位置控制。在这个意义下,鼠标关节的效果并不好。你可能需要考虑使用运动的物体(kinematic)来作为替换方案。
8.11方向盘关节(wheeljoint)
方向盘关节将物体B上的一个点限制到物体A上的一条线上,方向盘关节还提供了一个悬簧(suspensionspring),更多细节请参考b2WheelJoint.h和Car.h两个文件。
8.12焊接关节(weldjoint)
焊接关节用来限制两个物体间的所有相对位移,要查看焊接关节的效果,可以查看testbed中的Cantilever.h文件。
用焊接关节来定义一个可分裂的(breakable)物体,这个想法看起来很诱人,但是,由于Box2D的解析器是迭代解析器,所以焊接可能不够牢固,导致焊接在一起的物体可能会有松动。
相反的,我们可以通过将多个装置(fixture)绑定到一个物体上来创建一个可分裂的物体。当物体破裂的时候,你可以析构掉一个装置,然后在另一个物体上重建这个装置。你可以查看testbed中的可分裂物体的例子。
8.13绳索关节(ropejoint)
绳索关节限制了两个物体的距离最大值,这对于防止物体链(bodychain)拉伸非常有效,尽管计算负载非常高。更多细节请查看b2RopeJoint.h和RopeJoint.h。
8.14摩擦关节(frictionjoint)
摩擦关节用于自上而下的摩擦(top-downfriction),它提供了二维的摩擦效果(平移摩擦和角摩擦),更多细节请查看b2FrictionJoint.h和ApplyForce.h两个文件。
8.15马达关节
马达关节允许你通过指定一个目标位置(targetposition)和旋转偏移值(rotation offset)来控制物体的运动。你可以设置马达的力和转矩的最大值(maximum motor forceandtorque),这两个值会在物体向目标位置移动和旋转的时候用到。如果物体被限制了,它会停止运动,接触力与马达的力和转矩的最大值成一定比例。更多细节请查看b2MotorJoint.h和MotorJoint.h两个文件。