SpriteKit Project — AbsolutelyBrickingIt — Part 9: Detect the ball going out of bounds

atomicswiftdev
App Dev Pro Tips
Published in
4 min readAug 19, 2024

In this article, we’ll implement detecting when the ball goes out of bounds

Add an out of bounds shape to the game scene

Add another method to add an out of bounds node to GameScene, calling the method in didMove(to view:_). While we're at it, let's rename the wall size constants to hWallSize and vWallSize for the horizontal and vertical walls, respectively:

class GameScene: SKScene { 
...
private lazy var hWallSize = CGSize(width: size.width, height: 50)
private lazy var vWallSize = CGSize(width: 50, height: size.height)

override func didMove(to view: SKView) {
...
let topWallPosition = CGPoint(x: 0.5 * size.width, y: size.height + 0.5 * hWallSize.height)
let leftWallPosition = CGPoint(x: -0.5 * vWallSize.width, y: 0.5 * size.height)
let rightWallPosition = CGPoint(x: size.width + 0.5 * vWallSize.width, y: 0.5 * size.height)
let outOfBoundsPosition = CGPoint(x: 0.5 * size.width, y: 0.5 * hWallSize.height)

addWallNode(size: hWallSize, position: topWallPosition)
addWallNode(size: vWallSize, position: leftWallPosition)
addWallNode(size: vWallSize, position: rightWallPosition)
addOutOfBoundsNode(size: hWallSize, position: outOfBoundsPosition)
...
}
...
}

private extension GameScene {
...

func addOutOfBoundsNode(size: CGSize, position: CGPoint) {
let outOfBoundsNode = SKSpriteNode(color: .white, size: size)
outOfBoundsNode.name = "outOfBounds"
outOfBoundsNode.position = position
outOfBoundsNode.physicsBody = SKPhysicsBody(rectangleOf: outOfBoundsNode.size)
outOfBoundsNode.physicsBody?.categoryBitMask = outOfBoundsCategory
outOfBoundsNode.physicsBody?.isDynamic = false
addChild(outOfBoundsNode)
}
}

Running this in the simulator, we see the out of bounds shape at the bottom of the screen. The ball bounces off it just like the walls:

Out of bounds shape at the bottom of the game scene

Get the ball to pass through the out of bounds shape

As a first step towards getting the ball to pass through the out of bounds, we’ll define a physics category for each physics body in the scene:

class GameScene: SKScene {
...
private let ballCategory: UInt32 = 0x1
private let paddleCategory: UInt32 = 0x1 << 1
private let wallCategory: UInt32 = 0x1 << 2
private let outOfBoundsCategory: UInt32 = 0x1 << 3
}

Next we’ll set the categoryBitMask for each node:

private extension GameScene {
...
func addPaddleNode() -> SKSpriteNode {
...
paddleNode.physicsBody = SKPhysicsBody(rectangleOf: paddleNode.size)
paddleNode.physicsBody?.categoryBitMask = paddleCategory
...
}

func addBallNode() {
...
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: ballNode.size.width * 0.5)
ballNode.physicsBody?.categoryBitMask = ballCategory
...
}

func addWallNode(size: CGSize, position: CGPoint) {
...
wallNode.physicsBody = SKPhysicsBody(rectangleOf: wallNode.size)
wallNode.physicsBody?.categoryBitMask = wallCategory
...
}

func addOutOfBoundsNode(size: CGSize, position: CGPoint) {
...
outOfBoundsNode.physicsBody = SKPhysicsBody(rectangleOf: outOfBoundsNode.size)
outOfBoundsNode.physicsBody?.categoryBitMask = outOfBoundsCategory
...
}
}

If we run this in the Simulator now, the results will be the same. All physics bodies collide with all categories by default. Let’s update the ball to only collide with the paddle and walls by settings its collisionBitMask:

private extension GameScene {
...
func addBallNode() {
...
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: ballNode.size.width * 0.5)
ballNode.physicsBody?.categoryBitMask = ballCategory
ballNode.physicsBody?.collisionBitMask = paddleCategory | wallCategory
...
}
...
}

Now when we run in the Simulator, we can see that the ball passes through the out of bounds shape:

Ball passes through the out of bounds shape

Detect when the ball reaches the out of bounds shape

We need to set the contactBitMask for the ball to tell it we're interested in detecting contacts between the ball and the out of bounds shape:

private extension GameScene {
...
func addBallNode() {
...
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: ballNode.size.width * 0.5)
ballNode.physicsBody?.categoryBitMask = ballCategory
ballNode.physicsBody?.collisionBitMask = paddleCategory | wallCategory
ballNode.physicsBody?.contactTestBitMask = outOfBoundsCategory
...
}
...
}

Finally, we need to set the game scene as the contact delegate for the physics world in order to receive information about contacts:

class GameScene: SKScene {
...

override func didMove(to view: SKView) {
...
physicsWorld.gravity = .zero
physicsWorld.contactDelegate = self
}

...
}

extension GameScene: SKPhysicsContactDelegate {

func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node,
let nodeB = contact.bodyB.node else { return }

if nodeA.name == "ball" {
ball(node: nodeA, collidedWith: nodeB)
} else if nodeB.name == "ball" {
ball(node: nodeB, collidedWith: nodeA)
}
}

private func ball(node: SKNode, collidedWith other: SKNode) {
if other.name == "outOfBounds" {
// out of bounds detected
}
}
}

Update game scene coordinator to handle game over

Let’s update the game scene coordinator to enable our GameViewController to handle the game over event:

protocol GameSceneCoordinator: AnyObject {
func gameSceneGameEnded(_ scene: GameScene)
}

extension GameScene: SKPhysicsContactDelegate {
...

private func ball(node: SKNode, collidedWith other: SKNode) {
if other.name == "outOfBounds" {
coordinator?.gameSceneGameEnded(self)
}
}
}

We update GameViewController accordingly:

extension GameViewController: GameSceneCoordinator {

func gameSceneGameEnded(_ scene: GameScene) {
print("Show game over scene")
}
}

In the next article, we’ll add the game over scene, but for now we’ll output a message to the console:

Show game over screen as console output

Reposition out of bounds

Now that we know the visible out of bounds is working as expected, we can move it down off the bottom of the screen:

Move out of bounds off screen

Code

This article corresponds to the merge commit Detect out of bounds (#9) in the GitHub repository AbsolutelyBrickingIt

Next Part

Part 10: Add a game over scene

--

--