本博文是笔者的学习笔记,学习资料来自Box2D_v2.2.1帮助文档。
Box2D是一个为游戏设计的2d刚体仿真库。程序员可以在他们的游戏里使用它,它可以使物体的运动更加可信,让世界看起来更具交互性。从游戏的视角来看,物理引擎就是一个程序性动画(procedural animation)的系统,而不是由动画师去移动你的物体。
Box2D 是用可移植的 C++ 来写成的。引擎中定义的大部分类型都有 b2 前缀,希望这能消除它和你游戏引擎之间的名字冲突。
形状(shape) :一个2D的几何对象,例如圆或多边形。
刚体(rigid body) :一块十分坚硬的物质,它上面的任何两点之间的距离都是完全不变的。它们就像钻石那样坚硬。在后面的讨论中,我们用物体(body)来代替刚体。
固定装置(fixture):fixture绑定一个形状到物体,增加材料属性,例如密度,摩擦,恢复。
约束(constraint):一个约束(constraint)就是消除物体自由度的物理连接。在 2D 中,一个物体有 3 个自由度。如果我们把一个物体钉在墙上(像摆锤那样),那我们就把它约束到了墙上。这样,此物体就只能绕着这个钉子旋转,所以这个约束消除了它 2 个自由度。
接触约束(contact constraint):一个防止刚体穿透,以及用于模拟摩擦(friction)和恢复(restitution)的特殊约束。你永远都不必创建一个接触约束,它们会自动被 Box2D 创建。
关节(joint):它是一种用于把两个或多个物体固定到一起的约束。Box2D 支持的关节类型有:旋转,棱柱,距离等等。关节可以支持限制(limits)和马达(motors)。
关节限制(joint limit):一个关节限制限定了一个关节的运动范围。例如人类的胳膊肘只能做某一范围角度的运动。
关节马达(joint motor):一个关节马达能依照关节的自由度来驱动所连接的物体。例如,你可以使用一个马达来驱动一个肘的旋转。
世界(world):一个物理世界就是物体,形状和约束相互作用的集合。Box2D 支持创建多个世界,但这通常是不必要的。
解决器(solver):物理世界有一个解决器用来推进时间(advance time)和解决接触和关节约束。Box2D解决器是一个高效的迭代的解决器,它运行N次,N是约束的数目。
连续冲突(continuous collision):解决器用分离的时间步推进物体。没有它的介入,就会导致穿过(tunneling)。
Box2D包含专门的算法用来处理穿过。首先,collision算法为了找到第一时间的影响(TOI)可以篡改两个物体的移动。其次,sub-stepping解决器,它移动物体到他第一时间影响,然后解决collision。
模块
Box2D由三个模块组成:Common,Collision和Dynamics。Common模块包含分配,数学和设置(settings)。Collision模块定义形状,broad-phase和collision函数/资料库。最后Dynamics模块提供仿真模拟世界,物体,固定装置(fixtures)和关节。
工厂和定义
创建函数
b2Body* b2World::CreateBody(const b2BodyDef* def)
b2Joint* b2World::CreateJoint(const b2JointDef* def)
这个参数是定义,它包含所需创建对象的信息。
销毁函数
void b2World::DestroyBody(b2Body* body)
void b2World::DestroyJoint(b2Joint* joint)
Fixture的创建销毁:
b2Fixture* b2Body::CreateFixture(const b2FixtureDef* def)
void b2Body::DestroyFixture(b2Fixture* fixture)
快捷方式,通过形状和密度创建
b2Fixture* b2Body::CreateFixture(const b2Shape* shape, float32 density)
创建一个世界(world)
任何Box2D程序都是以创建一个b2Wrold对象开始的。
创建一个Box2D世界很容易,比如下面的例子:
首先创创一个重力,并允许睡眠(睡眠的对象不提供任何模拟)
b2Vec2 gravity(0.0f, -10.0f);
bool doSleep = true;
然后就可以创建一个世界对象了。
b2World world(gravity, doSleep);
我们的世界对象就创建好了,接下来我可以为它添加对象。
创建一个地面盒(ground box)
物体的创建有如下几步:
1. 使用位置,阻尼(damping)定义一个物体。
2. 使用世界对象创建这个物体。
3. 用几何形状,摩擦,密度等定义固定装置(friction)。
4. 在物体上创建固定装置。
用上面的方法我们创建一个地面物体(ground body),为此,我们需要定义物体。
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
第二步,将物体定义传递给世界对象创建地面物体。
b2Body* groundBody = world.CreateBody(&groundBodyDef);
第三步,创建几何形状。
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);
然而,SetAsBox是以半个宽半个高为参数的,这里就创建了一个100*20的地面物体。Box2D将转化为米,千克,秒为单位。所以你创建的物体都可以以这些单位考虑。
第四步,创建出固定装置
groundBody->CreateFixture(&groundBox, 0.0f);
Box2D 并不保存到形状或物体的引用。它把数据拷贝到 b2Shape对象中。注意,每一个fixture都必须有一个父物体,即使fixtures是静态的。并且你可以绑定所有的静态fixtures到一个单独的物体。
创建一个动态物体
现在我们已经有了以地面物体,我们可以用相同的技术图创建一个动态物体。这其中包括规模在内的主要的不同是我们必须建立动态的物体质量属性。
首先,我们用CreateBody创建物体,默认是静态的,所以需要在构造时设置b2BodyType为动态。
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);
注意,如果你想物体在受力后移动,你必须设置类型为b2_dynamicBody。
接下来创建绑定一个多边形形状。
首先创建一个盒子形状。
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f);
接着创建一个fixture定义。定义密度为1,默认是0;摩擦设置为0.3。
b2FixtureDef fixtureDef;
FixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;
使用fixture定义我们可以创建这个fixture了。它会自动更新这个物体的质量(根据形状和密度可以算出)。你也可以添加很多你想要的fixture到物体,每个都为总质量做贡献。
以上就是初始化,到这里我们就已经做好了模拟的准备工作。
模拟Box2D世界
初始化地面和和动态物体后,现在该让牛顿接手工作的时候了。
Box2D用一个计算性算法称为积分器(integrator)的东西在离散的时间点上去模拟物理方程。通常,游戏物理引擎的一个时间步至少60Hz或者说1/60秒。你一饿定义地更快,但你就要更小心的设置定义你的世界了。我们也不喜欢时间步变得太快,一个可变的时间步会导致一个可变的结果。为了使调试不至于变复杂,不要把时间步关联到帧频(除非你需要这么做)。
下面就是设置时间步
float32 timeStep = 1.0f / 60.0f;
除了积分器,Box2D还用来一个叫约束求解器(constraint solver)的东西去解决在模拟中所有的约束,一次一个。一个单独的约束可以解决地很完美,然而,当我们解决一个约束,它会稍微影响到下一个约束。为了得到一个好的解决方案,我们需要去遍历所有的约束多次。
在约束求解器里有两个阶段:速率阶段(velocity phase),定位阶段(position phase)。速率阶段解决器,计算让物体移动正确需要的拖动力;定位阶段解决器为减少重叠和关节分离调整物体位置。每个阶段有他自己的迭代次数。另外,如果错误小定位阶段可能过早退出迭代次数。
建议的Box2D迭代次数是速率阶段8次,定位阶段3次。你可以调整这次数,但是记住它是有一个速度和准确度的平衡的,少的迭代会增加性能并降低精度,同样地,更多的迭代会减少性能但提高模拟质量。下面是我们选择的迭代次数。
int32 velocityIterations = 6;
int32 positionIterations = 2;
注意时间步和迭代次数是无关的。一个迭代并不是一个子步。一次迭代就是在时间步之中的单次遍历所有约束,你可以在单个时间步内多次遍历约束。你可以再一个单时间步中多次传递约束。
现在我们准备开始模拟循环了。在你的游戏中模拟循环可以被你的游戏循环合并。在你的游戏循环中,你可以调用b2World::Step。每次调用一次就足够,根据你的帧率和你的物理时间步。
Hello World的简单程序模拟循环每秒60次时间步。
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
整理工作
当一个世界对象超出它的作用域,或通过指针将其 delete 时,所有物体,固定装置(fixtures)和关节的内存都会被释放。这能使你的生活变得更简单。然而,你应该将物体,fixtures,或关节的指针都清零(nullify),因为它们已经无效了。