Box2D v2.0.1 用户手册
原文:Box2D v2.0.2 User Manual
译者:Aman JIANG(江超宇),翻译信息。
1. 导言
1.1 关于
Box2D 是一个用于游戏的 2D 刚体仿真库。程序员可以在他们的游戏里使用它,它可以使物体的运动
更加可信,让世界看起来更具交互性。从游戏的视角来看,物理引擎就是一个程序性动画(procedural
animation)的系统,而不是由动画师去移动你的物体。你可以让牛顿来做导演。
Box2D 是用可移植的 C++ 来写成的。引擎中定义的大部分类型都有 b2 前缀,希望这能消除它和你
游戏引擎之间的名字冲突。
1.2 必备条件
在此,我假定你已经熟悉了基本的物理学概念,例如质量,力,扭矩和冲量。如果没有,请先考虑读
一下 Chris Hecker 和 David Baraff (google 这些名字)的那些教程,你不需要了解得非常细致,但他们
可以使你很好地了解一些基本概念,以便你使用 Box2D。
Wikipedia 也是一个极好的物理和数学知识的获取源,在某些方面它可能比 google 更有用,因为它
的内容经过了精心的整理。
这不是必要的,但如果你好奇 Box2D 内部是如何工作的,你可以看 这些文档。
因为 Box2D 是使用 C++ 写成的,所以你应该具备 C++ 程序设计的经验,Box2D 不应该成为你的
第一个 C++ 程序项目。你应该已经能熟练地编译,链接和调试了。
1.3 核心概念
Box2D 中有一些基本的对象,这里我们先做一个简要的定义,在随后的文档里会有更详细的描述。
刚体(rigid body)
一块十分坚硬的物质,它上面的任何两点之间的距离都是完全不变的。它们就像钻石那样坚硬。在后
面的讨论中,我们用物体(body)来代替刚体。
形状(shape)
一块严格依附于物体(body)的 2D 碰撞几何结构(collision geometry)。形状具有摩擦(friction)和恢
复(restitution)的材料性质。
约束(constraint)
一个约束(constraint)就是消除物体自由度的物理连接。在 2D 中,一个物体有 3 个自由度。如果我
们把一个物体钉在墙上(像摆锤那样),那我们就把它约束到了墙上。这样,此物体就只能绕着这个钉子旋
转,所以这个约束消除了它 2 个自由度。
接触约束(contact constraint)
一个防止刚体穿透,以及用于模拟摩擦(friction)和恢复(restitution)的特殊约束。你永远都不必创建
一个接触约束,它们会自动被 Box2D 创建。
关节(joint)
它是一种用于把两个或多个物体固定到一起的约束。Box2D 支持的关节类型有:旋转,棱柱,距离等
等。关节可以支持限制(limits)和马达(motors)。
关节限制(joint limit)
一个关节限制(joint limit)限定了一个关节的运动范围。例如人类的胳膊肘只能做某一范围角度的运
动。
关节马达(joint motor)
一个关节马达能依照关节的自由度来驱动所连接的物体。例如,你可以使用一个马达来驱动一个肘的
旋转。
世界(world)
一个物理世界就是物体,形状和约束相互作用的集合。Box2D 支持创建多个世界,但这通常是不必要
的。
2. Hello Box2D
2.1 创建一个世界
每个 Box2D 程序都将从一个世界对象(world object)的创建开始。这是一个管理内存,对象和模拟的
中心。
要创建一个世界对象,我们首先需要定义一个世界的包围盒。Box2D 使用包围盒来加速碰撞检测。尺
寸并不关键,但合适的尺寸有助于性能。这个包围盒过大总比过小好。
b2AABB worldAABB;
worldAABB.lowerBound.Set(-100.0f, -100.0f);
worldAABB.upperBound.Set(100.0f, 100.0f);
• 注意:worldAABB 应该永远比物体所在的区域要大,让 worldAABB 更大总比太小要好。如果一
个物体到达了 worldAABB 的边界,它就会被冻结并停止模拟。
接下来我们定义重力矢量。是的,你可以使重力朝向侧面(或者你只好转动你的显示器)。并且,我们
告诉世界(world)当物体停止移动时允许物体休眠。一个休眠中的物体不需要任何模拟。
b2Vec2 gravity(0.0f, -10.0f);
bool doSleep = true;
现在我们创建世界对象。通常你需要在堆(heap)上创建世界对象,并把它的指针保存在某一结构中。
然而,在这个例子中也可以在栈上创建。
b2World world(worldAABB, gravity, doSleep);
那么现在我们有了自己的物理世界,让我们再加些东西进去。
2.2 创建一个地面盒
物体通常由以下步骤来创建:
1. 使用位置(position),阻尼(damping)等定义一个物体
2. 使用世界对象创建物体
3. 使用几何结构,摩擦,密度等定义形状
4. 在物体上创建形状
5. 可选地调整物体的质量以和附加的形状相匹配
第一步,我们创建地面体。要创建它我们需要一个物体定义(body definition),通过物体定义我们来
指定地面体的初始位置。
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
第二步,将物体定义传给世界对象来创建地面体。世界对象并不保存到物体定义的引用。地面体是作
为静态物体(static body)创建的,静态物体之间并没有碰撞,它们是固定的。当一个物体具有零质量的
时候 Box2D 就会确定它为静态物体,物体的默认质量是零,所以它们默认就是静态的。
b2Body* ground = world.CreateBody(&groundBodyDef);
第三步,我们创建一个地面的多边形定义。我们使用 SetAsBox 简捷地把地面多边形规定为一个盒子
(矩形)形状,盒子的中点就位于父物体的原点上。
b2PolygonDef groundShapeDef;
groundShapeDef.SetAsBox(50.0f, 10.0f);
其中,SetAsBox 函数接收了半个宽度和半个高度,这样的话,地面盒就是 100 个单位宽(x 轴)以及
20 个单位高(y 轴)。Box2D 已被调谐使用米,千克和秒来作单位,所以你可以用米来考虑长度。然而,
改变单位系统是可能的,随后的文档中会有讨论。
在第四步中,我们在地面体上创建地面多边形,以完成地面体。
groundBody->CreateShape(&groundShapeDef);
重申一次,Box2D 并不保存到形状或物体的引用。它把数据拷贝到 b2Body 结构中。
注意每个形状都必须有一个父物体,即使形状是静态的。然而你可以把所有静态形状都依附于单个静
态物体之上。这个静态物体之需求是为了保证 Box2D 内部的代码更具一致性,以减少潜在的 bug 数
量。
可能你已经注意到了,大部分 Box2D 类型都有一个 b2 前缀。这是为了降低它和你的代码之间名字冲
突的机会。
2.3 创建一个动态物体
现在我们已经有了一个地面体,我们可以使用同样的方法来创建一个动态物体。除了尺寸之外的主要
区别是,我们必须为动态物体设置质量性质。
首先我们用 CreateBody 创建物体。
b2BodyDef bodyDef;
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);
接下来我们创建并添加一个多边形形状到物体上。注意我们把密度设置为 1,默认的密度是 0。并
且,形状的摩擦设置到了 0.3。形状添加好以后,我们就使用 SetMassFromShapes 方法来命令物体通
过形状去计算其自身的质量。这暗示了你可以给单个物体添加一个以上的形状。如果质量计算结果为 0,
那 么 物 体 会 变 成 真 正 的 静 态 。 物 体 默 认 的 质 量 就 是 零 , 这 就 是 为 什 么 我 们 无 需 为 地 面 体 调 用
SetMassFromShapes 的原因。
b2PolygonDef shapeDef;
shapeDef.SetAsBox(1.0f, 1.0f);
shapeDef.density = 1.0f;
shapeDef.friction = 0.3f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();
这就是初始化过程。现在我们已经准备好开始模拟了。
2.4 模拟(Box2D 的)世界
我们已经初始化好了地面盒和一个动态盒。现在是让牛顿接手的时刻了。我们只有少数几个问题需要
考虑。
Box2D 中有一些数学代码构成的积分器(integrator),积分器在离散的时间点上模拟物理方程,它将
与游戏动画循环一同运行。所以我们需要为 Box2D 选取一个时间步,通常来说游戏物理引擎需要至少
60Hz 的速度,也就是 1/60 的时间步。你可以使用更大的时间步,但是你必须更加小心地为你的世界调
整定义。我们也不喜欢时间步变化得太大,所以不要把时间步关联到帧频(除非你真的必须这样做)。直截
了当地,这个就是时间步:
float32 timeStep = 1.0f / 60.0f;
除了积分器之外,Box2D 中还有约束求解器(constraint solver)。约束求解器用于解决模拟中的所有
约束,一次一个。单个的约束会被完美的求解,然而当我们求解一个约束的时候,我们就会稍微耽误另
一个。要得到良好的解,我们需要迭代所有约束多次。建议的 Box2D 迭代次数是 10 次。你可以按自己
的喜好去调整这个数,但要记得它是速度与质量之间的平衡。更少的迭代会增加性能并降低精度,同样
地,更多的迭代会减少性能但提高模拟质量。这是我们选择的迭代次数:
int32 iterations = 10;
注意时间步和迭代数是完全无关的。一个迭代并不是一个子步。一次迭代就是在时间步之中的单次遍
历所有约束,你可以在单个时间步内多次遍历约束。
现在我们可以开始模拟循环了,在游戏中模拟循环应该并入游戏循环。每次循环你都应该调用
b2World::Step,通常调用一次就够了,这取决于帧频以及物理时间步。
这个 Hello World 程序设计得非常简单,所以它没有图形输出。胜于完全没有输出,代码会打印出动
态物体的位置以及旋转角度。Yay!这就是模拟 1 秒钟内 60 个时间步的循环:
for (int32 i = 0; i < 60; ++i)
{
world.Step(timeStep, iterations);
b2Vec2 position = body->GetPosition();
float32 angle = body->GetAngle();
printf("%4.2f %4.2f %4.2f\n", position.x, position.y, angle);
}
2.5 清理工作
当一个世界对象超出它的作用域,或通过指针将其 delete 时,所有物体和关节的内存都会被释放。
这能使你的生活变得更简单。然而,你应该将物体,形状或关节的指针都清零,因为它们已经无效了。
2.6 关于 Testbed
一旦你征服了 HelloWorld 例子,你应该开始看 Box2D 的 testbed 了。testbed 是一个单元测试框
架以及演示环境,这是一些它的特点:
• 可移动和缩放的摄像机
• 鼠标拣选动态物体的形状
• 可扩展的测试集
• 通过图形界面选择测试,调整参数,以及设置调试绘图
• 暂停和单步模拟
• 文字渲染
在 testbed 中有许多 Box2D 的测试用例,以及框架本身的实例。我鼓励你通过研究和修改它来学习
Box2D。
注意:testbed 是使用 freeglut 和 GLUI 写成的,testbed 本身并不是 Box2D 库的一部分。Box2D
本身对于渲染是无知的,就像 HelloWorld 例子一样,使用 Box2D 并不一定需要渲染。
3. API 设计
3.1 内存管理
Box2D 的许多设计决策都是为了能快速有效地使用内存。在本节我将论述 Box2D 如何和为什么要分
配内存。
Box2D 倾向于分配大量的小对象(50-300 字节左右)。这样通过 malloc 或 new 在系统的堆(heap)上
分配内存就太低效,并且容易产生内存碎片。多数这些小型对象的生命期都很短暂,例如触点
(contact),可能会维持几个时间步。所以我们需要为这些对象提供一个有效的分配器(allocator)。
Box2D 的解决方案是使用小型对象分配器(SOA),SOA 维护了许多不定尺寸的可生长的池(growable
pool)。当有内存分配请求时,SOA 会返回一块最匹配的内存。当内存块释放掉以后,它会回到池中。
这些操作都十分快速,导致很小的堆流量。
因为 Box2D 使用了 SOA,所以你应该永远也不必去 new 或 malloc 物体,形状或关节。你只需分配
一个 b2World,它为你提供了创建物体,形状和关节的工厂(factory)。这使得 Box2D 可以使用 SOA 并
且将赤裸的细节隐藏起来。永远也不要去 delete 或 free 一个物体,形状或关节。
当执行一个时间步的时候,Box2D 会需要一些临时的内存。为此,它使用了一个栈(stack)分配器来消
除单步堆分配。你不需要关心栈分配器,但在此作一个了解还是不错的。
3.2 工厂和定义
如上所述,内存管理在 Box2D API 的设计中担当了一个中心角色。所以当你创建一个 b2Body 或一
个 b2Joint 的时候,你需要调用 b2World 的工厂函数。
这些是创建函数:
b2Body* b2World::CreateBody(const b2BodyDef* def)
b2Joint* b2World::CreateJoint(const b2JointDef* def)
这是对应的摧毁函数:
void b2World::DestroyBody(b2Body* body)
void b2World::DestroyJoint(b2Joint* joint)
当你创建一个物体或关节的时候,你需要提供一个定义(definition,简写为 def)。这些定义包含了创
建物体或关节的所有相关信息。通过这样的方法,我们就能预防构造错误,使函数参数的数量较少,提
供有意义的默认参数,并减少访问子(accessor)的数量。
因为形状必须有父物体,所以 b2Body 上有创建和摧毁形状的工厂:
b2Shape* b2Body::CreateShape(const b2ShapeDef* def)
void b2Body::DestroyShape(b2Shape* shape)
工厂并不保留到定义的引用,所以你可以在栈上创建定义,临时的保存它们。
3.3 单位
Box2D 使用浮点数,所以必须使用一些公差来保证它正常工作。这些公差已经被调谐得适合米-千克-
秒(MKS)单位。尤其是,Box2D 被调谐得能良好地处理 0.1 到 10 米之间的移动物体。这意味着从罐头
盒到公共汽车大小的对象都能良好地工作。
作为一个 2D 物理引擎,如果能使用像素作为单位是很诱人的。很不幸,那将导致不良模拟,也可能
会造成古怪的行为。一个 200 像素长的物体在 Box2D 看来就有 45 层建筑那么大。想象一下使用一个被
调谐好模拟玩偶和木桶的引擎去模拟高楼大厦的运动。那并不有趣。
• 注意:Box2D 已被调谐至 MKS 单位。移动物体的尺寸大约应该保持在 0.1 到 10 米之间。你可能
需要一些缩放系统来渲染你的场景和物体。Box2D 中的例子是使用 OpenGL 的视口来变换的。
3.4 用户数据
b2Shape,b2Body 和 b2Joint 类都允许你通过一个 void 指针来附加用户数据。这在你测试 Box2D
数据结构,以及你想把它们联系到自己的引擎中的时候是较方便的。
举个典型的例子,在角色上的刚体中附加到角色的指针,这就构成了一个循环引用。如果你有角色,
你就能得到刚体。如果你有刚体,你就能得到角色。
GameActor* actor = GameCreateActor();
b2BodyDef bodyDef;
bodyDef.userData = actor;
actor->body = box2Dworld->CreateBody(&bodyDef);
这是一些需要用户数据的案例:
• 使用碰撞结果给角色施加伤害
• 当玩家进入一个包围盒时播放一段脚本事件
• 当 Box2D 通知你一个关节即将摧毁时访问一个游戏结构
记得用户数据是可选的,并且能放入任何东西。然而,你需要保持一致性。例如,如果你想在一个物
体中保存一个角色的指针,那你就应该在所有物体中都保存一个角色指针。不要在一个物体中保存角色
指针,却在另一个物体中保存一个其它指针。这可能会导致程序崩溃。
3.5 C++ 相关面
C++ 有着强大的封装和多态,但在 API 设计方面却不那么强大。在创建一个 C++ 库的时候总会存在
许多有意义的取舍。
我们是否应该使用抽象工厂或 pimpl 模式?它们能使 API 看起来更简洁,但它们最终会妨碍调试和高
效开发。
我们是否有必要使用私有数据和友元(friend)?也许,但最后友元的数量可能会变得荒谬。
我们是否应该用一个 C-API 封装 C++ 代码?也许,但这是额外的工作,并且可能会导致非最佳的内
部选择。另外,C-API 也难于调试和维护,一个 C-API 同时也破坏了封装。
我为 Box2D 选择了最容易的方法。有时候一个类可以包含其设计和函数,所以我使用公有函数和私
有数据。其它情况下我使用了全部公有的成员的类和结构。这样的选择使我能快速地开发代码,很容易
调试,并且当维护紧密的封装时最小化了内部混乱。如此,你并不能看见一个简单干净的 API。当然,
你拥有的这个漂亮的手册能帮助你摆脱困扰 :)
3.6 稻草人
如果你不喜欢这个 API 的设计,that's ok!你拥有源代码!诚挚地,如果你有任何关于 Box2D 的反
馈,请在 论坛 里留下意见。