An iOS 17 Sprite Kit Collision Handling Tutorial

In this chapter, the game created in the previous chapter, entitled An iOS 17 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 increase a score count in the event of such a collision. In the next chapter, this collision detection behavior will be further extended to add audio and visual effects so that the balls appear to burst when an arrow hits.

Defining the Category Bit Masks

Start Xcode and open the SpriteKitDemo project created in the previous chapter if not already loaded.

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

Only collisions between the arrow and ball sprite nodes are of interest for this demonstration game. 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
.
.Code language: Swift (swift)

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:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

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)
}Code language: Swift (swift)

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

func createArrowNode() -> SKSpriteNode {
    
    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")
    
    if let archerNode = self.childNode(withName: "archerNode"),
        let archerPosition = archerNode.position as CGPoint?,
        let archerWidth = archerNode.frame.size.width as CGFloat? {
    
        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
}Code language: Swift (swift)

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. However, before this can be implemented, code needs to be added to indicate whether the app needs to know about collisions, contacts, or both. When contact occurs, two nodes can 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. On the other hand, a collision involves contact between two nodes that cannot occupy the same space in the scene. The two nodes will typically bounce away from each other in such a situation (subject to prevailing physics body properties).

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 this example, we will specify that notification is required for both contact and collision between the arrow and ball categories:

func createArrowNode() -> SKSpriteNode {
    
    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")
    
    if let archerNode = self.childNode(withName: "archerNode"),
        let archerPosition = archerNode.position as CGPoint?,
        let archerWidth = archerNode.frame.size.width as CGFloat? {
    
        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
}Code language: Swift (swift)

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 app code that such an event has occurred.

It does this by calling methods on the class instance registered as the contact delegate for the physics world object associated with the scene where the contact occurred. The system can 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.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

For 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.gravity = CGVector(dx: 0, dy: -1.0)       
    self.physicsWorld.contactDelegate = self
    self.initArcheryScene() 
}Code language: Swift (swift)

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 {
.
.
.Code language: Swift (swift)

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
        }
    }
Code language: Swift (swift)

The code starts by extracting references to the two nodes that have collided. It then checks that the first node is 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 is 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). Finally, assuming 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 ensuring that the “Hit” message appears in the Xcode console when an arrow hits the side of a ball.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Game Over

All that now remains is to display the score to the user when all 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
}Code language: Swift (swift)

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)
        if let welcomeScene = GameScene(fileNamed: "GameScene") {
            self.scene?.view?.presentScene(welcomeScene,
                                       transition: transition)
        }
    })
    
    let sequence = SKAction.sequence([fadeOut, welcomeReturn])
    self.run(sequence)
}Code language: Swift (swift)

Finally, add a completion handler that calls the gameOver method after 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)
    })
}Code language: Swift (swift)

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 17 Sprite Kit Particle Emitter Tutorial, will cover using the Particle Emitter to add special effects to Sprite Kit games.

Summary

The Sprite Kit physics engine detects 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 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.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 


Categories