备注:由于本人最近在学习box2d引擎,而中文资料中好的文章比较少,我就在google上找了一些英文资料。于是,发现了网上的Box2Dtutorial系列文章,觉得写得挺好的,于是做了一些翻译和大家分享分享。由于这是我第一次翻译技术型文章,翻译不当的地方还请各位多理解。

下面我给出原文网址:http://www.iforce2d.net/b2dtut/user-data,本系列翻译文章仅用于学习交流之用,请勿用于商业用途。欢迎各位转载!

碰撞回调

当物体在物理场景中四处运动并发生两两碰撞时,Box2D会处理所有必需的碰撞检测和反应,因此我们没有必要担心这一点。但是所有的点进行物理游戏不是时常发生的事,这应该作为物体之间碰撞的结果。当一个玩家接触到怪物,这个玩家就应该死去,或者一个球碰撞到地面应该发出声音等等。我们需要一个方法从物理引擎中获取这个信息。

drawing your ownobjects 这一话题中,我们在游戏实体中引用了物体,并且检查每一帧来获得当前物体的位置并绘制它。这是个合理的事情,因为我们会在每一帧中渲染物体,并且locatin/rotation似乎在每一帧都会发生变化。但碰撞有些不同—---它们不会在每一帧中都出现,它们发生的频率低很多。所以在每一帧中都询问物理引擎是否有物体发生了碰撞,这就像一个坐在车上旅行的小孩持续不断地问“我们到了吗?”,这个回答几乎都是“No”。如果你打算在世界中为每一个实体都这样做,那么是很没效率的。对于司机来,说“保持安静并且我会告诉你什么时候一些事情会发生”是个明智的做法。

 

在Box2D中,我们通过callback函数来完成。这个函数的功能是在两个物体发生碰撞时,我们对它们进行一些设置。我们不会知道是那两个物体发生碰撞,因此在碰撞发生时,这两个物体必须作为参数传递给函数。然后我们会把这个函数传递给Box2D,这就想在说“当碰撞发生时,请调用这个函数”。

 

回调时间

在每一个时间步中,两个物体之间的碰撞都会在b2World::Step()函数中被检测。正如我们在 worlds这一话题知道的,b2World::Step函数处理模拟,移动所有的物体和诸如此类的事。一旦碰撞被检测到,程序执行会交给callback函数来执行一些事,然后再返回给b2World::Step函数继续处理更多的事情。有一点要注意的是,由于你的callback会在处理时间步时被调用,此时在当前场景下你不应该做任何的事情,因为可能在同一个时间步内会有更多的碰撞发生。

设置一个callback函数和实现debug draw功能的方法是一样的。我们创建一个集成b2ContactListener类的子类,这个类有很多的虚函数可以覆盖。这里是些我们要使用的函数:

//b2ContactListener

// Called when two fixtures begin to touch

virtualvoid BeginContact(b2Contact* contact);

 

// Called when two fixtures cease to touch

virtualvoid EndContact(b2Contact* contact);

注意,由于我们也可以在碰撞结束时进行检测,因此迟些我们都会用上它们。

Callback信息

一旦我们创建了b2ContactListener的子类,我们就可以知道一对物体什么时候发生碰撞,并且对他们采取相应措施。但等等….这里没有bodies提供给BeginContact/EndContact,那么我们要怎么才能辨别出那些物体发生了碰撞呢?碰撞参数中就包含了所有我们需要的信息。我们最想知道的是那两个fixture在碰撞中发生关系,在碰撞中,我们可以通过下面的这些函数获得上面的信息:

//b2Contact
    // Get the first fixture in this contact
    b2Fixture* GetFixtureA();
  
    // Get the second fixture in this contact
    b2Fixture* GetFixtureB();

但还是没有bodies….?fixture类有GetBody()函数,我们可以用这个函数获得每一个fixture的body。如果contact也有一个函数可以直接获取body,那会非常的方便,但实际上这是不必要的,我发现它做了一个很好的警示—--告诉我们碰撞时发生在fixtures之间,而不是bodies之间。为了重申这一点,并且澄清事物之间的关系,下面这个或许会有些帮助:

在这个例子中,dynamic body有四个fixtures,其中一个fixture开始和在它之下的只有一个fixture的static body发生碰撞。碰撞参数传递给BeginContact函数并且通过GetFixtureA()返回其中一个fixtures,并且用GetFixtureB()返回其他的fixture。由于我么不知道那个fixture属于那个body,因此我们需要做一些检查。

例子

这次的演示,让我们用BeginContact/EndContact碰撞回调来改变发生碰撞的球的颜色。一开始我们用drawing your ownobjects话题中的场景,在这之后我们也在对应位置绘制了笑脸。这一次我们的场景中只有一个球。

 

在Ball类中添加一个bool型的成员变量用来记录当前是否发生了一些撞击,并且告诉我们在物理引擎中接触发生改变时要调用函数。在渲染函数中,我们会根据当前的接触状态来设置颜色。

//Ball class member variable
  bool m_contacting;
  
  //in Ball class constructor
  m_contacting = false;
  
  //Ball class functions
  void startContact() { m_contacting = true; }
  void endContact() { m_contacting = false; }
  
  //in Ball::render
  if ( m_contacting )
    glColor3f(1,0,0);//red
  else
    glColor3f(1,1,1);//white

我们还要在构造函数中,将body的user data设置为Ball对象,在 user data这一章节提到过。

//in Ball constructor
  body->SetUserData( this ); //set this Ball object in the body's user data

现在,我们想Box2D告诉我们碰撞状态什么时候改变.创造一个继承了b2ContactListener的类,并实现BeginContact/EndContact函数:

class MyContactListener : publicb2ContactListener
  {
    void BeginContact(b2Contact* contact) {
  
      //check if fixture A was a ball
      void* bodyUserData = contact->GetFixtureA()->GetBody()->GetUserData();
      if ( bodyUserData )
        static_cast<Ball*>( bodyUserData )->startContact();
  
      //check if fixture B was a ball
      bodyUserData = contact->GetFixtureB()->GetBody()->GetUserData();
      if ( bodyUserData )
        static_cast<Ball*>( bodyUserData )->startContact();
  
    }
  
    void EndContact(b2Contact* contact) {
  
      //check if fixture A was a ball
      void* bodyUserData = contact->GetFixtureA()->GetBody()->GetUserData();
      if ( bodyUserData )
        static_cast<Ball*>( bodyUserData )->endContact();
  
      //check if fixture B was a ball
      bodyUserData = contact->GetFixtureB()->GetBody()->GetUserData();
      if ( bodyUserData )
        static_cast<Ball*>( bodyUserData )->endContact();
  
    }
  };

上面的代码看起来好像特别的多,但认真的看,你会发现其实它有很多部分是重复的。首先,我们检查fixture的body是否有任何的user data。如果有user data,那他一定是个ball,如果没有,它一定是其中一个静态物体的wall fixture。像这样简单的场景实现起来很容易,但是对于真实的游戏,你可能需要一些复杂的东西。无论怎么样,一旦我们确定fixture属于一个ball实体,我们就将user data指针强制转换为Ball指针,并调用合适的状态来改变改变函数。注意,我们需要对fixtures A和fixtures B都这样做,因为它们其中一个可能是ball。

现在告诉Box2D世界碰撞的时候使用这些函数,我们使用的就是SetContactListener函数。这个函数有一个指向b2ContactListener对象的指针,所以我们需要一个实例对象来传递。这只需在全局作用域上声明一个新的类的变量即可。

  //at global scope
  MyContactListener myContactListenerInstance;
  
  //in FooTest constructor
  m_world->SetContactListener(&myContactListenerInstance);

现在,当ball接触到wall(墙)时,它应该显示红色,并且当它移开是,它应该返回白色。

这个程序运行起来似乎没有问题,特别是一些像这样的简单场景,但是,这个方法有一个漏洞。用鼠标控制一个ball并将它放在一个角落上,这是它会同时和两个walls(墙)接触。现在沿着其中一幅墙移动,离开那个墙角。你会发现ball变回白色,即使它正在和wall(墙)接触!这是由于,在ball离开墙角时,EndContact函数就会被调用,因为有一幅墙没有再发生接触,因此m_contacting被设置为false。幸运的是,这个漏洞很容易修复----我们只需存储一个整数通过对它进行增加和减少来计算其他正在发生接触关系的fixtures,而不是存储一个简单的bool值判断接触状态。

//replace old boolean variable m_contacting in Ball class
  int m_numContacts;
  
  //in Ball constructor
  m_numContacts = 0;
  
  //new implementation for contact state change
  void startContact() { m_numContacts++; }
  void endContact() { m_numContacts--; }
  
  //altered rendering code
  if ( m_numContacts > 0 )
    glColor3f(1,0,0);//red
  else
    glColor3f(1,1,1);//white

现在再次实验一下在墙角发生接触,之前的问题应该被解决了。注意,如果是发生在一堆到处弹跳的球时,那么接触状态会以非常快的速度开启和关闭。这些都是正常的表现,但是如果你是从这些改变状态正在运行游戏逻辑,那么一定要记住这个。

在举多一个例子来演示我们如何来使用两个碰撞物体之间的关系。到目前为止,我们所做的就是判断一个ball是否和什么接触了,现在我们将知道他和什么接触。让我们创建一个只有一个红ball的场景,并且当它碰撞到其他的ball时,红色会从自己传到其他的ball。

在Ball类中添加一个bool变量来跟踪“红色”这一属性是否传给自己,并且在全局作用域处创建一个函数用来转嫁一个 ball的“红色”到另一个ball。

//in Ball class
  bool m_imIt;
  
  //in Ball constructor
  m_imIt = false;
  
  //in Ball::render
  if ( m_imIt )
    glColor3f(1,0,0);//red
  else
    glColor3f(1,1,1);//white
  
  //in global scope
  void handleContact( Ball* b1, Ball* b2 ) {
    bool temp = b1->m_imIt;
    b1->m_imIt = b2->m_imIt;
    b2->m_imIt = temp;
  }

这次,在contact listener回调,我们只需实现BeginContact函数即可:

void BeginContact(b2Contact* contact) {
      //check if both fixtures were balls
      void* bodyAUserData = contact->GetFixtureA()->GetBody()->GetUserData();
      void* bodyBUserData = contact->GetFixtureB()->GetBody()->GetUserData();
      if ( bodyAUserData && bodyBUserData )
          handleContact( static_cast<Ball*>( bodyAUserData ),
                         static_cast<Ball*>( bodyBUserData ) );
  }

最后,我们要初始化其中一个balls的”红色”这一属性:

//at end of FooTest constructor
  balls[0]->m_imIt = true;

为了能更清楚地看清楚发生的事情,最好关闭重力,这样balls会分的散一些。同时,balls会有较好的弹力,能保证在没有外力推动时弹跳时间更持久。

真实情景

这是一个非常基本的示范,但这个想法可以扩展到你的游戏中处理更多的逻辑,例如:一个玩家接触了一个怪物,等等。在一个更复杂的有许多种不同物体碰撞的场景中,你会想在fixture的user data中设置一些东西可以用来辨别这种实体代表什么。你可以设置一个对象的一个通用父类,所有的实体和Ball都是它的自己,而不是直接将Ball的指针放到user data中。这个父类有一个虚函数,它可以返回实体的类型,就想下面这样:

class Entity
  {
     virtualint getEntityType() = 0;
  };
  
  class Ball : public Entity
  {
     int getEntityType() { return ET_BALL; }
  }

在碰撞回调中,你可以检查哪种实体发生了碰撞并且对它们进行适当的处理。

另一种方法会用到dynamic casting。说实话,我还没想到一个令我自己满意的方法,因为它们的方法都需要用到大量的if/else来检查获得正确的选项。如果谁有一个号的方法处理,和我们一起分享!

打赏

发表评论

您的电子邮箱地址不会被公开。