SpriteKit Project — AbsolutelyBrickingIt — Part 8: Add the ball
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:
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.
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:
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:
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:
Code
This article corresponds to the merge commit Add ball (#8)
in the GitHub repository AbsolutelyBrickingIt