An iOS 10 Sprite Kit Collision Handling Tutorial
Previous | Table of Contents | Next |
An iOS 10 Sprite Kit Level Editor Game Tutorial | An iOS 10 Sprite Kit Particle Emitter Tutorial |
Learn SwiftUI and take your iOS Development to the Next Level |
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.
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 |
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 |
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 |
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 |
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 |
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 |
Previous | Table of Contents | Next |
An iOS 10 Sprite Kit Level Editor Game Tutorial | An iOS 10 Sprite Kit Particle Emitter Tutorial |