SpriteKit Project — AbsolutelyBrickingIt — Part 7: Add the paddle

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

In this article, we’ll add the paddle that the user will control to the game scene.

Add game assets

Drag and drop game images to add to the Assets:

Add game images to Assets

Add the background image and the paddle image as GameScene sprite nodes:

class GameScene: SKScene {

...

override func didMove(to view: SKView) {
let backgroundNode = SKSpriteNode(imageNamed: "background.menu")
backgroundNode.position = CGPoint(x: 0.5 * size.width, y: 0.5 * size.height)
addChild(backgroundNode)

let paddleNode = SKSpriteNode(imageNamed: "paddle")
paddleNode.position = CGPoint(x: 0.5 * size.width, y: 0.1 * size.height)
addChild(paddleNode)
}

...
}

Run in the simulator to see that the background and paddle are now shown in the game scene:

Run in simulator to see game background and paddle

Move paddle with user touches

We’ll move the paddle when the user touches anywhere in the game scene and drags their finger left or right. First let’s add the remaining touch handling methods to GameScene and remove the coordinator method call in touchesEnded(_: with:):

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

}

We’ll keep a reference to the paddle node so that we can change its position later in the touch handling methods:

class GameScene: SKScene {

...

private var paddleNode: SKSpriteNode?

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

addChild(paddleNode)
self.paddleNode = paddleNode
}

...
}

In order to move the paddle, we’ll keep a copy of the last touch location and update the paddle location when one of the touch events occurs, resetting the last touch location when the touchesBegan(_: with:) method occurs:

class GameScene: SKScene {

...

private var paddleNode: SKSpriteNode?
private var lastTouchLocation: CGPoint?

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

addChild(paddleNode)
self.paddleNode = paddleNode
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
lastTouchLocation = touches.first?.location(in: self)
}

...
}

Now in touchesMoved(_: with:) and touchesEnded(_: with:) methods, we can update the paddle position based on how much the touch location has changed horizontally:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let firstTouchLocation = touches.first?.location(in: self) else { return }
touchLocation(movedTo: firstTouchLocation)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let firstTouchLocation = touches.first?.location(in: self) else { return }
touchLocation(movedTo: firstTouchLocation)
}

private func touchLocation(movedTo point: CGPoint) {
guard let paddleNode,
let lastTouchLocation else { return }

paddleNode.position = CGPoint(
x: paddleNode.position.x + point.x - lastTouchLocation.x,
y: paddleNode.position.y)
self.lastTouchLocation = point
}

Now running the app in the Simulator, the paddle can be moved left and right by dragging a finger across the screen:

Run in simulator to see paddle moving, but not only within screen

Note that the paddle can be actually moved all the way off the screen, however, so we’ll address this behaviour next.

Keep the paddle on the screen

Instead of just calculating the new horizontal position in this way:

x: paddleNode.position.x + point.x - lastTouchLocation.x

We’ll create a method to clamp this value to ensure it stays between a minimum and maximum:

private func clamp(value: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat {
if value < min {
return min
} else if value > max {
return max
} else {
return value
}
}

With this method, we can change the paddle update to:

let destinationX = clamp(
value: paddleNode.position.x + point.x - lastTouchLocation.x,
min: paddleNode.size.width * 0.5,
max: size.width - paddleNode.size.width * 0.5)

paddleNode.position = CGPoint(x: destinationX, y: paddleNode.position.y)

Now when the app is run, we can see that the paddle is kept inside the screen:

Run in simulator to see paddle kept within screen

Clamping a value, in this case a CGFloat, is not functionality that is specific to GameScene. We can create a CGFloat extension to add it as a utility method for CGFloat:

extension CGFloat {

func clamp(min: CGFloat, max: CGFloat) -> CGFloat {
if self < min {
return min
} else if self > max {
return max
} else {
return self
}
}
}

(Note the change as this is moved to a CGFloat extension - we no longer pass a value, but use self instead.)

This helps keep GameScene cleaner - removing code that doesn't logically belong there - and hopefully easier to understand. GameScene needs to be updated to use this change:

let positionUpdateX = paddleNode.position.x + point.x - lastTouchLocation.x
let destinationX = positionUpdateX.clamp(
min: paddleNode.size.width * 0.5,
max: size.width - paddleNode.size.width * 0.5)
paddleNode.position = CGPoint(x: destinationX, y: paddleNode.position.y)

Running the app again, the behaviour is identical to before, but the code has been cleaned up a bit.

Code

This article corresponds to the merge commit Add paddle (#7) in the GitHub repository AbsolutelyBrickingIt

Next Part

Part 8: Add the ball

--

--