A Swift iOS 8 Sprite Kit Collision Handling Tutorial

From Techotopia
Revision as of 20:39, 1 February 2016 by Neil (Talk | contribs) (Text replacement - "<google>BUY_IOS8</google>" to "<htmlet>ios9_upgrade</htmlet>")

Jump to: navigation, search
PreviousTable of ContentsNext
An iOS 8 Swift Sprite Kit Level Editor Game TutorialAn iOS 8 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 8 Swift 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, implement a join between those two nodes so that the arrow appears to embed into the ball.


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

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")
    ball.position = CGPointMake(self.randomBetween(0, high: self.size.width-200), 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.childNodeWithName("archerNode")
    let archerPosition = archerNode?.position
    let archerWidth = archerNode?.frame.size.width

    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")
    arrow.position = CGPointMake(archerPosition!.x + archerWidth!, 
				archerPosition!.y)
    arrow.name = "arrowNode"
    arrow.physicsBody = SKPhysicsBody(rectangleOfSize: 
				arrow.frame.size)
    arrow.physicsBody?.usesPreciseCollisionDetection = true
    arrow.physicsBody?.categoryBitMask = arrowCategory
    return arrow
}

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.childNodeWithName("archerNode")
    let archerPosition = archerNode?.position
    let archerWidth = archerNode?.frame.size.width

    let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")
    arrow.position = CGPointMake(archerPosition!.x + archerWidth!, 
				archerPosition!.y)
    arrow.name = "arrowNode"
    arrow.physicsBody = SKPhysicsBody(rectangleOfSize: 
				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 didBeginContact and didEndContact 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.

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

override func didMoveToView(view: SKView) {
    self.physicsWorld.gravity = CGVectorMake(0, -1.0)
    self.physicsWorld.contactDelegate = self
    self.initArcheryScene()
}

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

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 didBeginContact method as follows:

func didBeginContact(contact: SKPhysicsContact) {
    let firstNode = contact.bodyA.node as! SKSpriteNode
    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)) {
            println("Hit")
            score++
        }
    }
}

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.

Implementing a Physics Joint Between Nodes

When a valid hit is detected, the arrow needs to appear to embed partway into the ball and stick there as the ball continues its descent. In order to achieve this, a new texture will be applied to the arrow sprite node that makes the arrow appear without a tip and slightly shorter.

The joining of the arrow node and ball will be achieved by implementing a physics joint at the point of contact between the two nodes. A number of different joint types are available, but for the purposes of this game, a fixed joint provides the exact behavior required.

The embedded arrow texture is contained in the ArrowHitTexture.png file. Locate this file in the SpriteImages folder of the sample code download and drag and drop it onto the Supporting Files folder in the project navigator. Within the ArcheryScene.swift file, modify the didBeginContact method to establish a fixed joint between the two nodes and to change the texture of the arrow:

func didBeginContact(contact: SKPhysicsContact) {
    let firstNode = contact.bodyA.node as! SKSpriteNode
    let secondNode = contact.bodyB.node as! SKSpriteNode

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

        let contactPoint = contact.contactPoint
        let contact_x = contactPoint.x
        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)) {
            let texture = SKTexture(imageNamed: "ArrowHitTexture")
            firstNode.texture = texture
            let joint = 
		SKPhysicsJointFixed.jointWithBodyA(contact.bodyA, 
			bodyB: contact.bodyB, 
			anchor: CGPointMake(contact_x, contact_y))
             self.physicsWorld.addJoint(joint)
             score++
        }
    }
}

Compile and run the application. When an arrow scores a hit on a ball the arrow will now appear to stick into the ball. Note that the attachment of the arrow to the ball causes the ball to rotate and change course as a result of the impact and shift in center of gravity (Figure 65-1). All of this is provided automatically by the Sprite Kit physics world.


An iOS 8 Sprite Kit collision with joint

Figure 65-1

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.redColor()
    scoreNode.position = CGPointMake(CGRectGetMidX(self.frame), 
		CGRectGetMidY(self.frame))
    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.waitForDuration(3.0), 
		SKAction.fadeOutWithDuration(3.0)])

    let welcomeReturn =  SKAction.runBlock({
        let transition = SKTransition.revealWithDirection(
		SKTransitionDirection.Down, duration: 1.0)
        let welcomeScene = GameScene(fileNamed: "GameScene")
        self.scene!.view?.presentScene(welcomeScene, 
				transition: transition)
    })

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

    self.runAction(sequence)
}

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.runBlock({ 
		self.createBallNode() }),
        		SKAction.waitForDuration(1)])

    self.runAction(SKAction.repeatAction(releaseBalls, 
			count: ballCount), completion: {
        let sequence = 
		SKAction.sequence([SKAction.waitForDuration(5.0), 
			SKAction.runBlock({ self.gameOver() })])
        self.runAction(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 8 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 didBeginContact and didEndContact 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.

Sprite Kit also allows joints to be formed between nodes. In this chapter a fixed joint was implemented to attach arrow nodes to the ball nodes in the event of contacts being detected.


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 8 Swift Sprite Kit Level Editor Game TutorialAn iOS 8 Sprite Kit Particle Emitter Tutorial