Box2D 的发行版中有一个 Hello World 项目。该程序创建了一个大型地面方框和一个小型动态方框。这段代码不包含任何图形。您将看到的只是控制台中随着时间推移而输出的方框位置文本。
这是一个如何使用 Box2D 启动和运行的良好示例。
创建世界
b2World 是管理内存、对象和模拟的物理中心。您可以在堆栈、堆或数据区分配物理世界。
创建 Box2D 世界非常简单。首先,我们定义重力矢量。
b2Vec2 gravity(0.0f,-10.0f);
现在我们创建世界对象。请注意,我们是在栈上创建世界,因此世界必须保留在作用域中。
b2World world(gravity);
现在我们有了物理世界,让我们开始添加一些东西吧。
创建GroundBox
使用以下步骤创建刚体:
1. 定义一个具有位置、阻尼等参数的刚体定义。
2. 使用世界对象创建刚体。
3. 用形状、摩擦力、密度等定义夹具。
4. 在刚体上创建夹具。
在步骤 1 中,我们创建地面刚体。为此,我们需要一个刚体定义。通过刚体定义,我们可以指定地面刚体的初始位置。
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
在步骤 2 中,刚体定义被传递给 "世界 "对象以创建地面 "刚体"。世界对象不会保留对刚体定义的引用。默认情况下,刚体是静态的。静态体不会与其他静态体发生碰撞,并且是不可移动的。
b2Body* groundBody = world.CreateBody(&groundBodyDef);
在步骤 3 中,我们创建一个地面多边形。我们使用 SetAsBox 方法将地面多边形形成一个方框形状,方框以父体的原点为中心。
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);
SetAsBox 函数获取半**宽**和半**高**(外延)。因此在本例中,地面方框的宽度为 100 个单位(X 轴),高度为 20 个单位(Y 轴)。Box2D 已针对米、千克和秒进行了调整。因此,您可以考虑以米为单位的范围。当物体的大小与现实世界中的典型物体相同时,Box2D 通常效果最佳。例如,一个桶大约有 1 米高。由于浮点运算的限制,使用 Box2D 来模拟冰川或尘埃粒子的运动并不是一个好主意。
在第 4 步中,我们通过创建形状夹具来完成地面刚体。对于这一步,我们有一个捷径。我们不需要更改默认的夹具材料属性,因此可以直接将形状传递给主体,而无需创建夹具定义。稍后我们将看到如何使用夹具定义来定制材料属性。第二个参数是形状密度(单位:千克/平方米)。根据定义,静态体的质量为零,因此在这种情况下不会使用密度。
groundBody->CreateFixture(&groundBox, 0.0f);
Box2D 不会保留形状的引用。它会将数据克隆到一个新的 b2Shape 对象中。
请注意,每个夹具都必须有一个父body,即使是静态夹具也不例外。但是,您可以将所有静态夹具附加到一个静态body上。
使用夹具将形状附加到body后,形状的坐标就会变成body的本地坐标。因此,当body移动时,形状也会移动。夹具的世界变换继承自父body。夹具没有独立于body的变换。因此我们不能在body上移动形状。我们不支持移动或修改刚体上的形状。原因很简单:具有变形形状的body不是刚体,但 Box2D 是一个刚体引擎。Box2D 中的许多假设都基于刚体模型。如果违反了这一点,很多事情都会被破坏
创建Dynamic Body
现在我们有了一个地面刚体。我们可以使用相同的技术来创建一个动态刚体。除了尺寸之外的主要区别是,我们必须确定动态刚体的质量属性。
首先,我们使用CreateBody创建刚体。默认情况下,刚体是静态的,因此我们应该在构造时设置b2BodyType为b2_dynamicBody以使刚体成为动态的。
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);
然后,我们使用这个矩形创建一个夹具定义。请注意,我们将密度设为1。默认密度为零。此外,形状的摩擦力被设置为0.3。
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;
注意:一个动态刚体应该至少有一个密度非零的夹具。否则会出现奇怪的行为。
使用夹具定义,我们现在可以创建夹具。这会自动更新刚体的质量。您可以向一个刚体添加任意数量的夹具。每个夹具都会对总质量产生影响。
body->CreateFixture(&fixtureDef);
初始化工作到此结束。现在我们准备开始模拟了。
我们已经初始化了地面框和一个动态框。现在我们准备让牛顿自由发挥。我们只需要考虑几个问题。
Box2D使用一种称为积分器的计算算法。积分器在离散的时间点模拟物理方程。这与传统的游戏循环相吻合,在屏幕上我们实际上有一个动作的翻页书。因此,我们需要为Box2D选择一个时间步长。通常,像游戏物理引擎一样,时间步长至少应该是60Hz或1/60秒。您可以使用更大的时间步长,但是您需要更加谨慎地设置您的世界定义。我们也不希望时间步长变化太大。可变时间步长会产生不确定结果,这样会使调试变得困难。因此,请不要将时间步长与帧速率绑定(除非您真的非常需要)。废话不多说,这是时间步长。
float timeStep = 1.0f / 60.0f;
除了积分器,Box2D还使用一个称为约束求解器的大段代码。约束求解器逐个解决模拟中的所有约束。一个单独的约束可以完美解决。然而,当我们解决一个约束时,我们会轻微干扰其他约束。为了获得良好的解决方案,我们需要多次迭代所有约束。
约束求解器有两个阶段:速度阶段和位置阶段。在速度阶段中,求解器计算使刚体正确移动所需的冲量。在位置阶段中,求解器调整刚体的位置以减少重叠和连接脱落。每个阶段都有自己的迭代次数。此外,如果错误很小,位置阶段可能会提前退出迭代。
Box2D建议的迭代次数是速度阶段为8,位置阶段为3。您可以根据自己的喜好调整这个数字,只需记住这会在性能和准确性之间产生权衡。使用更少的迭代次数会提高性能,但会降低准确性。同样,使用更多的迭代次数会降低性能,但会提高模拟的质量。对于这个简单的示例,我们不需要太多迭代。这里是我们选择的迭代次数。
int32 velocityIterations = 6;
int32 positionIterations = 2;
请注意,时间步长和迭代次数完全无关。一个迭代不是一个子步骤。一个求解器迭代是在一个时间步长内对所有约束进行单次遍历。您可以在一个时间步长内对约束进行多次遍历。
现在我们准备开始模拟循环。在您的游戏中,模拟循环可以与游戏循环合并。在游戏循环的每一次循环中,您都会调用b2World::Step。通常只需要一次调用,具体取决于您的帧速率和物理时间步长。
Hello World程序设计得很简单,因此没有图形输出。代码打印出动态体的位置和旋转。这是一个模拟循环,模拟60个时间步长,总计模拟1秒的时间。
for (int32 i = 0; i < 60; ++i)
{
world.Step(timeStep, velocityIterations, positionIterations);
b2Vec2 position = body->GetPosition();
float 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被删除时,为刚体、夹具和连接保留的所有内存都会被释放。这样做是为了提高性能并使您的生活更轻松。但是,您需要将任何已经存在的刚体、夹具或连接指针置为空,因为它们会变得无效。