Oh!Coder

Coding Life

Box2D C++ 教程-力和冲量

| Comments

声明:本教程翻译自:Box2D C++ tutorials-Forces and impulses,仅供学习参考。

  • 力和冲量(Forces and impulses)

让物体移动,需要对其施加力或者冲量。可以通过时间的不断积累对物体施加力的作用来改变物体的运动,然而冲量作用于物体则会立刻改变物体的速度。举个例子,想象一下现在你的车坏了,你想推车,可以选择驱动另外一辆车缓慢开向这辆坏了的车,当两辆车的保险杠接触之后,继续对这辆车施加推力,然后通过时间不断的积累慢慢的让其移动。或者也可以用冲量,驱动一辆完好的车全速驶向这辆坏了车。具体使用哪种方法取决于你。

也可以通过简单设置物体的位置来瞬间‘曲速(参见Warp drive)’物体。在游戏中你可能想有一个瞬移的特性,但是你要知道在真正的物理世界当中并不存在这样一种特性!Box2D的全部侧重点是让所模拟的物体看起来更真实,基于这一点考虑我会建议你尽量使用力和冲量来移动物体。大多数时候这么做看起来貌似过于严谨,但是在真实的世界里所有事情的发生都是通过力和冲量的,除非你就是想创建一个不是真实世界里的特性(瞬移,等等),要三思!而且如果你这么做下去的话,可能会给你带来更多的麻烦。

角运动也可以通过力和冲量来控制,效果就像上面提到的缓慢/瞬间的线性版本一样。角力矩也称为扭矩(torque)。可以想象成转动强度,就像你拧瓶盖儿一样-瓶盖不会跑出去,而且你可以持续对瓶盖施加作用力(扭矩,torque)。

本次话题,我们将创建三个物体,使用上面提到的两种力来分别作用于它们平移或扭曲(twist)它们。让我们设置一个和定制器(fixtures)话题中类似的场景,不同的是所有物体的形状都一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//class member variable to keep track of three bodies
b2Body* bodies[3];

FooTest() {
    //body definition
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_dynamicBody;

    //shape definition
    b2PolygonShape polygonShape;
    polygonShape.SetAsBox(1, 1); //a 2x2 rectangle

    //fixture definition
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &polygonShape;
    myFixtureDef.density = 1;

    //create identical bodies in different positions
    for (int i = 0; i < 3; i++) {
        myBodyDef.position.Set(-10+i*10, 20);
        bodies[i] = m_world->CreateBody(&myBodyDef);
        bodies[i]->CreateFixture(&myFixtureDef);
    }

    //a static floor to drop things on
    myBodyDef.type = b2_staticBody;
    myBodyDef.position.Set(0, 0);
    polygonShape.SetAsEdge( b2Vec2(-15,0), b2Vec2(15,0) );
    m_world->CreateBody(&myBodyDef)->CreateFixture(&myFixtureDef);
}

pic

  • 线性运动(Linear movement)

我们需要一个不使用鼠标就能影响物体运动的方法。现在是考验testbed框架中键盘输入特性的时候了。覆盖Test类中Keyboard方法,为每个物体定义不同的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Keyboard(unsigned char key)
{
    switch (key)
        {
            case 'q':
                //apply gradual force upwards
                bodies[0]->ApplyForce( b2Vec2(0,50), bodies[0]->GetWorldCenter() );
                break;
            case 'w':
                //apply immediate force upwards
                bodies[1]->ApplyLinearImpulse( b2Vec2(0,50), bodies[1]->GetWorldCenter() );
                break;
            case 'e':
                //teleport or 'warp' to new location
                bodies[2]->SetTransform( b2Vec2(10,20), 0 );
                break;
            default:
                //run default behaviour
                Test::Keyboard(key);

        }
}

其中SetTransform方法已经在物体(bodies)话题中讨论过了。ApplyForce和ApplyLinearImpulse方法传入了两个参数,第一个参数应该是所施加的力的方向,本例中是线性增加。第二个参数是作用于物体的具体位置-之后会再次提到。

运行测试然后按下q/w/e按键。受到冲量作用的物体就像被什么东西突然撞到一样。这个运动的物体被瞬间移动到一个新位置,但是注意它仍然保持了原来的线性和角速度。我们对物体施加了作用力之后到底发生了些什么呢?Keyboard方法只有当我们按下按键的时候才会被调用一次,不是每个时间步长都会调用。拿推车举例来说吧,就像在几分之一秒的时间内推一辆车,然后立马停止一样。既然作用力需要不停的进行施加,那么我们真正需要的是一个能够让作用力持续施加或停止的方法,并用此方法代替之前瞬间调用一次的方式。我们需要一个类的成员变量来记录保持当前作用力的状态,使用q键进行切换。对类做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//class member variable
bool forceOn;

//inside constructor
forceOn = false;

//modified case for q key in Keyboard() function
case 'q':
    forceOn = !forceOn;//toggle bool value
    break;

//inside Step() function
if (forceOn)
    bodies[0]->ApplyForce( b2Vec2(0,50), bodies[0]->GetWorldCenter() );

现在q键可以打开或关闭作用力的状态,并且能够看到对物体持续施加力的效果。

呃…我们使用了相同大小(50)的力和冲量,但是为什么力的效果看起来比冲量不明显呢?嗯,那是因为重力也是作用力。试着取消重力作用或许能让问题的答案更清晰一些。原因是因为力在每个时间步长(timestep)上让物体上移一点,随后在重力的作用下物体又下降一点。就这样不断的上下上下的纠结。那么对于冲量来说,在重力对物体进行干扰之前完成了所有的工作。失重状态下,试着让力作用大概1秒钟时间,紧接着停止施加作用力。你会注意到,一秒钟力的作用效果和冲量的作用效果有相同的速度。

现在让我们来看看之前在施加作用力/冲量方法中所忽略的第二参数是什么意思?到目前为止我们一直使用物体自身可以获取质心的GetWorldCenter()方法来设置此参数。正如我们所看到的,当作用力作用于质心的时候并不会引起物体的旋转。想象一下把一张CD放到一张带有摩擦不平的桌子上,例如air-hockey talbe(译者注:美国的一种桌球)。如果你把手指放到CD中间的洞中,然后沿着桌面弹开的话,CD移动的同时并不会发生旋转。但是如果你在CD的其它部位做这种操作,CD会边移动边发生旋转,并且如果你的手指离中心点越远,那么CD在移动的同时旋转的也越厉害。

让我们稍微偏移一下之前施加力/冲量的作用点。改变一下ApplyForce和ApplyLinearImpulse的作用点,使用GetWorldCenter()方法改变物体的质心,这次我们更改为物体的右上角作为力的作用点:

1
2
3
4
5
//before
GetWorldCenter()

//after
GetWorldPoint( b2Vec2(1,1) )

pic

这次,你应该会看到当盒子们运动的时候,会发生旋转。GetWorldPoint()方法用来将物体自身的坐标(物体坐标系(body coordinates))转换到世界坐标系中,所以即便盒子发生旋转之后我们依然可以把力作用于盒子的右上角。注意当上图出现在应用场景中的时候,可能会被认为是“拽着盒子角把盒子拎起来”,但是作用力并不是作用在可见的矩形定制器上-力和冲量是作用于物体上,不是它的定制器上。力可以轻松的作用于任何之前的点上,即便是没有定制器(fixtures)的空点上。

  • 角运动(Angular movement)

角运动被角力矩(扭矩,torque)和角动量所控制。它们的行为和线性运动相似,力是缓慢作用的而角动量是瞬间作用的。让我们在Keyboard()方法中添加一对它们的方法吧(虽然有第三种让物体立刻转动的方法,但我们打算还是像上面一样使用SetTransform()方法,所以这里我们跳过使用这种方法。)

1
2
3
4
5
6
7
8
case 'a':
    //apply gradual torque counter clockwise
    bodies[0]->ApplyTorque( 20 );
    break;
case 's':
    //apply immediate spin counter clockwise
    bodies[1]->ApplyAngularImpulse( 20 );
    break;

现在,让我们按下a/s按键看看发生了什么。你将会再次看到扭矩的作用效果,下面需要添加一个类的全局成员变量可持续的在每一个时间步长对扭矩的效果进行开/关效果的切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//class member variable
bool torqueOn;

//inside constructor
torqueOn = false;

//modified case for a key in Keyboard() function
case 'a':
    torqueOn = !torqueOn;//toggle bool value
    break;

//inside Step() function
if (torqueOn)
    bodies[0]->ApplyTorque( 20 );

打开重力效果,你会发现即便我们只是试图‘扭曲(twist)’这些盒子,但是它们还是会有一点点移动,这是因为它和地面之间有摩擦并且运动起来就像方轮子一样。为了能够更好的说明扭矩和角动量到底都做了些什么,关闭重力效果。在失重环境中,我们可以看到对于转动力矩/角动量方法为什么只需要一个参数-既然不会引起线性运动,那么唯一我们需要指定的就是转动的方向。由于物体的转动总是绕着物体质心展开,所以不会像上面线性力矩那样产生任何偏差。

和线性力矩一样,传入相同的参数,ApplyTorque执行一秒钟以后会和ApplyAngularImpulse立即执行所产生的角速度相同。

Comments