Omnomnipotent
Omnomnipotent

Reputation: 87

Inconsistent contact detection in Swift 3 using SpriteKit

I'm having an issue with contact detection in Swift 3 using SpriteKit. The contact detection is working...sometimes. It seems purely random as to when it fires and when it doesn't. I have a yellow "bullet" that moves up on the screen to hit a red sprite named targetSprite. The desired behavior is to have the bullet removed when it hits the target, but sometimes it just passes through underneath. I've found many questions about contact detection not working at all, but I haven't found any dealing with inconsistent detection.

What can I do to fix this?

Here's the code:

import SpriteKit
import GameplayKit

enum PhysicsCategory:UInt32 {
    case bullet = 1
    case sprite1 = 2
    case targetSprite = 4
    // each new value should double the previous
}

class GameScene: SKScene, SKPhysicsContactDelegate {

// Create sprites
let sprite1 = SKSpriteNode(color: SKColor.blue, size: CGSize(width:100,height:100))
let targetSprite = SKSpriteNode(color: SKColor.red, size: CGSize(width:100,height:100))
let bullet = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 20, height: 20))
// show the bullet?
var isShowingBullet = true

// Timers
//var timer:Timer? = nil
var fireBulletTimer:Timer? = nil

// set up bullet removal:
var bulletShouldBeRemoved = false


let bulletMask = PhysicsCategory.bullet.rawValue


override func didMove(to view: SKView) {

    // Physics
    targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)
    targetSprite.physicsBody?.affectedByGravity = false

    bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.centerRect.size)
    bullet.physicsBody?.affectedByGravity = false


    // Contact Detection:
    targetSprite.physicsBody?.categoryBitMask = PhysicsCategory.targetSprite.rawValue

    targetSprite.physicsBody?.contactTestBitMask =
        //PhysicsCategory.sprite1.rawValue |
        PhysicsCategory.bullet.rawValue

    targetSprite.physicsBody?.collisionBitMask = 0 // no collision detection


    // bullet physics
    bullet.physicsBody?.categoryBitMask = PhysicsCategory.bullet.rawValue

    bullet.physicsBody?.contactTestBitMask =
        PhysicsCategory.targetSprite.rawValue

    bullet.physicsBody?.collisionBitMask = 0 // no collision detection


    // execute once:
    fireBulletTimer = Timer.scheduledTimer(timeInterval: 1,
                                           target: self,
                                           selector: #selector(self.fireBullet),
                                           userInfo: nil,
                                           repeats: false)

    // Add sprites to the scene:
    self.addChild(sprite1)
    self.addChild(bullet)
    self.addChild(targetSprite)

    // Positioning
    targetSprite.position = CGPoint(x:0, y:300)
    // Note: bullet and sprite1 are at 0,0 by default

    // Delegate
    self.physicsWorld.contactDelegate = self

}

func didBegin(_ contact: SKPhysicsContact) {

    print("didBegin(contact:))")

    //let firstBody:SKPhysicsBody
   // let otherBody:SKPhysicsBody

    // Use 'bitwise and' to see if both bits are 1:
    if contact.bodyA.categoryBitMask & bulletMask > 0 {

        //firstBody = contact.bodyA
        //otherBody = contact.bodyB
        print("if contact.bodyA....")
        bulletShouldBeRemoved = true
    }
    else {
        //firstBody = contact.bodyB
        //otherBody = contact.bodyA
        print("else - if not contacted?")
    }

    /*
    // Find the type of contact:
    switch otherBody.categoryBitMask {
        case PhysicsCategory.targetSprite.rawValue: print(" targetSprite hit")
        case PhysicsCategory.sprite1.rawValue: print(" sprite1 hit")
        case PhysicsCategory.bullet.rawValue: print(" bullet hit")

        default: print(" Contact with no game logic")
    }
    */


} // end didBegin()


func didEnd(_ contact: SKPhysicsContact) {
    print("didEnd()")

}

func fireBullet() {

    let fireBulletAction = SKAction.move(to: CGPoint(x:0,y:500), duration: 1)
    bullet.run(fireBulletAction)

}

func showBullet() {

    // Toggle to display or not, every 1 second:
    if isShowingBullet == true {
        // remove (hide) it:
        bullet.removeFromParent()
        // set up the toggle for the next call:
        isShowingBullet = false
        // debug:
        print("if")

    }
    else {
        // show it again:
        self.addChild(bullet)
        // set up the toggle for the next call:
        isShowingBullet = true
        // debug:
        print("else")
    }

}

override func update(_ currentTime: TimeInterval) {
    // Called before each frame is rendered

    if bulletShouldBeRemoved {
        bullet.removeFromParent()
    }

}

}

Sorry for the inconsistent indentation, I can't seem to find an easy way to do this...

EDIT:

I have found that using 'frame' instead of 'centerRect' makes the collision area the size of the sprite. For example:

    targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)

should be:

    targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.frame.size)

Upvotes: 3

Views: 806

Answers (3)

Petr Lazarev
Petr Lazarev

Reputation: 3182

SpriteKit physics engine will calculate collision correctly if you do following:

1) set "usesPreciseCollisionDetection" property to true for bullet's physics body. This will change collision detection algorithm for this body. You can found more information about this property here, chapter "Working with Collisions and Contacts".

2) move your bullet using applyImpulse or applyForce methods. Collision detection will not woking correctly if you move body by changing it's position manually. You can find more information here, chapter "Making Physics Bodies Move".

Upvotes: 1

Whirlwind
Whirlwind

Reputation: 13675

First advice - Do not use NSTimer (aka Timer) in SpriteKit. It is not paired with a game loop and can cause different issues in a different situations. Read more here ( answer posted by LearnCocos2D)

So, do this:

 let wait = SKAction.wait(forDuration: 1)

 run(wait, completion: {
     [unowned self] in
      self.fireBullet()
 })

What I have noticed is that if I run your code in Simulator, I get the behaviour you have described. didBegin(contact:) is being fired randomly. Still, this is not happening on a device for me, and device testing is what matters.

Now, when I have removed Timer and did the same thing with SKAction(s) everything worked, means contact were detected every time.

Upvotes: 4

sicvayne
sicvayne

Reputation: 620

Have you tried adding

.physicsBody?.isDynamic = true
.physicsBody?.usesPreciseCollisionDetrction =true

Upvotes: 2

Related Questions