Box2D 中文教程


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 ... } 你也可以用类似的方法遍历物体的关节列表。 6 形状 6.1 关于 形状就是物体上的碰撞几何结构。另外形状也用于定义物体的质量。也就是说,你来指定密 度,Box2D 可以帮你计算出质量。 形状具有摩擦和恢复的性质。形状还可以携带筛选信息,使你可以防止某些游戏对象之间的碰撞。 形状永远属于某物体,单个物体可以拥有多个形状。形状是抽象类,所以在 Box2D 中可以实现许多 类型的形状。如果你有勇气,那便可以实现出自己的形状类型(和碰撞算法)。 6.2 形状定义 形状定义用于创建形状。通用的形状数据会保存在 b2ShapeDef 中,特殊的形状数据会保存在其派生 类中。 6.2.1 摩擦和恢复 摩擦可以使对象逼真地沿其它对象滑动。Box2D 支持静摩擦和动摩擦,但使用相同的参数。摩擦在 Box2D 中会被正确地模拟,并且摩擦力的强度与正交力(称之为库仑摩擦)成正比。摩擦参数经常会设置 在 0 到 1 之间,0 意味着没有摩擦,1 会产生强摩擦。当计算两个形状之间的摩擦时,Box2D 必须联合 两个形状的摩擦参数,这是通过以下公式完成的: float32 friction; friction = sqrtf(shape1->friction * shape2->friction); 恢复可以使对象弹起,想象一下,在桌面上方丢下一个小球。恢复的值通常设置在 0 到 1 之间,0 的 意思是小球不会弹起,这称为非弹性碰撞;1 的意思是小球的速度会得到精确的反射,这称为完全弹性碰 撞。恢复是通过这样的公式计算的: float32 restitution; restitution = b2Max(shape1->restitution, shape2->restitution); 当一个形状发生多碰撞时,恢复会被近似地模拟。这是因为 Box2D 使用了迭代求解器。当冲撞速度 很小时,Box2D 也会使用非弹性碰撞,这是为了防止抖动。 6.2.2 密度 Box2D 可以根据附加形状的质量分配来计算物体的质量以及转动惯量。直接指定物体质量常常会导致 不协调的模拟。因此,推荐的方法是使用 b2Body::SetMassFromShape 来根据形状设置质量。 6.2.3 筛选 碰撞筛选是一个防止某些形状发生碰撞的系统。譬如说,你创造了一个骑自行车的角色。你希望自行 车与地形之间有碰撞,角色与地形有碰撞,但你不希望角色和自行车之间发生碰撞 (因为它们必须重 叠)。Box2D 通过种群和组支持了这样的碰撞筛选。 Box2D 支持 16 个种群,对于任何一个形状你都可以指定它属于哪个种群。你还可以指定这个形状可 以和其它哪些种群发生碰撞。例如,你可以在一个多人游戏中指定玩家之间不会碰撞,怪物之间也不会 碰撞,但是玩家和怪物会发生碰撞。这是通过掩码来完成的,例如: playerShapeDef.filter.categoryBits = 0x0002; monsterShapeDef.filter.categoryBits = 0x0004; playerShape.filter.maskBits = 0x0004; monsterShapeDef.filter.maskBits = 0x0002; 碰撞组可以让你指定一个整数的组索引。你可以让同一个组的所有形状总是相互碰撞(正索引)或永远 不碰撞(负索引)。组索引通常用于一些以某种方式关联的事物,就像自行车的那些部件。在下面的例子 中,shape1 和 shape2 总是碰撞,而 shape3 和 shape4 永远不会碰撞。 shape1Def.filter.groupIndex = 2; shape2Def.filter.groupIndex = 2; shape3Def.filter.groupIndex = -8; shape4Def.filter.groupIndex = -8; 不同组索引之间形状的碰撞会按照种群和掩码来筛选。换句话说,组筛选比种群筛选有更高的优选 权。 注意在 Box2D 中的其它碰撞筛选,这里是一个列表: • 静态物体上的形状永远不会与另一个静态物体上的形状发生碰撞 • 同一个物体上的形状之间永远不会发生碰撞 • 你可以有选择地启用或禁止由关节连接之物体上的形状之间是否碰撞 有时你可能希望在形状创建之后去改变其碰撞筛选,你可以使用 b2Shape::GetFilterData 以及 b2Shape::SetFilterData 来存取已存在形状之 b2FilterData 结构。Box2D 会缓存筛选结果,所以你需 要使用 b2World::Refilter 手动地进行重筛选。 6.2.4 传感器 有时候游戏逻辑需要判断两个形状是否相交,但却不应该有碰撞反应。这可以通过传感器 (sensor)来 完成。传感器会侦测碰撞而不产生碰撞反应。 你可以将任一形状标记为传感器,传感器可以是静态或动态的。记得,每个物体上可以有多个形状, 并且传感器和实体形状是可以混合的。 myShapeDef.isSensor = true; 6.2.5 圆形定义 b2CircleDef 扩充了 b2ShapeDef 并增加一个半径和一个局部位置。 b2CircleDef def; def.radius = 1.5f; def.localPosition.Set(1.0f, 0.0f); 6.2.6 多边形定义 b2PolyDef 用于定义凸多边形。要正确地使用需要一点点技巧,所以请仔细阅读。最大顶点数由 b2_maxPolyVertices 定 义 , 当 前 是 8 。 如 果 你 需 要 更 多 顶 点 , 你 必 须 修 改 b2Settings.h 中 的 b2_maxPolyVertices。 当创建多边形定义时,你需要给出所用的顶点数目。这些顶点必须按照相对于右手坐标系之 z 轴逆时 针(CCW)的顺序定义。在你的屏幕上可能是顺时针的,这取决于你的坐标系统规则。 多边形必须是凸多边形,也就是,每个顶点都必须指向外面。最后,你也不应该重叠任何顶 点。Box2D 会自动地封闭环路。 这里是一个三角形的多边形定义的例子: b2PolygonDef triangleDef; triangleDef.vertexCount = 3; triangleDef.vertices[0].Set(-1.0f, 0.0f); triangleDef.vertices[1].Set(1.0f, 0.0f); triangleDef.vertices[2].Set(0.0f, 2.0f); 顶点会被定义于父物体之坐标系中。如果你需要在物体内偏移多边形,那就偏移所有顶点。 为了方便,有些函数可以把多边形初始化为矩形。可以是轴对齐的、中心点位于物体原点的矩形,或 者是有角度有偏移的矩形。 b2PolygonDef alignedBoxDef; float32 hx = 1.0f; // half-width float32 hy = 2.0f; // half-height alignedBodyDef.SetAsBox(hx, hy); b2PolygonDef orientedBoxDef; b2Vec2 center(-1.5f, 0.0f); float32 angle = 0.5f * b2_pi; orientedBoxDef.SetAsBox(hx, hy, center, angle); 6.3 形状工厂 初始化一个形状定义,而后将其传递给父物体;形状就是这样创建的。 b2CircleDef circleDef; circleDef.radius = 3.0f; circleDef.density = 2.5f; b2Shape* myShape = myBody->CreateShape(&circleDef); // [optionally store shape pointer somewhere] 这样就创建了形状,并将其添加到了物体之上。你无须存储形状的指针,因为当父物体摧毁时形状也 将自动地摧毁(请看 9.2 隐式摧毁)。 在添加了形状到物体之后,你可能需要根据形状重新计算物体的质量性质。 myBody->SetMassFromShapes(); 这个函数成本较高,所以你应该只在需要时使用它。 你可以轻易地摧毁物体上的一个形状,你可以此来模塑一个可破碎的对象。否则其实你可以忘记那些 形状,物体摧毁时那些形状也会摧毁。 myBody->DestroyShape(myShape); 移除物体上的形状之后,你可能需要再次调用 SetMassFromShapes。 6.4 使用形状 并没有太多需要讲解的东西。你可以得到一个形状的类型和其父物体。另外你也可以测试一个点是否 包含于形状之内。细节请查看 b2Shape.h。 7 关节 7.1 关于 关节的作用是把物体约束到世界,或约束到其它物体上。在游戏中的典型例子是木偶,跷跷板和滑 轮。关节可以用许多种不同的方法结合起来,创造出有趣的运动。 有些关节提供了限制(limit),以便你控制运动范围。有些关节还提供了马达(motor),它可以以指定的 速度驱动关节,直到你指定了更大的力或扭矩。 关节马达有许多不同的用途。你可以使用关节来控制位置,只要提供一个与目标之距离成正比例的关 节速度即可。你还可以模拟关节摩擦:将关节速度置零,并且提供一个小的、但有效的最大力或扭矩; 那么马达就会努力保持关节不动,直到负载变得过大。 7.2 关节定义 各种关节类型都派生自 b2JointDef。所有关节都连接两个不同的物体,可能其中一个是静态物体。如 果你想浪费内存的话,那就创建一个连接两个静态物体的关节 :) 你可以为任何一种关节指定用户数据。你还可以提供一个标记,用于预防相连的物体发生碰撞。实际 上,这是默认行为,你可以设置 collideConnected 布尔值来允许相连的物体碰撞。 很多关节定义需要你提供一些几何数据。一个关节常常需要一个锚点(anchor point)来定义,这是固 定于相接物体中的点。在 Box2D 中这些点需要在局部坐标系中指定,这样,即便当前物体的变化违反了 关节约束,关节还是可以被指定 —— 在游戏存取进度时这经常会发生。另外,有些关节定义需要默认的 物体之间的相对角度。这样才能通过关节限制或固定的相对角来正确地约束旋转。 初始化几何数据可能有些乏味。所以很多关节提供了初始化函数,消除了大部分工作。然而,这些初 始化函数通常只应用于原型,在产品代码中应该直接地定义几何数据。这能使关节行为更加稳固。 其余的关节定义数据依赖于关节的类型。下面我们来介绍它们。 7.2.1 距离关节 距离关节是最简单的关节之一,它描述了两个物体上的两个点之间的距离应该是常量。当你指定一个 距离关节时,两个物体必须已在应有的位置上。随后,你指定两个世界坐标中的锚点。第一个锚点连接 到物体 1,第二个锚点连接到物体 2。这些点隐含了距离约束的长度。 这是一个距离关节定义的例子。在此我们允许了碰撞。 b2DistanceJointDef jointDef; jointDef.Initialize(myBody1, myBody2, worldAnchorOnBody1, worldAnchorOnBody2); jointDef.collideConnected = true; 7.2.2 旋转关节 一个旋转关节会强制两个物体共享一个锚点,即所谓铰接点。旋转关节只有一个自由度:两个物体的 相对旋转。这称之为关节角。 要指定一个旋转关节,你需要提供两个物体以及一个世界坐标的锚点。初始化函数会假定物体已经在 应有位置了。 在此例中,两个物体被旋转关节连接于第一个物体之质心。 b2RevoluteJointDef jointDef; jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter()); 在 body2 逆时针旋转时,关节角为正。像所有 Box2D 中的角度一样,旋转角也是弧度制的。按规 定,使用那个 Initialize() 创建关节时,旋转关节角为 0,无论两个物体当前的角度怎样。 有时候,你可能需要控制关节角。为此,旋转关节可以模拟关节限制和马达。 关节限制会强制保持关节角度在一个范围内,为此它会应用足够的扭矩。范围内应该包括 0,否则在 开始模拟时关节会倾斜。 关节马达允许你指定关节的速度(角度之时间导数),速度可正可负。马达可以有无穷的力量,但这通 常没有必要。你是否听过这句话: • 注意:“当一个不可抵抗力遇到一个不可移动物体时会发生什么?” 我可以告诉你这并不有趣。所以你可以为关节马达提供一个最大扭矩。关节马达会维持在指定的速 度,除非其所需的扭矩超出了最大扭矩。当超出最大扭矩时,关节会慢下来,甚至会反向运动。 你还可以使用关节马达来模拟关节摩擦。只要把关节速度设置为 0,并设置一个小且有效的最大扭矩 即可。这样马达会试图阻止关节旋转,但它会屈服于过大的负载。 这里是对上面旋转关节定义的修订;这次,关节拥有一个限制以及一个马达,后者用于模拟摩擦。 b2RevoluteJointDef jointDef; jointDef.Initialize(body1, body2, myBody1->GetWorldCenter()); jointDef.lowerAngle = -0.5f * b2_pi; // -90 degrees jointDef.upperAngle = 0.25f * b2_pi; // 45 degrees jointDef.enableLimit = true; jointDef.maxMotorTorque = 10.0f; jointDef.motorSpeed = 0.0f; jointDef.enableMotor = true; 7.2.3 移动关节 移动关节(prismatic joint)允许两个物体沿指定轴相对移动,它会阻止相对旋转。因此,移动关节只有 一个自由度。 移动关节的定义有些类似于旋转关节;只是转动角度换成了平移,扭矩换成了力。以这样的类比,我 们来看一个带有关节限制以及马达摩擦的移动关节定义: b2PrismaticJointDef jointDef; b2Vec2 worldAxis(1.0f, 0.0f); jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter(), worldAxis); jointDef.lowerTranslation = -5.0f; jointDef.upperTranslation = 2.5f; jointDef.enableLimit = true; jointDef.motorForce = 1.0f; jointDef.motorSpeed = 0.0f; jointDef.enableMotor = true; 旋转关节隐含着一个从屏幕射出的轴,而移动关节明确地需要一个平行于屏幕的轴。这个轴会固定于 两个物体之上,沿着它们的运动方向。 就像旋转关节一样,当使用 Initialize() 创建移动关节时,移动为 0。所以一定要确保移动限制范围内 包含了 0。 7.2.4 滑轮关节 滑轮关节用于创建理想的滑轮,它将两个物体接地(ground)并连接到彼此。这样,当一个物体升起 时,另一个物体就会下降。滑轮的绳子长度取决于初始时的状态。 length1 + length2 == constant 你还可以提供一个系数(ratio)来模拟滑轮组,这会使滑轮一侧的运动比另一侧要快。同时,一侧的约 束力也比另一侧要小。你也可以用这个来模拟机械杠杆(mechanical leverage)。 length1 + ratio * length2 == constant 举个例子,如果系数是 2,那么 length1 的变化会是 length2 的两倍。另外连接 body1 的绳子的约 束力将会是连接 body2 绳子的一半。 当滑轮的一侧完全展开时,另一侧的绳子长度为零,这可能会出问题。此时,约束方程将变得奇异(糟 糕)。因此,滑轮关节约束了每一侧的最大长度。另外出于游戏原因你可能也希望控制这个最大长度。最 大长度能提高稳定性,以及提供更多的控制。 这是一个滑轮定义的例子: b2Vec2 anchor1 = myBody1->GetWorldCenter(); b2Vec2 anchor2 = myBody2->GetWorldCenter(); b2Vec2 groundAnchor1(p1.x, p1.y + 10.0f); b2Vec2 groundAnchor2(p2.x, p2.y + 12.0f); float32 ratio = 1.0f; b2PulleyJointDef jointDef; jointDef.Initialize(myBody1, myBody2, groundAnchor1, groundAnchor2, anchor1, anchor2, ratio); jointDef.maxLength1 = 18.0f; jointDef.maxLength2 = 20.0f; 7.2.5 齿轮关节 如果你想要创建复杂的机械装置,你可能需要齿轮。原则上,在 Box2D 中你可以用复杂的形状来模 拟轮齿,但这并不十分高效,而且这样的工作可能有些乏味。另外,你还得小心地排列齿轮,保证轮齿 能平稳地啮合。Box2D 提供了一个创建齿轮的更简单的方法:齿轮关节。 齿轮关节需要两个被旋转关节或移动关节接地(ground)的物体,你可以任意组合这些关节类型。另 外,创建旋转或移动关节时,Box2D 需要地(ground)作为 body1。 类似于滑轮的系数,你可以指定一个齿轮系数(ratio),齿轮系数可以为负。另外值得注意的是,当一 个是旋转关节(有角度的)而另一个是移动关节(平移)时,齿轮系数是长度或长度分之一。 coordinate1 + ratio * coordinate2 == constant 这是一个齿轮关节的例子: b2GearJointDef jointDef; jointDef.body1 = myBody1; jointDef.body2 = myBody2; jointDef.joint1 = myRevoluteJoint; jointDef.joint2 = myPrismaticJoint; jointDef.ratio = 2.0f * b2_pi / myLength; 注意,齿轮关节依赖于两个其它关节,这是脆弱的:当其它关节被删除了会发生什么? • 注意:齿轮关节总应该先于旋转或移动关节被删除,否则你的代码将会由于齿轮关节中的无效关节 指针而导致崩溃。另外齿轮关节也应该在任何相关物体被删除之前删除。 7.2.6 鼠标关节 在 testbed 中,鼠标关节用于通过鼠标来操控物体。细节请看 testbed 以及 b2MouseJoint.h。 7.3 关节工厂 关节是通过世界的工厂方法来创建和摧毁的,这引出了一个旧问题: • 注意:不要试图在栈上创建物体或关节,也不要使用 new 或 malloc 在堆上创建。物体以及关节必 须要通过 b2World 类的方法来创建或摧毁。 这是一个关于旋转关节生命期的例子: b2RevoluteJointDef jointDef; jointDef.body1 = myBody1; jointDef.body2 = myBody2; jointDef.anchorPoint = myBody1->GetCenterPosition(); b2RevoluteJoint* joint = myWorld->CreateJoint(&jointDef); // ... do stuff ... myWorld->DestroyJoint(joint); joint = NULL; 摧毁之后将指针清零总是一个好的方式。如果你试图使用它,程序也会以可控的方式崩溃。 关节的生命期并不简单。请留意这个警告: • 注意:当物体被摧毁时其上的关节也会摧毁。 并不总是需要这个防范。如果你的游戏引擎会总是在摧毁物体之前摧毁关节,你就无须实现监听类 了。更多细节请看 9.2 隐式摧毁。 7.4 使用关节 在许多模拟中,关节被创建之后便不再被访问了。然而,关节中包含着很多有用的数据,使你可以创 建出丰富的模拟。 首先,你可以在关节上得到物体,锚点,以及用户数据。 b2Body* GetBody1(); b2Body* GetBody2(); b2Vec2 GetAnchor1(); b2Vec2 GetAnchor2(); void* GetUserData(); 所有的关节都有反作用力和反扭矩,这个反作用力应用于 body2 的锚点之上。你可以用反作用力来 折断关节,或引发其它游戏事件。这些函数可能需要做一些计算,所以不要在不需要的时候调用它们。 b2Vec2 GetReactionForce(); float32 GetReactionTorque(); 7.4.1 使用距离关节 距离关节没有马达以及关节限制,所以它没有额外的运行时方法。 7.4.2 使用旋转关节 你可以访问旋转关节的角度,速度,以及扭矩。 float32 GetJointAngle() const; float32 GetJointSpeed() const; float32 GetMotorTorque() const; 你也可以在每步中更新马达参数。 void SetMotorSpeed(float32 speed); void SetMaxMotorTorque(float32 torque); 关节马达有一些有趣的能力。你可以在每个时间步中更新关节速度,这可以使关节像正弦波一样来回 移动,或者按其它什么函数运动。 // ... Game Loop Begin ... myJoint->SetMotorSpeed(cosf(0.5f * time)); // ... Game Loop End ... 你还可以使用关节马达来追踪某个关节角度。例如: // ... Game Loop Begin ... float32 angleError = myJoint->GetJointAngle() - angleTarget; float32 gain = 0.1f; myJoint->SetMotorSpeed(-gain * angleError); // ... Game Loop End ... 通常来讲你的增益参数不应过大,否则你的关节可能会变得不稳定。 7.4.3 使用移动关节 移动关节的用法类似于旋转关节,这是它的相关成员函数: float32 GetJointTranslation() const; float32 GetJointSpeed() const; float32 GetMotorForce() const; void SetMotorSpeed(float32 speed); void SetMotorForce(float32 force); 7.4.4 使用滑轮关节 滑轮关节提供了当前长度。 float32 GetLength1() const; float32 GetLength2() const; 7.4.5 使用齿轮关节 齿轮关节不提供任何多于 b2Joint 中定义的信息。 7.4.6 使用鼠标关节 通过在时间步中更新目标位置,鼠标关节可以控制连接的物体。 8 接触 8.1 关于 接触(contact)是由 Box2D 创建的用于管理形状间碰撞的对象。接触有不同的种类,它们都派生自 b2Contact,用于管理不同类型形状之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间 碰撞的类。通常这对你并不重要,我只是想或许你愿意了解一些。 这里是 Box2D 中的一些与碰撞有关的术语,但你也可能会在其它物理引擎中发现类似的术语。 触点(contact point) 两个形状相互接触的点。实际上当物体的表面相接触时可能会有一定接触区域,在 Box2D 则近似地 以少数点来接触。 接触向量(contact normal) 从 shape1 指向 shape2 的单位向量。 接触分隔(contact separation) 分隔相反于穿透,当形状相重叠时,分隔为负。可能以后的 Box2D 版本中会以正隔离来创建触点, 所以当有触点的报告时你可能会检查符号。 法向力(normal force) Box2D 使用了一个迭代接触求解器,并会以触点保存结果。你可以安全地使用法向力来判断碰撞强 度。例如,你可以使用这个力来引发破碎,或者播放碰撞的声音。 切向力(tangent force) 它是接触求解器关于摩擦力的估计量。 接触标识(contact ids) Box2D 会试图利用一个时间步中的触点压力(contact force)结果来推测下一个时间步中的情况。接触 标识用于匹配跨越时间步的触点,它包含了几何特征索引以便区分触点。 当两个形状的 AABB 重叠时,接触就被创建了。有时碰撞筛选会阻止接触的创建,有时尽管碰撞已筛 选了 Box2D 还是须要创建一个接触,这种情况下它会使用 b2NullContact 来防止碰撞的发生。当 AABB 不再重叠之后接触会被摧毁。 也许你会皱起眉头,为了没有发生实际碰撞的形状(只是它们的 AABB)却创建了接触。好吧,的确是 这样的,这是一个“鸡或蛋”的问题。我们并不知道是否需要一个接触,除非我们创建一个接触去分析 碰撞。如果形状之间没有发生碰撞,我们需要正确地删除接触,或者,我们可以一直等到 AABB 不再重 叠。Box2D 选择了后面这个方法。 8.2 接触监听器 通过实现 b2ContactListener 你就可以接受接触数据。当一个触点被创建时,当它持续超过一个时间 步时,以及当它被摧毁时,这个监听器(listener)就会发出报告。请留意两个形状之间可能会有多个触 点。 class MyContactListener : public b2ContactListener { public: void Add(const b2ContactPoint* point) { // handle add point } void Persist(const b2ContactPoint* point) { // handle persist point } void Remove(const b2ContactPoint* point) { // handle remove point } void Result(const b2ContactResult* point) { // handle results } }; • 注意:不要保存 b2ContactListener 中触点的引用,取而代之,用深拷贝将触点数据保存到你自己 的缓冲区中。下面的例子演示了一种方法。 连续性的物理模拟使用了子步,所以一个触点可能会创建和摧毁于同一个时间步中。通常这不是问 题,但你的代码应该温雅地处理它。 当触点创建,持续或删除时,会有即刻的报告。这出现于求解器调用之前,所以 b2ContactPoint 并 不包含已计算的冲量。然而,触点处的相对速度提供了,这样你可以估计出接触冲量。如果你实现了结 果监听函数,那么在求解器调用之后你就会收到 b2ContactResult 对象,包含了可靠的触点信息。这些 保存结果的结构中包含了子步的冲量。重申一次,由于连续性的物理模拟,在一个 b2World::Step 中你 可能会收到单个触点的多个结果。 在一个接触回调中去改变物理世界是诱人的。例如,你可能会以碰撞 来施加伤害,并试图摧毁关联的角色和它的刚体。然而,Box2D 并不允许你在回调中改变物理世界,因 为你可能会摧毁 Box2D 正在运算的对象,造成野指针。 处理触点的推荐方法是缓冲所有你关心的触点,并在时间步之后处理它们。一般在时间步之后你应该 立即处理它们,否则其它客户端代码可能会改变物理世界,使你的缓冲失效。当你处理触点缓冲的时 候,你可以去改变物理世界,但是你仍然应该小心不要造成无效的指针。在 testbed 中有安全处理触点 的例子。 这是一小段 CollisionProcessing 测试中的代码,它演示了在操作触点缓冲时如何处理孤立物体。请 注意注释。代码假定所有触点都缓冲于 b2ContactPoint 数组 m_points 中。 // We are going to destroy some bodies according to contact // points. We must buffer the bodies that should be destroyed // because they may belong to multiple contact points. const int32 k_maxNuke = 6; b2Body* nuke[k_maxNuke]; int32 nukeCount = 0; // Traverse the contact buffer. Destroy bodies that // are touching heavier bodies. for (int32 i = 0; i < m_pointCount; ++i) { ContactPoint* point = m_points + i; b2Body* body1 = point->shape1->GetBody(); b2Body* body2 = point->shape2->GetBody(); float32 mass1 = body1->GetMass(); float32 mass2 = body2->GetMass(); if (mass1 > 0.0f && mass2 > 0.0f) { if (mass2 > mass1) { nuke[nukeCount++] = body1; } else { nuke[nukeCount++] = body2; } if (nukeCount == k_maxNuke) { break; } } } // Sort the nuke array to group duplicates. std::sort(nuke, nuke + nukeCount); // Destroy the bodies, skipping duplicates. int32 i = 0; while (i < nukeCount) { b2Body* b = nuke[i++]; while (i < nukeCount && nuke[i] == b) { ++i; } m_world->DestroyBody(b); } 8.3 接触筛选 通常,你不希望游戏中的所有物体都发生碰撞。例如,你可能会创建一个只有某些角色才能通过的 门。这称之为接触筛选,因为一些交互被筛选出了。 通过实现 b2ContactFilter 类,Box2D 允许定制接触筛选。这个类需要一个 ShouldCollide 函数, 用于接收两个 b2Shape 的指针,如果应该碰撞那么就返回 true。 默认的 ShouldCollide 实现使用了 6 形状 中的 b2FilterData。 bool b2ContactFilter::ShouldCollide(b2Shape* shape1, b2Shape* shape2) { const b2FilterData& filter1 = shape1->GetFilterData(); const b2FilterData& filter2 = shape2->GetFilterData(); if (filter1.groupIndex == filter2.groupIndex && filter1.groupIndex != 0) { return filter1.groupIndex > 0; } bool collide = (filter1.maskBits & filter2.categoryBits) != 0 && (filter1.categoryBits & filter2.maskBits) != 0; return collide; } 9 杂项 9.1 世界边界 你可以实现一个 b2BoundaryListener,这样当有物体超出世界的 AABB 时 b2World 就能通知你。 当你得到回调时,你不应该试图删除物体;取而代之的是,你可以为角色做个删除或错误处理标记,在 物理时间步之后再进行这个事件的处理。 class MyBoundaryListener : public b2BoundaryListener { void Violation(b2Body* body) { MyActor* myActor = (MyActor*)body->GetUserData(); myActor->MarkForErrorHandling(); } }; 随后你可以在世界对象中注册你的边界监听器实例,这应该安排在世界初始化过程中。 myWorld->SetListener(myBoundaryListener); 9.2 隐式摧毁 Box2D 并不使用引用计数。所以当你摧毁一个物体后,它的确就不存在了。以指针访问一个已摧毁物 体的行为是未定义的,换句话说,你的程序可能会崩溃。有些时候,调试模式的内存管理器可能会帮助 找到这些问题。 如果你摧毁一个 Box2D 实体,你应该保证所有到它的引用都删除了。如果你只有实体的单个引用的 话,那就简单了。但如果你有很多个引用,你可能要考虑实现一个处理类来封装原始指针。 通常使用 Box2D 时你需要创建并摧毁许多物体,形状还有关节。管理这些实体有些自动化,如果你 摧毁一个物体,所有它的形状,关节,以及接触都会摧毁,这称为隐式摧毁。任何连接于这些关节或接 触之一的物体将被唤醒,通常这是便利的。然而,你应该意识到了一个关键问题: • 注意:当一个物体摧毁时,所有它的形状和关节都会自动摧毁。你应该将任何指向这些形状或关节 的指针置零,否则之后如果你试图访问它们,你的程序会崩溃。 Box2D 提供了一个名为 b2WorldListener 的监听器类,你可以实现它并提供给世界对象,随后当关 节将被隐式摧毁时世界对象就会提醒你。 你可以实现一个 b2DestructionListener,这样当一个形状或关节隐式摧毁时 b2World 就能通知 你,这可以帮助你预防访问无效指针。 class MyDestructionListener : public b2DestructionListener { void SayGoodbye(b2Joint* joint) { // remove all references to joint. } }; 随后你可以注册它,这应该在世界初始化过程中。 myWorld->SetListener(myDestructionListener); 10 设置 10.1 关于 Box2D 为用户提供了 b2Settings.h 和 b2Settings.cpp 两个定制源文件。 Box2D 使用浮点数工作,所以使用了一些公差,以便能良好的运行。 10.2 公差 许多设置依赖于 MKS 单位,在 3.3 单位 中有更多解释。个别公差解释请看 doxygen 文档。 10.3 内存分配 除了下面的情况以外,所有 Box2D 中的内存分配都是通过 b2Alloc 和 b2Free 完成的。 • b2World 可以在栈上建造,或任意你喜欢的地方。 • 其它非工厂方法创建的 Box2D 类,包括回调类和触点缓冲。 可以自由修改 b2Settings.cpp 来改变分配动作。 11 调试绘图 实现 b2DebugDraw 可得到物理世界的细部图,这里是可用的实体: • 形状轮廓 • 关节连通性 • 核心形状(为连续碰撞) • broad-phase AABB,包括世界 AABB • 多边形 OBB • broad-phase pair(潜在接触) • 质心 这是绘制这些物理实体的首选方法,优于直接访问数据。理由是许多必要信息都是内在的。 testbed 使用了调试绘图设施以及接触监听器来绘制物理实体,所以它是实现调试绘图的主要例子。 简体中文翻译信息 • 原文:Box2D v2.0.2 User Manual • 中文译者:Aman JIANG(江超宇) • 主页:lesslab.com 电子邮件:amanjiang@gmail.com
还剩30页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

王丰wf

贡献于2013-12-11

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf