How to Configure Collision Filtering in Planck.js?

Planck.js, a 2D physics engine for JavaScript, allows developers to control which fixtures collide with each other using a system of category and mask bits. This article explains how to set up categoryBits to define an object’s identity and maskBits to define what it can collide with, ensuring precise control over your game’s physics interactions. By configuring these bitfields and assigning them to fixture definitions, you can easily create complex collision rules, such as allowing a player to pass through certain obstacles while still colliding with the ground.

Understanding Category and Mask Bits

Collision filtering in Planck.js relies on bitwise operations using 16-bit integers. Every fixture can be assigned a categoryBits value and a maskBits value.

For two fixtures to collide, the categoryBits of the first fixture must match the maskBits of the second fixture, and the categoryBits of the second fixture must match the maskBits of the first fixture.

Defining Your Collision Categories

Because Planck.js uses a 16-bit integer for filtering, you can have up to 16 unique collision categories (from \(2^0\) to \(2^{15}\)). It is best practice to define these as hexadecimal constants at the top of your script.

const CATEGORY_PLAYER   = 0x0001; // 0000000000000001 in binary
const CATEGORY_ENEMY    = 0x0002; // 0000000000000010 in binary
const CATEGORY_PLATFORM = 0x0004; // 0000000000000100 in binary
const CATEGORY_POWERUP  = 0x0008; // 0000000000001000 in binary

Setting Up the Filter in Fixture Definitions

To apply these rules, you need to pass a filterCategoryBits and filterMaskBits property inside your fixture definition configuration object.

Example 1: Setting up the Player

If you want the player to collide with platforms and enemies, but pass right through powerups, you combine the target categories using the bitwise OR (|) operator for the mask.

const playerFixtureDef = {
  shape: pl.Circle(1.0),
  density: 1.0,
  filterCategoryBits: CATEGORY_PLAYER,
  filterMaskBits: CATEGORY_PLATFORM | CATEGORY_ENEMY
};

playerBody.createFixture(playerFixtureDef);

Example 2: Setting up a Powerup

If you want a powerup to only collide with the player, its mask should reflect only the player category.

const powerupFixtureDef = {
  shape: pl.Box(0.5, 0.5),
  isSensor: true, // Often true for items you pick up
  filterCategoryBits: CATEGORY_POWERUP,
  filterMaskBits: CATEGORY_PLAYER
};

powerupBody.createFixture(powerupFixtureDef);

Changing Filters Dynamically

If an object’s collision rules need to change during runtime—such as a player becoming invincible or a phased platform turning solid—you can update the filter directly on an existing fixture using setFilterData.

// Get the current filter data from a fixture
const filter = fixture.getFilterData();

// Update the mask bits to ignore enemies
filter.maskBits = CATEGORY_PLATFORM; 

// Apply the updated filter back to the fixture
fixture.setFilterData(filter);

// Note: You may need to call world.refilter(fixture) if the 
// objects are already touching and you want immediate separation.