An iOS 10 Sprite Kit Collision Handling Tutorial

From Techotopia
Revision as of 18:55, 7 November 2016 by Neil (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

PreviousTable of ContentsNext
An iOS 10 Sprite Kit Level Editor Game TutorialAn iOS 10 Sprite Kit Particle Emitter Tutorial


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


In this chapter, the game created in the previous chapter entitled An iOS 10 Sprite Kit Level Editor Game Tutorial will be extended to implement collision detection. The objective is to detect when an arrow node collides with a ball node and, in the event of such a collision, increase a score count. In the next chapter this collision detection behavior will be further extended to add both audio and visual effects so that the balls appear to burst when hit by an arrow.


Contents


Defining the Category Bit Masks

If not already loaded, start Xcode and open the SpriteKitDemo project created in the previous chapter.

When detecting collisions within a Sprite Kit scene, a delegate method is called each time a collision is detected. This method will only be called, however, if the colliding nodes are configured appropriately using category bit masks.

For the purposes of this demonstration game, only collisions between arrow and ball sprite nodes are of interest. The first step, therefore, is to declare collision masks for these two node categories. Begin by editing the ArcheryScene.swift file and adding these declarations at the top of the class implementation:

import UIKit
import SpriteKit

class ArcheryScene: SKScene {

    let arrowCategory: UInt32 = 0x1 << 0
    let ballCategory: UInt32 = 0x1 << 1
.
.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Assigning the Category Masks to the Sprite Nodes

Having declared the masks, these need to be assigned to the respective node objects when they are created within the game. This is achieved by assigning the mask to the categoryBitMask property of the physics body assigned to the node. In the case of the ball node, this code can be added in the createBallNode method as follows:

func createBallNode() {
    let ball = SKSpriteNode(imageNamed: "BallTexture.png")

    let screenWidth = self.size.width

    ball.position = CGPoint(x: randomBetween(-screenWidth/2, max:
        screenWidth/2-200), y: self.size.height-50)

    ball.name = "ballNode"
    ball.physicsBody = SKPhysicsBody(circleOfRadius:
                        (ball.size.width/2))

    ball.physicsBody?.usesPreciseCollisionDetection = true
    ball.physicsBody?.categoryBitMask = ballCategory
    self.addChild(ball)
}

Repeat this step to assign the appropriate category mask to the arrow node in the createArrowNode method:

func createArrowNode() -> SKSpriteNode {
    let archerNode = self.childNode(withName: "archerNode")
    let archerPosition = archerNode?.position
    let archerWidth = archerNode?.frame.size.width

    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")

    arrow.position = CGPoint(x: archerPosition!.x + archerWidth!,
                             y: archerPosition!.y)
    arrow.name = "arrowNode"

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

    arrow.physicsBody?.usesPreciseCollisionDetection = true
    arrow.physicsBody?.categoryBitMask = arrowCategory
    return arrow
} 

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Configuring the Collision and Contact Masks

Having assigned category masks to the arrow and ball nodes, these nodes are ready to be included in collision detection handling. Before this can be implemented however, code needs to be added to indicate whether the application needs to know about collisions, contacts or both. When a contact occurs, two nodes are able to touch or even occupy the same space in a scene. It might be valid, for example, for one sprite node to pass over another node and the game logic needs to be notified when this happens. A collision involves contact between two nodes that cannot occupy the same space in the scene. In such a situation (and subject to prevailing physics body properties) the two nodes will typically bounce away from each other.

The type of contact for which notification is required is specified by assigning contact and collision bit masks to the physics body of one of the node categories involved in the contact. For the purposes of this example, we will specify that notification is required for both contact and collision between the arrow and ball categories:

func createArrowNode() -> SKSpriteNode {
    let archerNode = self.childNode(withName: "archerNode")
    let archerPosition = archerNode?.position
    let archerWidth = archerNode?.frame.size.width

    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")

    arrow.position = CGPoint(x: archerPosition!.x + archerWidth!,
                             y: archerPosition!.y)
    arrow.name = "arrowNode"

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

    arrow.physicsBody?.usesPreciseCollisionDetection = true
    arrow.physicsBody?.categoryBitMask = arrowCategory
    arrow.physicsBody?.collisionBitMask = arrowCategory | ballCategory
    arrow.physicsBody?.contactTestBitMask =
                                arrowCategory | ballCategory
    return arrow
}

Implementing the Contact Delegate

When the Sprite Kit physics system detects a collision or contact for which appropriate masks have been configured it needs a way to notify the application code that such an event has occurred.

It does this by calling methods on the class instance that has been registered as the contact delegate for the physics world object associated with the scene in which the contact took place. In actual fact, the system is able to notify the delegate at both the beginning and end of the contact if both the didBegin(contact:) and didEnd(contact:) methods are implemented. Passed as an argument to these methods is an SKPhysicsContact object containing information about the location of the contact and references to the physical bodies of the two nodes involved in the contact.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

For the purposes of this tutorial we will use the ArcheryScene instance as the contact delegate and implement only the didBegin(contact:) method. Begin, therefore, by modifying the didMove(to view:) method in the ArcheryScene.swift file to declare the class as the contact delegate:

override func didMove(to view: SKView) {
    let archerNode = self.childNode(withName: "archerNode")
    archerNode?.position.y = 0
    archerNode?.position.x = -self.size.width/2 + 40       
    self.physicsWorld.contactDelegate = self
    self.initArcheryScene() 
}

Having made the ArcheryScene class the contact delegate the ArcheryScene.swift file needs to be modified to indicate that the class now implements the SKPhysicsContactDelegate protocol:

import UIKit
import SpriteKit

class ArcheryScene: SKScene, SKPhysicsContactDelegate {
.
.
.

Remaining within the ArcheryScene.swift file, implement the didBegin(contact:) method as follows:

func didBegin(_ contact: SKPhysicsContact) {
    let secondNode = contact.bodyB.node as! SKSpriteNode

    if (contact.bodyA.categoryBitMask == arrowCategory) &&
        (contact.bodyB.categoryBitMask == ballCategory) {

        let contactPoint = contact.contactPoint
        let contact_y = contactPoint.y
        let target_y = secondNode.position.y
        let margin = secondNode.frame.size.height/2 - 25

        if (contact_y > (target_y - margin)) &&
            (contact_y < (target_y + margin)) {
            print("Hit")
            score += 1
        }
    }
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

The code starts by extracting references to the two nodes that have collided. It then checks that the first node was an arrow and the second a ball (no points are scored if a ball falls onto an arrow). Next, the point of contact is identified and some rudimentary mathematics used to check that the arrow struck the side of the ball (for a game of app store quality more rigorous checking might be required to catch all cases). Assuming that the hit was within the defined parameters, a message is output to the console and the game score variable is incremented.

Run the game and test the collision handling by making sure that the “Hit” message appears in the Xcode console when an arrow hits the side of a ball.

Game Over

All that now remains is to display the score to the user when all of the balls have been released. This will require a new label node and a small change to an action sequence followed by a transition to the welcome scene so the user can start a new game. Begin by adding the method to create the label node in the ArcheryScene.swift file:

func createScoreNode() -> SKLabelNode {
    let scoreNode = SKLabelNode(fontNamed: "Bradley Hand")
    scoreNode.name = "scoreNode"

    let newScore = "Score \(score)"

    scoreNode.text = newScore
    scoreNode.fontSize = 60
    scoreNode.fontColor = SKColor.red
    scoreNode.position = CGPoint(x: self.frame.midX,
                                 y: self.frame.midY)
    return scoreNode
}

Next, implement the gameOver method which will display the score label node and then transition back to the welcome scene:

func gameOver() {
    let scoreNode = self.createScoreNode()
    self.addChild(scoreNode)
    let fadeOut = SKAction.sequence([SKAction.wait(forDuration: 3.0),
                SKAction.fadeOut(withDuration: 3.0)])

    let welcomeReturn =  SKAction.run({
        let transition = SKTransition.reveal(
            with: SKTransitionDirection.down, duration: 1.0)
        let welcomeScene = GameScene(fileNamed: "GameScene")
        self.scene!.view?.presentScene(welcomeScene!,
                                transition: transition)
    })

    let sequence = SKAction.sequence([fadeOut, welcomeReturn])
    self.run(sequence)
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Finally, add a completion handler that calls the gameOver method to the ball release action in the initArcheryScene method:

func initArcheryScene() {
    let releaseBalls = SKAction.sequence([SKAction.run({
    self.createBallNode() }),
    SKAction.wait(forDuration: 1)])

    self.run(SKAction.repeat(releaseBalls,
                        count: ballCount), completion: {
        let sequence =
                   SKAction.sequence([SKAction.wait(forDuration: 5.0),
                        SKAction.run({ self.gameOver() })])
        self.run(sequence)
    })
}

Compile, run and test. Also feel free to experiment by adding other features to the game to gain familiarity with the capabilities of Sprite Kit. The next chapter, entitled An iOS 10 Sprite Kit Particle Emitter Tutorial will cover the use of the Particle Emitter to add special effects to Sprite Kit games.

Summary

The Sprite Kit physics engine provides a mechanism for detecting when two nodes within a scene come into contact with each other. Collision and contact detection is configured through the use of category masks together with contact and collision masks. When appropriately configured, the didBegin(contact:) and didEnd(contact:) methods of a designated delegate class are called at the start and end of a contact between two nodes for which detection is configured. These methods are passed references to the nodes involved in the contact so that appropriate action can be taken within the game.


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book



PreviousTable of ContentsNext
An iOS 10 Sprite Kit Level Editor Game TutorialAn iOS 10 Sprite Kit Particle Emitter Tutorial