Collision groups are useful when you want to filter the collision that are possible between colliders on a granular level above and beyond collision type. This idea is also known as collision layers, collision filters, or collision filtering.
Collision groups provide an efficient way optimized in Excalibur to sort colliders into possible colliders without needed custom logic in collision event handlers.
To help demonstrate how collision groups work to filter collisions, let's consider a simple platformer that has players, enemies, and floors.
The rules of our game:
Additional info: see Aurelien's post on the subject of collision filtering as it applies to other libraries.
Think about collision groups is as different "categories" (other engines like Unity call collision groups "layers"). Each group is represented by a unique 32 bit integer represented by a power of 2. CollisionGroupManager.create works by assigning a unique power of 2 to each group (meaning only 32 groups are possible).
So the group bit categories in our example are
playersGroupCategory = 0b0001
enemyGroupCategory = 0b0010
floorGroupCategory = 0b0100
Each group also contains a mask
, this mask has the bit position of the groups it can collided with set to 1. The player group category is the least significant bit (furthest right), if a collision group mask has that bit set to 1 it means it collides with the player group.
By default the mask for a collision group is set to collide with every group apart from itself.
playersGroupMask = 0b11111111_11111111_11111111_11111110
enemyGroupMask = 0b11111111_11111111_11111111_11111101
floorGroupMask = 0b11111111_11111111_11111111_11111011
In Excalibur, the CollisionGroup.canCollide
logic works by performing a bitwise AND
between the current group category
and the target groups's mask
// Create a group for each distinct category of "collidable" in your game
const playerGroup = ex.CollisionGroupManager.create('player')
const enemyGroup = ex.CollisionGroupManager.create('enemyGroup')
const floorGroup = ex.CollisionGroupManager.create('floorGroup')
// 0b0001 & 0b11111111_11111111_11111111_11111110 = 0
// the mask is set to collide with all groups except player
playerGroup.canCollide(playerGroup) // false
// 0b0001 & 0b11111111_11111111_11111111_11111101 = 1
// the enemy mask has the player bit and floor bit set, so enemy and player can collide
playerGroup.canCollide(enemyGroup) // true
// 0b0001 & 0b11111111_11111111_11111111_11111011 = 1
// the floor mask has the player bit and enemy bit set, so player and floor can collide
playerGroup.canCollide(floorGroup) // true
Collision groups can be supplied at constructor time when building an actor. Here is a pattern you can follow
// player.ts
// Export the collision group, useful for referencing in other actors
export const PlayerCollisionGroup = CollisionGroupManager.create('player')
export class Player extends Actor {
constructor() {
super({
name: 'player',
pos: vec(200, 200),
collisionType: CollisionType.Active,
collisionGroup: PlayerCollisionGroup,
})
}
}
The collides with helper can assist in crafting groups that collide with other groups without needing to provide bitmasks.
To make sense of this helper let's take a look at the new group playersCanCollideWith
.
In the example below here are the collision group category bits.
playersGroup = 0b0001
npcGroup = 0b0010
floorGroup = 0b0100
enemyGroup = 0b1000
And the masks
playersGroupMask = 0b11111111_11111111_11111111_11111110
npcGroupMask = 0b11111111_11111111_11111111_11111101
floorGroupMask = 0b11111111_11111111_11111111_11111011
enemyGroupMask = 0b11111111_11111111_11111111_11110111
The CollisionGroup.collidesWith(...)
helper makes a new group that is all of the categories OR'd together then inverted. So in the example below creating the const playersCanCollideWith = CollisionGroup.collidesWith([ playersGroup, floorGroup, enemyGroup,]) group.
Working out how the helper works
playersCanCollideWith = ~(0b0001 | 0b0100 | 0b1000) = 0b0010
playersCanCollideWithMask = ~(0b0010) = 0b11111111_11111111_11111111_11111101
Note the group 'playersCanCollideWith' is equivalent to the npc group. playersCanCollideWith collides with playersGroup, floorGroup, and enemyGroup
// Create a group for each distinct category of "collidable" in your game
const playerGroup = ex.CollisionGroupManager.create('player')
const npcGroup = ex.CollisionGroupManager.create('npcGroup')
const floorGroup = ex.CollisionGroupManager.create('floorGroup')
const enemyGroup = ex.CollisionGroupManager.create('enemyGroup')
// Define your rules
// playersCanCollideWith = ~(0b0001 | 0b0100 | 0b1000) = 0b0010
// playersCanCollideWithMask = ~(0b0010) = 0b11111111_11111111_11111111_11111101
// Note the group 'playersCanCollideWith' is equivalent to the npc group. playersCanCollideWith collides with playersGroup, floorGroup, and enemyGroup
const playersCanCollideWith = ex.CollisionGroup.collidesWith([
playersGroup, // collide with other players
floorGroup, // collide with the floor
enemyGroup, // collide with enemies
])
const enemiesCanCollideWith = ex.CollisionGroup.collidesWith([
playerGroup, // collide with players
floorGroup, // collide with the floor
])
const npcGroupCanCollideWith = ex.CollisionGroup.collidesWith([
floorGroup, // only collides with the floor
])
const player = new ex.Actor({
collisionGroup: playersCanCollideWith,
})
const npc = new ex.Actor({
collisionGroup: npcGroupCanCollideWith,
})
const enemy = new ex.Actor({
collisionGroup: enemiesCanCollideWith,
})
const floor = new ex.Actor({
pos: ex.vec(100, 400),
width: 100,
height: 20,
collisionType: ex.CollisionType.Fixed
collisionGroup: floorGroup,
})