3

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)
Omnomnipotent
  • 87
  • 2
  • 6

3 Answers3

4

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.

Community
  • 1
  • 1
Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • Thank you for the advice, I'll be sure to check out the article. I'm a bit new to Swift, so I don't have a great background with it. – Omnomnipotent Feb 15 '17 at 14:39
2

Have you tried adding

.physicsBody?.isDynamic = true
.physicsBody?.usesPreciseCollisionDetrction =true
sicvayne
  • 620
  • 1
  • 4
  • 15
  • 1
    Hi sicvayne, the default value for `isDynamic` is always [true](https://developer.apple.com/reference/spritekit/skphysicsbody/1520132-isdynamic), so this line in this case is not needed – Alessandro Ornano Feb 15 '17 at 15:31
1

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".

Petr Lazarev
  • 3,102
  • 1
  • 21
  • 20
  • I agree with your first statement, but I cant say your second statement is really true, even if is a good advice. So I would put it like this : Contact detection, will work properly, if you move your sprites manually (either by changing sprite's position propery directly, or changing it indirectly using actions) if they are unaffected by gravity or any other forces. This is a sanctioned way in SpriteKit to use physics engine (well part of it) in conjuction with SKActions – Whirlwind Feb 15 '17 at 23:03
  • 1
    If you move body manually (e.g. using SKAction) - your body might fly through another small body or do not response on some collisions, because physics engine calculations rate is not the same as re-drawing frame-rate. That's why you will need to move all physics objects using impulses and forces. – Petr Lazarev Feb 16 '17 at 07:02
  • 1
    Where did you see this documented (about drawing rate)? Because iirc, when moving nodes too fast, either by actions or by forces, no matter if you use precise collision detection, contacts might end up undetected. – Whirlwind Feb 16 '17 at 07:03
  • I'm sure about it because this rule is true not only for SpriteKit, but for most physics engines, e.g. Box2D. Also you can see that in most game tutorials like AirHockey objects follows to fingers using impulses. And it is much harder then SKActions. But SKAction will not work properly. – Petr Lazarev Feb 16 '17 at 07:07
  • Please check links in my answer. "You can also apply your own forces and impulses to a body. After the scene completes these calculations, it updates the positions and orientations of the node objects." So you can see that if you set position manually, some calculation will be skipped. So it will work normal only with slow movements and with big objects. – Petr Lazarev Feb 16 '17 at 07:14
  • 1
    From what I've seen, fast moving nodes will just run through other nodes if too fast no matter if you move them by actions or forces. Speed only matters. I mean, what you are saying would be nice, but I havent noticed that behaviour... – Whirlwind Feb 16 '17 at 07:14
  • That's way you should use ONLY forces/impulses AND set "usesPreciseCollisionDetection" property to true for all fast moving bodies. – Petr Lazarev Feb 16 '17 at 09:31
  • 1
    Okay, but as I mentioned, in practice (you can try it by yourself), `usesPreciseCollisionDetection` will not solve the issue even if you move nodes by forces. So my point is, no matter what, behaviour will be the same for (super) fast moving objects. And for an appropriate fast moving objects, SKAction will actually work. Still, this is my experience from testing in past. I haven't done similar test recently, so maybe something changed in the meanwhile. – Whirlwind Feb 16 '17 at 09:41
  • Check this: https://developer.apple.com/library/content/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html You can see that SpriteKit engine evaluates physics and then simulates physics on each frame. That means that if you move small body fast via SKAction - then in frame $n it can be before some wall and in frame#(n+1) it will be already behind it! No collisions will be detected! But if you will use applyForce/Impulse + "usesPreciseCollisionDetection" property - then engine will predict all collisions, even for very fast objects. – Petr Lazarev Feb 18 '17 at 15:31