Understanding SpriteKit CollisionBitMask

I am learning to use SpriteKit and I am following a tutorial for colllisions. I am struggling to understand the following code:

struct PhysicsCategory {
  static let None      : UInt32 = 0
  static let All       : UInt32 = UInt32.max
  static let Monster   : UInt32 = 0b1       // 1
  static let Projectile: UInt32 = 0b10      // 2
}

Why do we assign these things called bitMaps and how do they work later on in the code below?:

func didBegin(_ contact: SKPhysicsContact) {

    // 1
    var firstBody: SKPhysicsBody
    var secondBody: SKPhysicsBody
    if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
        firstBody = contact.bodyA
        secondBody = contact.bodyB
    } else {
        firstBody = contact.bodyB
        secondBody = contact.bodyA
    }

    // 2
    if ((firstBody.categoryBitMask & PhysicsCategory.Monster != 0) &&
        (secondBody.categoryBitMask & PhysicsCategory.Projectile != 0)) {
        if let monster = firstBody.node as? SKSpriteNode, let
            projectile = secondBody.node as? SKSpriteNode {
            projectileDidCollideWithMonster(projectile: projectile, monster: monster)

Thanks!


BitMasks are flags used to describe an item in a binary format

so imagine you have 8 ways to describe something. (In Spritekit you have 32)

We can fit these 8 things into a single byte, since 8 bits are in a byte, allowing us to save space and perform operations faster.

Here is an example of 8 descriptions

Attackable 1 << 0  
Ranged     1 << 1  
Undead     1 << 2  
Magic      1 << 3  
Regenerate 1 << 4  
Burning    1 << 5  
Frozen     1 << 6  
Poison     1 << 7  

Now I have an archer and want to classify him. I want to say he is a living friendly unit that is ranged

I would use the categoryBitmask to classify him:

archer.categoryBitmask = Ranged

This would be represented in 1 byte as

00000010
||||||||_ Attackable
|||||||_ Ranged
||||||_ Undead
|||||_ Magic
||||_ Regenerate
|||_ Burning
||_ Frozen
|_ Poison

Now let's say his arrows are fire arrows, I would classify this like:

arrow.categoryBitmask = Burning

00100000
||||||||_ Attackable
|||||||_ Ranged
||||||_ Undead
|||||_ Magic
||||_ Regenerate
|||_ Burning
||_ Frozen
|_ Poison

and finally, we have a zombie that can be hit and regenerates over time

zombie.categoryBitmask = Attackable + Undead + Regenerate

00010101
||||||||_ Attackable
|||||||_ Ranged
||||||_ Undead
|||||_ Magic
||||_ Regenerate
|||_ Burning
||_ Frozen
|_ Poison

Now I want my arrow to only hit Attackable sprites (zombie in this case)

I would set the contactTestBitmask to tell the arrow what he can hit

arrow.contactTestBitmask = Attackable 00000001

Now we need to check when an arrow hits a zombie, this is where didBeginContact comes in

What didBeginContact will do, is check the contactTestBitmask of the moving item to the categoryBitmask that it hits by using an AND operation to find a match

In our case

arrow.contactTestBitmask =  00000001
zombie.categoryMask      =  00010101 AND
                            --------
                            00000001

Since our value is > 0, a contact was successful.

This means didBegins fired.

Now that we are in didBegins, we need to determine which physics body is our arrow, and which physics body is our zombie

this is where this next statement comes in

func didBegin(_ contact: SKPhysicsContact) {

    // 1
    var firstBody: SKPhysicsBody
    var secondBody: SKPhysicsBody
    if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
        firstBody = contact.bodyA
        secondBody = contact.bodyB
    } else {
        firstBody = contact.bodyB
        secondBody = contact.bodyA
}

Since arrow = 00100000 and zombie = 00010101, we know that zombie has a lower value than arrow, so in this case, zombie is < arrow.

We assign firstBody to zombie, and secondBody to arrow

Now we need to provide a condition.

We want to say if an undead being is hit by a burnable object, do something.

So in code this would be

if (firstBody & Undead > 0) && (secondBody & Burning > 0)
{
//burn zombie
}

But what if the arrow was a ice arrow? We do not want to go into that if statement.

Well now we can add a second condition to allow us to freeze the zombie.

if (firstBody & Undead > 0) && (secondBody & Frozen > 0)
{
//freeze zombie
}

What these ifs are doing, is making sure that the body has certain features turned on, and then perform some action in response to them.

To find out more about how bitmasks work, I would research how to do truth tables. That is essentially what this comes down to. We are just creating a few truth tables, and trying to figure out if a statement is true, and if it is true, perform an action.


Manipulating contactTest and collison bitmasks to enable/disable specific contact and collisions.

For this example, we will used 4 bodies and will show only the last 8 bits of the bit masks for simplicity. The 4 bodies are 3 SKSpriteNodes (each with a physics body) and a boundary:

let edge = frame.insetBy(dx: 0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: edge)

Note that the 'edge' physics body is the physics body of the scene, not a node.

We define 4 unique categories

let purpleSquareCategory:   UInt32 = 1 << 0  // bitmask is ...00000001
let redCircleCategory:      UInt32 = 1 << 1  // bitmask is ...00000010
let blueSquareCategory:     UInt32 = 1 << 2  // bitmask is ...00000100
let edgeCategory:           UInt32 = 1 << 31  // bitmask is 10000...00000000

Each physics body is assigned the categories that it belongs to:

    //Assign our category bit masks to our physics bodies
    purpleSquare.physicsBody?.categoryBitMask = purpleSquareCategory
    redCircle.physicsBody?.categoryBitMask = redCircleCategory
    blueSquare.physicsBody?.categoryBitMask = blueSquareCategory
    physicsBody?.categoryBitMask = edgeCategory  // This is the edge for the scene itself

If a bit in a body's collisionBitMask is set to 1, then it collides (bounces off) any body that has a '1' in the same position in it's categoryBitMask. Similarly for contactTestBitMask.

Unless you specify otherwise, everything collides with everything else and no contacts are generated (your code won't be notified when anything contacts anything else):

purpleSquare.physicsBody.collisonBitMask = 11111111111111111111111111111111 // 32 '1's.

Every bit in every position is '1', so when compared to any other categoryBitMask, Sprite Kit will find a '1' so a collision will occur. If you do not want this body to collide with a certain category, you will have to set the correct bit in the collisonBitMask to '0'

and its contactTestbitMask is set to all 0s:

redCircle.physicsBody.contactTestBitMask = 00000000000000000000000000000000  // 32 '0's

Same as for collisionBitMask, except reversed.

Contacts or collisions between bodies can be turned off (leaving existing contact or collision unchanged) using:

nodeA.physicsBody?.collisionBitMask &= ~nodeB.category

We logically AND nodeA's collision bit mask with the inverse (logical NOT, the ~ operator) of nodeB's category bitmask to 'turn off' that bit nodeA's bitMask. e.g to stop the red circle from colliding with the purple square:

redCircle.physicsBody?.collisionBitMask = redCircle.physicsBody?.collisionBitMask & ~purpleSquareCategory

which can be shortened to:

redCircle.physicsBody?.collisionBitMask &= ~purpleSquareCategory

Explanation:

redCircle.physicsBody.collisonBitMask = 11111111111111111111111111111111
purpleSquareCategory  = 00000000000000000000000000000001
~purpleSquareCategory = 11111111111111111111111111111110 
11111111111111111111111111111111 & 11111111111111111111111111111110 = 11111111111111111111111111111110 

redCircle.physicsBody.collisonBitMask now equals 11111111111111111111111111111110 redCircle no longer collides with bodies with a category of ....0001 (purpleSquare)

Instead of turning off individual bits in the collsionsbitMask, you can set it directly:

blueSquare.physicsBody?.collisionBitMask = (redCircleCategory | purpleSquareCategory)
i.e. blueSquare.physicsBody?.collisionBitMask = (....00000010 OR ....00000001)

which equals blueSquare.physicsBody?.collisionBitMask = ....00000011

blueSquare will only collide with bodies with a category or ..01 or ..10

Contacts or collisions between 2 bodies can be turned ON (without affecting any existing contacts or collisions) at any point using:

redCircle.physicsBody?.contactTestBitMask |= purpleSquareCategory

We logically AND redCircle's bitMask with purpleSquare's category bitmask to 'turn on' that bit in redcircle's bitMask. This leaves any other bits in redCircel's bitMas unaffected.

You can make sure that every shape 'bounces off' a screen edge as follows:

// Make sure everything collides with the screen edge
enumerateChildNodes(withName: "//*") { node, _ in
    node.physicsBody?.collisionBitMask |= self.edgeCategory  //Add edgeCategory to the collision bit mask
}

Note:

Collisions can be one-sided i.e. object A can collide (bounce off) object B, whilst object B carries on as though nothing had happened. If you want 2 object to bounce off each other, they must both be told to collide with the other:

blueSquare.physicsBody?.collisionBitMask = redCircleCategory
redcircle.physicsBody?.collisionBitMask = blueSquareCategory

Contacts however are not one-sided; if you want to know when object A touched (contacted) object B, it is enough to set up contact detection on object A with regards to object B. You do not have to set up contact detection on object B for object A.

blueSquare.physicsBody?.contactTestBitMask = redCircleCategory

We don't need redcircle.physicsBody?.contactTestBitMask= blueSquareCategory