Oh!Coder

Coding Life

Box2D C++ 教程-定制器

| Comments

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

  • 定制器(Fixtures)

定制器用来描述场景中对象的大小,形状,材质属性等。一个物体可以附加多个定制器,物体的质心会因为定制器的附加顺序所影响。当两个物体相撞时,会根据各自的定制器作出相应的反应。定制器的主要属性如下:

-形状 - 多边形或圆弧
-恢复 - 定制器的弹力
-摩擦 - 光滑程度
-密度 - 物体大小的重量

我们会谈到上面的每一个概念,并针对它们做一些实验(isSensor属性可以让定制器变成’传感器‘,之后的教程里我们会谈到它)。让我们就以上一次话题’Bodies‘作为起点,创建一个简单的动态物体作为开始吧。那么FooTest类的构造函数看起来会是下面这个样子:

1
2
3
4
5
6
7
FooTest() {
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_dynamicBody; //this will be a dynamic body
    myBodyDef.position.Set(-10, 20); //a little to the left

    b2Body* dynamicBody1 = m_world->CreateBody(&myBodyDef);
}
  • 形状(Shapes)

每个定制器都有形状属性,当定制器在场景中移动的时候,它们之间的碰撞监测会依赖形状属性。形状属性可以设置成弧形或是多边形。这里让我们设置一个弧形…

1
2
3
b2CircleShape circleShape;
circleShape.m_p.Set(0, 0); //position, relative to body position
circleShape.m_radius = 1;  //radius

…使用形状的定制器:

1
2
3
b2FixtureDef myFixtureDef;
myFixtureDef.shape = &circleShape; //this is a pointer to the shape above
dynamicBody1->CreateFixture(&myFixtureDef); //add a fixture to the body

现在运行程序,像之前的场景一样,会在四方框内会看到一个圆弧:

pic

设置圆弧的坐标时,因为圆弧的位置与物体(body)的坐标位置有了关联(译者注:因为形状所附加的定制器同时被附加在物体上)。虽然我们设置圆弧本身的初始坐标为(0, 0),但是因为圆弧形状所附加的定制器同时被附加到位置在(0, 20)的物体上,所以圆弧也将被设置在坐标为(0, 20)的位置。

现在让我们尝试一下多边形。使用多边形,你可以独立的设置多边形的每一个顶点坐标来自定义多边形的形状,如果你想要盒子或者直线形状,有一对快捷的创建方法。这里我们自定义了一个多边形的顶点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
b2Vec2 vertices[5];
vertices[0].Set(-1,  2);
vertices[1].Set(-1,  0);
vertices[2].Set( 0, -3);
vertices[3].Set( 1,  0);
vertices[4].Set( 1,  1);

b2PolygonShape polygonShape;
polygonShape.Set(vertices, 5); //pass array to the shape

myFixtureDef.shape = &polygonShape; //change the shape of the fixture
myBodyDef.position.Set(0, 20); //in the middle
b2Body* dynamicBody2 = m_world->CreateBody(&myBodyDef);
dynamicBody2->CreateFixture(&myFixtureDef); //add a fixture to the body

pic

如果使用这种方式创建多边形定制器,这里有几个需要注意的事情。首先,默认情况下每个多边形的顶点为8个,如果你想要设置更多的顶点,需要修改b2Settings.h头文件中的b2_maxPolygonVertices值,顶点必须以逆时针顺序排列,并且组成的是凸多边形。凸多边形的特点是,如果沿着任意一条边做直线,多边形均在直线的同侧(要么在左侧,要么在右侧)。

如果你想要一个四边形定制器,最简单的方法就是像上一个话题提到的那样,使用SetAsBox方法:

1
2
3
4
5
polygonShape.SetAsBox(2, 1); //a 4x2 rectangle
myBodyDef.position.Set(10,20); //a bit to the right

b2Body* dynamicBody3 = m_world->CreateBody(&myBodyDef);
dynamicBody3->CreateFixture(&myFixtureDef); //add a fixture to the body

pic

注意SetAsBox中的参数指的是盒子的“半宽”和“半高”,而且它的中心点会被附加到物体上当做物体的中心点。另外还有一点需要注意,由于定制器对形状使用的是指针引用,所以对形状所做的任何改变,我们都不需要对定制器做额外的工作,所以此处我们不用做任何其它代码的改动。

有时候相对于实心的多边形而言,你有可能只想要一条0厚度的直线。这可以通过另外一个便利SetAsEdge的方法来实现,此方法设置了一条直线的两点。让我们创建一个静态的线形定制器,放到场景的底部,让动态的物体落到它的上面。

1
2
3
4
5
6
myBodyDef.type = b2_staticBody; //change body type
myBodyDef.position.Set(0,0); //middle, bottom

polygonShape.SetAsEdge( b2Vec2(-15,0), b2Vec2(15,0) ); //ends of the line
b2Body* staticBody = m_world->CreateBody(&myBodyDef);
staticBody->CreateFixture(&myFixtureDef); //add a fixture to the body

pic

注意:SetAsEdge创建多边形的方法已经在Box2D的v2.1.2版本之后被移除了,因为这种单一的线形状已经给出了形状类型,b2EdgeShape。可以使用b2EdgeShape方法画出等同的图形:

1
2
3
4
b2EdgeShape edgeShape;
edgeShape.Set(b2Vec2(-15,0), b2Vec2(15,0));

myFixtureDef.shape = &edgeShape;
  • 密度(Density)

到目前为止,你可能在纳闷,为什么我用鼠标光标拖拽物体的时候,物体不会像期望中的那样旋转呢?那是因为我们还没有为定制器设置任何密度值。物体的面积乘以密度然后生成定制器的质量。返回到我们第一次声明定制器的地方,然后设置密度值如下:

1
2
3
b2FixtureDef myFixtureDef;
...
myFixtureDef.density = 1; //new code

既然所有物体都使用了相同的定制器的定义,所以都会受到同一个定制器的影响。

pic

在这个例子中,所有定制器都有相同的密度,那就意味着它们的重量是由它们可见的形状所决定的,其中圆弧是最轻的一个。试着通过改变定制器的密度,看看如何设定物体的‘重量’。记住这是由于所有的物体都使用了相同的定制器,另外你还要确保每次调用CreateFixture方法之前设置定制器的相关属性。

  • 多个定制器(Multiple fixtures)

我们可以把这个三个定制器附加到同一个物体上,而且看起来非常简单-回到每次调用CreateFixture方法的地方,把对dynamicBody1,dynamicBody2,dynamicBody3所做的附加操作全部更改到dynamicBody1上。

pic

正如你所看到的这会让所有定制器关联到dynamicBody1物体上。很直观的看到所有定制器附着在它们的物体上,但是并不是必须一定要像这样。定制器可以被添加到物体的任何位置,甚至它们之间可以产生空隙。例如像之前一样,可以让它们之间产生同样的间隔,我们可以返回之前创建它们的位置,然后手动调整它们创建时的位置。

1
2
3
4
5
6
7
8
9
10
//for the custom polygon, add 10 to each x-coord
vertices[0].Set(-1 +10,  2);
vertices[1].Set(-1 +10,  0);
vertices[2].Set( 0 +10, -3);
vertices[3].Set( 1 +10,  0);
vertices[4].Set( 1 +10,  1);
...
//for the box, use an extended version of the SetAsBox function which allows
//us to set a location and angle (location is offset from body position)
polygonShape.SetAsBox(2, 1, b2Vec2(20,0), 0); //moved 20 units right, same angle

运行的结果看起来像下面这个样子(这张插图很好的表现了调整后的定制器)。

pic

  • 摩擦(Friction)

现在我们知道了如何为同一个物体设置多个定制器(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
Test() {
    //set up a dynamic body
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_dynamicBody;
    myBodyDef.position.Set(0, 20); //middle
    b2Body* dynamicBody = m_world->CreateBody(&myBodyDef);

    //prepare a shape definition
    b2PolygonShape polygonShape;
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &polygonShape;
    myFixtureDef.density = 1;

    //add four square shaped fixtures around the body center
    for ( int i = 0; i < 4; i++) {
        b2Vec2 pos( sinf(i*90*DEGTORAD), cosf(i*90*DEGTORAD) ); //radial placement
        polygonShape.SetAsBox(1, 1, pos, 0 ); //a 2x2 rectangle
        dynamicBody->CreateFixture(&myFixtureDef); //add a fixture to the body
     }

     //make a static floor to drop things on
     myBodyDef.type = b2_staticBody;
     myBodyDef.position.Set(0, 0); //middle, bottom
     b2Body* staticBody = m_world->CreateBody(&myBodyDef);
     polygonShape.SetAsEdge( b2Vec2(-15,0), b2Vec2(15,3) ); //slightly sloped 
     staticBody->CreateFixture(&myFixtureDef); //add a fixture to the body
}

pic

在这个场景中,你可以看到物体沿着斜坡滑开始滑动一点儿然后滚落下来。当定制器和地面接触的时候,定制器中的摩擦系数会用来计算物体的滑动速度。摩擦系数的大小可以设置在0~1之间,0意味着完全没有摩擦。当两个物体发生碰撞接触的时候,摩擦碰撞的结果往往是趋向于摩擦系数低的方向运动。

试着给定制器定义零摩擦系数:

1
2
myFixtureDef.density = 1;
myFixtureDef.friction = 0; //new code

这次物体的旋转会小很多而且根本不能在斜坡上停留,在下滑的过程中速度没有任何损耗。现在试着把摩擦系数的值设置为1进行比较。最终,说明摩擦系数是定制器的独立属性和物体自身无关,那么为每一个定制器设置不同的摩擦系数如何呢?-在CreateFixture方法前添加一行代码:

1
myFixtureDef.friction = i/4.0;

通过对上面物体的演示,你需要设置每个定制器的摩擦系数。

注意:摩擦系数为1并不能保证每次都没有滑动出现。

  • 恢复(Restitution)

恢复衡量了定制器‘弹性’的大小。像摩擦一样,恢复系数的也控制在0~1之间,0意味着定制器根本没有弹性,1意味着无能量损失,全部反弹回来。当两个定制器之间发生碰撞时,碰撞反弹的方向取决于恢复系数较高的定制器。

可以像上面设置摩擦系数值一样对恢复系数做一些实验。试着为不同定制器设置不同的恢复系数。

1
2
myFixtureDef.friction = ...;
myFixtureDef.restitution = 0; //new code

注意:恢复系数为0并不总能保证没有反弹
注意:现实中少量的能量会在反弹过程中损耗

  • 运行中改变定制器属性

有时候在游戏中,你可能希望依赖游戏中的某个事件对定制器的属性进行修改。你可以通过定制器中的setter方法来改变摩擦系数,密度系数以及恢复系数。如果你已经有了那个需要改变的定制器的引用,那么你可以轻易的像这样进行改变:

1
2
3
fixture->SetDensity( ... );
fixture->SetRestitution( ... );
fixture->SetFriction( ... );

如果你只有物体的引用,你需要像下面代码实例那样遍历物体的所有定制器来找到你想要改变的那个定制器。

设置密度的时候有一个额外的操作,当你想要让修改产生效果,首先通过定制器的SetDensity()方法进行值的设置,然后调用定制器所附加的物体上的ResetMassData()方法才能产生效果。

  • 遍历物体的定制器

如果你有一个物体,并且想查看附加在物体上的所有定制器,你可以像下面这样做。GetFixtureList()方法返回物体上定制器链表的第一个元素。

1
2
3
4
for (b2Fixture* f = body->GetFixtureList(); f; f = f->GetNext())
{
    //do something with the fixture 'f'
}

如果你知道物体上只有一个定制器,你只要获取定制器链表的第一个元素就可以了:

1
2
b2Fixture* f = body->GetFixtureList();
//do something with the fixture 'f'
  • 清除

如果你想要从一个物体身上移除定制器,那么就调用物体自身的DestroyFixture方法:

1
2
3
b2Fixture* myFixture = dynamicBody->CreateFixture(&myFixtureDef);
...
dynamicBody->DestroyFixture(myFixture);

记住做完此操作之后就不要再使用此定制器的指针!如果你销毁了物体,那么附加在物体上的所有定制器也会被销毁,同样也不要再使用定制器的指针。

通常情况下,如果你了解游戏的逻辑,并且了解物体是如何被销毁的,就可以合理安排程序尽量避免使用定制器的野指针。不过即便你面对的是一个复杂的项目,你也可以通过使用Box2D中提供的“destruction listener”特性来轻松搞定。这个特性会在定制器每次销毁的时候通知你,然后你就知道不要再使用这个定制器了。相关方法可以在用户手册的“Loose Ends”一章中查看“Implicit destruction”的使用。

Comments