Swift/SpriteKit Multiple Collision Detection?

Betkowski picture Betkowski · Nov 2, 2014 · Viewed 8.1k times · Source

enter image description here

Hello.

I have a multiple collision problem. There is a bullet, which hits the enemy(red rectangle). Then, it ++ the score. There is a spiral (red circle) which is supossed to trigger the scene to end when the enemy (red rectangle) touches it.

In this situation, when enemy hits the spiral, it works, the scene ends, and we go to the menu screen. But, when bullet hits the enemy, the same thing happens, and I don't know why.

Now, here's my code:

struct PhysicsCategory {
    static let None : UInt32 = 0
    static let All : UInt32 = UInt32.max
    static let enemyOne : UInt32 = 0b1
    static let enemyTwo : UInt32 = 0b1
    static let bullet : UInt32 = 0b10
    static let spiral : UInt32 = 0b111
}

 spiral.physicsBody = SKPhysicsBody(rectangleOfSize: spiral.size)
        spiral.physicsBody?.categoryBitMask = PhysicsCategory.spiral
        spiral.physicsBody?.contactTestBitMask = PhysicsCategory.enemyOne
        spiral.physicsBody?.collisionBitMask = PhysicsCategory.None
...
        enemyOne.physicsBody = SKPhysicsBody(rectangleOfSize: enemyOne.size)
        enemyOne.physicsBody?.dynamic = true
        enemyOne.physicsBody?.categoryBitMask = PhysicsCategory.enemyOne
        enemyOne.physicsBody?.contactTestBitMask = PhysicsCategory.bullet | PhysicsCategory.spiral
        enemyOne.physicsBody?.collisionBitMask = PhysicsCategory.None

...

        bullet.physicsBody = SKPhysicsBody(circleOfRadius: bullet.size.width / 2)
        bullet.physicsBody?.dynamic = true
        bullet.physicsBody?.categoryBitMask = PhysicsCategory.bullet
        bullet.physicsBody?.contactTestBitMask = PhysicsCategory.enemyOne
        bullet.physicsBody?.collisionBitMask = PhysicsCategory.None
        bullet.physicsBody?.usesPreciseCollisionDetection = true

...

    func bulletDidCollideWithEnemy(bullet: SKSpriteNode, enemyOne: SKSpriteNode) {

        scoreOnScreen.text = String(score)
        score++
        bullet.removeFromParent()
        enemyOne.removeFromParent()
    }

    func enemyDidCollideWithSpiral(enemyOne: SKSpriteNode, spiral: SKSpriteNode) {

        let transition = SKTransition.revealWithDirection(SKTransitionDirection.Down, duration: 1.0)
        let skView = self.view! as SKView
        let scene = MenuScene(size: skView.bounds.size)
        scene.scaleMode = SKSceneScaleMode.AspectFill

        skView.presentScene(scene, transition: SKTransition.crossFadeWithDuration(0.5))
    }

 // Did Begin Contact
    func didBeginContact(contact: SKPhysicsContact) {
        var firstBody : SKPhysicsBody
        var secondBody : SKPhysicsBody
        var thirdBody : SKPhysicsBody
        var fourthBody : SKPhysicsBody

        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
            firstBody = contact.bodyA
            secondBody = contact.bodyB
        } else {
            firstBody = contact.bodyB
            secondBody = contact.bodyA
        }

        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
            thirdBody = contact.bodyA
            fourthBody = contact.bodyB
        } else {
            thirdBody = contact.bodyB
            fourthBody = contact.bodyA
        }



        if (firstBody.categoryBitMask & PhysicsCategory.enemyOne != 0) && (secondBody.categoryBitMask & PhysicsCategory.bullet != 0) {
            bulletDidCollideWithEnemy(firstBody.node as SKSpriteNode, enemyOne : secondBody.node as SKSpriteNode)
        }

        if (thirdBody.categoryBitMask & PhysicsCategory.enemyOne != 0) && (fourthBody.categoryBitMask & PhysicsCategory.spiral != 0) {
            enemyDidCollideWithSpiral(thirdBody.node as SKSpriteNode, spiral : fourthBody.node as SKSpriteNode)
        }

Now, I know it's a mess, but can anyone help me? I think it the problem has to do with the bodyA.categoryBitMask and bodyB being set to different things even thought they are the same(?). I don't know. Anyone?

Answer

rickster picture rickster · Nov 3, 2014

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.