As a relatively new SpriteKit developer, I have quickly learnt that the primary Game Scene rapidly becomes full of boiler-plate code and scaffolding for the numerous nodes that you need for any non-trivial game. In a SwiftUI app, this is generally solved using multiple discrete views. In SpriteKit, especially in tutorials, no such effort is made and you seem to just live with massive, complex, files.
That's not necessary though. When I created a game, I decided I needed a game dashboard that showed the current fuel level, the game elapsed time and the score. In the first pass, this was coded straight into the Game Scene, resulting in a lot of code and clutter that distracted from the game itself.
Worse, the result was very basic, consisting of only three labels. I needed something better.
That led to me building a dashboard node and abstracting the code out into a separate SKNode component.
The Component...
Before coding the component, I needed a background. The text of the fuel level, elapsed time and score would be generated at run time, but the background could be created as a simple graphic. So I fired up Affinity Designer on the iPad and created the background graphic:

The image is 720x110 and provides the place holders for the text components. It's not fancy, but fitted in with the style of the game. Your requirements may well be different.
Next, I needed a component to display the dashboard. This is a sub-class of SKNode. The dasboard class consists of four nodes;
- The background image
- an SKLabelNode for the score
- an SKLabelNode fore the time elapsed
- an SKLabelNode for the remaining fuel
import SpriteKit
class DashboardNode: SKNode {
let background = SKSpriteNode(imageNamed: "dashboard.png")
let scoreLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let timeLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let fuelLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
Creating the nodes in the class is fine, but none of the nodes is properly set-up using the basic initialisers, so we need to do additional initialisation in the class init:
import SpriteKit
class DashboardNode: SKNode {
let background = SKSpriteNode(imageNamed: "dashboard.png")
let scoreLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let timeLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let fuelLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
override init() {
super.init()
background.anchorPoint = .zero // (0, 0) is bottom/left
background.zPosition = -1
background.position = CGPoint(x: 0, y: 0)
addChild(background)
setupFuelGauge()
setupTimeLabel()
setupScoreLabel()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupFuelGauge() {
fuelLabel.position = CGPoint(x: 25, y: 21)
fuelLabel.fontSize = 20.0
fuelLabel.horizontalAlignmentMode = .left
fuelLabel.fontColor = .black
background.addChild(fuelLabel)
fuelRemaining = 100.0
}
private func setupTimeLabel() {
timeLabel.position = CGPoint(x: 145, y: 21)
timeLabel.fontSize = 20.0
timeLabel.horizontalAlignmentMode = .left
timeLabel.fontColor = .black
background.addChild(timeLabel)
timeElapsed = 0
}
private func setupScoreLabel() {
scoreLabel.position = CGPoint(x: 305, y: 21)
scoreLabel.fontSize = 20.0
scoreLabel.horizontalAlignmentMode = .left
scoreLabel.fontColor = .black
background.addChild(scoreLabel)
score = 0
}
}
The default anchor point for the node is (0.5, 0.5) which places everything relative to the center. Personally, I find this a bit of a pain, so i change the anchor point to (0, 0) so relative to the bottom left. Our background image node is then positioned to the lower left.
The background image is positioned behind any other content, which ensures that the labels will appear on top of it. We are going to position our labels relative to the background, so they will also be positioned relative to the lower left.
Each of the display labels can then be initialised and assed to the background node.
All that remains is to add some properties to handle changes to the score, remaining time and fuel level.
var fuelRemaining: Double = 100.0 {
didSet { fuelLabel.text = "\(fuelRemaining)%" }
}
var score: Int = 0 {
didSet { scoreLabel.text = "\(score)" }
}
var timeElapsed: TimeInterval = 0 {
didSet { timeLabel.text = "\(timeElapsed.stringFormatted())" }
}
Each property will be set from the parent Scene and, when a value changes, it will set the text of the equivalent label.
The full component source code is:
import SpriteKit
class DashboardNode: SKNode {
let background = SKSpriteNode(imageNamed: "dashboard.png")
let scoreLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let timeLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
let fuelLabel = SKLabelNode(fontNamed: "AvenirNextCondensed-Bold")
override init() {
super.init()
background.anchorPoint = .zero // (0, 0) is bottom/left
background.zPosition = -1
background.position = CGPoint(x: 0, y: 0)
addChild(background)
setupFuelGauge()
setupTimeLabel()
setupScoreLabel()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var fuelRemaining: Double = 100.0 {
didSet { fuelLabel.text = "\(fuelRemaining)%" }
}
var score: Int = 0 {
didSet { scoreLabel.text = "\(score)" }
}
var timeElapsed: TimeInterval = 0 {
didSet { timeLabel.text = "\(timeElapsed.stringFormatted())" }
}
private func setupFuelGauge() {
fuelLabel.position = CGPoint(x: 25, y: 21)
fuelLabel.fontSize = 20.0
fuelLabel.horizontalAlignmentMode = .left
fuelLabel.fontColor = .black
background.addChild(fuelLabel)
fuelRemaining = 100.0
}
private func setupTimeLabel() {
timeLabel.position = CGPoint(x: 145, y: 21)
timeLabel.fontSize = 20.0
timeLabel.horizontalAlignmentMode = .left
timeLabel.fontColor = .black
background.addChild(timeLabel)
timeElapsed = 0
}
private func setupScoreLabel() {
scoreLabel.position = CGPoint(x: 305, y: 21)
scoreLabel.fontSize = 20.0
scoreLabel.horizontalAlignmentMode = .left
scoreLabel.fontColor = .black
background.addChild(scoreLabel)
score = 0
}
}
In the parent Scene, we can use our dashboard by adding the dashboard node:
let dashboard = DashboardNode()
...
private func initialiseDashboard() {
dashboard.position = CGPoint(x: -490, y: 300)
dashboard.zPosition = 2
gameNode.addChild(dashboard)
}
This is added as a child of the GameScene. We can then update the dashboard and have it redrawn by setting it's properties:
dashboard.timeElapsed += 0.35
dashboard.fuelRemaining = min(dashboard.fuelRemaining + 10, 100)
dashboard.score += 1