Oh!Coder

Coding Life

Box2D C++ 教程-连接器-旋转

| Comments

声明:本文翻译自Box2D C++ tutorial-Joints-revolute,仅供学习参考。

旋转连接器(Revolute joints)

旋转连接器可以想象成枢纽,锚点,或者转轴。每个物体上都定义一个锚点,物体移动的时候锚点总是重合的,之间的相对转动也是没有限制的(译者注:详细可见Box2D手册第八章旋转连接器部分,此处有一张图,一目了然)。

旋转连接器可以设置限制,让物体的旋转限定在一个固定范围内。也可以设定马达(motor),通过指定扭矩,物体会试着按照给定的速度进行旋转。对于旋转连接器场常见的包括如下:

  • 滚轮或滚筒
  • 链条或悬桥(使用多个旋转连接器)
  • 破布娃娃的关节
  • 转门,弹射器,杠杆

创建旋转连接器

创建旋转连接器首先设置b2RevoluteJointDef属性,然后传递给CreateJoint方法从而获得一个b2RevoluteJoint对象实例。我们在连接器-概述中看到所有连接器的定义都有一些共同的属性-连接两个物体,它们之间是否会发生碰撞。那么先让我们对这些进行设置:

1
2
3
4
b2RevoluteJointDef revoluteJointDef;
revoluteJointDef.bodyA = bodyA;
revoluteJointDef.bodyB = bodyB;
revoluteJointDef.collideConnected = false;

然后我们看一堆关于旋转连接器的其它属性。

  • localAnchorA - 在物体A上的点,并围绕此点旋转
  • localAnchorB - 在物体B上的点,并围绕此点旋转
  • referenceAngle - 两个物体之间角度为零时看作连接器角度
  • enableLimit - 连接器的限制是否开启
  • lowerAngle - 角度的最低限制
  • upperAngle - 角度的最高限制
  • enableMotor - 连接器马达是否开启
  • motorSpeed - 连接器马达的目标速度
  • maxMotorTorque - 马达允许使用的最大扭矩值

下面让我们详细看下这些属性到底是做什么用的。

本地锚点(Local anchors)

本地锚点(Local anchors)是物体自身的一个点,此点的坐标以物体自身为基础,物体的旋转也以此点作为中心旋转点。比如说,如果bodyA有一个2x2的四方形定制器,bodyB有一个环形定制器,并且你想让环形围绕四方形的一个角进行旋转…

pic

…你可能分别需要知道锚点(1,1)和(0,0)。那么事例代码应该是这样:

1
2
revoluteJointDef.localAnchorA.Set(1,1);
revoluteJointDef.localAnchorB.Set(0,0);

注意一下,这里并不一定非要在连接器的锚点处有一个定制器。在上面的例子中,bodyA的锚点可以轻松的设置成(2,2)或者任何其它坐标点,根本不会有任何问题,即便是环形和四方形之外也没关系。连接器连接的是物体,而不是定制器。

创建完连接器之后可以通过GetAnchorA()和GetAnchorB()方法访问锚点。这里要注意的是返回的坐标值是机遇物体自身坐标系的,设置方式和你定义连接器一样。

参照角(Reference angle)

在连接器创建的时候,参照角可以指定’连接器角度(joint angle)’,以此设定物体之间的角度。如果你想之后在程序中用GetJointAngle()方法查看连接器的旋转程度,或者你想使用连接器限制(joint limits),参照角还是挺有用的。

通常来说,你会对物体做一些设置,’连接器角度’的初始值认为是零。这里有一个使用GetJointAngle()方法的典型例子,例子中对referenceAngle做了非零设定。

pic

例子简单如下(默认的值为零,这也意味着什么都不做)。

1
revoluteJointDef.referenceAngle = 0;

注意一下,bodyB的旋转可以作为连接器的角度值,并且相对于bodyA是逆时针方向。如果两个物体同时被移动或旋转,连接器角度是不会改变的,因为它代表的是两个物体的相对角度。例如,下图中的例子通过使用GetJointAngle方法返回的数值和上面图中右边的例子的数值相同。

pic

还有,请记住Box2D中的角度值用的是弧度,这里我临时用的角度,因为这方便我叙述:P 。

旋转连接器限制(Revolute joint limits)

根据目前为止我们所讲解的属性来看,连接器中的两个物体可以无限制的围绕锚点旋转,但是旋转连接器也可以对旋转范围作出限制。可以对旋转的上限和下限作出设定,可以在’连接角度(joint angle)’方面作出设定。

假设你想对上面的例子在初始设定角度的时候范围限定在45度内。

pic

这幅图很好的做出了解释-bodyB逆时针旋转意味着连接器角度的增加,角度用弧度表示为(译者注:别忘了,其实这里用的不是弧度,只不过为的是容易说明而已):

1
2
3
revoluteJointDef.enableLimit = true;
revoluteJointDef.lowerAngle = -45 * DEGTORAD;
revoluteJointDef.upperAngle =  45 * DEGTORAD;

默认的enableLimit属性值为false。你也可以在创建连接器之后通过get或set方法设置限制属性,具体设置可以通过下面的方法:

1
2
3
4
5
6
7
8
//alter joint limits
void EnableLimit(bool enabled);
void SetLimits( float lower, float upper );

//query joint limits
bool IsLimitEnabled();
float GetLowerLimit();
float GetUpperLimit();

当使用连接器限制的时候有些东西需要牢记…

  • 对于enableLimits属性的设置会同时影响两个限制,所以如果你只想对其中一个做限制,需要把限制设置成一个非常高(例如,upper limit)或者非常低的值(例如,lower limit),以保证实际使用过程中永远达不到此值。
  • 旋转连接器限制的设置可以超过一个全旋转,比如说把一对lower/upper限制设置为-360,360,可以允许物体之间的相对旋转为两圈以内。
  • 可以很方便的把限制设定为相同值,可以把连接器’夹’在给定的角度上。这个角度可以逐步的改变直到最后达到合适的位置,并保持其与另一个物体的之前的状态,并且不需要连接器马达(joint motor)。
  • 在有限的时间步长内,快速的旋转会穿过设定的限制,直到旋转速度被修正。
  • 检查当前是否达到限制非常简单:
1
2
bool atLowerLimit = joint->GetJointAngle() <= joint->GetLowerLimit();
bool atUpperLimit = joint->GetJointAngle() >= joint->GetUpperLimit();

旋转连接器马达(Revolute joint motor)

旋转连接器默认的行为是没有阻力的。如果你想控制物体运动让其旋转,需要对其施加力矩或转动惯量。也可以设置能够让连接器以某个特定的线速度旋转物体的连接器’马达’。如果你想模拟具有动力效果的事物,比如汽车轮子或吊桥类型的门,这个特性还是很有用的。

线速度仅仅是一个目标速度,这也就意味着连接器并不能保证达到这个速度。通过为连接器马达指定最大可允许的扭矩,你能够控制连接器最终达到目标速度的快慢程度,甚至在有些例子中决定了最终是否能够达到目标速度。扭矩作用于连接器的行为可以参考力和冲量话题。典型的设置看起来像这样。

1
2
3
revoluteJointDef.enableMotor = true;
revoluteJointDef.maxMotorTorque = 20;
revoluteJointDef.motorSpeed = 360 * DEGTORAD; //1 turn per second counter-clockwise

enableMotor默认值为false。在连接器创建之后,你依然可以通过相应的get或set方法来对马达属性通过使用下面这些方法进行设置:

1
2
3
4
5
6
7
8
9
//alter joint motor
void EnableMotor(bool enabled);
void SetMotorSpeed(float speed);
void SetMaxMotorTorque(float torque);

//query joint motor
bool IsMotorEnabled();
float GetMotorSpeed();
float GetMotorTorque();

使用连接器马达的时候需要注意一些事情…

  • 为最大力矩设置一个小值,连接器会花一些时间到达期望的速度。如果你增加连接器所连接的物体,让其更重一点儿的话,如果你还想让物体保持同样的加速度,你需要增加最大扭矩值。
  • 可以设置马达速度为零,让连接器保持静止。为最大扭矩设置一个较低的值,起到刹车的作用,逐渐让连接器慢下来。使用高最大扭矩值可以让连接器迅速停下来,需要一个很大的力移动连接器,有点类似生锈的轮子。
  • 驱动汽车或者说是车辆的轮子可以通过直接改变马达的速度,当汽车停止的时候,通常会把目标速度设置为零。

旋转连接器示例

Okay,让我们在testbed框架下构造一些旋转连接器,并试着做一些设置,看看它们是如何工作的。首先,我们先尽可能的做一个简单的连接器。我们将会做一个类似于本页上第一副图展示的连接器,使用四方形和圆环定制器进行构建。既然之前我们已经有过很多相关例子,我就不再展示所有的代码清单了,但是基本上就像之前的有些话题一样,都是先创建一个’围栏’类型的场景,防止物体飞出场景。

进入场景我们首先需要创建一个盒子和圆环。然后很正常的将它们布置到合适的位置然后将它们连起来,但是为了演示的需要,我会对物体做一些改变以让我们能够清楚的看到不同情形下连接器的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//body and fixture defs - the common parts
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
b2FixtureDef fixtureDef;
fixtureDef.density = 1;

//two shapes
b2PolygonShape boxShape;
boxShape.SetAsBox(2,2);
b2CircleShape circleShape;
circleShape.m_radius = 2;

//make box a little to the left
bodyDef.position.Set(-3, 10);
fixtureDef.shape = &boxShape;
m_bodyA = m_world->CreateBody( &bodyDef );
m_bodyA->CreateFixture( &fixtureDef );

//and circle a little to the right
bodyDef.position.Set( 3, 10);
fixtureDef.shape = &circleShape;
m_bodyB = m_world->CreateBody( &bodyDef );
m_bodyB->CreateFixture( &fixtureDef );

这里我省略了一些细节,比如说声明类的变量来存储物体,但是现在你应该很聪明了,不会再为这些事情抓耳挠腮了,对吧?接下来我们使用上面讲述的一些属性来构建旋转连接器:

1
2
3
4
5
6
7
b2RevoluteJointDef revoluteJointDef;
revoluteJointDef.bodyA = m_bodyA;
revoluteJointDef.bodyB = m_bodyB;
revoluteJointDef.collideConnected = false;
revoluteJointDef.localAnchorA.Set(2,2);//the top right corner of the box
revoluteJointDef.localAnchorB.Set(0,0);//center of the circle
m_joint = (b2RevoluteJoint*)m_world->CreateJoint( &revoluteJointDef );

运行之后你应该看到盒子和圆环连接到了一起,并且圆环的中心在盒子的一角,自由的旋转。如果你在控制面板选中’draw joints’选项,你会看到物体坐标位置和锚点之间有有一条蓝线。你可以也会喜欢在Step()方法中添加几行代码,用来实时判断连接器角度:

1
2
3
4
m_debugDraw.DrawString(5, m_textLine, "Current joint angle: %f deg", m_joint->GetJointAngle() * RADTODEG);
m_textLine += 15;
m_debugDraw.DrawString(5, m_textLine, "Current joint speed: %f deg/s", m_joint->GetJointSpeed() * RADTODEG);
m_textLine += 15;

如果你停止模拟然后重启,点击’single step’,你可以看到短暂的瞬间画面,两个物体开始的时候在初始位置,之后随着不断的单步运行,可以看到连接器限制所引起的效果。

pic

记住,既然连接器不能像真实世界一样创建一个完美的约束,某些时候你或许会发现连接器物体会偏离自身的正确位置,在连接器概述中有相关提醒。

试着移动锚点位置观察在不同位置设置支点。例如,试试下面的方法:

1
2
3
4
5
//place the bodyB anchor at the edge of the circle
revoluteJointDef.localAnchorB.Set(-2,0);

//place the bodyA anchor outside the fixture
revoluteJointDef.localAnchorA.Set(4,4);

现在让我们做一些连接器限制。接着上面的例子,我们设置其移动角度的范围为-45~45度之间。

1
2
3
revoluteJointDef.enableLimit = true;
revoluteJointDef.lowerAngle = -45 * DEGTORAD;
revoluteJointDef.upperAngle =  45 * DEGTORAD;

你应该可以看到对旋转物体的限制。如果你把四方物体放到一个角落保持静止,然后拉圆环,会由于限制产生推的反作用,你会看到当你使用很大的力去作用圆环的时候,会有一点超越限制的现象,对于这一点上面也提到过。

下面,让我们添加一个连接器马达,还是像上面例子那样。有趣的是能够在运行的时候切换马达的方向,当然你也可以添加一个类变量来记录当前的马达方向,然后使用Keyboard()方法进行改变,正如前面的话题中的例子一样。

1
2
3
revoluteJointDef.enableMotor = true;
revoluteJointDef.maxMotorTorque = 5;
revoluteJointDef.motorSpeed = 90 * DEGTORAD;//90 degrees per second

本次设置的目的就是为了每秒旋转90度,这也意味着在一秒钟内你应该就可以获得从一个限制状态到另一个限制状态(译者注:意思就是说lower limit和upper limit在一秒内你都可以获取到)。对于其中物体的转动惯量来说扭矩有点低。如果你实现了用键盘改变马达方向的方法,按下向前和向后按键你会发现需要花费一点点时间加速。

pic

如果你把最大扭矩设定一个更高的值,60左右,你会看到连接器可以非常快的加速,而且绝对可以做到每秒90度角从一个极限到另一个极限。试着仅用限制,你会看到马达将会让连接器保持一个稳定的速度。再添加一个轮子你就可以得到一辆小汽车啦!

简单链子示例

既然使用旋转连接器构建链子非常常见,在这里我们就以此为示例做一个尝试。一条链子其实就是把大量的物体连接在一起,所以它们之间就很类似。首先让我们在这个世界中试着创建一个正在掉落的松散的链子。

最好使用循环创建一个链子,因为链子的每一个节点都是重复的,使用循环我们只要改一个参数就可以构造出一个更长的链子。在迭代循环中我们需要每次创建一个新的物体作为链子的节点,然后附加到前一个节点上。作为开始,我们先创建一个头节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//body and fixture defs are common to all chain links
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(5,10);
b2FixtureDef fixtureDef;
fixtureDef.density = 1;
b2PolygonShape polygonShape;
polygonShape.SetAsBox(1,0.25);
fixtureDef.shape = &polygonShape;

//create first link
b2Body* link = m_world->CreateBody( &bodyDef );
link->CreateFixture( &fixtureDef );

//use same definitions to create multiple bodies
for (int i = 0; i < 10; i++) {
    b2Body* newLink = m_world->CreateBody( &bodyDef );
    newLink->CreateFixture( &fixtureDef );

    //...joint creation will go here...

    link = newLink;//prepare for next iteration
}

这个例子很好的演示了如何定义一次物体或定制器,然后对此定义多次使用:)。注意所有的物体都创建在同一个位置,所以当测试程序开始运行的时候你会发现一个奇怪的情景,它们会把彼此挤压出去:

pic

现在让我们考虑一下,链子的锚点将会如何。假设我们希望把物体的一端作为节点连接起来,最好把物体的中心线而不是一个角儿作为连接点,在物体的一端稍微靠里一点,当链子弯曲的时候至少要避免有大的缝隙。

pic

代码看起来很简单,只有简单的几行:

1
2
3
4
5
6
7
8
9
//set up the common properties of the joint before entering the loop
b2RevoluteJointDef revoluteJointDef;
revoluteJointDef.localAnchorA.Set( 0.75,0);
revoluteJointDef.localAnchorB.Set(-0.75,0);

    //inside the loop, only need to change the bodies to be joined
    revoluteJointDef.bodyA = link;
    revoluteJointDef.bodyB = newLink;
    m_world->CreateJoint( &revoluteJointDef );

pic

模拟开始的时候,链子的节点依然是从上到下罗列在一起的,就像一个正常的游戏一样,你可能想把链子摆放到一个适当的位置作为开始,但是相邻节点的锚点的位置是相同的。注意collideConnected属性(默认是false)意味着链子的相邻节点不会发生碰撞,但是会和除此之外的节点有碰撞。

最后,让我们试着把链子的一端附加到一个地面物体上。使用圆环定制器创建一个动态物体,设置一个旋转连接器连接到一个静态物体的中心点。testbed框架里已经在(0,0)点有了一个静态物体,所以我们使用这个就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//body with circle fixture
b2CircleShape circleShape;
circleShape.m_radius = 2;
fixtureDef.shape = &circleShape;
b2Body* chainBase = m_world->CreateBody( &bodyDef );
chainBase->CreateFixture( &fixtureDef );

//a revolute joint to connect the circle to the ground
revoluteJointDef.bodyA = m_groundBody;//provided by testbed
revoluteJointDef.bodyB = chainBase;
revoluteJointDef.localAnchorA.Set(4,20);//world coords, because m_groundBody is at (0,0)
revoluteJointDef.localAnchorB.Set(0,0);//center of circle
m_world->CreateJoint( &revoluteJointDef );

//another revolute joint to connect the chain to the circle
revoluteJointDef.bodyA = link;//the last added link of the chain
revoluteJointDef.bodyB = chainBase;
revoluteJointDef.localAnchorA.Set(0.75,0);//the regular position for chain link joints, as above
revoluteJointDef.localAnchorB.Set(1.75,0);//a little in from the edge of the circle
m_world->CreateJoint( &revoluteJointDef );

pic

Comments