Swift/SpriteKit Multiple Collision Detection?
Asked Answered
A

1

8

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?

Allative answered 2/11, 2014 at 18:43 Comment(2)
the block where you decide between first, second, third and fourth body makes no sense. The categorybitmaks of enemy is always less than both bullet and spiral categories. Less than operator is not useful for comparing bitmasks. Also you really don't need four body variables if you compare the bitmasks correctly.Malvina
I don't understand how spiral can be removed if it's never being removed in my code. Could you pass me some kind of solution that I can learn from, or a sample code with smiliar problem? I don't really know what to do at this point.Allative
F
35

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.

Fulkerson answered 3/11, 2014 at 21:19 Comment(3)
Problem Solved! I cannot thank you enough, I thought this is the problem that's going to knock me down, but thanks to you, I got back up, and can continue working on the game! You just made someone very happy!Allative
What do you think of using this system (#24070203) for defining bit masks without extra zeros?: 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 }Resnick
Looks like they're just leaving off leading zeroes? Just a matter of style, but I prefer leading zeroes so that it's easy to see the difference between 0b1 and 0b10 (etc) when lined up vertically — when you're working with things where the position of a bit within a byte is important, it can help to make that position clear. It's the same reason that some prefer bit-shift notation (1 << 0, 1 << 1, etc).Fulkerson

© 2022 - 2024 — McMap. All rights reserved.