Oh!Coder

Coding Life

Box2D C++ 教程-碰撞回调

| Comments

声明:本教程翻译自:Box2D C++ tutorials Collision callbacks,仅供学习参考。

  • 碰撞回调(Collision callbacks)

当物理场景中的物体移动或者彼此之间发生碰撞的时候,Box2D会为我们处理所有必要的碰撞监测,对于这一点我们不必过于担心。但是就整个游戏来说,偶尔我们需要在物体发生碰撞的时候触发一些事件。例如当游戏玩家触碰到怪物的时候,玩家应该over,等等。我们需要一种从物理引擎获取此信息的方法。

在画自己的图像这个话题中,我们在游戏场景中创建了一个物体作为研究对象,并在每一帧对物体当前位置做了查询操作,并对物体进行了渲染。之所以每帧对物体都进行渲染,是因为物体的位置/旋转每一帧似乎也在不停的变化。但是碰撞则有一点点不一样,它并不是每一帧都会发生,它的发生频率通常少于渲染帧率。所以如果还是每帧都去询问物理引擎某个物体是否发生碰撞,这就像是旅行当中,车中后座的孩子不停的问你:”我们到了吗?怎么还没到啊…“,而大部分时候得到的答案是:”还没有,马上就到”,所以如果每一帧都对物理世界中的实体进行遍历并不一定每次都能获得新状态的变化(除非物体确实有了新变化),而且这么做也是低效的。所以如果是开车远行的话,最好告诉车后的孩子们:”保持安静!当到达目的地的时候我会告诉你们的”。

在Box2D中我们就是通过这种方式进行方法回调的。当物体发生碰撞时,我们可以在这个方法中完成一些操作。当然了,事先我们不确定是哪两个物体发生碰撞,所以当物体真正发生碰撞的时候,我们需要把方法作为参数传递过去,这样我们就把方法交给了Box2D,就好像告诉Box2D说:”hi,有碰撞发生的时候,调用这个方法!”。

  • 回调时间(Callback timing)

在每次调用b2World::Step()方法时,将会对物理世界中实体之间的碰撞进行检测。就像我们在worlds话题中提到的那样,b2World::Step()方法推动了整个物理模拟过程,移动或旋转物体实体等。一旦检测到碰撞发生,程序就会回调你的方法来完成你指定的操作。当执行完指定的操作之后,会再次回到b2World::Step()方法执行剩下的程序,此刻你不应该做任何改变场景的操作,因为在同一时间步长中或许还有更多的碰撞发生。

pic

设置碰撞回调方法的过程和自定义debug draw类似。创建一个b2ContactListener类的子类,类中包含了一堆可以重写的虚方法,其中有一些方法我们会用到:

1
2
3
4
5
//b2ContactListener
// Called when two fixtures begin to touch
virtual void BeginContact(b2Contact* contact);
// Called when two fixtures cease to touch
virtual void EndContact(b2Contact* contact);

注意,这里也提到了一个检测碰撞结束的方法,后面我们会做有提到。

  • 回调信息(Callback information)

一旦你创建了b2ContactListener的子类,我们将会知道哪两个物理实体会发生碰撞,然后我们可以就可以根据自己的需要完成一些自定义的功能。但是等下….貌似并没有哪个物理实体支持BeginContact/EndContact方法,那么我们怎么如何知道到底是哪两个物体发生了碰撞呢?其实contact参数包含了所有我们想要的信息。其中最主要的一件事是想知道哪两个定制器(fixtures)发生了碰撞,我们可以通过下面这两个方法获得对应的定制器(fixtures)。

1
2
3
4
5
//b2Contact
// Get the first fixture in this contact
b2Fixture* GetFixtureA();
// Get the second fixture in this contact
b2Fixture* GetFixtureB();

但是貌似还是不知道是哪个物体…?定制器(fixture)类本身有一个GetBody()方法可以获得对应的物体,如果contact也有一个方法可以直接获得物体就好了,其实这么做并没有必要,实际中我发现碰撞是发生在定制器(fixture)之间的,不是物体之间。为了更好的说明之间的关系,下面这张图对于说明这一点可能会有所帮助:

pic

上图这个例子中,动态物体(Dynamic body)包括四个定制器(Fixture),当中的一个定制器和静态物体相接触,在BeginContact方法中,contact参数可以通过GetFixtrueA()方法返回其中一个定制器,GetFixtureB()方法可以返回另一个。但我们并不知道哪个是哪个,所以需要我们自己做判断。

  • 举例(Example)

作为本次话题的例子,我们会使用BeginContact/EndContact碰撞回调方法,在小球发生碰撞的时候改变它们的颜色。场景的构造方面,我们在画自己的图像这个话题中,讨论了在正确的位置画出笑脸。而此时,场景中只需要一个小球而已。

为Ball类添加布尔成员变量,来记录当前小球是否发生碰撞,当有contacts参数发生变化的时候,物理引擎会会通知我们,并进行方法回调。在渲染方法中,我们会根据contacts参数状态来对颜色进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
//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

我们也可以在Ball类型的构造函数中,把物体UserData数据设置成小球对象自身的数据:

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

现在,当contact状态改变的时候,希望Box2D能够通知我们。创建一个b2ContactListener子类,实现BeginContact/EndContact方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyContactListener : public b2ContactListener
{
    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();
    }
};

乍看起来好像写了很多代码,其实仔细看会发现很多都是重复的。首先我们检查物体的定制器中的UserData字段是否有值,如果有,一定是ball,如果没有,那么一定是静态物体的墙。对于这样一个简单的场景来说,做起来还算容易,但是对真正的游戏开发来说,实现起来恐怕要会复杂一些。不管怎么说,既然我们决定把定制器附加到一个Ball类对象上,并且把UserData指针指向Ball对象,而且还可以适当的调用改变一些状态的方法。注意我们需要对定制器A和B都做同样的操作,因为两个可能都是小球。现在,可以在Box2D世界中使用这些方法进行碰撞操作,我们可以使用SetContactListener方法,此方法可以设置一个指向b2ContactListener对象的指针,所以我们需要一个类的实例,这个需求很容易满足,只要在你的新类中声明一个全局变量就ok了。

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

现在,当小球和墙接触的时候,就会变成红色,离开墙的时候,就会变回白色。

pic

看起来已经ok了,特别是对于这个简单场景来说。但是方法中还存在着一个问题,用鼠标强制把小球推到墙角,这时小球和两面墙都有接触。现在让小球沿着一面墙进行滑动,离开墙角。小球会变成白色,即便它现在仍然和一面墙有所接触!当小球离开墙角的时候到底发生了什么呢?当一面墙不再接触的一刹那,EndContact被触发了,小球的m_contacting变量被设置成了false,幸运的是,这比较容易发现,由于此处使用布尔值简单的记录on/off状态,我们只需要使用一个整型值代替,以此来增加/减少当前接触的定制器(fixture)的数量。

1
2
3
4
5
6
7
8
9
10
11
12
//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

现在再进行一次测试,问题应该就被解决了。现在添加一堆球,把它们推到角落,进一步验证一下此次的修改。注意当物体不停的蹦跳着互相碰撞的时候,发生接触的时候可能会产生短暂的闪烁现象。如果你在运行游戏逻辑的时候出现这种情况,这没有什么意外的,注意这一点。

pic

让我们再举一个例子,来示范一下碰撞对象之间是如何进行关联的。到目前为止,我们举的例子都是在说一个球是否发生了碰撞,现在让我们来看看这个球到底和谁发生了碰撞。下面,让我们创建一个只有一个红球的场景,当它碰撞到别的小球的时候,就把红色传递给另一个小球,同时自己变成白色,以此来标记“这个球”当前是哪一个。

为Ball类添加一个布尔值变量,以此来标记当前是不是“这个球”,创建一个全局方法,来完成传递颜色变化的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//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方法就行:

1
2
3
4
5
6
7
8
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 ) );
}

最后,我们需要设置一下第一个红色小球:

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

为了更清楚的看到发生了什么,或许你想去掉重力作用,这样可以让小球运动时间更长一点,或者把小球的恢复系数(restitution)设置的更高一点,这样可以长时间在不用手来重新推动小球。

pic

  • 真实场景(Real scenarios)

这是一个非常基础的范例,但是这个思路可以在游戏中实现很多有趣的逻辑,比如玩家撞到怪兽身上,等等。在复杂的场景中,可以在不同的实体之间发生碰撞,或许你想在定制器(fixtures)的用户数据(User Data)中设置一些你想关联的实体类型。一个可能的方法就是直接把Ball类型的指针放到用户数据(User Data)中,当然你可以设置一个通用的父类,其它类都继承自此通用类,父类中可以有一个虚方法返回类型的实例,类似于这样:

1
2
3
4
5
6
7
8
class Entity
{
    virtual int getEntityType() = 0;
};
class Ball : public Entity
{
    int getEntityType() { return ET_BALL; }
}

可以在碰撞回调中使用这个方法监测碰撞实体的类型,并进行类型转换。

另外可以做dynamic类型转换。老实说并没有一个我特别喜欢的方法,因为不管沿用哪种方法都要用许多if/else判断,看起来都很臃肿。如果有人有其它好的方法,希望能够告知大家!

Comments