SpriteKit Project — AbsolutelyBrickingIt — Part 6: Add the about scene

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

In this article, we’ll add the about scene that the user sees when they tap the about button.

Add new assets and scene

Add the text.about images for the about scene to Assets:

Add about scene text asset

Add a new file, called AboutScene to the project and add the background, logo, and text nodes as children of the scene:

Add about scene
import SpriteKit

class AboutScene: 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 logoNode = SKSpriteNode(imageNamed: "logo")
logoNode.position = CGPoint(x: 0.5 * size.width, y: 0.8 * size.height)
addChild(logoNode)

let textNode = SKSpriteNode(imageNamed: "text.about")
textNode.position = CGPoint(x: 0.5 * size.width, y: 0.4 * size.height)
addChild(textNode)
}
}

Handle about button taps

Update MenuScene to keep a reference to the about node and to call a coordinator method when about gets touched:

protocol MenuSceneCoordinator: AnyObject {
func menuScenePlayTapped(_ scene: MenuScene)
func menuSceneAboutTapped(_ scene: MenuScene)
}

class MenuScene: SKScene {

weak var coordinator: MenuSceneCoordinator?

private var aboutNode: SKSpriteNode?
private var playNode: SKSpriteNode?

override func didMove(to view: SKView) {
...
addChild(aboutNode)
self.aboutNode = aboutNode
...
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let point = touch.location(in: self)

if playNode?.contains(point) == true {
coordinator?.menuScenePlayTapped(self)
} else if aboutNode?.contains(point) == true {
coordinator?.menuSceneAboutTapped(self)
}
}
}
}

GameViewController needs to be updated to include this new coordinator method, since it already conforms to MenuSceneCoordinator:

extension GameViewController: MenuSceneCoordinator {

func menuSceneAboutTapped(_ scene: MenuScene) {
guard let view = self.view as? SKView else { return }

let scene = AboutScene(size: CGSize(width: 375.0, height: 667.0))
scene.scaleMode = .resizeFill

view.presentScene(scene)
}

func menuScenePlayTapped(_ scene: MenuScene) {
...
}
}

Running the app, we can now tap About to open the AboutScene:

About scene

Returning to the menu from the about scene

We’ll follow a similar approach to return to the menu. Let’s add a coordinator to AboutScene with a method that gets called whenever a touch completes anywhere on the about screen:

protocol AboutSceneCoordinator: AnyObject {
func aboutSceneScreenTapped(_ scene: AboutScene)
}

class AboutScene: SKScene {

weak var coordinator: AboutSceneCoordinator?

...

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
coordinator?.aboutSceneScreenTapped(self)
}
}

Let’s set the GameViewController as the coordinator for the AboutScene and open MenuScene whenever the coordinator method gets called:

extension GameViewController: MenuSceneCoordinator {

func menuSceneAboutTapped(_ scene: MenuScene) {
...
let scene = AboutScene(size: CGSize(width: 375.0, height: 667.0))
scene.coordinator = self
...
}
}

extension GameViewController: AboutSceneCoordinator {

func aboutSceneScreenTapped(_ scene: AboutScene) {
guard let view = self.view as? SKView else { return }

let scene = MenuScene(size: CGSize(width: 375.0, height: 667.0))
scene.coordinator = self
scene.scaleMode = .resizeFill

view.presentScene(scene)
}
}

Now when the app runs, the user can tap About to open the AboutScene and tap anywhere on the about screen to open the MenuScene again.

Returning to the menu from the game scene

Following the exact same approach, we can navigate back and forth between GameScene and MenuScene. This is just a temporary measure, since we'll have to update it once the game is in place, but it's useful for early development. Update GameScene to:

import SpriteKit

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

class GameScene: SKScene {

weak var coordinator: GameSceneCoordinator?

override func didMove(to view: SKView) {

}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
coordinator?.gameSceneScreenTapped(self)
}
}

(Note GameplayKit was also removed, since we won’t be using that for now.)

In GameViewController set the coordinator for the GameScene and implement the GameSceneCoordinator interface:

extension GameViewController: MenuSceneCoordinator {

func menuScenePlayTapped(_ scene: MenuScene) {
...
let scene = GameScene(size: CGSize(width: 375.0, height: 667.0))
scene.coordinator = self
...
}
}

extension GameViewController: GameSceneCoordinator {

func gameSceneScreenTapped(_ scene: GameScene) {
guard let view = self.view as? SKView else { return }

let scene = MenuScene(size: CGSize(width: 375.0, height: 667.0))
scene.coordinator = self
scene.scaleMode = .resizeFill

view.presentScene(scene)
}
}

Now when the app runs, the user can tap Play to open the GameScene and tap anywhere on the game screen to open the MenuScene again.

Cleaning up GameViewController a bit

You’ve probably noticed there’s a lot of duplicated/nearly identical code in GameViewController. Let's refactor this to reduce duplication and hopefully make it easier to understand. First, we can define a constant for the screen size:

private let sceneSize = CGSize(width: 375.0, height: 667.0)

This can be used everywhere a scene is created, e.g.:

let scene = MenuScene(size: sceneSize)

Next, let’s update viewDidLoad() as follows:

override func viewDidLoad() {
super.viewDidLoad()

setupView()
present(scene: createMenuScene())
}

func setupView() {
guard let view = self.view as? SKView else { return }

view.ignoresSiblingOrder = false
view.showsFPS = true
view.showsNodeCount = true
}

func present(scene: SKScene) {
guard let view = self.view as? SKView else { return }

view.presentScene(scene)
}

func createMenuScene() -> MenuScene {
let scene = MenuScene(size: sceneSize)
scene.coordinator = self
scene.scaleMode = .resizeFill
return scene
}

It’s much easier to see that viewDidLoad() does 2 things now: sets up the view and presents the menu scene.

It makes sense to implement createMenuScene() as a computed var instead in this case, which helps make the code a little cleaner still:

override func viewDidLoad() {
super.viewDidLoad()

setupView()
present(scene: menuScene)
}

var menuScene: MenuScene {
let scene = MenuScene(size: sceneSize)
scene.coordinator = self
scene.scaleMode = .resizeFill
return scene
}

We can also define computed vars for the about scene and the game scene in this way:

var aboutScene: AboutScene {
let scene = AboutScene(size: sceneSize)
scene.coordinator = self
scene.scaleMode = .resizeFill
return scene
}

var gameScene: GameScene {
let scene = GameScene(size: sceneSize)
scene.coordinator = self
scene.scaleMode = .resizeFill
return scene
}

Now the various coordinator implementations can be simplified right down:

extension GameViewController: MenuSceneCoordinator {

func menuSceneAboutTapped(_ scene: MenuScene) {
present(scene: aboutScene)
}

func menuScenePlayTapped(_ scene: MenuScene) {
present(scene: gameScene)
}
}

extension GameViewController: AboutSceneCoordinator {

func aboutSceneScreenTapped(_ scene: AboutScene) {
present(scene: createMenuScene())
}
}

extension GameViewController: GameSceneCoordinator {

func gameSceneScreenTapped(_ scene: GameScene) {
present(scene: createMenuScene())
}
}

Running the app will give the exact same results as before, but now the code is a lot cleaner and easier to follow.

At this point, we have a menu scene, an about scene, and an empty game scene. We can navigate back and forth between them by tapping the buttons on the menu and tapping the other screens to return to the menu!

Code

This article corresponds to the merge commit Add about scene (#6) in the GitHub repository AbsolutelyBrickingIt

Next Part

Part 7: Add the paddle

--

--