Arcade Physics Collision Categories

Hello, I’m using Phaser’s Arcade Physics and working with collision categories. My goal is to control how different units (players and enemies) collide in various scenarios. Specifically, I want to:

  1. Enable collisions between all units (players vs. players, enemies vs. enemies, and players vs. enemies) by default.
  2. Disable collisions between friendly units (e.g., player vs. player or enemy vs. enemy) when specific units are “firing” or performing certain actions.
  3. Restore friendly collisions when the units return to a different state (e.g., melee combat).

Here’s how I currently have everything set up:

Scene Setup

In the scene constructor, I define collision categories using this.physics.nextCategory():

this.categories = {
  enemy: this.physics.nextCategory(),
  player: this.physics.nextCategory(),
};

I also create Arcade Physics groups for the different unit types:

context.physicsGroups = {
  enemyGroups: context.physics.add.group(),
  playerGroups: context.physics.add.group(),
  enemyBaseZones: context.physics.add.group(),
  playerBaseZones: context.physics.add.group(),
  arrows: context.physics.add.group(),
};

Then, I add colliders for different scenarios:

context.physics.add.collider(
  context.physicsGroups.playerGroups,
  context.physicsGroups.enemyGroups,
  handleOpponentCollision,
  undefined,
  context
);

context.physics.add.collider(
  context.physicsGroups.enemyGroups,
  context.physicsGroups.enemyGroups,
  handleFriendlyCollision,
  undefined,
  context
);

context.physics.add.collider(
  context.physicsGroups.playerGroups,
  context.physicsGroups.playerGroups,
  handleFriendlyCollision,
  undefined,
  context
);

In the constructor for each unit, I assign the collision category dynamically based on the unit’s side (player or enemy):

if (this.info.unitSide === "enemy") {
  this.body.setCollisionCategory(this.scene.categories.enemy);
} else {
  this.body.setCollisionCategory(this.scene.categories.player);
}
console.log(this.body.collisionCategory); // Verifying category assignment

// By default, allow collisions with both categories
this.body.setCollidesWith([this.scene.categories.enemy, this.scene.categories.player]);

When the unit enters a “firing” state, I disable friendly collisions:

this.body.removeCollidesWith(
  this.info.unitSide === "enemy" ? this.scene.categories.enemy : this.scene.categories.player
);

When the unit returns to melee combat, I restore collisions with friendly units:

this.body.setCollidesWith([this.scene.categories.enemy, this.scene.categories.player]);

The Problem

While testing, I noticed that enemy units (Category X) and player units (Category Y) no longer collide with each other. They completely ignore each other, even though the categories are set correctly.

Here are some additional details:

  • Enemy units have a collision category X and player units have category Y.
  • I’ve verified the categories are being assigned correctly by logging this.body.collisionCategory.
  • When collisions stop, neither handleOpponentCollision nor handleFriendlyCollision callbacks are triggered.

My Understanding of the Problem

From the Phaser documentation, Arcade Physics supports setCollisionCategory(), setCollidesWith(), addCollidesWith(), and removeCollidesWith(). And I don’t want to use Matter physics.

However, something is causing the collisions to break entirely, and I can’t figure out why.

My Question

  1. Is there something I’m missing in how Arcade Physics handles collision categories?
  2. Are there known limitations or specific configurations required to make these methods work with Arcade Physics?
  3. Does this approach look correct, or should I manage this differently?

Any guidance would be greatly appreciated. Thank you!

Does the problem also happen if you remove the firing state switch?

You can put a breakpoint in Phaser.Physics.Arcade.World#canCollide and see what’s going on.

Hi. The same problem happens as if the arcade physics would not support categories.

If you can make a minimal test case, please make a bug report.

Hi, after doing a small test case I am not sure if it’s a bug.

So I am relying on body.setCollisionCategory() and body.setCollidesWith(). I want certain bodies to ignore collisions with each other, but if I add a group-level collider (e.g., this.physics.add.collider(groupA, groupA, ...)), those bodies still collide, even though I’ve excluded their categories from each other’s collision masks.

I’ve verified the categories by logging them, and I see the correct bitmasks. However, Arcade Physics still pushes bodies apart as if setCollisionCategory() and setCollidesWith() weren’t set. When I remove the group-level collider calls, it behaves closer to what I expect, but then I lose some of the collision callbacks I need for other interactions.

Here you can have a minimal example of what I want, working fine but defined separately with the add.collider separately:

class MainScene extends Phaser.Scene {
  constructor() {
    super({ key: "MainScene" });
  }

  preload() {
    // Not loading any images since we're just using rectangles in containers
  }

  create() {
    // Arcade Physics with no gravity, debug on
    this.physics.world.setBounds(0, 0, 800, 800);

    // Hard-coded bitmask categories:
    this.categories = {
      item1: 1,
      item2: 2,
      item3: 3
    };

    // === CREATE CONTAINERS WITH RECTANGLES ===
    this.container1 = this.add.container(200, 400);
    this.container2 = this.add.container(600, 400);
    this.container3 = this.add.container(400, 600);



    // Enable Arcade Physics for the containers
    this.physics.add.existing(this.container1);
    this.physics.add.existing(this.container2);
    this.physics.add.existing(this.container3);

    // Make sure each has a body
    this.container1.body.setCollideWorldBounds(true);
    this.container2.body.setCollideWorldBounds(true);
    this.container3.body.setCollideWorldBounds(true);

    // === ASSIGN COLLISION CATEGORIES AND MASKS ===
    // Item 1 collides only with item2 (2)
    this.container1.body.setCollisionCategory(this.categories.item1);
    this.container1.body.setCollidesWith(this.categories.item2);

    // Item 2 collides with item1 (1) and item3 (3)
    this.container2.body.setCollisionCategory(this.categories.item2);
    this.container2.body.setCollidesWith([this.categories.item1, this.categories.item3]);

    // Item 3 collides only with item2 (2)
    this.container3.body.setCollisionCategory(this.categories.item3);
    this.container3.body.setCollidesWith(this.categories.item2);

    // === ADD COLLIDERS (REQUIRED FOR ARCADE PHYSICS COLLISIONS) ===
    this.physics.add.collider(this.container1, this.container2, () => {
      console.log("Item 1 collided with Item 2");
    });

    this.physics.add.collider(this.container2, this.container3, () => {
      console.log("Item 2 collided with Item 3");
    });

    // If you ever see this triggered, something’s off:
    this.physics.add.collider(this.container1, this.container3, () => {
      console.log("Item 1 collided with Item 3 (should NOT happen)");
    });

    // === MOVE TOWARDS THE CENTER ===
    // Container1 => from left to center
    this.container1.body.setVelocityX(50);
    // Container2 => from right to center
    this.container2.body.setVelocityX(-50);
    // Container3 => from bottom to center
    this.container3.body.setVelocityY(-50);
  }


}

const game = new Phaser.Game({
  type: Phaser.AUTO,
  width: 800,
  height: 800,
  backgroundColor: '#222222',
  physics: {
    default: "arcade",
    arcade: {
      gravity: { y: 0, x: 0 },
      debug: true,
      forceX: false
    }
  },
  scene: [ MainScene ]
});

Below is a minimal demo for Phaser’s Arcade Physics showing that even though we set body.setCollisionCategory() and body.setCollidesWith(), group-level colliders can force collisions to occur anyway. In this example:

  • Container1 is assigned category 1 and should only collide with category 2.
  • Container2 is category 2 and collides with categories 1 & 3.
  • Container3 is category 3 and should only collide with category 2.

I do NOT add a direct collider between container1 and container3. Instead, I use a group collider on groupAll. Despite the categories saying “1 should not collide with 3,” you’ll see them push each other and stop when they meet.

You can paste this into the Phaser Sandbox to reproduce the issue:

class MainScene extends Phaser.Scene {
  constructor() {
    super({ key: "MainScene" });
  }

  preload() {
    // Not loading any images since we're just using rectangles in containers
  }

  create() {
    // Arcade Physics with no gravity, debug on
    this.physics.world.setBounds(0, 0, 800, 800);

    // Hard-coded bitmask categories:
    // Using powers of two for clarity, but it's not strictly required in Arcade
    this.categories = {
      item1: 1, // container1
      item2: 2, // container2
      item3: 4  // container3
    };

    // === CREATE CONTAINERS ===
    this.container1 = this.add.container(100, 400);
    this.container2 = this.add.container(400, 400);
    this.container3 = this.add.container(700, 400);

    // Create some placeholder graphics to visualize the bodies
    const graphics1 = this.add.rectangle(0, 0, 50, 50, 0xff0000);
    const graphics2 = this.add.rectangle(0, 0, 50, 50, 0x00ff00);
    const graphics3 = this.add.rectangle(0, 0, 50, 50, 0x0000ff);

    this.container1.add(graphics1);
    this.container2.add(graphics2);
    this.container3.add(graphics3);

    // === ENABLE PHYSICS ON THE CONTAINERS ===
    this.physics.add.existing(this.container1);
    this.physics.add.existing(this.container2);
    this.physics.add.existing(this.container3);

    // Make sure each has a body
    this.container1.body.setCollideWorldBounds(true);
    this.container2.body.setCollideWorldBounds(true);
    this.container3.body.setCollideWorldBounds(true);

    // === ASSIGN COLLISION CATEGORIES AND MASKS ===
    // Container1 => category 1, only collides with category 2
    this.container1.body.setCollisionCategory(this.categories.item1);
    this.container1.body.setCollidesWith(this.categories.item2);

    // Container2 => category 2, collides with categories 1 and 4
    // (We use 4 below for container3)
    this.container2.body.setCollisionCategory(this.categories.item2);
    this.container2.body.setCollidesWith([this.categories.item1, this.categories.item3]);

    // Container3 => category 4, only collides with category 2
    this.container3.body.setCollisionCategory(this.categories.item3);
    this.container3.body.setCollidesWith(this.categories.item2);

    // === MAKE A GROUP & USE A GROUP-LEVEL COLLIDER ===
    // Despite the collision masks, this collider forces collisions among all 3 containers.
    const groupAll = this.physics.add.group([this.container1, this.container2, this.container3]);
    this.physics.add.collider(groupAll, groupAll, (objA, objB) => {
      console.log(
        "Collision detected between:",
        objA.displayList?.name || objA.name,
        "and",
        objB.displayList?.name || objB.name
      );
    });

    // === MOVE THEM TOWARD EACH OTHER ===
    // container1 => moves right
    this.container1.body.setVelocityX(50);

    // container2 => stays still in the middle
    this.container2.body.setVelocity(0);

    // container3 => moves left
    this.container3.body.setVelocityX(-50);
  }
}

const game = new Phaser.Game({
  type: Phaser.AUTO,
  width: 800,
  height: 800,
  backgroundColor: '#222222',
  physics: {
    default: "arcade",
    arcade: {
      gravity: { y: 0, x: 0 },
      debug: true,
      forceX: false
    }
  },
  scene: [ MainScene ]
});

Is this a known limitation? Is there a recommended way to prevent collisions between specific categories while still using group-level colliders? Any guidance would be appreciated. Thank you!