Oh!Coder

Coding Life

Box2D C++ 教程-碰撞过滤

| Comments

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

  • 碰撞过滤(Collision filtering)

到目前为止所有场景都是我们自己创建的,场景中的每个定制器(fixture)之间可以互相发生碰撞。这是默认的物理环境,但是可以通过设置’碰撞过滤’(Collision filtering)来控制定制器之间的碰撞状态。在创建定制器的时候,可以设置一些标志位来实现碰撞过滤的功能,具体标志位有:

-类别标志位(categoryBits)
-遮罩标志位(maskBits)
-分组索引(groupIndex)

每一个标志位是一个16位的整数,因此你可以设置16种不同的碰撞类型。其实还是有很大灵活度的,因为可以组合这些标志位,以此可以决定两个定制器之间是否会发生碰撞。带有分组索引标志位的定制器,可以覆盖带有类别/遮罩标志位的定制器集合。

  • 类别和遮罩标志位(category and mask bits)

为定制器设置类别标志位(category Bits),可以想像成这个定制器在说’我是一个…‘,设置遮罩标志位可以想象成该定制器在说’我将会和…发生碰撞’。重要的是满足这些条件的两个定制器必须可以发生碰撞才行。

比如说你有两个类别,分别是猫和老鼠,此时猫可能会说’我是一只猫,我会和猫或者老鼠发生碰面’,但是老鼠多半并不想碰到猫,所以老鼠可能会说’我是一只老鼠,我会和老鼠碰面’,在这样一种规则下,猫/猫之间会发生碰撞,老鼠/老鼠之间会发生碰撞,但是猫/老鼠之间就不能发生碰撞(就算对于猫来说ok也不行)。特别的,通过检查两个标志位对定制器进行辨别,下面这段代码或许能更清楚的进行说明:

1
2
3
bool collide =
          (filterA.maskBits & filterB.categoryBits) != 0 &&
          (filterA.categoryBits & filterB.maskBits) != 0;

类别标志位(categoryBits)默认值是0x0001,遮罩标志位(maskBits)默认值是0xFFFF,可以想象成定制器在说’我是一个物件,我可以和其它任何物件发生碰撞’。既然所有的定制器都有相同的默认规则,所以默认情况下它们之间是可以发生碰撞的。

让我们做一个试验,改变这些标志位,看看这些标志位如何使用。我们需要一个有很多小球的场景,并且让它们在一起碰撞而不会飞出场景。那么这里就使用画自己的图像中,四个“篱笆”中有很多小球的那个场景吧:

pic

到现在为止,你应该可以比较轻松的实现这样一个场景了,所以这里就不再复述实现了。如果你想自己定义一个场景也是可以的,只要有大量的不同颜色和大小的小球就可以。在这个例子中我们会对每一个小球设置不同的大小和颜色,在测试的一段时间内让它们保持一定的颜色。我们在构造函数中有一个半径的参数,渲染的时候,可以用代码设置其不同的颜色。当然了,我们还可以在参数中为每个小球设置类别标志位(categoryBits)和遮罩标志位(maskBits):

1
2
3
4
5
6
7
8
9
10
11
//Ball class member variable
b2Color m_color;
//edit Ball constructor
Ball(b2World* world, float radius, b2Color color, uint16 categoryBits, uint16 maskBits)
{
    m_color = color;
    ...
    myFixtureDef.filter.categoryBits = categoryBits;
    myFixtureDef.filter.maskBits = maskBits;
    //in Ball::render
    glColor3f(m_color.r, m_color.g, m_color.b);

ok,那么小球的大小和颜色有什么用处呢?有一个比较好的例子也许能够更好的说明问题,例如有一个从上到下竖屏的战斗场景,场景中有大量不同种类的汽车,这些汽车中只有个别的可以发生碰撞等等类似约束,或者地面的汽车不能和飞机发生碰撞。类似的我们可以构造一个这样外加船和飞机的场景,场景中的每一个实体都分为友军或敌军两种状态。我们使用小球的大小来表示是船还是飞机,使用颜色来表示友军还是敌军。现在就让我们在场景中创建大量的小球,不过目前先把小球的标志位置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//in FooTest constructor
b2Color red(1,0,0);
b2Color green(0,1,0);
//large and green are friendly ships
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 3, green, 0, 0 ) );
//large and red are enemy ships
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 3, red, 0, 0 ) );
//small and green are friendly aircraft
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 1, green, 0, 0 ) );
//small and red are enemy aircraft
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 1, red, 0, 0 ) );

当然你也可以让世界变成失重状态,这样更便于观察。你的场景或许看起来像是这样:

pic

很明显,目前没有发生任何碰撞。这就是把定制器中的遮罩标志位(maskBits)设为零所得到的效果,而且永远都不会发生碰撞。那么,这并不是我们想要的效果。下面让我们再进一步对目前这种“失控”的状态做一些修改,我们将按照如下规则,实现小球之间的碰撞效果:

  1. 所有的路面汽车都可以和边界发生碰撞
  2. 船和飞机不能发生碰撞
  3. 所有的船之间可以发生碰撞
  4. 飞机可以和敌军发生碰撞,但是不会和友军发生碰撞

看起来规则还是蛮复杂的,但是如果我们通过类别来分析的话,其实并没有想象中那么麻烦。首先,让我们看下每类实体的标志位是如何定义的:

1
2
3
4
5
6
7
enum _entityCategory {
    BOUNDARY =          0x0001,
    FRIENDLY_SHIP =     0x0002,
    ENEMY_SHIP =        0x0004,
    FRIENDLY_AIRCRAFT = 0x0008,
    ENEMY_AIRCRAFT =    0x0010,
};

既然对于定制器的类别定义是从1开始,而且这么排序也就意味着我们对于边框的不需要做任何特殊的操作。对于其它类型,我们需要对每种车辆来考虑,其它类型属于“我是…(I am a…)”还是”我会和…发生碰撞(I collide with…(maskBits))”:

|Entity   |I am a…(categoryBits)   |I collide with…(maskBits)
|——————————|————————————–|————————————— |Friendly ship   |FRIENDLY_AIRCRAFT   |BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP |Enemy ship   |ENEMY_SHIP   |BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP |Friendly aircraft   |FRIENDLY_AIRCRAFT   |BOUNDARY | ENEMY_AIRCRAFT
|Enemy aircraft   |ENEMY_AIRCRAFT   |BOUNDARY | FRIENDLY_AIRCRAFT

根据上面这个列表,我们返回到创建每个实体的代码处,重新设定创建时候的参数。

1
2
3
4
5
6
7
8
9
10
11
12
//large and green are friendly ships
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 3, green, FRIENDLY_SHIP, BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP ) );
//large and red are enemy ships
for (int i = 0; i < 3; i++)
  balls.push_back( new Ball(m_world, 3, red, ENEMY_SHIP, BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP ) );
//small and green are friendly aircraft
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 1, green, FRIENDLY_AIRCRAFT, BOUNDARY | ENEMY_AIRCRAFT ) );
//small and red are enemy aircraft
for (int i = 0; i < 3; i++)
    balls.push_back( new Ball(m_world, 1, red, ENEMY_AIRCRAFT, BOUNDARY | FRIENDLY_AIRCRAFT ) );

现在再次运行这段代码,你会发现实体之间的碰撞规则会按照我们指定的方式进行碰撞。

pic

  • 使用分组索引(Using group indexes)

定制器的分组索引(groupIndex)可以覆盖分类(category)和遮罩(mask)标志位。从名字来看可以推断出,分组索引可以对定制器进行分组,可以分成总是碰撞或是永远不发生碰撞。分组索引使用一个整型值代替标志位。这是它的工作原理-第一次读取这个值的时候可能会感到有些迷惑,可以慢慢来。查看两个定制器之间是否会发生碰撞:

-定制器如果有分组索引为零,那么使用分类/遮罩规则
-如果两个分组索引的值不为零并且不相同,那么使用分类/遮罩规则
-如果两个分组索引的值相同,并且为正数,则产生碰撞
-如果两个分组索引的值相同,并且为负数,则不产生碰撞

分组索引(groupIndexes)的数值默认为0,所以到目前为止还它还没有凸显过什么有用的功能。让我们做一个简单的例子,试着覆盖分类/遮罩设置。我们把围墙和其中一辆车放到同一组,并设置该组值为负值。如果你留心上面的规则,你会发现,有一辆车会偷偷摸摸的从边界溜掉。

你可以在Ball类的构造方法里添加一个参数来设定此分组索引(groupIndex)的数值,或者干脆简单粗暴:

1
2
3
4
5
6
7
8
//in FooTest constructor, before creating walls
myFixtureDef.filter.groupIndex = -1;//negative, will cause no collision
//(hacky!) in global scope
bool addedGroupIndex = false;
//(hacky!) in Ball constructor, before creating fixture
if ( !addedGroupIndex )
    myFixtureDef.filter.groupIndex = -1;//negative, same as boundary wall groupIndex
addedGroupIndex = true;//only add one vehicle to the special group

pic

如果我们把分组索引值设置成正值,车辆就会和墙一直发生碰撞。现在例子中的这个车辆和所有其它车辆都会和四面墙发生碰撞,并没有任何区别,这也是为什么之前我们把其中一个车辆的分组索引值设置成负值的原因。有些情况下可能会有特殊的情况,比如按说船和飞机应该永远都不会相撞,但是你可能会说,某些飞机还是可能会和船相撞的,比如低空飞行的水上飞机…:)

  • 需要更多控制(Need more control)

如果你想更好的控制物体之间的碰撞关系,可以在物理世界中设置一个接触过滤器(contact filter),当Box2D需要监测碰撞的时候会调用接触过滤器来判断是否有碰撞,这种方法可以代替上面提到的规则,当然这两种方法到底使用哪种还要取决于你。具体实现方法,和实现debug draw和碰撞回调一样,需要继承自b2ContactFilter类型,实现下面这个方法,并通过设置世界的SetContactFilter()方法来让引擎知道调用下面这个方法。

1
bool b2ContactFilter::ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB);
  • 运行时改变碰撞过滤器(Changing the collision fliter at run-time)

有时候改变碰撞过滤的条件取决于游戏的事件。你可以通过为定制器(fixture)创建一个新的b2Filter对象来对分类标志位(categoryBits)、遮罩标志位(maskBits)、分组索引(groupIndex)。大部分情况下你可能只想改变其中一个而已,那么你可以首先获取已经存在的,然后改变你想改变的部分,之后再赋值回去。如果现在你已经有了一个定制器的引用,那么实现起来会非常简单:

1
2
3
4
5
6
7
8
//get the existing filter
b2Filter filter = fixture->GetFilterData();
//change whatever you need to, eg.
filter.categoryBits = ...;
filter.maskBits = ...;
filter.groupIndex = ...;
//and set it back
fixture->SetFilterData(filter);

如果你只有物体的引用,你需要参照定制器话题来找到你想要改变的那个定制器。

Comments