SpriteKit Project — AbsolutelyBrickingIt — Part 8: Add the ball

atomicswiftdev
App Dev Pro Tips
Published in
5 min readAug 17, 2024

In this article, we’ll add the ball to the game scene.

Add the ball to the game scene

First up, let’s add the ball to GameScene. Add to the bottom of didMove(to:_):

let ballNode = SKSpriteNode(imageNamed: "ball")
ballNode.position = CGPoint(x: 0.5 * size.width, y: 0.2 * size.height)
addChild(ballNode)

Running in the Simulator, we can see the ball above the paddle:

Run in Simulator to see ball in game scene

Add physics to paddle and ball

To get the ball moving, we need to add physics. We can add physics bodies to the paddle and the ball so that they can interact physically with each other.

We can update the paddle code in didMove(to:_) to set a rectangular physics body for the paddle as follows:

let paddleNode = SKSpriteNode(imageNamed: "paddle")
paddleNode.position = CGPoint(x: 0.5 * size.width, y: 0.1 * size.height)
paddleNode.physicsBody = SKPhysicsBody(rectangleOf: paddleNode.size)
paddleNode.physicsBody?.isDynamic = false
addChild(paddleNode)

Physics bodies are dynamic by default, meaning the physics engine moves them, but we don’t want that for the paddle so we set isDynamic to false.

We do want the ball to be dynamic though, so we don’t change this property for it. We do however need to set a few other properties to make it bounce around the screen without friction and resistance:

let ballNode = SKSpriteNode(imageNamed: "ball")
ballNode.position = CGPoint(x: 0.5 * size.width, y: 0.2 * size.height)
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: ballNode.size.width * 0.5)
ballNode.physicsBody?.friction = 0.0
ballNode.physicsBody?.restitution = 1.0
ballNode.physicsBody?.linearDamping = 0.0
ballNode.physicsBody?.velocity = CGVector(dx: 0.0, dy: -200.0)
addChild(ballNode)

We’ll also turn off gravity for the physics world so that the ball doesn’t just fall down all the time. Add this to the end of didMove(to:_):

physicsWorld.gravity = .zero

For development purposes, it’s also useful to turn on physics debugging for the SpriteKit view. In GamewViewController add the following to setupView():

view.showsPhysics = true

In the Simulator, we now see the ball moving down, bouncing off the paddle, and travelling up to the top of the screen. Of course, there’s nothing to make it bounce back down so it continues up off the screen.

Watch the ball bounce up from the paddle in Simulator

Add walls

Let’s add a wall to the top of the screen to bounce the ball back down. In didMove(to:_):

let topWallNode = SKSpriteNode(color: .white, size: CGSize(width: size.width, height: 50))
topWallNode.position = CGPoint(x: 0.5 * size.width, y: size.height - 25)
topWallNode.physicsBody = SKPhysicsBody(rectangleOf: topWallNode.size)
topWallNode.physicsBody?.isDynamic = false
addChild(topWallNode)

We can see the top wall in the Simulator and bounce the ball back and forth between it and the paddle:

See interaction in Simulator

Let’s also add walls to the left and right of the screen:

let leftWallNode = SKSpriteNode(color: .white, size: CGSize(width: 50, height: size.height))
leftWallNode.position = CGPoint(x: 25, y: 0.5 * size.height)
leftWallNode.physicsBody = SKPhysicsBody(rectangleOf: leftWallNode.size)
leftWallNode.physicsBody?.isDynamic = false
addChild(leftWallNode)

let rightWallNode = SKSpriteNode(color: .white, size: CGSize(width: 50, height: size.height))
rightWallNode.position = CGPoint(x: size.width - 25, y: 0.5 * size.height)
rightWallNode.physicsBody = SKPhysicsBody(rectangleOf: rightWallNode.size)
rightWallNode.physicsBody?.isDynamic = false
addChild(rightWallNode)

Running again in the Simulator, we can now bounce the ball around the scene a bit:

Left, right, and top walls in Simulator

Clean up the code

GameScene could be cleaned up a little.

First, under the private var properties of GameScene, let's define constants for the sizes of the top and side walls:

private lazy var topWallSize = CGSize(width: size.width, height: 50)
private lazy var sideWallSize = CGSize(width: 50, height: size.height)

(In this case, we cannot use let because these constants use the scene size property, so we need to use lazy var, which only calculates the value when first used and size is available.)

Now we can update the wall node definitions like so:

let topWallNode = SKSpriteNode(color: .white, size: topWallSize)
...

let leftWallNode = SKSpriteNode(color: .white, size: sideWallSize)
...

let rightWallNode = SKSpriteNode(color: .white, size: sideWallSize)
...

Next up, let’s move the private method into a private extension to break up the class further:

private extension GameScene {

func touchLocation(movedTo point: CGPoint) {
...
}
}

Now let’s move code out of didMove(to:_) into separate private methods to break up the game logic. For the background node:

override func didMove(to view: SKView) {
addBackgroundNode()
...
}

private extension GameScene {

...

func addBackgroundNode() {
let backgroundNode = SKSpriteNode(imageNamed: "background.menu")
backgroundNode.position = CGPoint(x: 0.5 * size.width, y: 0.5 * size.height)
addChild(backgroundNode)
}
}

For the paddle and ball nodes:

override func didMove(to view: SKView) {
...
paddleNode = addPaddleNode()
addBallNode()
}

private extension GameScene {

...

func addBackgroundNode() {
let backgroundNode = SKSpriteNode(imageNamed: "background.menu")
backgroundNode.position = CGPoint(x: 0.5 * size.width, y: 0.5 * size.height)
addChild(backgroundNode)
}

func addPaddleNode() -> SKSpriteNode {
let paddleNode = SKSpriteNode(imageNamed: "paddle")
paddleNode.position = CGPoint(x: 0.5 * size.width, y: 0.1 * size.height)
paddleNode.physicsBody = SKPhysicsBody(rectangleOf: paddleNode.size)
paddleNode.physicsBody?.isDynamic = false
addChild(paddleNode)
return paddleNode
}

func addBallNode() {
let ballNode = SKSpriteNode(imageNamed: "ball")
ballNode.position = CGPoint(x: 0.5 * size.width, y: 0.2 * size.height)
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: ballNode.size.width * 0.5)
ballNode.physicsBody?.friction = 0.0
ballNode.physicsBody?.restitution = 1.0
ballNode.physicsBody?.linearDamping = 0.0
ballNode.physicsBody?.velocity = CGVector(dx: 0.0, dy: -200.0)
addChild(ballNode)
}
}

Since the top and side walls are very similar, we can make a single private method that they all use:

override func didMove(to view: SKView) {
...
addWallNode(size: topWallSize, position: CGPoint(x: 0.5 * size.width, y: size.height - 25))
addWallNode(size: sideWallSize, position: CGPoint(x: 25, y: 0.5 * size.height))
addWallNode(size: sideWallSize, position: CGPoint(x: size.width - 25, y: 0.5 * size.height))
}

private extension GameScene {

...

func addWallNode(size: CGSize, position: CGPoint) {
let wallNode = SKSpriteNode(color: .white, size: size)
wallNode.position = position
wallNode.physicsBody = SKPhysicsBody(rectangleOf: wallNode.size)
wallNode.physicsBody?.isDynamic = false
addChild(wallNode)
}
}

The results will be the same running this in the Simulator, but the code has been divided up more cleanly.

Reposition walls

The final step is to move the walls up and out until they’re just off screen. We’ll also use local constants for the positions to help make the code a little more readable:

let topWallPosition = CGPoint(x: 0.5 * size.width, y: size.height + 0.5 * topWallSize.height)
let leftWallPosition = CGPoint(x: -0.5 * sideWallSize.width, y: 0.5 * size.height)
let rightWallPosition = CGPoint(x: size.width + 0.5 * sideWallSize.width, y: 0.5 * size.height)

addWallNode(size: topWallSize, position: topWallPosition)
addWallNode(size: sideWallSize, position: leftWallPosition)
addWallNode(size: sideWallSize, position: rightWallPosition)

Now when we run in the Simulator, we no longer see the walls and the ball bounces around off the paddle and the walls:

Final result in Simulator

Code

This article corresponds to the merge commit Add ball (#8) in the GitHub repository AbsolutelyBrickingIt

Next Part

Part 9: Detect the ball going out of bounds

--

--

App Dev Pro Tips
App Dev Pro Tips

Published in App Dev Pro Tips

Welcome to the App Dev Pro Tips community! We’re a cross-platform collaboration dedicated to sharing the best practices, troubleshooting guides, and code snippets to help you master the art of mobile app creation.