So far in every scene we have made, all the shapes were able to collide with all the other shapes. That is the default behaviour, but it's also possible to set up 'collision filters' to provide finer control over which shapes can collide with each other. Collision filtering is implemented by setting some flags in the shape definition when we create the shape. These flags are:
- categoryBits
- maskBits
- groupIndex
Each of the 'bits' values are a 16 bit integer so you can have up to 16 different categories for collision. There is a little more to it than that though, because it is the combination of these values that determines whether two shapes will collide. The group index can be used to override the category/mask settings for a given set of shapes.
Category and mask bits
The categoryBits flag can be thought of as the shape saying 'I am a ...', and the maskBits is like saying 'I will collide with a ...'. The important point is that these conditions must be satisfied for both shapes in order for collision to be allowed (the system requires mutual consent!)
For example let say you have two categories, cat and mouse. The cats might say 'I am a cat and I will collide with cats and mice', but mice generally not being so interested in colliding with cats, could say 'I am a mouse and I will collide with mice'. With this set of rules, a cat/cat pair will collide, and a mouse/mouse pair will collide, but a cat/mouse pair will not collide (even though the cats were ok with it). Specifically, the check is done by a logical AND joining the bitwise AND of these two flags:
const collide =
(filterA.maskBits & filterB.categoryBits) !== 0 &&
(filterA.categoryBits & filterB.maskBits) !== 0;
The default values are 0x0001 for categoryBits and 0xFFFF for maskBits, or in other words every shape says 'I am a thing and I will collide with every other thing', and since all the shapes have the same rules we found that everything was indeed colliding with everything else.
Let's experiment with changing these flags. We need a scenario with many entities:
// Ball class definition
class Ball {
constructor(world, radius, color, categoryBits, maskBits) {
this.color = color;
const bodyDef = b2DefaultBodyDef();
bodyDef.type = DYNAMIC;
const bodyId = b2CreateBody(world, bodyDef);
const shapeDef = b2DefaultShapeDef();
shapeDef.filter.categoryBits = categoryBits;
shapeDef.filter.maskBits = maskBits;
const circle = new b2Circle(new b2Vec2(0, 0), radius);
this.shapeId = b2CreateCircleShape(bodyId, shapeDef, circle);
}
render() {
// Draw using color property
// Implementation depends on rendering system
}
}
Let's set up a scenario with ships and aircraft:
// In test setup
const red = {r: 1, g: 0, b: 0};
const green = {r: 0, g: 1, b: 0};
// large and green are friendly ships
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 3, green, 0, 0));
}
// large and red are enemy ships
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 3, red, 0, 0));
}
// small and green are friendly aircraft
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 1, green, 0, 0));
}
// small and red are enemy aircraft
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 1, red, 0, 0));
}
It's immediately obvious that nothing is colliding with anything anymore. This is what you get if the maskBits are zero in a fixture, it will never collide. However that's not what we wanted, so let's look at how to get more than just simple 'all or nothing' control. We'll use the following rules to set the allowable collisions between entities:
- All vehicles collide with the boundary
- Ships and aircraft do not collide
- All ships collide with all other ships
- Aircraft will collide with opposition aircraft, but not with their teammates
Here are the category definitions:
const BOUNDARY = 0x0001;
const FRIENDLY_SHIP = 0x0002;
const ENEMY_SHIP = 0x0004;
const FRIENDLY_AIRCRAFT = 0x0008;
const ENEMY_AIRCRAFT = 0x0010;
And the implementation:
// large and green are friendly ships
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 3, green, FRIENDLY_SHIP,
BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP));
}
// large and red are enemy ships
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 3, red, ENEMY_SHIP,
BOUNDARY | FRIENDLY_SHIP | ENEMY_SHIP));
}
// small and green are friendly aircraft
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 1, green, FRIENDLY_AIRCRAFT,
BOUNDARY | ENEMY_AIRCRAFT));
}
// small and red are enemy aircraft
for (let i = 0; i < 3; i++) {
balls.push(new Ball(worldId, 1, red, ENEMY_AIRCRAFT,
BOUNDARY | FRIENDLY_AIRCRAFT));
}
Using group indexes
The groupIndex flag can override category/mask settings. When checking two shapes:
- If either has groupIndex of zero, use category/mask rules
- If both have different non-zero groupIndex, use category/mask rules
- If both have same positive groupIndex, collide
- If both have same negative groupIndex, don't collide
Example of using groupIndex:
// Before creating walls
const shapeDef = b2DefaultShapeDef();
shapeDef.filter.groupIndex = -1; // negative, will cause no collision
let addedGroupIndex = false;
// In Ball constructor, before creating shape
if (!addedGroupIndex) {
shapeDef.filter.groupIndex = -1; // negative, same as boundary wall groupIndex
addedGroupIndex = true; // only add one vehicle to the special group
}
If we had set the groupIndex to a positive value, this vehicle would always collide with the wall. In this case all the vehicles already collide with the walls anyway so it wouldn't have made any difference, that's why we set it negative so we could see something happen. In other situations it might be the opposite, for example the ships and aircraft never collide, but you might like to say that a subset of the aircraft can actually collide with ships. Low-flying seaplanes perhaps... :)
Need more control?
If you need even finer control over what should collide with what, you can set a contact filter callback in the world so that when Box2D needs to check if two fixtures should collide, instead of using the above rules it will give you the two fixtures and let you decide. The callback is used in the same way as the debug draw and collision callbacks, by subclassing b2ContactFilter, implementing the function below and letting the engine know about this by calling the world's SetContactFilter function.
b2World_SetCustomFilterCallback(worldId, (shapeIdA, shapeIdB) => {
// Implement your special rules in here
// Return true to allow collision, false to prevent
return true;
}, null);
Changing the collision filter at run-time
Sometimes you might want to alter the collision filter of a shape depending on events in the game. You can change each of the categoryBits, maskBits, groupIndex by setting a new b2Filter in the shape. Quite often you only want to change one of these, so it's handy to be able to get the existing filter first, change the field you want, and put it back. If you already have a reference to the shape you want to change, this is pretty easy:
// Get the existing filter
const filter = b2Shape_GetFilter(shapeId);
// Change whatever you need to, eg.
filter.categoryBits = newCategory;
filter.maskBits = newMask;
filter.groupIndex = newGroup;
// And set it back
b2Shape_SetFilter(shapeId, filter);
If you only have a reference to the body, you'll need to iterate over the body's shapes to find the one you want.
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.