Contacts & Collisions

Now for a more detailed look at using the SpriteKit physics to detect contacts.

Contacts vs contacts

SpriteKit supports two kinds of interaction between physics bodies that come into contact or attempt to occupy the same space:

  • A contact is used when you need to know that two bodies are touching each other. In most cases, you use contacts when you need to make gameplay changes when a contact occurs.
  • A collision is used to prevent two objects from interpenetrating each other. When one body strikes another body, SpriteKit automatically computes the results of the collision and applies impulse to the bodies in the collision.

In this example we are using contacts. If we were building a physics based game where we wanted to have objects bounce off each other we would use collision.

Enabling Physics Bodies

We can give our game objects physics bodies (a SKPhysicsBody) to enable physics for those bodies.

self.physicsBody = SKPhysicsBody(rectangleOf: self.size, 
            center: CGPoint(x: 0.0, y: self.size.height/2))
self.physicsBody?.affectedByGravity = false

The first line of the code above creates a SKPhysicsBody and attaches it to the game object (in this case self). The first parameter rectangleOf: self.size creates a rectangular physics body the size of self. It will create the body so that the rectangle envelopes the texture of self. The second parameter center tells the system where to center the rectangular physics body in relation to the self object. (The specific values here depend on the situation, these are taken from the code used in this section).

The second line of code tells the system not to apply gravity to the physics object.

The ? in this line is interesting. It tells the compiler that the property affectedByGravity should exist and to set it to false, but if it doesn't exist (because the physics body failed to be created) then don't worry about it.

In SpriteKit games we should give a physics body to every object that could be involved in a contact - the player, any enemies, collectibles, walls, the floor, etc.

Gloopdrop

We’re going to look at contacts using the gloopdrop game. On Moodle there are two versions of Gloopdrop. One (GloopdropBegin) contains a copy of the project as it should be at the beginning of this exercise. The other (GloopdropEnd) contains a copy of the code after we’ve completed this exercise. Download GloopdropBegin, unzip it, and open in Xcode on the Mac emulator.

Adding Physics Bodies

First we need to add physics bodies to everything that could be involved in a contact - the player object, the collectible drops and the foreground (the foreground is the floor).

To Player.swift at the end of the init() method add the code below.

// Add physics body
self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x: 0.0, y: self.size.height/2))
self.physicsBody?.affectedByGravity = false

To Collectible.swift at the end of the init() method add the code below.

// Add physics body
self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x: 0.0, y: -self.size.height/2))
self.physicsBody?.affectedByGravity = false

To GameScene.swift, in the didMove() method, just before addChild(foreground) add the code below.

// Add physics body
foreground.physicsBody = SKPhysicsBody(edgeLoopFrom: foreground.frame)
foreground.physicsBody?.affectedByGravity = false

Finally, also within GameScene.swift change the following lines

var level: Int = 8 to var level: Int = 1
view.showPhysics = false to view.showPhysics = true

If you now run the game you should see faint boxes around the player and the collectibles. At this point we have added the physics bodies but we are not using them to detect contacts.

Configuring Physics Categories

We need to setup different categories for the physics objects. This will allow us to determine which objects are involved in contacts and handle the contacts correctly.

In SpriteKitHelper.swift , just after the imports, add

// SpriteKit Physics Categories
enum PhysicsCategory {
  static let none:        UInt32 = 0
  static let player:      UInt32 = 0b1   // 1
  static let collectible: UInt32 = 0b10  // 2
  static let foreground:  UInt32 = 0b100 // 4
}

We’ve created four categories. none will be used to signify objects which will be ignored for contacts. player, collectible and foreground will be used to identify specific types of objects. Not that the values assigned to each category is a power of two (1, 2, 4). This allows us to use bitwise binary logic to identify which objects take part in contacts using bit masks.

Now we can setup a number of bit masks for each physics object in the game.

To Player.swift at the end of the init() method add the four new lines of code below.

// Add physics body
self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x: 0.0, y: self.size.height/2))
self.physicsBody?.affectedByGravity = false

// Set up physics categories for contacts
self.physicsBody?.categoryBitMask = PhysicsCategory.player          // used to identify the object 
self.physicsBody?.contactTestBitMask = PhysicsCategory.collectible  // used to identify other objects we want to detect contacts with
self.physicsBody?.collisionBitMask = PhysicsCategory.none           // used to identify other objects we want to detect contacts with

To Collectible.swift at the end of the init() method add the four new lines of code below.

// Add physics body
self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x: 0.0, y: -self.size.height/2))
self.physicsBody?.affectedByGravity = false

// Set up physics categories for contacts
self.physicsBody?.categoryBitMask = PhysicsCategory.collectible
self.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.foreground
self.physicsBody?.collisionBitMask = PhysicsCategory.none

To GameScene.swift, in the didMove() method, just before addChild(foreground) add the four new lines of code below.

// Add physics body
foreground.physicsBody = SKPhysicsBody(edgeLoopFrom: foreground.frame)
foreground.physicsBody?.affectedByGravity = false

// Set up physics categories for contacts
foreground.physicsBody?.categoryBitMask = PhysicsCategory.foreground
foreground.physicsBody?.contactTestBitMask = PhysicsCategory.collectible
foreground.physicsBody?.collisionBitMask = PhysicsCategory.none

At this point we have added physics bodies to the game objects and told the system what each physics object is and which contacts we are interested in handling. Next we need to handle the processing of the contacts.

Configure the Physics Contact Delegate

We need to modify GameScene to handle the contacts. We could modify our GameScene directly but it is better to use an extension to keep your code organised.

Open GameScene.swift and add a new extension to handle the contacts. Add the following to the bottom of the file.

// MARK: - COLLISION DETECTION

/* ############################################################ */
/*         COLLISION DETECTION METHODS START HERE               */
/* ############################################################ */

extension GameScene: SKPhysicsContactDelegate {
}

This extension now declares that the GameScene class can act as a delegate fo SKPhysicsContactDelegate. We still need to enable this by adding the following line at the top of the didMove() method (in GameScene).

// Set up the physics world contact delegate
physicsWorld.contactDelegate = self

Detecting Contacts

Inside the SKPhysicsContactDelegate extension add the following method.

  func didBegin(_ contact: SKPhysicsContact) {
    // Use bitwise OR to store both physics categories in collision
    let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
    
    // Did the [PLAYER] collide with the [COLLECTIBLE]?
    // use bitwise OR to OR the category types together and then compare to the collision variable
    if collision == PhysicsCategory.player | PhysicsCategory.collectible {
      print("player hit collectible")      
    }

    // Or did the [COLLECTIBLE] collide with the [FOREGROUND]?
    if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
      print("collectible hit foreground")      
    }
  }

If you run the code now you should see messages in the output box describing the collisions that occur.

Handling Contacts

Now we can implement code to handle what actually happens when a contact occurs.

To Collectible.swift at the end of the class, add the two methods shown below.

// Handle Contacts
func collected() {
  let removeFromParent = SKAction.removeFromParent()
  self.run(removeFromParent)
}

func missed() {
  let removeFromParent = SKAction.removeFromParent()
  self.run(removeFromParent)
}

To GameScene.swift update the SKPhysicsContactDelegate extension to

extension GameScene: SKPhysicsContactDelegate {
  func didBegin(_ contact: SKPhysicsContact) {
    // Check collision bodies
    let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
    
    // Did the [PLAYER] collide with the [COLLECTIBLE]?
    if collision == PhysicsCategory.player | PhysicsCategory.collectible {
      print("player hit collectible")
      
      // Find out which body is attached to the collectible node
      let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ?
        contact.bodyA.node :
        contact.bodyB.node

      // Verify the object is a collectible
      if let sprite = body as? Collectible {
        sprite.collected()
      }
    }

    // Or did the [COLLECTIBLE] collide with the [FOREGROUND]?
    if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
      print("collectible hit foreground")
      
      // Find out which body is attached to the collectible node
      let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ?
        contact.bodyA.node :
        contact.bodyB.node

      // Verify the object is a collectible
      if let sprite = body as? Collectible {
        sprite.missed()
      }
    }
  }
}

Run the code to see this all in action. You may want to turn the show physics back off again.