How do mask bits control planck.js fixture collisions?

In planck.js (a 2D JavaScript physics engine based on Box2D), mask bits act as a selective filter that determines which categories of objects a specific fixture is allowed to collide with. Every fixture is assigned a collision filter consisting of a category bitmask and a mask bitmask. By performing a bitwise AND operation between one object’s categories and another object’s mask, the physics engine can instantly decide whether to simulate a physical impact or let the objects pass through each other. This mechanism is crucial for optimizing game performance and creating complex interactions, such as ignoring collisions between teammates or allowing a character to pass through platforms from below.

Understanding the Planck.js Collision Filter

To understand mask bits, you must first look at the three primary components of the Filter object in planck.js:

Both categoryBits and maskBits are 16-bit integers, meaning you have up to 16 unique collision groups available (ranging from \(0x0001\) to \(0x8000\)).

The Bitwise Collision Formula

When two fixtures (Fixture A and Fixture B) look like they are about to touch, planck.js evaluates their filters using bitwise logic. A collision is only allowed to occur if both of the following conditions are met:

const collideA = (filterA.maskBits & filterB.categoryBits) !== 0;
const collideB = (filterB.maskBits & filterA.categoryBits) !== 0;

const willCollide = collideA && collideB;

If either evaluation results in 0, the physics engine completely ignores the collision.

Defining Roles via Mask Bits

Mask bits allow developers to create sophisticated multi-layered physics environments. They essentially fulfill three critical roles:

1. Specifying Selective Immunity

By default, every fixture has a maskBits value of 0xFFFF (all bits set to 1), meaning it collides with everything. By changing the mask bits, you can grant an object immunity to specific elements. For example, a player projectile can be configured to ignore the player who shot it by omitting the player’s category bit from the projectile’s mask bits, while keeping the enemy and wall bits active.

2. Creating Multi-Layered Environments

Games often require backgrounds or decorative foreground elements that interact with certain mechanics but not others. Mask bits allow objects like particles or weather effects to collide exclusively with the ground (maskBits = GROUND_BIT) while seamlessly passing through characters, enemies, and items.

3. Enhancing Performance

Evaluating full physical contacts (calculating manifolds, friction, and restitution) is computationally expensive. Mask bits filter out unwanted collisions at a very early stage in the physics pipeline (the broad-phase collision detection). By ensuring that objects only check for collisions with relevant categories, you significantly reduce the CPU overhead in scenes with hundreds of active bodies.

Implementing Mask Bits in Code

When setting up your fixtures, you apply these filters directly to the fixture definition or update them dynamically on an existing fixture.

// Define the categories using hexadecimal bit shifts
const CATEGORY_PLAYER = 0x0001; // 0000000000000001
const CATEGORY_ENEMY  = 0x0002; // 0000000000000010
const CATEGORY_WALL   = 0x0004; // 0000000000000100

// Player Fixture Definition
// The player belongs to PLAYER, and collides with ENEMY and WALL
playerFixtureDef.filterCategoryBits = CATEGORY_PLAYER;
playerFixtureDef.filterMaskBits = CATEGORY_ENEMY | CATEGORY_WALL; 

// Enemy Fixture Definition
// The enemy belongs to ENEMY, and collides only with WALL and PLAYER
enemyFixtureDef.filterCategoryBits = CATEGORY_ENEMY;
enemyFixtureDef.filterMaskBits = CATEGORY_WALL | CATEGORY_PLAYER;

In this scenario, if you were to add an independent decorative asset with a category of 0x0008, neither the player nor the enemy would physically interact with it because 0x0008 was omitted from their respective filterMaskBits.