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 的反
馈,请在 论坛 里留下意见。
4. 世界
4.1 关于
b2World 类包含着物体和关节。它管理着模拟的方方面面,并允许异步查询(就像 AABB 查询)。你与
Box2D 的大部分交互都将通过 b2World 对象来完成。
4.2 创建和摧毁一个世界
创建一个世界十分的简单。你只需提供一个包围盒和一个重力向量。
轴对齐包围盒(AABB)应该包围着世界。稍微比世界大一些的包围盒可以提升性能,比方 2 倍大小才安
全。如果你的许多物体都掉进了深渊,你应该侦测并移除它们。这能提升性能并预防浮点溢出。
要创建或摧毁一个世界你需要使用 new 和 delete:
b2World* myWorld = new b2World(aabb, gravity, doSleep);
// ... do stuff ...
delete myWorld;
• 注意:请回忆,AABB 的世界应该比你物体所在的区域要大。如果物体离开了 AABB,它们将被冻
结。这不是一个 bug。
4.3 使用一个世界
世界类包含着用于创建和摧毁物体与关节的工厂,这些工厂会在后面的物体和关节的章节中讨论。在
此我们讨论一些 b2World 的其它交互。
4.3.1 模拟
世界类用于驱动模拟。你需要指定一个时间步和一个迭代次数。例如:
float32 timeStep = 1.0f / 60.f;
int32 iterationCount = 10;
myWorld->Step(timeStep, iterationCount);
在时间步完成之后,你可以调查物体和关节的信息。最可能的情况是你会获取物体的位置,这样你才
能更新你的角色并渲染它们。你可以在游戏循环的任何地方执行时间步,但你应该意识到事情发生的顺
序。例如,如果你想要在一帧中得到新物体的碰撞结果,你必须在时间步之前创建物体。
正如之前我在 HelloWorld 教程中说明的,你需要使用一个固定的时间步。使用大一些的时间步你可
以在低帧率的情况下提升性能。但通常情况下你应该使用一个不大于 1/30 秒 的时间步。1/60 的时间步
通常会呈现一个高质量的模拟。
迭代次数控制了约束求解器会遍历多少次世界中的接触以及关节。更多的迭代总能产生更好的模拟,
但不要使用小频率大迭代数。60Hz 和 10 次迭代远好于 30Hz 和 20 次迭代。
4.3.2 扫描世界
如上所述,世界就是一个物体和关节的容器。你可以获取世界中所有物体和关节并遍历它们。例如,
这段代码会唤醒世界中的所有物体:
for (b2Body* b = myWorld->GetBodyList(); b; b = b->GetNext())
{
b->WakeUp();
}
不幸的是生活有时很复杂。例如,下面的代码是错误的:
for (b2Body* b = myWorld->GetBodyList(); b; b = b->GetNext())
{
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor->IsDead())
{
myWorld->DestroyBody(b); // ERROR: now GetNext returns garbage.
}
}
在一个物体摧毁之前一切都很顺利。一旦一个物体摧毁了,它的 next 指针就变得非法,所以
b2Body::GetNext() 就会返回垃圾。解决方法是在摧毁之前拷贝 next 指针。
b2Body* node = myWorld->GetBodyList();
while (node)
{
b2Body* b = node;
node = node->GetNext();
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor->IsDead())
{
myWorld->DestroyBody(b);
}
}
这能安全地摧毁当前物体。然而,你可能想要调用一个游戏的函数来摧毁多个物体,这时你需要十分
小心。解决方案取决于具体应用,但在此我给出一种方法:
b2Body* node = myWorld->GetBodyList();
while (node)
{
b2Body* b = node;
node = node->GetNext();
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor->IsDead())
{
bool otherBodiesDestroyed = GameCrazyBodyDestroyer(b);
if (otherBodiesDestroyed)
{
node = myWorld->GetBodyList();
}
}
}
很明显要保证这个能正确工作,GameCrazyBodyDestroyer 对它都摧毁了什么必须要诚实。
4.3.3 AABB 查询
有时你需要求出一个区域内的所有形状。b2World 类为此使用了 broad-phase 数据结构,提供了一
个 log(N) 的快速方法。你提供一个世界坐标的 AABB,而 b2World 会返回一个所有大概相交于此
AABB 的形状之数组。这不是精确的,因为函数实际上返回那些 AABB 与规定之 AABB 相交的形状。例
如,下面的代码找到所有大概与指定 AABB 相交的形状并唤醒所有关联的物体。
b2AABB aabb;
aabb.minVertex.Set(-1.0f, -1.0f);
aabb.maxVertex.Set(1.0f, 1.0f);
const int32 k_bufferSize = 10;
b2Shape *buffer[k_bufferSize];
int32 count = myWorld->Query(aabb, buffer, k_bufferSize);
for (int32 i = 0; i < count; ++i)
{
buffer[i]->GetBody()->WakeUp();
}
5. 物体
5.1 关于
物体具有位置和速度。你可以应用力,扭矩和冲量到物体。物体可以是静态的或动态的,静态物体永
远不会移动,并且不会与其它静态物体发生碰撞。
物体是形状的主干,物体携带形状在世界中运动。在 Box2D 中物体总是刚体,这意味着同一刚体上
的两个形状永远不会相对移动。
通常你会保存所有你所创建的物体的指针,这样你就能查询物体的位置,并在图形实体中更新它的位
置。另外在不需要它们的时候你也需要通过它们的指针摧毁它们。
5.2 物体定义
在创建物体之前你需要创建一个物体定义(b2BodyDef)。你可以把物体定义创建在栈上,也可以在你
的游戏数据结构中保存它们。这取决于你的选择。
Box2D 会从物体定义中拷贝出数据,它不会保存到物体定义的指针。这意味着你可以循环使用一个物
体定义去创建多个物体。
让我们看一些物体定义的关键成员。
5.2.1 质量性质
有多种建立物体质量性质的方法:
1. 在物体定义中显式地设置
2. 显式地在物体上设置(在其创建之后)
3. 基于物体上形状之密度设置
在很多游戏环境中,根据形状密度计算质量是有意义的。这能帮助确保物体有合理和一致的质量。
然而,其它游戏环境可能需要指定质量值。例如,可能你有一个机械装置,需要一个精确的质量。
你可以这样在物体定义中显式地设置质量性质:
bodyDef.massData.mass = 2.0f; // the body's mass in kg
bodyDef.center.SetZero(); // the center of mass in local coordinates
bodyDef.I = 3.0f; // the rotational inertia in kg*m^2.
其它设置质量性质的方法在本文档其它部分有描述。
5.2.2 位置和角度
物体定义为你提供了一个在创建时初始化位置的机会,这要比在世界原点创建物体而后移动它到某个
位置更具性能。
一个物体上主要有两个令人感兴趣的点。其中一个是物体的原点,形状和关节都相对于物体的原点而
被附加。另一个点是物体的质心。质心取决于物体上形状的质量分配,或显式地由 b2MassData 设
置。Box2D 内部的许多计算都要使用物体的质心,例如 b2Body 会存储质心的线速度。
当你构造物体定义的时候,可能你并不知道质心在哪里,因此你会指定物体的原点。你可能也会以弧
度指定物体的角度,角度并不受质心位置的影响。如果随后你改变了物体的质量性质,那么质心也会随
之移动,但是原点以及物体上的形状和关节都不会改变。
bodyDef.position.Set(0.0f, 2.0f); // the body's origin position.
bodyDef.angle = 0.25f * b2_pi; // the body's angle in radians.
5.2.3 阻尼
阻尼用于减小物体在世界中的速率。阻尼与摩擦是不同的,因为摩擦仅在物体有接触的时候才会发
生,而阻尼的模拟要比摩擦便宜多了。然而,阻尼并不能取代摩擦,往往这两个效果需要同时使用。
阻尼参数的范围可以在 0 到无穷之间,0 的就是没有阻尼,无穷就是满阻尼。通常来说,阻尼的值应
该在 0 到 0.1 之间,我通常不使用线性阻尼,因为它会使物体看起来发飘。
bodyDef.linearDamping = 0.0f;
bodyDef.angularDamping = 0.01f;
阻尼相似于稳定性与性能,阻尼值较小的时候阻尼效应几乎不依赖于时间步,而阻尼值较大的时候阻
尼效应将随着时间步而变化。如果你使用固定的时间步(推荐)这就不是问题了。
5.2.4 休眠参数
休眠是什么意思?好的。模拟物体的成本是高昂的,所以如果物体更少,那模拟的效果就能更好。当
一个物体停止了运动时,我们喜欢停止去模拟它。
当 Box2D 确定一个物体(或一组物体)已经停止移动时,物体就会进入休眠状态,消耗很小的 CPU 开
销。如果一个醒着的物体接触到了一个休眠中的物体,那么休眠中的物体就会醒来。当物体上的关节或
触点被摧毁的时候,它们同样会醒来。你也可以手动地唤醒物体。
通过物体定义,你可以指定一个物体是否可以休眠,或者创建一个休眠的物体。
bodyDef.allowSleep = true;
bodyDef.isSleeping = false;
5.2.5 子弹
有的时候,在一个时间步内可能会有大量的刚体同时运动。如果一个物理引擎没有处理好大幅度运动
的问题,你就可能会看见一些物体错误地穿过了彼此。这种效果被称为隧道效应(tunneling)。
默认情况下,Box2D 会通过连续碰撞检测(CCD)来防止动态物体穿越静态物体,这是通过从形状的旧
位置到新位置的扫描来完成的。引擎会查找扫描中的新碰撞,并为这些碰撞计算碰撞时间(TOI)。物体会
先被移动到它们的第一个 TOI,然后一直模拟到原时间步的结束。如果有必要这个步骤会重复执行。
一般 CCD 不会应用于动态物体之间,这是为了保持性能。在一些游戏环境中你需要在动态物体上也
使用 CCD,譬如,你可能想用一颗高速的子弹去射击薄壁。没有 CCD,子弹就可能会隧穿薄壁。
高速移动的物体在 Box2D 被称为子弹(bullet),你需要按照游戏的设计来决定哪些物体是子弹。如果
你决定一个物体应该按照子弹去处理,使用下面的设置。
bodyDef.isBullet = true;
子弹开关只影响动态物体。
CCD 的成本是昂贵的,所以你可能不希望所有运动物体都成为子弹。所以 Box2D 默认只在动态物体
和静态物体之间使用 CCD,这是防止物体逃脱游戏世界的一个有效方法。然而,可能你有一些高速移动
的物体需要一直使用 CCD。
5.3 物体工厂
物体的创建和摧毁是由世界类提供的物体工厂来完成的。这使得世界可以通过一个高效的分配器来创
建物体,并且把物体加入到世界数据结构中。
物体可以是动态或静态的,这取决于质量性质。两种类型物体的创建和摧毁方法都是一样的。
b2Body* dynamicBody = myWorld->CreateBody(&bodyDef);
... do stuff ...
myWorld->DestroyBody(dynamicBody);
dynamicBody = NULL;
• 注意:永远不要使用 new 或 malloc 来创建物体,否则世界不会知道这个物体的存在,并且物体也
不会被适当地初始化。
静态物体不会受其它物体的作用而移动。你可以手动地移动静态物体,但你必须小心,不要挤压到静
态物体之间的动态物体。另外,当你移动静态物体的时候,摩擦不会正确工作。在一个静态物体上附加
数个形状,要比在多个静态物体上附加单个形状有更好的性能。在内部,Box2D 会设置静态物体的质
量,并把质量反转为零,这使得大部分算法都不必把静态物体当成特殊情况来看待。
Box2D 并不保存物体定义的引用,也不保存其任何数据(除了用户数据指针),所以你可以创建临时的
物体定义,并复用同样的物体定义。
Box2D 允许你通过删除 b2World 对象来摧毁物体,它会为你做所有的清理工作。然而,你必须小心
地处理那些已失效的物体指针。
5.4 使用物体
在创建完一个物体之后,你可以对它进行许多操作。其中包括设置质量,访问其位置和速度,施加
力,以及转换点和向量。
5.4.1 质量数据
你可以在运行时调整一个物体的质量,这通常是在添加或移除物体上之形状时完成的。可能你会根据
物体上的当前形状来调整其质量。
void SetMassFromShapes();
可能你也会直接设置质量。例如,你可能会改变形状,但你只想使用自己的质量公式。
void SetMass(const b2MassData* massData);
通过以下这些函数可以获得物体的质量数据:
float32 GetMass() const;
float32 GetInertia() const;
const b2Vec2& GetLocalCenter() const;
5.4.2 状态信息
物体的状态含有多个方面,通过这些函数你可以访问这些状态数据:
bool IsBullet() const;
void SetBullet(bool flag);
bool IsStatic() const;
bool IsDynamic() const;
bool IsFrozen() const;
bool IsSleeping() const;
void AllowSleeping(bool flag);
void WakeUp();
The bullet state is described in Section 5.2.5, “Bullets”. The frozen state is described in
Section 9.1, “World Boundary”.
其中,子弹状态在 5.2.5 子弹 中有描述,冻结状态在 9.1 世界边界 中有描述。
5.4.3 位置和速度
你可以访问一个物体的位置和角度,这在你渲染相关游戏角色时很常用。你也可以设置位置,尽管这
不怎么常用。
bool SetXForm(const b2Vec2& position, float32 angle);
const b2XForm& GetXForm() const;
const b2Vec2& GetPosition() const;
float32 GetAngle() const;
你可以访问世界坐标的质心。许多 Box2D 内部的模拟都使用质心,然而,通常你不必访问它。取而
代之,你一般应该关心物体变换。
const b2Vec2& GetWorldCenter() const;
你可以访问线速度与角速度,线速度是对于质心所言的。
void SetLinearVelocity(const b2Vec2& v);
b2Vec2 GetLinearVelocity() const;
void SetAngularVelocity(float32 omega);
float32 GetAngularVelocity() const;
5.4.4 力和冲量
你可以对一个物体应用力,扭矩,以及冲量。当应用一个力或冲量时,你需要提供一个世界位置。这
常常会导致对质心的一个扭矩。
void ApplyForce(const b2Vec2& force, const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyImpulse(const b2Vec2& impulse, const b2Vec2& point);
应用力,扭矩或冲量会唤醒物体,有时这是不合需求的。例如,你可能想要应用一个稳定的力,并允
许物体休眠来提升性能。这时,你可以使用这样的代码:
if (myBody->IsSleeping() == false)
{
myBody->ApplyForce(myForce, myPoint);
}
5.4.5 坐标转换
物体类包含一些工具函数,它们可以帮助你在局部和世界坐标系之间转换点和向量。如果你不了解这
些概念,请看 Jim Van Verth 和 Lars Bishop 的“Essential Mathematics for Games and Interactive
Applications”。这些函数都很高效,所以可放心使用。
b2Vec2 GetWorldPoint(const b2Vec2& localPoint);
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);
5.4.6 列表
你可以遍历一个物体的形状,其主要用途是帮助你访问形状之用户数据。
for (b2Shape* s = body->GetShapeList(); s; s = s->GetNext())
{
MyShapeData* data = (MyShapeData*)s->GetUserData();
... do something with data ...
}
你也可以用类似的方法遍历物体的关节列表。