目录

我的学习分享

记录精彩的程序人生

【Box2D】16.Box2D C++ 教程-碰撞剖析

https://blog.csdn.net/Const_Gong/article/details/51464567

声明:本教程翻译自:Box 2D C++ turorials - Anatomy of a collision,仅供学习参考。

在Box2D中,经常会遇到物体之间的碰撞问题,当一个碰撞发生时,就是利用定制器(fixtures)用来做碰撞检测的。碰撞可以以很多种方式进行,我们可以在碰撞过程中获取大量信息用在游戏逻辑中。

比如说,你可能想知道这几条信息:

  • 碰撞的开始和结束
  • 定制器中的哪个点有碰撞接触
  • 定制器之间法向量的关系
  • 碰撞过程中产生了多大的能量以及会产生有多大的响应

通常情况下碰撞发生的速度非常快,但是这篇文章我们会对一个碰撞进行讨论,把它的速度降到很慢,以此让我们来仔细看看碰撞过程中到底发生了什么,以及我们可以从中得到哪些相关信息。

故事发生在两个多边形定制器(fixtures)之间,为了能够更好的控制这两个多边形,我们选择在失重的世界(重力加速度为零)创建它们。其中一个是静止的四方盒子,另一个是面向四方盒子进行横向移动的三角形。

anatomyscenario.png

场景中,三角形底部的角会和盒子上方的角发生碰撞。话说为什么要设置成这样的碰撞场景,原因不是本次讨论的重点,本次的重点是碰撞进行过程的不同阶段我们可以得到哪些信息,如果你想重现这里提到的碰撞场景,可以直接到源代码中查看。

  • 获取碰撞信息

b2Contact对象包含了碰撞的信息。从对象中可以得知哪两个定制器发生了碰撞,以及碰撞的位置和碰撞之后的反作用方向。在Box2D中有两个方法可以获取b2Contact对象,一个是遍历接触(contacts)链表每一个物体,另外一种方法是用接触监听器(contact listener)。下面先让我们快速浏览一下接下来要讨论的两种方法。

-查看接触链表

你可以在任何时候,通过查看世界接触链表来获取当前所有接触:

  for (b2Contact* contact = world->GetContactList(); contact; contact = contact->GetNext())
      contact->... // do something with the contact

或者通过某个物体查看它的接触:

  for (b2ContactEdge* edge = body->GetContactList(); edge; edge = edge->next)
      edge->contact->... //do something with the contact

如果你使用这种方法,有一点非常重要的地方就是,即便链表中存在接触(contact)也并不代表着两个定制器之间正在发生接触-这仅仅代表了两个物体的AABB框发生了接触而已。如果你想知道定制器自身是否真正发生了碰撞,需要通过判断IsTouching()方法来检测。

-接触监听器

使用接触链表进行碰撞检测,对于需要进行大量快速的碰撞信息获取来说,显得就很低效。设置接触监听器可以让Box2D告诉你你所感兴趣的事情什么时候会发生,比起你只是为了得知碰撞的开始和结束而兴师动众的完成所有重量级工作(例如不停的遍历接触链表)来说,显得非常高效。接触监听器是一个包括四个方法的类(b2ContactListener),这些方法可以根据你的需要进行重写。

void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);

具体的事件内容取决于当前正在发生什么,一些事件只是给我们传递了b2Contact对象。世界中的Step方法执行过程中,当Box2D检测到某个事件发生时,就会回调这些方法,你就会知道到底发生了什么事情。实际当中的应用会在“碰撞回调”中讨论,这里我们只是关注碰撞什么时候发生。

通常我比较建议使用接触监听器这种方法来做碰撞检测。乍看起来它好像有点烦杂笨重,但从长远来看,这种方法更加高效和有用。

上述两种方法都可以获取接触,它们都包含了相同的信息。其中最根本的信息就是获取哪两个定制器发生了碰撞,可以通过如下方法获取:

  b2Fixture* a = contact->GetFixtureA();
  b2Fixture* b = contact->GetFixtureB();

如果你正在使用遍历接触链表的方法来检测碰撞,那么或许你已经知道发生碰撞的定制器是哪两个。但是如果你使用的方法是接触监听器,你需要依靠两个方法来判断哪两个定制器发生了碰撞。发生碰撞的定制器A和定制器B之间是没有次序关系的,所以你需要通过对定制器(fixtures)或物体(bodies)设置用户数据来说明当前的定制器属于哪一个。通过定制器,你可以通过GetBody()方法来找到对应的物体。

  • 拆解碰撞

现在让我们深入的来了解上面提到的碰撞场景中,所发生的一系列碰撞事件。幸运的是,我们可以通过表格的形势进行呈现(译者注:为了编辑方便偷了个懒,这里用了列表的形式,:P),表格的左边可以展现每一步的场景。你还通过testbed教程源代码页面下载源码,边运行边看教程。在testbed中,你可以对模拟器进行暂停和重新开始操作,然后按“Single Step”按键可以详细观察发生的一切。

我们就从定制器的AABB框尚未重叠的那一刻开始,这样我们就可以了解全程。单击’AABBs’选择框,来查看围绕在每个定制器外面的紫色四边形(译者注:这就是AABB框)。

anatomyaabbs.png

abc.png

  • 碰撞点和法向量

此时我们有了一个已经重叠的接触,这就意味着现在我们就可以回答文章一开始提出的几个问题。首先,让我们先获取位置和法向量,下面这段代码,我们假定你要么放到接触监听器的BeginContact方法中,要么是通过获取接触链表,放到手动检测物体接触链表方法中。

其中,接触以物体自身坐标系统为标准,存储了碰撞点的坐标信息,然而这对我们来说用处不大。但是我们可以利用接触获取更有用的’world manifold’,它以世界坐标系为标准存储了碰撞点的位置信息。’Manifold’只不过是凭空想出来,能更好的区分两个定制器的那条线的名字而已。

  // normal manifold contains all info...
  int numPoints = contact->GetManifold()->pointCount;
  
  // ...world manifold is helpful for getting locations
  b2WorldManifold worldManifold;
  contact->GetWorldManifold( &worldManifold );
  
  // draw collision points
  glBegin(GL_POINTS);
  for (int i = 0; i < numPoints; i++)
      glVertex2f(worldManifold.points[i].x, worldManifold.points[i].y);
  glEnd();

anatomymanifold1.png

当作用一个冲量,让两个定制器分开时,这些点就是碰撞反作用点。虽然这些点不是定制器第一次接触时精准的坐标点(除非你使用bullet类型物体)。实践中,这些点足够在游戏逻辑中使用。

下面让我们展示一下从定制器A指向定制器B的法向量:

  float normalLength = 0.1f;
  b2Vec2 normalStart = worldManifold.points[0] - normalLength * worldManifold.normal;
  b2Vec2 normalEnd = worldManifold.points[0] + normalLength * worldManifold.normal;
  
  glBegin(GL_LINES);
  glColor3f(1,0,0);// red
  glVertex2f( normalStart.x, normalStart.y );
  glColor3f(0,1,0);// green
  glVertex2f( normalEnd.x, normalEnd.y );
  glEnd();

具体看起来像是这样,以最快的方法解算出重叠产生的冲量,以此将三角形的一个角推向左上方,同时将四边形的这个角推向右下方。请注意法向量只不过是一个方向而已,它并没有坐标也没有连接其中任何一个点-我仅仅是图方便,画到了points[0]点而已。

anatomymanifold2.png

这里有一个很重要的一点是碰撞法向量并不能为你提供两个碰撞定制器之间的角度-还记得这里的三角形是水平移动的,对吧?它只能给出定制器不会重叠的短距离移动方向。比如,想象一下如果三角形移动的速度再快一点重叠的部分看起来像这样:

anatomymanifold3.png

…那么短距离内使两个定制器分离,会把三角形向右上方推开。这就可以明显看出,使用法向量来作为定制器的碰撞角度并不是一个好的办法。如果你想知道两个定制器实际所受影响的角度,可以这样:

  b2Vec2 vel1 = triangleBody->GetLinearVelocityFromWorldPoint( worldManifold.points[0] );
  b2Vec2 vel2 = squareBody->GetLinearVelocityFromWorldPoint( worldManifold.points[0] );
  b2Vec2 impactVelocity = vel1 - vel2;

…以此来获取碰撞过程中每个物体上点的实际相对速度向量。作为一个简单的例子,我们看看是否也可以获取三角形的线速度,因为我们知道四边形盒子是静态的并且三角形是不旋转的,但是上面的代码仍然考虑到了两个物体同时旋转或者平移的情况。

另外需要注意的一点是,不是所有碰撞都会有两个碰撞点。本实例我是特地找了一个复杂点的例子,其中多边形的两个角发生重叠,但是现实中更常见的碰撞情景是只有一个碰撞点。这里展示了一些只有一个碰撞点的例子:

anatomymanifold4.png

anatomymanifold5.png

anatomymanifold6.png

ok,现在我们已经知道如何找到碰撞点以及法向量,并且我们也了解到这些点和法向量将会被Box2D用来正确响应碰撞重叠。下面让我们继续回到事件序列的正轨上来…

defbe9c0c96.png.jpg

  • PreSolve和PostSolve

PreSovle和PostSolve方法都为你提供了一个b2Contact指针,那么我们就可以访问同一个指针以及在BeginContact方法中进行查看的常用信息。PreSolve方法为我们提供了一个在碰撞响应计算之前改变接触特性的机会,或者甚至是同时取消响应,而且通过PostSolve方法我们可以找到碰撞响应的具体信息。

下面是可以在PreSolve方法中对于接触做出的一些改变:

  void SetEnabled(bool flag);// non-persistent - need to set every time step
  
  // these available from v2.2.1
  void SetFriction(float32 friction);// persists for duration of contact
  void SetRestitution(float32 restitution);// persists for duration of contact

调用SetEnabled(false)方法将会关闭接触,这也就意味着正常的碰撞响应将会被跳过。你可以利用这一点暂时允许物体之间彼此穿透。一个典型的例子是单面墙或平台,玩家可以从一面穿过,另一面却是实墙,这只能在运行时根据不同的条件进行界定,类似于玩家此时的位置以及面部朝向,等等。

需要注意的重要的一点是接触的状态会在下一个时间步长恢复,所以如果你希望像上面那样关闭接触,那么每个时间步长都应该调用SetEnabled(false)方法。

除了接触指针以外,PreSolve还有第二个参数,此参数为我们提供了先前时间步长中关于碰撞的相关信息。如果有人知道这个参数用来做什么,也让我了解一下 :D

PostSolve方法在碰撞响应计算以及应用之后进行调用。它也有第二个参数,我们可以获取应用于碰撞的冲量信息。**对于这个信息更常用的地方是用来检查所产生的碰撞响应是否超过给定阀值,通过检查这个阀值可以判断某个物体是否会发生爆炸,等等。**详见’黏性抛体(“sticky projectiles”)’话题中的例子,使用PostSolve方法来检测当一支箭进行射击时是否应该黏到目标上。

Ok,回到时间线上…

ac.png

当调用EndContact方法的时候,接触链表会传入一个b2Contact指针,此时定制器将不再有接触,所以也不会再获得有效的相关信息。即便如此EndContact事件仍然是接触链表当中不可或缺的一部分,因为它允许你检查定制器/物体/游戏对象中哪个结束了接触。详见下一个话题中的例子。

  • 总结

希望此次话题,通过对Box2D碰撞事件毫秒级别一步步的讲解,能够让你对此有一个清晰的大致了解。这个话题看起来也许不是那么有意思(可以肯定的是,到目前为止应该是令人兴奋的话题!)但是我从一些论坛的问题中感受到,这里所讨论的一些细节经常被忽略掉,并且大部分时间都花在盲目的解决问题上,而不是真正了解到底是怎么回事。我还注意到一种回避使用接触监听器的倾向,从长远来看以此会让监听器承担更少的工作。了解这些细节可以让你真正理解实际过程是什么样的,可以让你有更好的设计,节约实现的时间。


  1. Box2D C++ 教程-简介
  2. Box2D C++ 教程-环境设置
  3. Box2D C++ 教程-Testbed结构
  4. Box2D C++ 教程-创建测试
  5. Box2D C++ 教程-物体
  6. Box2D C++ 教程-定制器
  7. Box2D C++ 教程-设置世界
  8. Box2D C++ 教程-力和冲量
  9. Box2D C++ 教程-自定义重力
  10. Box2D C++ 教程-匀速运动
  11. Box2D C++ 教程-旋转到指定角度
  12. Box2D C++ 教程-跳跃
  13. Box2D C++ 教程-使用debug Draw
  14. Box2D C++ 教程-画自己的图像
  15. Box2D C++ 教程-用户数据
  16. Box2D C++ 教程-碰撞剖析
  17. Box2D C++ 教程-源代码
  18. Box2D C++ 教程-碰撞回调
  19. Box2D C++ 教程-碰撞过滤
  20. Box2D C++ 教程-传感器
  21. Box2D C++ 教程-射线投射
  22. Box2D C++ 教程-查询 World
  23. Box2D C++ 教程-安全地移除物体
  24. Box2D C++ 教程-跳跃问题
  25. Box2D C++ 教程-幽灵顶点
  26. Box2D C++ 教程-连接器-概述
  27. Box2D C++ 教程-连接器-旋转
  28. Box2D C++ 教程-连接器-平移
  29. Box2D C++ 教程-开发环境设置(iPhone)