iossprite-kitcocos2d-xbitmask

Game Engine Collison Bitmask... Why 0x01 etc?


Coming across this situation both in Sprite Kit (iOS Development) and in Cocos2d-x (which I know was pretty much the inspiration for Sprite Kit, hence why they use a lot of the same tools), I finally decided to figure out why this happens:

When using a physic engine, I create a sprite, and add a physicsBody to it. For the most part, I understand how to set the category, collision, and contact bitmasks, and how they work. The problem is the actual bitmask number:

SpriteKit:

static const uint32_t missileCategory     =  0x1 << 0;
sprite.physicsBody.categoryBitMask = missileCategory;

Cocos2D-X:

sprite->getPhysicsBody()->setCategoryBitmask(0x01); // 0001

I'm totally confused as to why I would write 0x01 or 0x1 << 0 in either case. I somewhat get that they're using hex, and it has something to do with 32-bit integers. And as far as I've been able to google, 0x01 is 0001 in binary which is 1 in decimal. And 0x02 is 0010 in binary which is 2 in decimal. Okay, so there are these conversions, but why in the world would I use them for something simple like categories?

As far as my logic goes, if I have lets say a player category, an enemy category, a missile category, and a wall category, that's just 4 categories. Why not use strings for the category? Or even just binary numbers that any non-CS person would understand like 0,1,2, and 3?

And finally, I'm confused why there are 32 different categories available? I thought a 32-bit integer had numbers 0-some billion number (unsigned of course). So why do I not have billions of different possible categories?

Is there some sort of optimization I am not understanding? Or this is just an old convention they use, but is not needed? Or is there something going on that someone with only 2 semesters of college course CS training wouldn't understand?


Solution

  • The reason for the bitmasks is that it enables you / the program to easily and very quickly compute wether a collision between two objects occurs or does not occur. Therefore: yes it is some sort of optimization.

    Assuming we have the three categories

    Now we have a Player instance, its category is set to player. Its collision bitmask is set to missile | player | wall (+ instead of | works too) since we want to be able to collide with all three types: other players, the level walls and the bullets / missiles flying around.

    Now we have a Missile with category set to missile and collision bitmask set to player | wall: it does not collide with other missiles but hits players and walls.

    If we now want to evaluate wether two objects can collide with each other we take the category bitmask of the first one and the collision bitmask of the second one and simply & them:

    The setup described above looks like the following in code:

    let player : UInt8 = 0b1 << 0  // 00000001 = 1
    let missile : UInt8 = 0b1 << 1 // 00000010 = 2
    let wall : UInt8 = 0b1 << 2    // 00000100 = 4
    
    let playerCollision = player | missile | wall // 00000111 = 7
    let missileCollision = player | wall          // 00000101 = 5
    

    The subsequent reasoning is basically:

    if player & missileCollision != 0 {
        print("potential collision between player and missile") // prints
    }
    if missile & missileCollision != 0 {
        print("potential collision between two missiles") // does not print
    }
    

    We are using some bit arithmetics here, each bit represents a category. You could simply enumerate the bitmasks 1,2,3,4,5... but then you could not do any math on them. Because you do not know if a 5 as category bitmask is really a category 5 or it was an object of both categories 1 and 4.

    However using only bits we can do just that: the only representation in terms of powers of 2 of a 7 is 4 + 2 + 1: therefore whatever object posses collision bitmask 7 collides with category 4, 2 and 1. And the one with bitmask 5 is exactly and only a combination of category 1 and 4 - there is no other way.

    Now since we are not enumerating - each category uses one bit and the regular integer has only 32 (or 64) bits we can only have 32 (or 64) categories.

    Take a look at the following and a bit more extensive code which demonstrates how the masks are used in a more general term:

    let playerCategory : UInt8 = 0b1 << 0
    let missileCategory : UInt8 = 0b1 << 1
    let wallCategory : UInt8 = 0b1 << 2
    
    struct EntityStruct {
        var categoryBitmask : UInt8
        var collisionBitmask : UInt8
    }
    
    let player = EntityStruct(categoryBitmask: playerCategory, collisionBitmask: playerCategory | missileCategory | wallCategory)
    let missileOne = EntityStruct(categoryBitmask: missileCategory, collisionBitmask: playerCategory | wallCategory)
    let missileTwo = EntityStruct(categoryBitmask: missileCategory, collisionBitmask: playerCategory | wallCategory)
    let wall = EntityStruct(categoryBitmask: wallCategory, collisionBitmask: playerCategory | missileCategory | wallCategory)
    
    func canTwoObjectsCollide(first:EntityStruct, _ second:EntityStruct) -> Bool {
        if first.categoryBitmask & second.collisionBitmask != 0 {
            return true
        }
        return false
    }
    
    canTwoObjectsCollide(player, missileOne)     // true
    canTwoObjectsCollide(player, wall)           // true
    canTwoObjectsCollide(wall, missileOne)       // true
    canTwoObjectsCollide(missileTwo, missileOne) // false
    

    The important part here is that the method canTwoObjectsCollide does not care about the type of the objects or how many categories there are. As long as you stick with bitmasks that is all you need to determine wether or not two objects can theoretically collide (ignoring their positions, which is a task for another day).