Swift/Spritekit Multiple Collision Detection

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.

Detecting multiple collisions in SpriteKit

I've not used the OptionSet technique for cagegoryBitMasks, so here's how I'd do it:

Define unique categories, ensure your class is a SKPhysicsContactDelegate and make yourself the physics contact delegate:

//Physics categories
let PlayerCategory: UInt32 = 1 << 0 // b'00001'
let EnemyBulletCategory: UInt32 = 1 << 1 // b'00010'
let PlayerBulletCategory: UInt32 = 1 << 2 // b'00100'
let EnemyCategory: UInt32 = 1 << 3 // b'01000'
let BossCategory: UInt32 = 1 << 4 // b'10000'

class GameScene: SKScene, SKPhysicsContactDelegate {
physicsWorld.contactDelegate = self

Assign the categories (usually in didMove(to view:) :

player.physicsBody.catgeoryBitMask = PlayerCategory
bullet.physicsBody.catgeoryBitMask = BulletCategory
enemy.physicsBody.catgeoryBitMask = EnemyCategory
enemyBullet.physicsBody.catgeoryBitMask = EnemyBulletCategory
boss.physicsBody.catgeoryBitMask = BossCategory

(not sure about bulletSpecial - looks the same as bullet)

Set up contact detection:

player.physicsBody?.contactTestBitMask = EnemyCategory | EnemyBulletCategory | BossCategory
bullet.physicsBody?.contactTestBitMask = EnemyCategory | BossCategory
enemy.physicsBody?.contactTestBitMask = PlayerCategory | PlayerBulletCategory
enemyBullet.physicsBody?.contactTestBitMask = PlayerCategory
boss.physicsBody?.contactTestBitMask = PlayerCategory | PlayerBulletCategory

Turn off collisions: (on by default)

player.physicsBody?.collisionBitMask = 0
bullet.physicsBody?.collisionBitMask = 0
enemy.physicsBody?.collisionBitMask = 0
enemyBullet.physicsBody?.collisionBitMask = 0
boss.physicsBody?.collisionBitMask = 0

Implement didBegin:

  func didBegin(_ contact: SKPhysicsContact) {

print("didBeginContact entered for \(String(describing: contact.bodyA.node!.name)) and \(String(describing: contact.bodyB.node!.name))")

let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

switch contactMask {
case PlayerCategory | EnemyCategory:
print("player has hit enemy")
case PlayerBulletCategory | EnemyCategory:
print("player bullet has hit enemy")
case PlayerBulletCategory | BossCategory:
print("player bullet has hit boss")
case PlayerCategory | BossCategory:
print("player has hit boss")
case PlayerCategory | EnemyBulletCategory:
print("player has hit enemy bullet")
default:
print("Undetected collision occurred")
}
}

It's a bit late here, so hopefully I haven't made any stupid mistakes.

=======================

You could also include this function and then call it via checkPhysics() once you have set up all your physics bodies and collisions and contact bit masks. It will go through every node and print out what collides with what and what contacts what (it doesn't check the isDynamic property, so watch out for that):

//MARK: - Analyse the collision/contact set up.
func checkPhysics() {

// Create an array of all the nodes with physicsBodies
var physicsNodes = [SKNode]()

//Get all physics bodies
enumerateChildNodes(withName: "//.") { node, _ in
if let _ = node.physicsBody {
physicsNodes.append(node)
} else {
print("\(String(describing: node.name)) does not have a physics body so cannot collide or be involved in contacts.")
}
}

//For each node, check it's category against every other node's collion and contctTest bit mask
for node in physicsNodes {
let category = node.physicsBody!.categoryBitMask
// Identify the node by its category if the name is blank
let name = node.name != nil ? node.name : "Category \(category)"

if category == UInt32.max {print("Category for \(String(describing: name)) does not appear to be set correctly as \(category)")}

let collisionMask = node.physicsBody!.collisionBitMask
let contactMask = node.physicsBody!.contactTestBitMask

// If all bits of the collisonmask set, just say it collides with everything.
if collisionMask == UInt32.max {
print("\(name) collides with everything")
}

for otherNode in physicsNodes {
if (node != otherNode) && (node.physicsBody?.isDynamic == true) {
let otherCategory = otherNode.physicsBody!.categoryBitMask
// Identify the node by its category if the name is blank
let otherName = otherNode.name != nil ? otherNode.name : "Category \(otherCategory)"

// If the collisonmask and category match, they will collide
if ((collisionMask & otherCategory) != 0) && (collisionMask != UInt32.max) {
print("\(name) collides with \(String(describing: otherName))")
}
// If the contactMAsk and category match, they will contact
if (contactMask & otherCategory) != 0 {print("\(name) notifies when contacting \(String(describing: otherName))")}
}
}
}
}

It will produce output like:

Optional("shape_blueSquare") collides with Optional("Screen_edge")
Optional("shape_redCircle") collides with Optional("Screen_edge")
Optional("shape_redCircle") collides with Optional("shape_blueSquare")
Optional("shape_redCircle") notifies when contacting Optional("shape_purpleSquare")
Optional("shape_redCircle") collides with Optional("shape_greenRect")
Optional("shape_redCircle") notifies when contacting Optional("shape_greenRect")
Optional("shape_purpleSquare") collides with Optional("Screen_edge")
Optional("shape_purpleSquare") collides with Optional("shape_greenRect")
Category for Optional("shape_greenRect") does not appear to be set correctly as 4294967295
Optional("shape_greenRect") collides with Optional("Screen_edge")
Optional("shape_yellowTriangle") notifies when contacting Optional("shape_redCircle")
Optional("shape_yellowTriangle") collides with Optional("shape_greenRect")
Optional("shape_yellowTriangle") notifies when contacting Optional("shape_greenRect")

etc.

Sprite-Kit registering multiple collisions for single contact

OK - it would appear that a simple:

        if bomb == nil {return}

is all that's required. This should be added as follows:

        let bomb = contact.bodyA.categoryBitMask == category.bomb.rawValue ? contact.bodyA.node : contact.bodyB.node
if bomb == nil {return}

This works to prevent multiple collisions for a node that you removeFromParent in didBeginContact. If you don;t remove the node but are still registering multiple collisions, then use the node's userData property to set some sort of flag indicating that the node i s'anactive' Alternately, subclass SKSPriteNode and add a custom 'isActive' property, which is what I did to solve my problem of bullets passing up the side of an invader and taking out that invader and the one above it. This never happens on a 'direct hit'.

It doesn't answer the underlying question as to why SK is registering multiple collisions, but it does mean that I can remove all the extra code concerning setting contactTestBitMasks to 0 and then back to what they should be later etc.

Edit: So it appears that if you delete a node in didBeginContact, the node is removed but the physics body isn't. So you have to be careful as Sprite-Kit appears to build an array of physicsContacts that have occurred and then calls dBC multiple times, once for each contact. So if you are manipulating nodes and also removing them in dBC, be aware that you might run into an issue if you force unwrap a node's properties.

How to Detect collision in Swift, Sprite kit

  1. Define unique categories, ensure your class is a SKPhysicsContactDelegate and make yourself the physics contact delegate:

//Physics categories

let enemyCategory:    UInt32 = 1 << 1
let bulletCategory: UInt32 = 1 << 2

class GameScene: SKScene, SKPhysicsContactDelegate {
physicsWorld.contactDelegate = self

  1. Assign the categories (usually in didMove(to view:) :

    enemy.physicsBody.catgeoryBitMask = enemyCategory
    bullet.physicsBody.catgeoryBitMask = bulletCategory

(Make sure you've created physics bodies for each node)


  1. Set up collisions:

enemy.physicsBody?.collisionBitMask = 0 // enemy collides with nothing
bullet.physicsBody?.collisionBitMask = 0 // bullet collides with nothing

or even:

for node in [enemy, bullet] {
node.physicsBody?.collisionBitMask = 0 // collides with nothing
}

  1. Set up contacts

    bullet.physicsBody?.collisionBitMask = enemyCategory // bullet contacts enemy

Make sure that at least one of the objects involved in each potential contact has the isDynamic property on its physics body set to true, or no contact will be generated. It is not necessary for both of the objects to be dynamic.

You should now get didBegin called when the bullet and the enemy make contact. You could code didBegin like this:

  func didBegin(_ contact: SKPhysicsContact) {
print("didBeginContact entered for \(String(describing: contact.bodyA.node!.name)) and \(String(describing: contact.bodyB.node!.name))")

let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

switch contactMask {
case bulletCategory | enemyCategory:
print("bullet and enemy have contacted.")
let bulletNode = contact.bodyA.categoryBitMask == bulletCategory ? contact.bodyA.node : contact.bodyB.node
enemyHealth -= 10
bulletNode.removeFromParent
default:
print("Some other contact occurred")
}

}

Swift SpriteKit Collisions Registered Multiple Times

Give your bullet node a name:

let bullet = SKSPriteNode()
bullet.name = "bullet"

Then check your physics contact

if (contact.bodyA.node?.name == "bullet") {
contact.bodyA.node?.removeFromParent()
} else if contact.bodyB.node?.name == "bullet" {
contact.bodyB.node?.removeFromParent()
}

Good luck!



Related Topics



Leave a reply



Submit