Day 46 - 100 Days of Swift
Project 11 (part 2)
Day 46 is the second part of the eleventh project. Today you get the game into a playable state. You add the slots that the balls will land in. You add a score and detect collisions to update it. And you add the ability for the user to add obstacles for the ball to hit.
First you add a function to make the slots for the ball to hit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func makeSlot(at position: CGPoint, isGood: Bool) {
let slotBase: SKSpriteNode
if isGood {
slotBase = SKSpriteNode(imageNamed: "slotBaseGood")
} else {
slotBase = SKSpriteNode(imageNamed: "slotBaseBad")
}
slotBase.position = position
slotBase.physicsBody = SKPhysicsBody(rectangleOf: slotBase.size)
slotBase.physicsBody?.isDynamic = false
addChild(slotBase)
}
And then add them in didMove(to:)
:
1
2
3
4
5
6
var isGood = false
for i in 0..<4 {
let x = i * 256 + 128
isGood.toggle()
makeSlot(at: CGPoint(x: x, y: 0), isGood: isGood)
}
Then you add a glow to the background of the slots so that the function looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func makeSlot(at position: CGPoint, isGood: Bool) {
let slotBase: SKSpriteNode
let slotGlow: SKSpriteNode
if isGood {
slotBase = SKSpriteNode(imageNamed: "slotBaseGood")
slotGlow = SKSpriteNode(imageNamed: "slotGlowGood")
} else {
slotBase = SKSpriteNode(imageNamed: "slotBaseBad")
slotGlow = SKSpriteNode(imageNamed: "slotGlowBad")
}
slotBase.position = position
slotGlow.position = position
slotBase.physicsBody = SKPhysicsBody(rectangleOf: slotBase.size)
slotBase.physicsBody?.isDynamic = false
addChild(slotBase)
addChild(slotGlow)
}
Then, for a little visual interest, you add a spin to the glow so that it slowly rotates:
1
2
3
4
5
// At end of makeSlot(at:isGood:)
let spin = SKAction.rotate(byAngle: .pi,
duration: Double.random(in: 8...15))
let spinForever = SKAction.repeatForever(spin)
slotGlow.run(spinForever)
Next, in order to keep track of collisions, you give names to the nodes that we care about detecting:
1
2
3
4
5
6
7
8
9
10
11
12
13
// In makeSlot(at:)
if isGood {
slotBase = SKSpriteNode(imageNamed: "slotBaseGood")
slotBase.name = "good"
slotGlow = SKSpriteNode(imageNamed: "slotGlowGood")
} else {
slotBase = SKSpriteNode(imageNamed: "slotBaseBad")
slotBase.name = "bad"
slotGlow = SKSpriteNode(imageNamed: "slotGlowBad")
}
// In touchesBegan(:with:)
ball.name = "ball"
Next you have to make the scene adopt SKPhysicsContactDelegate
and set it as the contactDelegate
for the physics world:
1
2
3
4
class GameScene: SKScene, SKPhysicsContactDelegate {
// In didMove(to:)
physicsWorld.contactDelegate = self
Then you have to set the contactTestBitMask
on the ball’s physicsBody
, to tell it what contacts we care about being notified for. To keep things simple, we just set it to the physics body’s collisionBitMask
, which basically says “notify us about all collisions”:
1
ball.physicsBody?.contactTestBitMask = ball.physicsBody!.collisionBitMask
Now that we are actually being notified about contacts, you add a delegate method to do something with that information, along with a couple of helper methods to keep things a little cleaner:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node else { return }
guard let nodeB = contact.bodyB.node else { return }
if nodeA.name == "ball" {
collisionBetween(ball: nodeA, object: nodeB)
} else if nodeB.name == "ball" {
collisionBetween(ball: nodeB, object: nodeA)
}
}
func collisionBetween(ball: SKNode, object: SKNode) {
if object.name == "good" {
destroy(ball: ball)
} else if object.name == "bad" {
destroy(ball: ball)
}
}
func destroy(ball: SKNode) {
ball.removeFromParent()
}
The delegate method determines which is of the nodes is the ball and which is not and calls collisionBetween(ball:object:)
with that information. Then, collisionBetween(ball:object:)
determines if the object was a good or bad slot, which is where we will update the score, and then destroy’s the ball.
Then you add a scoreLabel
and a score
property to keep track of the value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var scoreLabel: SKLabelNode!
var score = 0 {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
// In didMove(to:)
scoreLabel = SKLabelNode(fontNamed: "Chalkduster")
scoreLabel.text = "Score: 0"
scoreLabel.horizontalAlignmentMode = .right
scoreLabel.position = CGPoint(x: 980, y: 700)
addChild(scoreLabel)
And then add the updates to the score in collisionBetween(ball:object:)
:
1
2
3
4
5
6
7
if object.name == "good" {
destroy(ball: ball)
score += 1
} else if object.name == "bad" {
destroy(ball: ball)
score -= 1
}
That gives us a way to keep track of the score and displays it to the user. It is just not a very difficult game yet, because there are no obstacles. So you add an edit mode to allow the user to add some obstacles. The first thing you do is add a label to show the user which mode they are in:
1
2
3
4
5
6
7
8
9
10
11
12
13
var editLabel: SKLabelNode!
var isInEditingMode: Bool = false {
didSet {
editLabel.text = isInEditingMode ? "Done" : "Edit"
}
}
// In didMove(to:)
editLabel = SKLabelNode(fontNamed: "Chalkduster")
editLabel.text = "Edit"
editLabel.position = CGPoint(x: 80, y: 700)
addChild(editLabel)
Then, in touchesBegan(:with:)
, you add some code to check if the touch was on the label:
1
2
3
4
5
6
7
let objects = nodes(at: location)
if objects.contains(editLabel) {
isInEditingMode.toggle()
} else {
// make ball
}
That updates the label, but it will keep making a ball no matter which mode you are in, so now we need another if
statement:
1
2
3
4
5
6
7
8
9
if objects.contains(editLabel) {
isInEditingMode.toggle()
} else {
if isInEditingMode {
makeBox(at: location)
} else {
makeBall(at: location)
}
}
I pulled the ball making code out to its own method, to keep things a little cleaner:
1
2
3
4
5
6
7
8
9
func makeBall(at position: CGPoint) {
let ball = SKSpriteNode(imageNamed: "ballRed")
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width / 2)
ball.physicsBody?.contactTestBitMask = ball.physicsBody!.collisionBitMask
ball.physicsBody?.restitution = 0.4
ball.position = position
ball.name = "ball"
addChild(ball)
}
And then added a makeBox(at:)
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func makeBox(at position: CGPoint) {
let size = CGSize(width: Int.random(in: 16...128),
height: 16)
let box = SKSpriteNode(color: randomColor(),
size: size)
box.zRotation = CGFloat.random(in: 0...3)
box.position = position
box.physicsBody = SKPhysicsBody(rectangleOf: box.size)
box.physicsBody?.isDynamic = false
addChild(box)
}
func randomColor() -> UIColor {
let red = CGFloat.random(in: 0.3...1)
let green = CGFloat.random(in: 0.3...1)
let blue = CGFloat.random(in: 0.3...1)
return UIColor(red: red,
green: green,
blue: blue,
alpha: 1)
}
With that, it works, but I added a few small tweaks to make the game a little more playable. Right now, you can add a ball anywhere on the screen, including right over the slot. You can also place boxes over the slots to completely block them. So I added a couple of guard statements to make sure the locations make sense:
1
2
3
4
5
// At the top of makeBall(at:)
guard position.y > 600 else { return }
// At the top of makeBox(at:)
guard position.y < 550, position.y > 100 else { return }
Here’s what it looks like:

You can find my version of this project at the end of day 46 on Github here.
Reflections
This was really interesting to me. I’ve never made a game like this before, but I totally understand the logic of what is happening all the way through. I just didn’t know what API to use or how to structure it. So I feel like I’m learning a lot more in this project than in some of the past ones. And this is a pretty solid little game for being less than 200 lines of code.