Reit Games
  • News
  • Games
  • About
  • Cover image

    Understanding Physics Collision filtering with Phaser and MatterJs

    Uncover the power of collision filtering and the binary magic that lies beneath. Learn how to elegantly manage collisions using binary numbers, categories, and masks, all while gaining valuable insights applicable not only to Phaser but also to other game engines.

    Simplifying Physics Collisions with Phaser and MatterJs

    In my basketball game, I decided to switch from a side-view to a front-view perspective using Phaser with MatterJs, a 2D physics engine. For this small-scoped game, I needed to ensure that specific game objects, like the net and basketball hoop edges, didn't collide even though they visually overlapped.

    MatterJs provides collision groups and filters to specify which objects should collide. Initially, I attempted to change the collision group of the objects. However, it became apparent that the category/mask rules also play a crucial role.

    Collision Group, Mask, and Category in Depth

    To clarify, if two bodies have different values for their collision group, the category/mask rules come into play. These rules state that two bodies A and B will collide if each includes the other's category in its mask.

    However, MatterJs uses binary numbers to determine collisions, making it powerful but potentially tricky to handle. Using numbers like 1 or 3 in their decimal form might cause confusion when dealing with multiple filters.

    The Initial Problem

    The first hurdle was preventing specific game objects, like the net and basketball hoop edges, from colliding. Visually, they sat right on top of each other, but I didn't want them to interact physically.

    blog post image

    MatterJs employs collision groups and filters to determine which objects should or shouldn't collide. Initially, I thought changing the collision group of the objects would be enough.

    collisionGroupOnly.js

    // "this" is a Scene.
    const net = this.matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        group: 1,
      },
    }
    
    // Changing group is not enough to prevent them from colliding
    const hoopEdge = this.matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        group: 2, 
      },
    }
    

    However, I quickly realized that changing the group alone wasn't sufficient. According to MatterJs's documentation on filtering, it's crucial to understand the following rules:.

    • If the two bodies have the same non-zero value of collisionFilter.group, they will always collide if the value is positive, and they will never collide if the value is negative.
    • If the two bodies have different values of collisionFilter.group or if one (or both) of the bodies has a value of 0, then the category/mask rules apply as follows:

    Changing the groups mean the second rule applies, and we need to follow the category/mask rule.

    Again, from the documentation:

    Using the category/mask rules, two bodies A and B collide if each includes the other's category in its mask, i.e. (categoryA & maskB) !== 0 and (categoryB & maskA) !== 0 are both true.

    Knowing this, we can change the mask and category for our objects like the following example:

    collisionGroupPlusFilter

    // "this" is a Scene.
    const net = matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        group: 1,
        mask: 2, // 2 is different from the hoopEdge's category (1), meaning no collision
        category: 2,
      },
    }
    
    const hoopEdge = matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        group: 2,
        mask: 1, // 1 is different from the net's category (2), meaning no collision
        category: 1,
      },
    }
    

    Problem solved. But there's a catch, and an important one too. MatterJs uses the bits in the binary number to determine whether objects should collide or not. This is extremely important and useful, because we're able to filter more than just 1 type of object, but we have to utilize the magic of binary numbers. Using numbers like above (in their decimal form,) will cause you a lot of headache if you need more than just 1 filter.

    Binary Magic

    In binary (we can write a binary number in JavaScript by using 0b), we can represent 1, 2, 3 as 01, 10, 11 respectively:

    binaryNumbers.js

    const one = 0b01;
    const two = 0b10;
    const three = 0b11;

    Knowing this comes in handy when we know how MatterJs handles collision filtering. From the documentation:

    Each body also defines a collision bitmask, given by collisionFilter.mask which specifies the categories it collides with (the value is the bitwise AND value of all these categories).

    For instance, changing the category of the net to 3 makes objects collide unexpectedly. This is because, in binary, both the number 1 and the number 3 share the same '1'-bit in the last position in it's binary form. Remember, 1 can be represented as 0b01, and 3 can be represented as 0b11, thus MatterJs will make sure those objects collide. Here's an example:

    canCollide.js

    // the '&' operator is a JavaScript Bitwise AND operator. It will return the a new binary number of '1' shared on the same position in it's binary form
    canCollide(mask,category) {
      return mask & category !== 0;
    }
    
    // returns false (does not shares a single '1' on the same position)
    canCollide(1,2) // 0b01 & 0b10 -> 0b00
    
    
    // returns true (becase 1, and 3 share a '1' on the right most position in their binary form)
    canCollide(1,3) // 0b01 & 0b11 -> 0b01
    
    blog post image

    With this understanding, I realized we could create masks and categories using binary numbers, but decided to make the code more readable by using a category enum. It will make our code easier to maintain, and less prone to bugs.

    Here's an example, where we organize our binary numbers in a category enum (we don't have to use an enum. Using constants instead is perfectly fine.)

    categoryMaskExample.ts

    /**
     * We assign each category a unique bit
     * We can have up to 32 unique categories,
     * because that's what MatterJs supports
     */
    enum CollisionCat {
      ball = 0b001,
      hoop = 0b010,
      net = 0b100,
    }
    
    // "this" is a Scene.
    const net = this.matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        mask: CollisionCat.ball, // will collide with the ball only
        category: CollisionCat.net,
      },
    }
    
    const hoopEdge = this.matter.add.rectangle(10,10,10,10,{
      collisionFilter: {
        mask: CollisionCat.ball, // will collide with the ball only
        category: CollisionCat.hoop,
      },
    }
    
    const ball = this.matter.add.circle(10,10,10,{
      collisionFilter: {
        mask: CollisionCat.hoop | CollisionCat.net, // bitwise OR | operator, will collide with both hoop and net.
        category: CollisionCat.ball,
      },
    }
                                   
    

    There's one addition in the example I haven't explained yet. And that's the bitwise OR ('|') operator. And here's where the magic of using the category/mask comes in handy. Why will the ball collide with both the net AND the hoop in the above example?

    First we need to know how the bitwise OR operator works. The OR operator will check if either of the numbers have a 1 in their binary form. Let's take a look using the numbers from the above example:

    bitwiseOrExample.js

    // ball = 0b001
    // hoop = 0b010,
    // net = 0b100, 
    
    const mask = CollisionCat.hoop | CollisionCat.net; // mask = 6 (0b110)
    
    // writing it down, it will look like this:
    
    //   0b010
    // | 0b100
    // = 0b110

    We already know how the AND operator works, and MatterJs's rule states that objects will collide if they share at least one (1) in their binary form. Let's write the example out with both the net and the hoop and the ball

    collisionExample.js

    // CollisionCat.ball = 0b001
    // CollisionCat.hoop = 0b010
    // CollisionCat.net = 0b100 
    
    // ball's mask = 0b10 | 0b100 = 0b110
    // hoop's mask = 0b001
    // net's mask = 0b001
    
    // Let's see if the ball collides with the net.
    // We compare the net's category with the ball's mask
    // using the AND (&) operator
    // 
    //   0b100
    // & 0b110
    // = 0b100 -> can collide because they share a positive bit
    
    // Let's see if the ball collides with the hoop.
    // We compare the hoop's category with the ball's mask
    // using the AND (&) operator
    // 
    //   0b010
    // & 0b110
    // = 0b010 -> can collide because they share a positive bit

    And there we have it. We can create a Matrix, marking which objects should collide, and easily map it to our code with these rules.

    blog post image

    If you've noticed that the balls don't currently collide with each other, that's okay. To enable collisions between all balls, we have two options:

    1. Include the balls in the same group -> will always collide regardless of their mask/category
    2. Add CollisionCategory.ball using the bitwise OR (|) operator in the ball's mask.

    This knowledge about MatterJs's collision filtering is valuable not only for Phaser but also for other game engines like PICO-8, Godot, and Unity. I'd argue that these rules are consistent across various game engines when it comes to filtering collisions.

    In conclusion, understanding the basics of MatterJs collision filtering and the bitwise operations involved empowers us to write cleaner, bug-free code. This knowledge fosters a deeper understanding of collision management in game development.

    Experiment on your own

    I've prepared an example CodePen, where you can experiment by yourself. The example contains the following objects

    • Player (blue ball)
    • Ghost (orange ball)
    • Wall (the black wall in the middle of the scene)
    • Ceiling and a floor

    The ghost can pass the wall, wheras the player cannot. This is all controlled using mask and category. All objects except the wall is in the same collision group, ensuring they all can collide with each other.

    Try to open the code example, and experiment with different filters and masks. Are you able to make the player go through the wall as well?

    The same example is played in the bottom of this blog post.

    Don't hesitate to reach out via my contact form, or Twitter (𝕏) @ReitGames, if you need help or have questions. I'm here to assist you on your game development journey.

    Consider sharing this post and following me on Twitter (𝕏) @ReitGames. Subscribe to my weekly newsletter for more posts about game development.

    Happy coding!