10.2 碰撞检测

虽然说物体之间的碰撞Box2d已经很好地模拟出来了,但还是要了解一下Box2d的碰撞检测,因为在很多时候需要知道碰撞的具体信息。例如,怪物被子弹碰到了,除了被打飞,或者打倒,还需要做很多其他的操作,如把怪物删除,然后增加经验、金币之类的事情,这就需要用到碰撞监听。而在玩CS之类游戏的时候,当把友军伤害选项给屏蔽掉之后,我们发射的子弹只对敌军产生影响,这样的功能就涉及碰撞过滤,本节主要介绍这两项内容。

10.2.1 碰撞监听

Box2d里面使用接触(Contact)来描述碰撞信息,在每次两个物体的AABB出现重叠的时候,Box2d会产生相应的触点,当物体的AABB分离的时候,又会把触点删除。使用World的GetContactList()函数可以获取当前世界所有的接触信息,通过这些接触信息,可以获取到接触的物体以及接触点等信息。但这并不是明智的做法,因为无法捕捉到所有的接触。例如在一次循环中,在很多力的作用下,两个物体短暂地接触了,之后又快速地分开,在接触列表中是不会找到这个接触对象的。并且在每一帧轮询这些接触对象本身也是一个冗余的运算,就像坐公交车,每个站台都问一句XX站到了没?明智的做法是使用接触监听,它会在到达XX站台时自动通知你。

使用监听需要实现一个监听者,叫作接触监听器,名字为b2ContactListener,位于b2WorldCallbacks.h内,可以实现它的几个接触相关的接口。

        ///在两个fixtures开始碰撞的时候回调(当它们开始重叠的时候,只会在step中调用)
        virtual void BeginContact(b2Contact* contact) { B2_NOT_USED(contact); }

        ///在两个fixtures结束碰撞的时候回调(当它们分开的时候,物体被销毁的时候也会调用)
        virtual void EndContact(b2Contact* contact) { B2_NOT_USED(contact); }

        ///在接触更新完成之后调用(碰撞检测发生之后,碰撞冲突处理之前).此时可以禁用触点,实现
        单面碰撞。例如,是男人就上100层这样的小游戏的楼梯,在玩家跳上去时不发生碰撞,在玩家掉
        落的时候才碰撞。通过禁用触点可以避免此次碰撞处理,并且不会调用到接触处理完成的回调,但
        是可能会在一小段时间内连续收到PreSolve回调
        virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
        {
            B2_NOT_USED(contact);
            B2_NOT_USED(oldManifold);
        }

        ///在接触被处理完之后调用,这时候物理模拟已经完成,在这里可以获得接触对象碰撞之后产生
        的力量、旋转等信息
        virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse)
        {
            B2_NOT_USED(contact);
            B2_NOT_USED(impulse);
        }

Box2d不允许在碰撞回调中修改物理世界,因为上面可能发生在一个step回调之中,当出现多个对象同时碰撞,会依次处理每两个对象之间的碰撞,而这时候会调用到回调函数,在回调函数中修改了对象,可能会导致其他对象的碰撞结果不正确,假设在回调中释放了对象,还有可能导致程序崩溃。如果需要删除或者做修改,可以将触点信息保存起来,在step完成之后,再来处理。

上面的回调中可以做的修改仅仅是禁用触点,来规避此次碰撞。例如超级玛丽游戏中的单面墙,可以通过判断碰撞的方向来决定是否规避此次碰撞,代码如下。

          virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold)
          {
           b2WorldManifold worldManifold;
           contact->GetWorldManifold(&worldManifold);
           if (worldManifold.normal.y > 0.5f)
           {
              contact->SetEnabled(false);
           }
          }

在每次回调都会有一个b2Contact对象被传进来,描述了发生碰撞的两个物体的详细信息,通过GetFixtureA()函数和GetFixtureB()函数可以分别获取这两个物体,但是需要你自己判断这两个物体是什么,可以根据指针来判断,也可以设置UserData,根据UserData来判断。在冲突处理之前的PreSolve回调中,调用b2Contact对象的SetEnabled()函数可以设置是否禁用接触。

最后,在使用的时候,需要用new操作符创建一个这样的监听器对象,通过调用World->SetContactListener(myContactListener),把它设置给Wold。

10.2.2 碰撞过滤

在创建Fixture的时候,可以通过设置过滤标识,来控制Fixture之间的碰撞过滤,Fixture有两种过滤标识,每个标识都是一个16位的int16变量(相当于短整型),可以表示16种不同类型的碰撞。

        b2Filter()
        {
            categoryBits = 0x0001;      //类别标志位
            maskBits = 0xFFFF;          //遮罩标志位
            groupIndex = 0;             //分组索引
        }

类别标志位定义了Fixture的类别,而遮罩标志位则定义了可以与之发生碰撞的类别,举个例子:

❑ 我是一个公主a = 1。

❑ 我是一个战士b = 2。

❑ 我是一个英雄c = 4。

我是一个公主,那么我的categoryBits |= a,我只能和英雄发生碰撞,那么我的maskBits|= c,这是我的逻辑,那么这个碰撞过滤,还得看英雄愿不愿意和我碰一下,也就是英雄的maskBits & a是否等于0,用一句代码来解释,就是:

        isCollid = A.maskBits & B.categoryBits ! = 0 && A.categoryBits & B.maskBits ! =0

在b2Filter的构造函数中,将categoryBits设置为1,而将maskBits设置为0xFFFF,表示我可以和所有的Fixture发生碰撞,而且所有的Fixture在创建的时候,categoryBits都是1。

那么分组索引是干什么的呢?其用来描述更加复杂的规则。

❑ 当A和B的分组索引相同且为正数,则产生碰撞。

❑ 当A和B的分组索引相同且为负数,则不产生碰撞。

❑ 在其他的情况下,都使用正常的类别/遮罩过滤规则。

简单概括就是,如果是大于0的相同组,则一定碰撞,如果是小于0的相同组,则一定不碰撞。

在使用上面的标志和组无法解决问题的时候,还可以通过触点过滤器来进行碰撞过滤,这有点类似于碰撞监听,继承一个b2ContactFilter对象,然后实现它的碰撞过滤方法,通过在World中调用SetContactFilter()函数,来传入触点过滤器,使其接受触点消息。

      virtual bool ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB);

当这两个Fixture的AABB包围盒发生重叠的时候,就会调用这个回调,这时候它们可能还没有真正发生碰撞,可以通过自己设置的UserData来判断过滤,也可以根据Fixture的过滤标识结合自己定义的规则来判断过滤。如果允许它们发生碰撞,则返回true;否则返回false,让它们直接穿透,不做碰撞处理。另外,在游戏过程中也可以动态地修改Fixture的过滤标识,来达到想要的效果。