Swift/SpriteKit Multiple Collision Detection?

Several problems here.

  1. You’re defining categories in a way that keeps them from being easily tested.
  2. You’re testing categories in a way that doesn’t get you the unique answers you want.
  3. You’ve confused your code by trying to track up to four bodies in one contact. Any contact will always have exactly two bodies.

Let’s solve them one at a time…

1. Defining Categories

You want to define collision categories so that each kind of body in your game uses its own bit in the mask. (You’ve got a good idea using Swift’s binary literal notation, but you’re defining categories that overlap.) Here’s an example of non-overlapping categories:

struct PhysicsCategory: OptionSet {
    let rawValue: UInt32
    init(rawValue: UInt32) { self.rawValue = rawValue }

    static let enemy  = PhysicsCategory(rawValue: 0b001)
    static let bullet = PhysicsCategory(rawValue: 0b010)
    static let spiral = PhysicsCategory(rawValue: 0b100)
}

I’m using a Swift OptionSet type for this, because it makes it easy to make and test for combinations of unique values. It does make the syntax for defining my type and its members a bit unwieldy compared to an enum, but it also means I don’t have to do a lot of boxing and unboxing raw values later, especially if I also make convenience accessors like this one:

extension SKPhysicsBody {
    var category: PhysicsCategory {
        get {
            return PhysicsCategory(rawValue: self.categoryBitMask)
        }
        set(newValue) {
            self.categoryBitMask = newValue.rawValue
        }
    }
}

Also, I’m using the binary literal notation and extra whitespace and zeroes in my code so that it’s easy to make sure that each category gets its own bit — enemy gets only the least significant bit, bullet the next one, etc.

2 & 3. Testing & Tracking Categories

I like to use a two-tiered approach to contact handlers. First, I check for the kind of collision — is it a bullet/enemy collision or a bullet/spiral collision or a spiral/enemy collision? Then, if necessary I check to see which body in the collision is which. This doesn’t cost much in terms of computation, and it makes it very clear at every point in my code what’s going on.

func didBegin(_ contact: SKPhysicsContact) {
    // Step 1. To find out what kind of contact we have,
    // construct a value representing the union of the bodies' categories
    // (same as the bitwise OR of the raw values)
    let contactCategory: PhysicsCategory = [contact.bodyA.category, contact.bodyB.category]

    if contactCategory.contains([.enemy, .bullet]) {
        // Step 2: We know it's an enemy/bullet contact, so there are only
        // two possible arrangements for which body is which:
        if contact.bodyA.category == .enemy {
            self.handleContact(enemy: contact.bodyA.node!, bullet: contact.bodyB.node!)
        } else {
            self.handleContact(enemy: contact.bodyB.node!, bullet: contact.bodyA.node!)
        }
    } else if contactCategory.contains([.enemy, .spiral]) {
        // Here we don't care which body is which, so no need to disambiguate.
        self.gameOver()

    } else if contactCategory.contains([.bullet, .spiral]) {
        print("bullet + spiral contact")
        // If we don't care about this, we don't necessarily
        // need to handle it gere. Can either omit this case,
        // or set up contactTestBitMask so that we
        // don't even get called for it.

    } else {
        // The compiler doesn't know about which possible
        // contactCategory values we consider valid, so
        // we need a default case to avoid compile error.
        // Use this as a debugging aid:
        preconditionFailure("Unexpected collision type: \(contactCategory)")
    }
}

Extra Credit

Why use if statements and the OptionSet type’s contains() method? Why not do something like this switch statement, which makes the syntax for testing values a lot shorter?

switch contactCategory {
    case [.enemy, .bullet]:
        // ...
    case [.enemy, .spiral]:
        // ...

    // ... 

    default:
        // ...
}

The problem with using switch here is that it tests your OptionSets for equality — that is, case #1 fires if contactCategory == [.enemy, .bullet], and won’t fire if it’s [.enemy, .bullet, .somethingElse].

With the contact categories we’ve defined in this example, that’s not a problem. But one of the nice features of the category/contact bit mask system is that you can encode multiple categories on a single item. For example:

struct PhysicsCategory: OptionSet {
    // (don't forget rawValue and init)
    static let ship   = PhysicsCategory(rawValue: 0b0001)
    static let bullet = PhysicsCategory(rawValue: 0b0010)
    static let spiral = PhysicsCategory(rawValue: 0b0100)
    static let enemy  = PhysicsCategory(rawValue: 0b1000)
}

friendlyShip.physicsBody!.category = [.ship]
enemyShip.physicsBody!.category = [.ship, .enemy]
friendlyBullet.physicsBody!.category = [.bullet]
enemyBullet.physicsBody!.category = [.bullet, .enemy]

In a situation like that, you could have a contact whose category is [.ship, .bullet, .enemy] — and if your contact handling logic is testing specifically for [.ship, .bullet], you’ll miss it. If you use contains instead, you can test for the specific flags you care about without needing to care whether other flags are present.

Leave a Comment