Sensors and collision filtering

Modern games include fixtures that do not react physically to collisions with other fixtures but still have an important mission, just like radars that detect intruders within an area.

Usually, allies do not crash between themselves and others like ghosts will collide only with fire.

All such gameplay variables are easily implemented with Box2D, so you will not have to fill your collision handlers with many lines of code. In addition, filtering collisions will allow you to decrease the CPU load.

In this recipe, you will learn how to use sensors and manage the different ways of filtering your collisions depending on the complexity of the game.

Getting ready

If you have already taken a look at the Box2DCollisionFiltering.java file, you will have noticed the two quiet dinosaurs that get really angry whenever any of the friendly cavemen get closer to them. The characters that belong to the same faction will not collide.

How to do it…

The code of this example comes with two inner classes defined with the purpose of making things clearer. Each of them will represent a faction: cavemen and dinosaurs. As their content is mostly not new for us, we will not put them in the spotlight but please feel free to take a look at the source code.

Sensors

Each dinosaur will have a nonvisible sphere around him to detect whether a caveman is within its vicinity, as shown in the following screenshot:

Sensors

That is what we will call a sensor, and its usage is really simple:

  1. Create a second fixture for each enemy body and enable the sensor flag:
    CircleShape circle = new CircleShape();
    circle.setRadius(1f);
    
    FixtureDef sensorFD = new FixtureDef();
    sensorFD.isSensor = true;
    sensorFD.shape = circle;
    enemyBody.createFixture(sensorFD);
  2. Implement the ContactListener interface and within the beginContact(…) function, check whether the sensor is overlapped by another fixture:
    if (fixtureA.isSensor()) {
      Enemy e = (Enemy) bodyA.getUserData();
      e.setAngry(true);
    }

    A reference to the object itself is attached into the UserData field of the body within the Enemy constructor so that it can be retrieved later. In addition, the SetAngry function will just change the tint of the dinosaur to red when the received argument is true.

    The brightest readers will have realized that we are only checking whether one of the fixtures involved in the contact is a sensor and it is an insufficient condition to determine that a caveman has gone into a dinosaur's territory. However, that simple condition becomes valid because of collision filtering, which directly omits the undesired fixtures.

Group collision filtering

Filtering collisions by groups is the simplest and quickest way to avoid collisions between certain types of bodies. It is intended for games where bodies can collide with anyone except for the ones that belong to its own group.

Configuring our example with groups would be very easy but it will not be flexible enough to deal with all the conditions automatically, so some extra code will be necessary. For educational purposes, the group approach is described here but notice that it is not present in the sample source code:

  1. The first thing is to declare the groups as member fields:
    final short GROUP_CAVEMAN = -1;
    final short GROUP_DINOSAUR = -2;
    final short GROUP_SCENERY = 3;

    The common positive value means always collide while the same negative value means never collide.

  2. Next, match the groups with your bodies. Set sensors as GROUP_DINOSAUR so they will not have the chance of colliding with the real dinosaur fixture:
    FixtureDef sensorFD = new FixtureDef();
    sensorFD.isSensor = true;
    sensorFD.shape = circle;
    sensorFD.filter.groupIndex = GROUP_DINOSAUR;
    enemyBody.createFixture(sensorFD);
    …

Once group collision filtering is explained, it is not hard to find out that sensors will collide as much with cavemen as with the ground, so it would need extra work to distinguish them.

Flexible collision filtering

The long route takes you through a more tedious path but much more powerful too. It consists of categories and masks.

The categories should have one entry for each object type while the masks filters collisions between them. They both are bit fields represented by shorts, which can store up to 16 varieties as they have 16 bits.

  1. If you are afraid of this notation, you will soon realize how simple it is to define new categories:
    final short CATEGORY_PLAYER = 0x0001;
    final short CATEGORY_ENEMY = 0x0002;
    final short CATEGORY_GROUND = 0x0004;
    final short CATEGORY_SENSOR = 0x0008;

    Some readers will prefer going for another notation that makes use of bit shifting operations while keeping the decimal numerical system. The next code snippet is equivalent to the previous one:

    final short CATEGORY_PLAYER = 1<<0;
    final short CATEGORY_ENEMY = 1<<1;
    final short CATEGORY_GROUND = 1<<2;
    final short CATEGORY_SENSOR = 1<<3;

    Feel free to use the notation that you are more comfortable with.

  2. With those categories ready to be used, masks are even easier:
    // Cannot collide with player objects
    final short MASK_PLAYER = ~CATEGORY_PLAYER;
    // Cannot collide with enemy objects
    final short MASK_ENEMY =  ~CATEGORY_ENEMY;
    // Can collide only with players
    final short MASK_SENSOR =  CATEGORY_PLAYER;
    // Can collide with everything
    final short MASK_GROUND = -1;
  3. All you have to do now is to assign the proper categories and masks to each fixture, as shown in the following example:
    FixtureDef sensorFD = new FixtureDef();
    sensorFD.isSensor = true;
    sensorFD.shape = circle;
    sensorFD.filter.categoryBits = CATEGORY_SENSOR;
    sensorFD.filter.maskBits = MASK_SENSOR;
    enemyBody.createFixture(sensorFD);

How it works…

All these bits calculation might seem complex but in the next paragraphs, you will find a brief explanation to make it clear.

The way that Box2D determines whether to filter a certain collision or not, is performed with the following code snippet:

const b2Filter& filterA = fixtureA->GetFilterData();
const b2Filter& filterB = fixtureB->GetFilterData();

if (filterA.groupIndex == filterB.groupIndex && filterA.groupIndex != 0)
{
  return filterA.groupIndex > 0;
}

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

For group filtering, Box2D makes use of signed integers.

On one hand, two fixtures will collide if they share the same positive groupIndex. On the other hand, they will not collide if they belong to the same groupIndex and it is negative. For the rest of the cases, colliding delegates to category/mask filtering.

Masks and categories are set through bit fields. The previous code checks that after applying an AND operation to the mask bits of a fixture and the category bits of another, it does not result in 0. An example might shed some light on this:

final short CATEGORY_PLAYER = 0x0001;
final short CATEGORY_ALLY = 0x0002;
final short MASK_PLAYER = ~CATEGORY_ALLY;

The explanation of why a player cannot collide with an ally is because doing an AND operation between CATEGORY_ALLY(00000010) and MASK_PLAYER(11111101) results in 0 (00000000).

Note

When defining the mask, you will have come across the ~ symbol. It represents one's complement, which in bit terms means replacing 0s with 1s and vice versa.

Bit fields are stored as short values, which in Java takes 16 bits, ranging from -32768 up to 32767. The next table shows the correspondence between decimal and hexadecimal values:

Hexadecimal

Decimal

0x0000

0

0x0001

1

0x7FFF

32767

0x8000

-32767

0x8001

-32766

0xFFFF

-1

As you will have seen, the decimal column is not ordered, which can lead to errors when setting a bit field to a specific value. This is one reason to find hexadecimal notation more frequently within this context.

In addition, values must be powers of two under this scope.

There's more…

Groups, categories, and masks can live together, being aware of the higher precedence of groups. However, my advice here is to keep it as simple as possible.

In addition, you can explicitly find a set of cases in the Box2D manual where they internally make use of collision filtering:

  • A fixture on a static body can only collide with a dynamic body.
  • A fixture on a kinematic body can only collide with a dynamic body.
  • Fixtures on the same body never collide with each other.
  • You can optionally enable/disable collision between fixtures on bodies connected by a joint.

See also

  • The Querying the world recipe
  • The Implementing a deferred raycaster recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.163.175