第二章 HelloBox2D
在Box2D中包含一个HelloWorld项目。程序创建了一个大的地面盒子(ground box)和一个小的动态盒子。工程代码中没有包含任何图形,你仅能在文字输出控制台中看到随时间更新的盒子位置。
从如何搭建并运行一个简单地Box2D模拟程序的角度来讲,这是一个很好的例子。
2.1创建一个世界
每个Box2D程序都从创建一个b2World对象开始,b2World是一个管理内存、物体和模拟的中心,你可以在堆栈中或者数据区域中为物理世界对象分配空间。
创建一个Box2D世界对象很容易,首先我们定义一个重力向量:
b2Vec2gravity(0.0f, -10.0f)
接着我们创建世界对象。注意我们这里是在栈中创建世界对象,因此我们需要在程序中保持该对象。
b2Worldworld(gravity);
这样我们就创建好了我们的物理世界,我们继续向其中添加一些其他的东西。
2.2创建一个地面盒子
物体通过下面的步骤创建完成:
1. 创建一个具有位置和阻尼(damping)(position)等属性的物体定义(BodyDef)
2. 利用世界对象创建物体
3. 使用形状、摩擦力、密度等属性定义装置定义(FixtureDef)
4. 在物体上创建装置
第一步我们创建地面盒子,需要一个物体定义来指定盒子的位置。
b2BodyDefgroundBodyDef;
groundBodyDef.position.Set(0.0f,-10.0f);
第二步我们将物体定义作为参数传递给世界对象来创建地面盒子。世界对象并不保存物体定义的引用。默认创建出来的物体都是静态的(static),静态物体不能移动,也不会与其他静态物体发生碰撞。
b2Body*groundBody = world.CreateBody(&groundBodyDef);
第三步我们创建一个地面多边形作为地面盒子的形状,我们使用SetAsBox方法来创建一个矩形,将盒子居中对齐到父物体的原点。
b2PolygonShapegroundBox;
groundBox.SetAsBox(50.0f,10.0f);
SetAsBox方法参数中的长宽属性只是实际长宽的一半,所以在本例中地面盒子的宽实际为100(x轴),长为20(y轴)。Box2D中的基本单位为米,千克和秒,所以我们可以认为这个例子中多边形的尺寸是以米作单位的。Box2D在处理大小和真实世界的物体相近的东西时,模拟效果最好,例如,在Box2D中一个桶应该大约1米高。相反的,由于浮点数计算的限制,用Box2D来模拟冰川或者模拟灰尘的粒子效果可不是一个好主意。
我们接着做完第四步,创建装置,来完成地面物体的创建。对于这一步,我们有一个小的捷径,由于我们不需要去修改默认的装置属性,所以我们直接将形状作为第一个参数传给物体对象,而不需要去创建一个装置定义(稍后我们会看到如何通过装置定义来定义物体的基本属性),传入的第二个参数是密度,单位是千克/平方米(这个地方应该因为是二维的物体,所以单位是平方米而不是立方米),静态物体默认没有质量,所以密度在这里没有作用。
groundBody->CreateFixture(&groundBox,0.0f);
Box2D不保留形状的引用,而是将形状对象的一个拷贝作为参数传入。
注意每一个装置都有一个父物体,即使装置是静态的。但是,你可以将所有的静态装置附加(attach)到一个相同的静态物体上。
当你将一个形状通过装置附加到物体上时,形状的坐标系就变为物体的本地坐标系了,所以当物体移动时,形状也随之移动。装置的世界变换继承自它的父物体,没有任何装置能够脱离物体进行变换,所以我们不去在物体上调整形状的位置,另一方面,移动和修改物体上的形状也是不支持的,理由很简单:一个形状可变的物体不是刚体,而Box2D是一款基于刚体的物理引擎,Box2D中的很多假设都是基于刚体模型的,所以如果物体不是刚体,很多事情就不对了。
2.3创建一个动态物体
现在我们有了一个地面物体,我们通过同样的方法来创建一个动态物体,唯一的不同时(除了尺寸之外),我们需要指定动态物体的质量属性。
首先我们通过CreateBody来创建物体,默认创建出来的是静态物体,所以在构造时我们需要将b2BodyType属性设置为动态的。
b2BodyDefbodyDef;
bodyDef.type= b2_dynamicBody;
bodyDef.position.Set(0.0f,4.0f);
b2Body*body = world.CreateBody(&bodyDef);
接着我们用装置定义来创建并绑定一个形状。首先我们创建一个盒子形状:
b2PolygonShapedynamicBox;
dynamicBox.SetAsBox(1.0f,1.0f);
接着我们用这个创建好的形状来创建一个装置定义,注意我们设置定义的密度属性为1,默认的密度属性为0,此外我们将形状的摩擦力属性设置为0.3。
b2FixureDeffixtureDef;
fixtureDef.shape= &dynamicBox;
fixtureDef.density= 1.0f;
fixtureDef.friction= 0.3f;
现在我们可以使用装置定义来定义一个物体了,物体的质量属性会被自动更新,你可以为物体添加多个装置,每个装置添加完成后,物体的质量都会随之增加。
body-CreateFixture(&fixtureDef);
这样我们就完成了初始化,我们下面开始进行模拟。
2.4模拟世界(Box2D)
我们完成了地面盒子和动态盒子的初始化操作,下面该让牛顿出场了。在此之前,我们还需要考虑一些事情。
Box2D使用一套名为集成器(integrator)的算法在离散的时间点上来模拟物理方程,这和我们传统游戏中使用的循环方法相同(例如我们通过循环帧来在屏幕上表现一本翻动的书)。因此我们需要为Box2D设置一个时间间隔(time step),大多数游戏引擎都希望时间间隔至少为1/60秒(频率为60Hz)。你也可以选择更大的时间间隔,但是你要在初始化世界的时候更加小心。我们不希望对时间间隔做过多的修改,变化的时间间隔导致变化的结果,调试的难度也随之增加,因此不在万不得已的情况下,不要将时间间隔和你的帧频率混为一谈。话不多说,下面我们定义好时间间隔。
float32timeStep = 1.0f/60.0f;
除了集成器外,Box2D还用一大段代码定义了约束解析器(constraint solver),用来在模拟的过程中逐个解析所有的约束。一个单独的约束能够被完美的解析,然而,当我们解析某个约束时,其他约束会被暂时中断,因此,如果需要得到一个好的解析结果,我们需要多次遍历所有的约束。
约束解析器的工作流程分为两个阶段:一个速度阶段,一个位置阶段。在速度阶段解析器计算所有能够使物体正确运动的冲量,在位置阶段,解析器调整物体的位置,以避免重叠,防止关节脱落。每个阶段有自己的迭代次数,在仅需微调的情况下位置阶段有可能提前结束。
Box2D中建议设置速度阶段的迭代次数为8次,位置阶段为3次,你可以自己按照需要做修改,但是请在计算的精确度和程序的运行效率上做好平衡。使用更少的迭代次数能够获得更高的执行效率,但是精确度要差一些,反之,更多的迭代次数带来更高的精度,但是却损失了效率。对于我们这个简单的例子来说,我们不需要太多的迭代次数,我们按照下面的代码来设定。
int32velocityIterations = 6;
int32positionIterations = 2;
注意到时间间隔和迭代次数完全无关,一次迭代并不是在一个时间间隔内,解析器做一次迭代的时间一个时间间隔,一个时间周期内可以出现多次迭代。
我们接下来可以编写用于模拟的循环了,在你的游戏中,模拟的循环代码可以被放到你游戏的循环中。在游戏的每一次循环中,调用b2World::Step,一次循环调用一次就可以了,与你的帧频率和物理时间间隔有关。
“Hello World”小程序应该尽量简洁,所以我们不加入任何的图形输出,仅用代码打印出动态盒子的位置和旋转而已。下面就是用来模拟的循环代码,模拟了60个时间间隔(总计1秒钟)。
for(int32 i = 0; i < 60; ++i)
{
world.Step(timeStep,velocityIterations, positionIterations);
b2Vec2position = body->GetPosition();
float32angle = 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语句清除掉时,所有为了物体、装置和关节保留的内存都会被释放掉,以提高性能。然而,你需要将所有的物体、装置和关节指针指向NULL,否则这些指针将指向被释放的区域。
2.6测试床(Testbed)
当你完成HelloWorld这个例子后,你应该简单了解一下Box2D的测试床。测试床是一个单元测试架构和演示环境(demo environment)。下面是它的一些功能:
1. 可移动和缩放的摄影机
2. 使用鼠标选择动态物体的形状
3. 可扩展的测试集
4. 可通过界面选择测试,设置参数和调试绘图选项
5. 暂停和单步(step)模拟
6. 文字渲染
在测试床中有许多关于Box2D的测试用例和框架的例子。我们希望你能够通过研究学习和实际操作来更好的学习Box2D。
注意:测试床是使用freeglut和GLUI写的,测试床不是Box2D库的一部分,渲染对于Box2D来说是透明的,就像在HelloWorld例子中我们看到的一样,我们不需要任何的渲染器。
Box2D及Testbed的代码请用SVN下载,checkout地址为:http://box2d.googlecode.com/svn/tags/v2.3.1