Oh!Coder

Coding Life

第二章 Hello Box2D

| Comments

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

本章利用Box2D创建了一个Hello World项目。程序中在地面上创建了一个静态的地面大盒子和一个动态的小盒子。代码中不包括任何图形。随着时间的推进,所有你能看到的是从console中进行文本输出的盒子的位置。

这是一个学习Box2D如何起步并让其运行的好例子。

2.1 创建一个世界

每一个Box2D程序都是从创建一个b2World对象开始的。b2World就像一个管理内存,物体以及模拟的物理枢纽。你可以在堆,栈或者数据段上创建物理世界。

创建一个Box2D的物理世界很简单。首先,我们定义一个重力向量,然后我们需要告诉世界,其中闲置的物体可以进入睡眠状态。一个处于睡眠状态的物体无需做任何模拟计算(译者注:这样可以提高运行效率)

1
2
b2Vec2 gravity(0.0f, -10.0f);
bool doSleep = true;

接下来我们创建一个世界对象。注意我们把世界对象创建到了栈里,所以我们需要在世界对象的作用域范围内进行操作。

1
b2World world(gravity, doSleep);

现在我们有了自己的物理世界,让我们往里添加一个东西吧。

2.2 创建地面盒子

创建物体需要按照下面步骤:

  1. 定义物体的位置,阻尼(damping),等。
  2. 用世界来创建物体。
  3. 用形状,摩擦,密度等来定义定制器(fixtures)
  4. 为物体创建定制器(fixtures)

根据第一步我们创建地面物体。首先我们需要定义物体,通过物体定义我们可以初始化地面物体的位置。

1
2
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);

根据第二步我们把物体定义传给世界对象,以此来创建地面物体。世界对象不会保留对物体定义的引用。物体默认是静态的。静态物体相当于不动产,所以静态物体之间不会产生碰撞。

1
b2Body* groundBody = world.CreateBody(&groundBodyDef);

第三步我们创建地面多边形。我们使用SetAsBox方法方便的把地面多边形快速的变成盒子形状,并把盒子的中心点作为多边形的源点。

1
2
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);

SetAsBox方法的参数设置了盒子的半宽和半高(方法本身会对参数进行扩展)。所以上面那行代码其实设置地面盒子为100单位宽(x轴)和20单位高(y轴)。在Box2D中单位需要相应的转换成米,千克和秒。所以这里你可以认为是以米为单位进行扩展的。当你以真实世界里的大小创建物体的时候,Box2D能够更好的工作。比方说,一个大约1米高的水桶。由于浮点运算的局限性,此时如果使用Box2D来模拟冰川或者尘埃的特效并不是一个好的想法。

我们按照步骤4通过创建形状定制器(fixture)完成了地面盒子的创建。通过这一步我们有了快捷的创建方式。我们不需要改变定制器(fixture)中的默认材质属性,所以我们可以在不用创建定制器的情况下,可以直接给物体传递形状参数。稍后我们看到的是,如何使用定制器(fixture)来自定义材质属性。第二个参数是形状以千克为单位,每平方米的密度。一个静态物体需要定义质量为0,所以在这个例子中不需要定义密度。

1
groundBody->CreateFixture(&groundBox, 0.0f);

Box2D不会保留对形状的引用。它会把数据克隆到一个新的b2Shape对象里。

需要注意的是每一个定制器(fixture)都必须附加到一个物体上,即便是静态定制器也需如此。不管怎样,你可以选择附加所有的静态定制器到单个静态物体上。

2.3 创建动态物体

那么现在我们有了一个地面物体。我们可以用同样的方式创建一个动态物体。主要的不同点,除了尺寸不一样以外,我们必须为动态物体建立质量属性。

首先我们使用CreateBody方法创建一个物体。默认的物体是静态的,所以我们需要在创建时设置b2BodyType类型为动态物体。

1
2
3
4
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);

警告
如果你想让物体在受力的时候有所反应,那么必须设置物体的类型为b2_dynamicBody

下面我们创建一个多边形形状对象,并将其附加到定制器的创建当中。首先,我们创建一个盒子形状:

1
2
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f);

接下来我们使用盒子来创建一个定制器(fixture)。注意我们设置密度为1.默认的密度值为0.同时设置形状的摩擦系数为0.3。

1
2
3
4
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;

使用定义好的定制器(fixture)我们可以为动态盒子设置定制器(fixture),可以自动更新动态盒子的质量。你可以为一个物体添加更多的定制器。每一个定制器都会贡献物体的总质量。

1
body->CreateFixture(&fixtureDef);

上面做所的工作都是在初始化。现在我们要开始模拟模拟啦!

2.4 模拟(Box2D)世界

那么我们初始化完成了地面盒子和一个动态盒子。现在我们准备交给牛顿,让他去做他应该做的事情。我们仅仅需要几个问题。

Box2D使用一个称为积分器(integrator)的算法。在离散的时间点上积分器可以模拟物理方程。例如我们在屏幕上进行翻书,那么Box2D会随着传统游戏的循环一起执行。所以我们需要为Box2D挑一个时间步长。通常游戏中的物理引擎时间步长至少是60H或者1/60秒。你可以设置更长的时间步长,但是你不得不更加小心的建设你的物理世界。我们也不喜欢时间步长改变的太大。一个变化的时间步长会产生很难调试的变化的结果。所以不要把时间步长绑定到你的游戏框架的帧率上(除非你真的,真的需要这么做)。

事不宜迟,这就是所说的时间步长。

1
float32 timeStep = 1.0f/60.0f;

除了积分器以外,Box2D还使用了大量的位代码来调用约束求解器(constraint solver)。约束求解器每次解决一个约束,解决了模拟过程中所有的约束问题。单个约束可以被完美的解决。尽管如此,每当我们解决了一个约束,我们又轻易的扰乱了其他约束。为了解决所有问题,我们需要多次遍历所有约束。

在约束求解器(constraint solver)中有两个阶段:速度阶段和位置阶段。在速度阶段,求解器为使物体能够正确的移动需要计算必要的冲量。在位置阶段,求解器调整物体的位置来降低物体之间的重叠和连接的分离程度(译者注:比如两个单独的物体碰撞完之后会有瞬间重叠的情况,还有多节点物体在碰撞完之后会有瞬间的分离的情况,所以会有重叠和分离两种情况)。每一个阶段都有其自身需要的迭代次数。另外,在位置阶段如果有误差足够小,可能会提早退出迭代。

Box2D建议的迭代次数是速度阶段8次,位置阶段3次。你可以根据你的喜好改变这个数字,但是要记住,这需要在效率和精度上做一个权衡。用更少的迭代次数会增加提升效率,但是会影响精度。同样,用更多的迭代次数会降低效率但会提升模拟质量,会产生更高的精度。对于这个简单的例子而言,我们不需要太多的迭代次数,下面是我们选择的迭代次数。

1
2
int32 velocityIterations = 6;
int32 positionIterations = 2;

注意时间步长和迭代次数完全不相关。一次迭代不是一个分步长。一次求解迭代是在一个时间步长单次遍历所有约束。你也可以在单个时间步长内多次遍历约束。

我们现在准备开始模拟循环。在你的游戏里模拟循环可以被合并到游戏循环当中。每当遍历你的游戏循环,你可以调用b2World::Step方法。每次只调用一次就足够了,当然这取决于你的游戏帧率和你的物理时间步长。

这里对Hello World项目进行了简单的设计,所以没有图形输出。代码打印出动态盒子的位置信息和旋转信息。这里模拟了一秒钟内60个时间步长的循环。

1
2
3
4
5
6
7
for(int32 i = 0; i < 60; ++i)
{
    world.Step(timeStep, velocityIterations, positionIterations);
    b2Vec2  position = body->GetPosition();
    float32  angle = body->GetAngle();
    printf(%4.2f %4.2f %4.2f\n, position.x, position.y,  angle);
}

输入显示了动态盒子掉落到地面盒子的情况。你的输出看起来像是这个样子:

0.00 4.00 0.00
0.00 3.99 0.00
0.00 3.98 0.00

0.00 1.25 0.00
0.00 1.13 0.00
0.00 1.01 0.00

2.5 清理

当离开世界对象的作用域或者通过调用delete删除指针指向的世界对象时,所有在内存中保留的物体,定制器(fixtures),连接器都会被释放。这么做既提高了性能又使你的生活轻松愉快。尽管如此,你仍然需要将物体,定制器,连接器的指针清零,因为它们将会变的不可用。

2.6 Testbed

一旦你征服了HelloWorld这个例子,你就应该开始看Box2D自带的testbed。testbed是一个单元测试框架和demo演示环境。例子中包含了一些特性:

  • 可平移和缩放的摄像头
  • 鼠标选中依附在动态物体上的形状
  • 可扩展的测试集合
  • GUI选择测试,参数调整,调试绘图
  • 暂停单步模拟
  • 文本渲染

pic

在用testbed进行用例测试的时候,有很多关于Box2D自身框架用法的例子。我鼓励你通过探索和思考testbed来学习Box2D。

注意:testbed使用freeglut和GLUI编写。testbed不是Box2D程序库的一部分。Box2D程序库并不知道如何进行渲染。就像HelloWorld例子所展示的那样,使用Box2D并不一定需要渲染。

Comments