Oh!Coder

Coding Life

第八章 连接器

| Comments

声明:此文章翻译自Box2D v2.2.0用户手册,仅供学习参考。

8.1 关于(About)

连接器起到限制世界当中物体自身或物体之间的作用。典型的例子是游戏中的木偶,跷跷板,滑轮。连接器可以用很多不同的方式创建有趣的运动。

有些连接器提供了限制,你可以以此来控制运动的范围。有的连接器提供了用指定速度驱动物体的马达,直到有一个更大的力或扭矩来进行抵消。很多地方可以使用马达。可以使用马达来控制位置,只要通过指定连接器中目标和当前位置成正比例的速度值即可。还可以使用马达模拟连接器摩擦:设置连接器速度为零然后提供一个小的但是有效的最大力或扭矩的马达。然后马达将会尝试着让连接器静止直到负载变的足够大。

8.2 连接器定义(The Joint Definition)

每一个连接器都有一个定义派生自b2JointDef。所有连接器在不同的物体之间进行连接。一个物体可能是静态的。连接器也可以在静态物体以及(或者)运动学物体之间进行连接,但是不会有任何效果只会占用处理时间。

你可以指定用户数据(user data)为任何连接器类型,并且可以提供一个标志来阻止所附加的物体彼此碰撞。事实上这是默认属性你必须设置collideConnected的布尔值才能实现连接着的两个物体碰撞。

很多连接器的定义需要提供一些几何数据。通常一个连接器通过锚定来定义。这些点附加在物体上。Box2D需要这些点在本地坐标中设定。即便当前物体自身的变换违反了限制条件还是可以指定连接器–普遍的情况是游戏的存储和载入。另外,有些连接器的定义需要知道两个物体之间默认的相对角度。这才能正确的限制旋转。

初始化几何数据很乏味,所以很多连接器提供了初始化方法来减少大部分工作。然而,这些初始化方法通常只是用来做原型。产品代码需要直接定义几何体。这将使得连接器的行为更具健壮性。

接下来的连接器的定义取决于连接器的类型。现在我们开始了解。

8.3 连接器工厂(Joint Factory)

使用世界工厂方法来创建和销毁连接器。这又让人想起一个老问题:

警告
不要使用new或者malloc关键字在栈或堆里创建连接器。你必须要像创建和销毁物体一样,对连接器操作也要使用b2World类型中创建和销毁方法。

这里是一个旋转连接器生命周期的例子:

1
2
3
4
5
6
7
8
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;

当指针指向的对象被销毁的时候赋值为空无论何时都是一个好习惯。这使得如果你重用这个指针,程序会以一种可控的方式崩溃。

控制连接器的生命周期并不容易。最好注意下面这条:

注意
当附加的物体销毁时,连接器才会被销毁。

上面的注意并非每次都必要。你可以管理游戏引擎来保证附加的物体销毁之前连接先销毁就可以了。在这个例子中你不要实现监听器类型(listener class)。更多细节请看11.1小节隐式销毁(Implicit Destruction)。

8.4 使用连接器(Using Joints)

在很多模拟场景中,创建完连接器之后直到销毁再也没有被访问过。然而,连接器中包含了很多有用的数据可以使模拟更加丰富多彩。

首先,你可以获取物体,锚定点,和连接器中的用户数据(user data)。

1
2
3
4
5
b2Body* GetBodyA();
b2Body* GetBodyB();
b2Vec2 GetAnchorA();
b2Vec2 GetAnchorB();
void* GetUserData();

所有连接器都有反作用力和反扭矩。这个反作用力应用在锚定点的body 2上。你可以使用反作用力来触发其它连接器或者游戏事件中的触发器。这些方法可以做一些计算,当然如果你不需要这些结果你可以不用调用它们。

1
2
b2Vec2 GetReactionForce();
float32 GetReactionTorque();

8.5 距离连接器(Distance Joint)

其中最简单的连接器,通常所说的在两个物体上两点之间保持一定距离的距离连接器。当你指定一个距离连接器时,相应的两个物体应该已经在应有的位置上了。然后在世界坐标系中指定两个锚定点。第一个锚定点连接body1,第二个锚定点连接body2。这些点代表着应该保持的距离的常量。

pic

这里给出一个距离连接器的定义。在这个例子中我们决定允许物体之间有碰撞。

1
2
3
b2DistanceJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, worldAnchorOnBodyA, worldAnchorOnBodyB);
jointDef.collideConnected = true;

距离连接器可以变成软的,就像连接一个弹簧一样。可以在testbed中通过Web例子看看到底是什么样的行为。

在定义中通过调节:频率(frequency)和阻尼率(damping ratio)两个常量来取得柔软的效果。想象一下谐波震荡器中的频率(像琴弦一样)。频率以赫兹(Hertz)为单位。通常频率应该比半个时间步长要短。所以如果你的时间步长是60Hz,那么距离连接器的频率应该小于30Hz。原因是由奈奎斯特频率(Nyquist frequency)有关。

阻尼率属于无单位量纲(non-dimensional)并且通常在0到1之间,但是可以更大。1是阻尼的临界值(此时无震荡)。

1
2
jointDef.frequencyHz = 4.0f;
jointDef.dampingRatio = 0.5f;

8.6 旋转连接器(Revolute Joint)

旋转连接器同时作用于两个物体,并使两个物体共享同一个锚定点,经常称之为铰链点(hinge point)。旋转连接器有一个自由度:相对于两个物体的旋转来说。这个角称为连接角(joint angle)。

pic

为了定制一个旋转连接,你需要在世界中提供两个物体和一个简单的锚定点。初始化方法假设物体已经在正确的位置。

在这个例子中,两个物体通过旋转连接器以第一个物体的质心作为铰链点(hinge Point)连接在一起。

1
2
b2RevoluteJointDef jointDef;
jointDef.Initialize(myBodyA, myBodyB, myBodyA->GetWorldCenter());

当bodyB逆时针旋转的时候,转动连接器的角度为正值。就像Box2D中的所有其它角度一样,旋转角度以弧度为基准。一般来说,旋转连接器使用Initialize()方法创建完成之后,旋转连接器的角度为零,和两个物体当前的角度无关。

在一些场合下你可能希望控制连接角(joint angle)。为此,旋转连接器可选的模拟连接限制和(或)马达。

连接限制作用力可将连接角限制在一定范围内。为此将会尽可能的应用扭矩来达到效果。限制最小值应该包括零,否则在开始模拟的时候连接将会比较困难。

连接器马达允许你指定角速度(角的时间导数)。速度可以是正值或负值。虽然马达可以产生无限大的力,但是通常这么做是不可取的。想想那个永恒的问题:

当一个无穷大的力碰到一个不可移动的物体上,会发生什么呢?

我告诉你这并不好笑。因此你应该为连接器马达提供一个最大值的力。连接器马达将会维持特定的速度除非扭矩超过了指定的最大值。当最大的扭矩值被超过时,连接器将会慢下来或者反向运动。

你可以使用连接器马达来模拟连接摩擦力。只要设置连接速度为零,然后设置最大的扭矩为某个很小的值,只要能显示出效果就可以。这样马达将会尝试阻止连接旋转,除非产生明显的过载。

这里对上面旋转连接器做一个修订;此时,连接器既有限制也有马达。马达用来模拟连接摩擦。

1
2
3
4
5
6
7
8
b2RevoluteJointDef jointDef;
jointDef.Initialize(bodyA, bodyB, 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;

你可以获取一个旋转连接器的角度,速度以及马达扭矩。

1
2
3
float32 GetJointAngle() const;
float32 GetJointSpeed() const;
float32 GetMotorTorque() const;

你可以在每一步对马达控制器参数进行更新。

1
2
void SetMotorSpeed(float32 speed);
void SetMaxMotorTorque(float32 torque);

连接器马达有一些有趣的功能。你可以在每个时间步长进行设置更新,然后连接就会像正弦波那样前后摆动或者根据你自定义的方法做一些改变。

1
2
3
4
5
...Game Loop Begin...
float32 angleError = myJoint->GetJointAngle() - angleTarget;
float32 gain = 0.1f;
myJoint->SetMotorSpeed(-gain*angleError);
...Game Loop End...

通常你的增益参数不能太大。否则连接或许会变的不稳定。

8.7 平移连接(Prismatic Joint)

平移连接允许相关联的两个物体沿着特定的坐标轴进行平移。平移连接阻止相对旋转。因此平移连接只有一个自由度。

pic

平移连接定义的描述类似于旋转连接;只是把角度替换成平移以及把扭矩替换成力矩。以此类推让我们看一个平移连接器中的连接限制和摩擦马达如何定义的:

1
2
3
4
5
6
7
8
9
b2PrismaticJointDef jointDef;
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.Initialize(myBodyA, myBodyB, myBodyA->GetWorldCenter(), worldAxis);
jointDef.lowerTranslation = -5.0f;
jointDef.upperTranslation = 2.5f;
jointDef.enableLimit = true;
jointDef.maxMotorForce = 1.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;

旋转连接器有一个从屏幕射出的隐藏的轴。平移连接器需要一个明确的和屏幕平行的轴。并且这个轴固定在两个物体上并且跟随它们一起移动。就像旋转连接一样,平移连接当使用Initilialize()方法创建时初始平移为零。所以确保零在你的最低和最高平移范围内。

使用平移连接器就像使用旋转连接器一样。这里给出一些相关方法:

1
2
3
4
5
float32 GetJointTranslation() const;
float32 GetJointSpeed() const;
float32 GetMotorForce() const;
void SetMotorSpeed(float32 speed);
void SetMotorForce(float32 force);

8.8 滑轮连接器(Pulley Joint)

滑轮连接器用来创建一个理想的滑轮。滑轮连接器将两个物体之间进行连接并同时连接到地面上。当一个物体上升,另一个物体就下降。滑轮连接器绳子的长度取决于初始化的配置。

    length1 + length2 == constant

你可以提供一个比值(ratio)来模拟木块和滑车(block and tackle)。这会出现滑轮一侧绳子比另一侧拉长的速度快。于此同时,这一侧的力也会比另一侧的力要较小。你可以用这种方法来创建机械杠杆(mechanical leverage)。

    length1 + ratio*length2 == constant

比如说,如果比值(ratio)是2,那么length1的变化将是length2的两倍。同样附加在body1上的力将是附加在body2上力的一半儿。

pic

当滑轮一侧的绳子被完全展开时可能会出现麻烦。另一侧的绳子长度将会是零。此时恒等式将会变的奇特(糟糕)。你应该设置形状碰撞来阻止这种情况的发生。

这里给出一个滑轮连接器的定义:

1
2
3
4
5
6
7
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, goundAnchor2, anchor1, anchor2, ratio);

滑轮连接器提供了获取当前绳子长度的方法。

1
2
float32 GetLengthA() const;
float32 GetLengthB() const;

8.9 齿轮连接器(Gear Joint)

如果你想创建一个精密的机械东东你也许会想起使用齿轮。在Box2D中,原则上来说你可以使用几何重叠来模拟带牙齿的齿轮。这并不高效而且还很乏味。你还要认真的考虑排列齿轮的牙齿以保证其平滑。Box2D有一个简单的方法来创建齿轮:齿轮连接器(gear joint)。

pic

齿轮连接器只能连接旋转和(或者)平移连接器。

就像滑轮比值(pulley ratio)一样,你可以指定一个齿轮比值(gear ratio),此时齿轮比值可以设置成负值。仍然要注意的是当其中一个连接器是旋转连接器(角度)和另一个是平移连接器(平移)的时候,这时齿轮比将会有单位长度或者单位长度的倒数(one over length)。

    coordinate1 + ratio * coordinate2 == constant

这里有一个齿轮连接器的例子。物体myBodyA和物体myBodyB是任意两个连接器,只要它们不是附加的同一个物体就行。

1
2
3
4
5
6
b2GearJointDef jointDef;
jointDef.bodyA = myBodyA;
jointDef.bodyB = myBodyB;
jointDef.joint1 = myRevoluteJoint;
jointDef.joint2 = myPrismaticJoint;
jointDef.ratio = 2.0f * b2_pi / myLength;

注意齿轮连接器依赖其它两个连接器。这里是薄弱的地方。如果删除其它连接器将会发生什么?

警告
在删除旋转/平移连接器之前总是先删除齿轮连接器。否则你的代码将会由于齿轮指针变为无效指针以糟糕的方式崩溃。所以在你删除任何其它连接器之前应该先删除齿轮连接器。

8.10 鼠标连接器(Mouse Joint)

在testbed例子中,使用鼠标连接器操纵物体。它尝试在物体上驱动一个点,拖向当前鼠标位置。在旋转方面没有限制。

鼠标连接器定义有目标点(target point),最大力矩(maximum force),频率(frequency)以及阻尼率(damping ratio)。目标点最初与物体的锚定点重合。最大力矩防止多个动态物体作用时的激烈反应。你可以把它设置的足够大。频率和阻尼率是用来对距离连接器(distance joint)创建弹簧/减震器类似效果的。

许多用户为了游戏的可玩性想尝试修改鼠标连接器。用户通常希望能够实现精确定位和及时相应。在这方面鼠标连接器工作的并不是很好。你可以考虑一下动力学物体(kinematic bodies)进行代替。

8.11 滚轮连接器(Wheel Joint)

滚轮连接器限制了bodyB上的一个点系一个绳子,绳子另一边系在bodyA上。滚轮连接器也提供了一个悬挂弹簧(suspension spring)。详细查看b2WheelJoint.h和Car.h文件。

pic

8.12 焊接连接器(Weld Joint)

焊接连接器尝试限制两个物体之间所有相对运动。可以参看testbed例子当中Cantilever.h文件关于焊接连接器的具体行为。

使用焊接连接器定义一个易碎的物体结构很有吸引力。即便如此,Box2D的求解器是进行迭代的,所以焊接连接器不是很稳定。所以使用焊接连接器连接起来的物体链看起来将会很柔软。

所以用附有多个定制器的单个物体来创建易碎物体更合适。当物体摔碎时,你可以销毁一个定制器,在一个新物体上重新创建它。详情参见testbed中的Breakable例子。

8.13 绳索连接器(Rope Joint)

绳索连接器限制了两个点之间最大距离。即便是高负载,绳索连接器也防止了物体链的过度拉伸。详情参见b2RopeJoint.h和RopeJoint.h文件。

8.14 摩擦连接器(Friction Joint)

摩擦连接器用来进行自上而下摩擦(top-down friction)。摩擦连接器提供了2D的平移摩擦和角摩擦。详情参见b2FrictionJoint.h和ApplyForce.h文件。

Comments