Search code examples
swiftsprite-kitcollision-detection

How to detect collision with more than 2 objects in Swift/SpriteKit


So I am creating an air hockey game and there are 5 major SKSpriteNodes. I need to detect collision between the puck and goal1 and goal2. When it does detect collision it needs to add 1 to the scoreLeft or scoreRight which are SKLabelNode's. I have used the Gamescene.sks file to make the visual representation. I commented the didBeginContact because thats where it doesn't work. Thank You in advance.

class GameScene: SKScene, SKPhysicsContactDelegate {

let paddle1CategoryName = "paddle1"
let paddle2CategoryName = "paddle2"
let puckCategoryName = "puck"
let goal1CategoryName = "goal1"
let goal2CategoryName = "goal2"
let scoreLeftCategoryName = "scoreLeft"
let scoreRightCategoryName = "scoreRight"
var isFingerOnPaddle1 = false
var isFingerOnPaddle2 = false


var paddle1Category: UInt32 = 0x1 << 0
var paddle2Category: UInt32 = 0x1 << 1
var puckCategory: UInt32 = 0x1 << 2
var goal1Category: UInt32 = 0x1 << 3
var goal2Category: UInt32 = 0x1 << 4

override func didMoveToView(view: SKView) {
    /* Setup your scene here */
    let borderBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
    borderBody.friction = 0
    self.physicsBody = borderBody
    let paddle1 = childNodeWithName(paddle1CategoryName) as! SKSpriteNode
    let paddle2 = childNodeWithName(paddle2CategoryName) as! SKSpriteNode
    let puck = childNodeWithName(puckCategoryName) as! SKSpriteNode
    let goal1 = childNodeWithName(goal1CategoryName) as! SKSpriteNode
    let goal2 = childNodeWithName(goal2CategoryName) as! SKSpriteNode
    let scoreLeft = childNodeWithName(scoreLeftCategoryName) as! SKLabelNode
    let scoreRight = childNodeWithName(scoreRightCategoryName) as! SKLabelNode

}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
   /* Called when a touch begins */
    var touch = touches.first! as UITouch
    var location = touch.locationInNode(self)
    if let body = physicsWorld.bodyAtPoint(location){
        if body.node!.name == paddle1CategoryName {
            print("The game has begun.")
            isFingerOnPaddle1 = true
        } else if body.node!.name == paddle2CategoryName{
            print("The game has begun.")
            isFingerOnPaddle2 = true
        }
    }
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let paddle1 = childNodeWithName(paddle1CategoryName) as! SKSpriteNode
    let paddle2 = childNodeWithName(paddle2CategoryName) as! SKSpriteNode
    let puck = childNodeWithName(puckCategoryName) as! SKSpriteNode
    let goal1 = childNodeWithName(goal1CategoryName) as! SKSpriteNode
    let goal2 = childNodeWithName(goal2CategoryName) as! SKSpriteNode
    var touch = touches.first! as UITouch
    var touchLocation = touch.locationInNode(self)
    var previousLocation = touch.previousLocationInNode(self)



    if isFingerOnPaddle1{

    let paddle1X = paddle1.position.x + (touchLocation.x - previousLocation.x)
    let paddle1Y = paddle1.position.y + (touchLocation.y - previousLocation.y)
    if (touchLocation.x <= paddle1.position.x && (touchLocation.y <= paddle1.position.y || touchLocation.y >= paddle1.position.y)){
    paddle1.position = CGPoint(x: paddle1X, y: paddle1Y)
    }
    }
    if isFingerOnPaddle2{
    let paddle2X = paddle2.position.x + (touchLocation.x - previousLocation.x)
    let paddle2Y = paddle2.position.y + (touchLocation.y - previousLocation.y)

    if (touchLocation.x >= paddle2X && (touchLocation.y >= paddle2Y || touchLocation.y < paddle2.position.y)){
        paddle2.position = CGPoint(x: paddle2X, y: paddle2Y)
    }
    }

}
/*
func didBeginContact(contact: SKPhysicsContact) {
    let scoreLeft = childNodeWithName(scoreLeftCategoryName) as! SKLabelNode
    let scoreRight = childNodeWithName(scoreRightCategoryName) as! SKLabelNode
    var score = 0
    let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask


    switch contactMask{
    case puckCategory | goal1Category:
        if contact.bodyA.categoryBitMask == puckCategory{
            puckCategory = contact.bodyA.categoryBitMask
            goal1Category = contact.bodyB.categoryBitMask
            score++
            scoreRight.text = "\(score)"
        }else{
            puckCategory = contact.bodyB.categoryBitMask
            goal1Category = contact.bodyA.categoryBitMask
            score++
            scoreRight.text = "\(score)"
        }
    case puckCategory | goal2Category:
        if contact.bodyA.categoryBitMask == puckCategory{
            puckCategory = contact.bodyA.categoryBitMask
            goal2Category = contact.bodyB.categoryBitMask
            score++
            scoreLeft.text = "\(score)"
        }else{
            puckCategory = contact.bodyB.categoryBitMask
            goal2Category = contact.bodyA.categoryBitMask
            score++
            scoreLeft.text = "\(score)"
        }
    default:

        // Nobody expects this, so satisfy the compiler and catch
        // ourselves if we do something we didn't plan to
        fatalError("other collision: \(contactMask)")

    }
}*/
override func update(currentTime: CFTimeInterval) {
    /* Called before each frame is rendered */
}

}


Solution

  • One issue is using a local var score = 0 at the beginning of your function. Remove it. The score should be stored in the game's model so that logic about winning can be applied but matching your code, at least do this.

    if let score = Int(scoreLeft.text) {
      scoreLeft.text = "\(score + 1)"
    }
    

    Additionally there is a problem with

      case puckCategory | goal1Category:
            if contact.bodyA.categoryBitMask == puckCategory{
                puckCategory = contact.bodyA.categoryBitMask
    

    What you are saying is if contact.bodyA.categoryBitMask is equal to puckCategory, then set puckCategory to contact.bodyA.categoryBitMask. That is a no-op, nothing changes AND more importantly you don't want to change the bit masks that your child nodes are compared against. Make puckCategory, goal1Categoy, and goal2Category constants. Good luck.

    Follow Up To OP Comment:

    So, throw away the code in didBeginContact that set the category mask variables for two reasons. 1) Setting variables like puckCatergory to their same value is wasteful and confusing. 2) Since variables like puckCategory are compared to child node bit masks, it would be disasterous if they ever did change. That function should look like this. Also be sure and add handlers for the other types of contacts before you get too far.

    func didBeginContact(contact: SKPhysicsContact) {
        let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
    
        switch contactMask {
        case puckCategory | goal1Category:
           let scoreRight = childNodeWithName(scoreRightCategoryName) as! SKLabelNode
           if let score = Int(scoreRight.text) {
              scoreRight.text = "\(score + 1)"
           }
        case puckCategory | goal2Category:
           let scoreLeft = childNodeWithName(scoreLeftCategoryName) as! SKLabelNode
           if let score = Int(scoreLeft.text) {
              scoreLeft.text = "\(score + 1)"
           }
        default:
            // There are other contacts that need to be handled; paddle+puck,puck+wall, etc
            fatalError("other collision: \(contactMask)")
        }
    }