Day 46 - 100 Days of Swift

8 minute read

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:

Gif of working game

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.